けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

マトロイドの凸構造

 この記事は、Competitive Programming Advent Calendar Div2012の12日目の記事として書きました。

0. はじめに

 今回はマトロイドについて書きたいと思います。
 マトロイドはGreedyとの関連でよく耳にします。では、そもそもマトロイドがGreedy性を持つのは何故でしょうか?実は、マトロイドは単に「Greedyの一例」として出て来るばかりでなく、「現在効率的なアルゴリズムが知られている問題の殆どはマトロイドが何かしら関わっている」と言える程にイイ構造を持っています。以前、以下のようなツイートをしました。

dpやってていつも思うのが、なんか凸凹してるなーと。凸凹し過ぎてdpじゃなきゃ解けないよな、みたいな感じ。マトロイドは凹んでるところがない凸なイメージ。だから、局所最適狙う貪欲法だけで最適解に辿り着ける。焼き鈍しなんて必要ない。

 本記事では、この「マトロイドの凸なイメージ」をもう少し具体的に実感できるような内容を目指します。こうした営みは直接的には競プロの腕を上げることには繋がらないかもしれませんが、

  • この問題は、なんかキレイだぞ………もしや、フローに落とせないかな???
  • この最小手数問題は、最小手数を達成する手順の様相がバラバラだ。BFSなりするしかなさそう。。。
  • この最大化問題は、色々試したけど規則性がありそうもない。DPなり半分全列挙なりするしかないようだ。

といった着想を産む効果があると思います。



<コンテンツ>
0. はじめに
  
1. マトロイドとは
  
2. 凸凹でも通用!DPのパワー!
  
3. Marathonではお馴染み、ローカルサーチ
  3-1. ローカルサーチ
  3-2. 凸構造
  
4. マトロイドの凸構造に迫る!
  
5. おわりに


1. マトロイドとは

 まずは、マトロイドとは何ぞや?から始めます。

 数学では、集合Mが与えられたときに、そこに何かしらの構造を入れることをよくやります。有名所だと、順序集合、位相空間、確率空間、群、…といった感じで、マトロイドもその仲間です。
 まず、n個の要素からなる有限集合Mが与えられたとします。Mの部分集合は2^n個あります。例えば、M = {1, 2, 3}なら、その部分集合は、{ {}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3} }の8個です。それら1つ1つについて、「独立」と「従属」のどちらかに割り振り、「独立」に割り振られたモノの集合をIとします。この割り振り方が特別な条件を満たすとき、それをマトロイドと呼びます。

 <マトロイドの定義>
 有限集合Mとその部分集合の集合Iが、以下の条件を満たすとき、(M, I)をマトロイドという。

  • (A1) 空集合\phiについて、\phi \in I
  • (A2) Y \in Iで、X \subset Yならば、X \in I
  • (A3) X, Y \in Iで、|X| > |Y|ならば、Xには含まるがYには含まれない e \in Mが存在して、Y\cup{e} \in I

 また、Iに含まれる集合は「独立」であるという。(|X|とは、Xの要素数を表す)

 なお、(A1)と(A2)を満たすモノは独立性システム、(A1)と(A3)を満たすモノはグリードイドと呼びます。


 例えば、M = {1, 2, 3}として、以下のような感じです。

ex.1 I = { {} } 自明なマトロイド
ex.2 I = { {}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3} } マトロイド
ex.3 I = { {}, {1}, {2}, {1,2} } マトロイド(上図のマトロイド
ex.4 I = { {}, {1}, {2}, {3}, {1,2} } マトロイドでない(上図の独立性システム
ex.5 I = { {}, {1}, {1,2} } マトロイドでない(上図のグリードイド
  • ex.4は、{1,2}と{3]とを比較したときに、{3}に1,2のいずれを追加してもIに含まれていないので、(A3)を満たさない。
  • ex.5は、{1,2}の部分集合であるはずの{2}がIに含まれていないので、(A2)を満たさない。

(なお、上図は、wikipediaの図を少し改造したモノです。)


 「独立」「従属」といった用語からも分る通り、マトロイドは元々はベクトルの一次独立性を一般化する形で、Whitneyによって1935年に導入されました。実際、行列Aが与えられたとき、Aの列ベクトルをa_1, a_2, …, a_nとすると、M = {a_1, a_2, …, a_n}、I = (Mの部分集合のうち、一次独立なモノの集合)とすると、(M, I)はマトロイドとなります。これはベクトルマトロイドと呼ばれます。

 競プロにおいて重要なのは以下のグラフ的マトロイドでしょう。

 無向グラフGが与えられる。

  • Mを、Gの枝集合Eとする
  • Iを、Eの部分集合のうち、閉路を含まない集合(つまりは、森)とする

 このとき、(M, I)はマトロイドであり、グラフ的マトロイドと呼ばれる。

 これがマトロイドであることの証明は、T.コルメン等著, 浅野哲夫等訳 : "アルゴリズムイントロダクション 改訂2版"にあります。


マトロイド上のGreedyアルゴリズム

 さて、マトロイドの最も重要な性質は、Greedyアルゴリズムが適用できることです。以下のような問題を考えてみましょう。


Problem Statement

マトロイド(M, I)が与えられる。Mの各要素eについて、0以上の重みw(e)が与えられている。
このとき、Mの「独立」な部分集合Xのうち、各要素の重みの総和が最大となるモノについて、
その最大値を求めよ。

Examples
0) 
 M = {1, 2, 3}
 I = { {}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3} }
 w = {4, 5, 6}

 Returns : 11 ({2,3}としたときが最大です)

 競プロの体裁になっていませんが御容赦下さい^^;
 まず1つの観察として、重みはどれも0以上なのでなるべくXの要素数は大きくしたいです。で、いきなりですが、この問題は以下のようなGreedyアルゴリズムで解くことができます。

 
マトロイド上のGreedyアルゴリズム

1. X ← {}
2. res ← 0
3. (M, w)を、wの大きい順にソートする
4. for i ← 0 to M.size() do
5.     e ← M[i]
6.     if X∪{e}が「独立」 then 
7.         X ← X∪{e}
8.         res ← res + w(e)
9. return res

 つまりは、Mの要素のうち、重みwが大きい順に見て行き、Xに追加できるなら追加していくことを行います。このアルゴリズムを見て、最小全域木を求めるKruskal法を想起された方も多いと思います。実際、このGreedyアルゴリズムをグラフ的マトロイドに適用したものが、Kruskal法に他なりません。(但し、重み最大のところを、最小にする。)
 
 このGreedyアルゴリズムを用いて解ける問題として、Kruskal法の他に、

があります。後者は、hirosegolfさんより紹介頂きました。



2. 凸凹でも通用!DPのパワー!

 では、何故マトロイドはこのようなGreedyアルゴリズムが上手くいくのでしょうか?それを探る前に、少しDPの話をします。DPと言うと、

 全探索だと間に合わないから、DPを考えないと…

のように、「全探索 vs DP」のような図式で語られがちです。しかし、DPでやっていることは結局全探索なのではないかと思います。というのは、

 DPは、まとめられる処理をまとめて、同じ計算を行うムダをなくしているだけで、結局あり得る場合を、"全"て、"探索"していることには変わりない

からです。
 それに対し、上のような「マトロイド上でのGreedyアルゴリズム」は、猪突猛進!最適解に向かって一直線に走っているのみです。あり得る場合を全て探索するようなことはしていません。この違いがどうして生まれるのかを考えるために、ナップサック問題を考えてみましょう。(蟻本の01ナップサック問題と全く同じ問題です。)


Problem Statement

n個の品物があり、それぞれの重さと価値は、wi, viで与えられます。
これらの品物の中から、重さの総和がWを超えないように選んだときの、
価値の総和の最大値を求めよ。

Constraints
・1 <= n <= 100
・1 <= wi, vi <= 100
・1 <= W <= 10000

Examples
0)
 n = 6
 (w,v) = {(2,3), (1,2), (3,6), (2,1), (1,3), (5,85)}
 W = 8

 Returns : 91 (2, 5番の品物を選んだときが最大です)

 この問題は以下のようなDPで解くことができます。なおこの解法は、蟻本のP.55のCOLUMNに記載されている解法と本質的に一緒ですが、後の説明の都合上、DPの状態の持ち方を僅かに変えています。どちらでも漸化式自体は変わりませんが、dpの初期条件とリターン部分がビミョーに変わります。

dp[i+1][j] := 0番目からi番目までの品物について、
重さの総和が"丁度j"となるように選んだときの価値の総和の最大値

として、

dpテーブル全体を-INFにセット
dp[0][0] = 0
dp[i+1][j] = max(dp[i][j], dp[i][j-wi] + v[i])

return max(0<=j<=W)(dp[n][j])

 上のDP漸化式は、ちゃんと実装するときは、j >= wiかどうかで場合分けが必要です。


ナップサック問題の凸凹な様相

 さて、上のナップサック問題のExampleについて、実際にDPテーブルを書いてみましょう。


(品物は、(w,v) = {(2,3), (1,2), (3,6), (2,1), (1,3), (5,85)})

 さて、このDP表を眺めているだけでも、かなりゴチャゴチャと色んな状況が現れているのが見てとれます。

  • 漸化式の遷移状況がバラバラ!
    • 青マス(dp[4][6])を見ると、dp[3][6] vs dp[3][4] + 1 → dp[3][4]の勝ち!
    • 黄マス(dp[5][3])を見ると、dp[4][3] vs dp[4][2] + 3 → 同点!
    • 赤マス(dp[6][9])を見ると、dp[5][9] vs dp[5][4] + 85 → dp[5][4] + 85の勝ち!
  • これらの3パターンがバラバラに混在しています
  • dp[4][6]とdp[4][7]とを見比べると、なんと逆転しています!!!

 こうして見ると、「あー、なんかヤバそうな問題だなぁ…」と直感がはたらいて来ます。
 ヤバそうというのは、「入力ケースによって色んなパターンが生まれて来て、とても規則性など見出せそうにない」という感じです。ですので結局、「DPなり半分全列挙なりするにせよ、あり得る場合の全体を探索しないといけない」という雰囲気を感じます。(枝刈りや分枝限定法のことは一旦置いときます…)。


探索のココロ

 話は若干逸れますが、競プロを始めるに当たって最初の関門は、「探索」の考え方に馴染む部分ではないかと思います。逆に、問題を深く洞察することによって、探索は殆どしなくても陽的に解けてしまうこともあります。所謂、「数学ゲー」というヤツですね。例えば、

  • 「1以上の整数Nが与えられたとき、それが2以上の2つの偶数の和として表すことができるか判定せよ」

とかは、数学ゲーの最もシンプルな例の1つと言えます。(Nが偶数かどうかを判定するだけです。N=2の場合がコーナーケースになりますが…)
 他にも、蟻本の最初の方に出て来る、

も数学ゲーと言えるでしょう。これも、minを使った数式にはなりますが、明示的に解を書き下すことができます。

 しかし、世の中の問題の多くは、このようなキレイな構造は持っていないことが殆どです。
 規則性など見つかりそうもなく、入力ケースによって挙動がバラバラなので結局探索するしかない。僕は、競プロを始める前までは、「問題を解く」とは、「問題を陽的に解く」ことだと無意識のうちに思っていました。そして競プロの世界に入って、「問題が陽的に解けなくとも、解を出力するアルゴリズムを見出せば良い」という考え方を知り、感動して目からウロコだったのを覚えています。

 話が大分逸れてしまいましたが、要するに、ナップサック問題は、規則性など見つかりそうもない凸凹した問題だということです。せっかくある山の頂上に辿り着いても、もっと高い山があるかもしれないのです。実際、ナップサック問題はNP-Hardであることが知られています。そんな凸凹な問題であっても何とか高速に全体像を把握しよう!!!その代表的な手法が、(SRM系コンテストでは)DPとか、半分全列挙とか、…という感じですね。

 なお、具体的に探索スキルを学ぼう、と思うと、chokudaiさんの最強最速アルゴリズマー養成講座の各記事が面白かったです。



3. Marathonではお馴染み、ローカルサーチ

 前章にて、ナップサック問題の凸凹感を書いてみました。同時に、こんな凸凹な空間全体を高速に駆け抜けてしまうDPのパワーにも触れました。しかし、凸凹空間を何とか扱うアルゴリズムはDPだけではありません。僕はまだあまり詳しくないのですが、Marathon Matchでは多彩な技が繰り広げられているだろうと思います。その中でも特に「ローカルサーチ(とそれを基にしたアルゴリズム)」はお馴染みですね。
 

3-1. ローカルサーチ

 ローカルサーチは、別名「山登り法」とも言われます。
 今、ある関数を最大化する問題を考えたいとします。関数は或る意味、山脈のようなモノと考えることができます。よって最大化問題は一種の山登りと考えられます。しかし普通の山登りと違い、「景色全体が見渡せるわけではなく、自分のすぐ近くしか見えない」という状況となっています。このすぐ近くのことを、「近傍」と呼びます。
 この設定の下で、ローカルサーチとは、

  1. 「近傍」の中で現在の高さよりも高い場所が見つかれば、そこへ移動する
  2. 「近傍」の中で現在の高さよりも高い場所がなければ、現在の解を出力する

というアルゴリズム手法です。
 「近傍」を上手く設定することがとても大切になって来ます。

ローカルサーチの困難


 例えば上のような山脈において、頂上の木がある場所へ辿り着くことを考えてみましょう。このとき、例えば一度左下の一番低い山を登ってしまったとします。このとき、その山の頂上に辿り着いてしまったら、もう身動きが取れなくなってしまいます。この状態を、「ローカル最適に陥る」と言います。
 
 この状況を克服するための改善策として、

  • マルチスタートローカルサーチ(ローカルサーチのスタート地点を複数点設けます)
  • 焼き鈍し法
  • 遺伝的アルゴリズム
  • タブーサーチ

などがあります。これらの詳細については、アドベントカレンダー24日に、nico_shindanninさんが焼き鈍し法についての記事を予定されているので、楽しみにしましょう。

3-2. 凸構造

 さて、このローカルサーチ。山脈相手には色々と難しいが、1つの山が相手としたら、ローカル最適がそのままグローバル最適になります!そんな夢の手法を実現させてくれるのが凸構造です。
 凸構造の何が嬉しいかって、「ローカルサーチするだけでグローバル最適解が求まる」だけではありません!「今いる地点の近傍により良い解がないならば、それはグローバル最適解であることが保証される」というのが著しい嬉しさです。
 例えば、巡回セールスマン問題を考えてみます。最短っぽい経路が求まったとします。それで、コレが本当に最短であることをどうやって証明したらよいでしょうか???巡回セールスマン問題のような凸凹な問題は、最短経路を求めるどころか、最適性の判定すらままならない難問です。



4. マトロイドの凸構造に迫る!

 さて、いよいよマトロイドの本質に迫って行きましょう。何故マトロイドは、あのようなGreedyアルゴリズムが適用できるのか?
 マトロイドの定義を再掲しましょう。

 <マトロイドの定義>
 有限集合Mとその部分集合の集合Iが、以下の条件を満たすとき、(M, I)をマトロイドという。

  • (A1) 空集合\phiについて、\phi \in I
  • (A2) Y \in Iで、X \subset Yならば、X \in I
  • (A3) X, Y \in Iで、|X| > |Y|ならば、Xには含まるがYには含まれない e \in Mが存在して、Y\cup{e} \in I

 また、Iに含まれる集合は「独立」であるという。(|X|とは、Xの要素数を表す)

 マトロイドを、Greedyアルゴリズムとの関連で見るときにはこの定義がピッタリなのですが、ローカルサーチとの関連で見るときは少し書き換えた方が良いので、書き換えて行きます。


極大独立集合

 さて、マトロイドの「独立」な集合のうち、もうこれ以上何を追加しても従属になってしまうような集合のことを「極大独立集合」と呼びます。
 例えば、M = {1, 2, 3}として、以下のような感じです。

独立集合 極大独立集合
ex.1 I = { {} } {}
ex.2 I = { {}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3} } {1,2,3}
ex.3 I = { {}, {1}, {2}, {1,2} } {1,2}
ex.4 I = { {}, {1}, {2}, {3} } {1}と{2}と{3}
ex.5 I = { {}, {1}, {2}, {3}, {1,2}, {1,3} } {1,2}と{1,3}

 また、重要な例として、グラフ的マトロイドにおいては、「極大独立集合」とは、即ち、「全域木(森)」のことです。

 上の例のように極大独立集合は複数あることもありますが、必ず「極大独立集合はどれもサイズが等しい」ことが簡単に示せます。XYも極大独立集合として、もし|X| > |Y|であるならば、(A3)より、Xには含まるがYには含まれないe \in Mが存在して、Y \cup{e}もまた独立集合となります。しかし、これはYが極大独立集合であることに矛盾します。
 また、極大独立集合のことを、マトロイドのとも呼びます。以後、この用語を用います。


マトロイドの交換公理

 上のマトロイドの定義では、Mの独立な部分集合を全て集めた集合Iを持ち出して定義したのですが、代わりに、マトロイドMの「基」のみを全て集めた集合Bを用いてマトロイドを以下のように定義することもできます!

 <マトロイドの定義(基による定義)>
 有限集合Mとその部分集合の集合Bが、以下の条件を満たすとき、(M, B)をマトロイドという。

  • (B1) B \neq \phi
  • (B2) 任意のS, T \in Bと、任意の e \in T-Sに対し、ある f \in S-Tが存在して、 T - {e} \cup {f} \in Bと、S \cup {e} - {f} \in Bが、同時に成り立つ

 (集合X, Yに対して、X-Yとは、Xには含まれるがYには含まれない要素の集合を表す)

 この(B2)を、マトロイドの交換公理と呼びます。

 簡単に述べると、S君とT君は同数の要素を持っています。このとき、S君は自分が持っていないモノでT君が持っているモノeを欲しがったとします。このとき、逆にS君は持っているけどT君は持っていないモノfが上手い具合にあって、S君とT君とでeとfとを交換することができます。交換しても、S君とT君は、相変わらず基のままなのです!!!!!


 実は、最初のマトロイドの定義と、基によるマトロイドの定義は、本質的に全く等価であることが知られています。コレは決して自明ではありません。何故なら、基による定義の方が、圧倒的にイイ構造に思えるからです。上の定義で、「同時に」の部分が驚異的です。

 今までマトロイドの2通りの定義を扱いましたが、実はまだまだ等価な言い換えがあります。代表的なモノに以下があります。

  1. 独立集合による定義(コレ
  2. 基による定義1
  3. 基による定義2(コレ
  4. ランク関数による定義
  5. サーキットによる定義

 このうち、「独立集合による定義」と「基による定義2」について扱いました。「基による定義1」は、wikipediaの「基の族」の項目のところにあります。一見、「基による定義1」と「基による定義2」は殆ど同じに見えますが、よくよく見ると「基による定義2」の方が圧倒的に強いことを言っていることが分かります。4番目のランク関数については、5-1. 劣モジュラ関数にて簡単に触れたいと思います。なお、「基による定義2」以外の定義については、wikipediaに記載があります。
 こうして見ると、マトロイドは本当に豊かな構造を持っていることが感じられます。


マトロイドの凸構造

 さて、このマトロイドの交換公理。如何にも凸っぽい感じがするのではないでしょうか?そのイメージを明確にするためにもう少し"交換"によって何が起こるかを見てみましょう。下で書くことは、あくまでイメージでしかないのですが、こうしたイメージを持つことは意味ある営みだと思います。



上図のようなイメージです。S君もT君も「基」なので、同数の要素を持っています。そのうちの幾つかは被っている状態です。そして、始めはT君は持っているけどS君が持っていないモノは5個あります (これを S と T との距離とよぶことにします) が、ここで、(B2)の交換公理に従ってeとfとを交換します。

このとき、最初S君とT君はキョリ5だけ離れていたのに対し、交換後のS君は、T君とのキョリが1だけ縮まって 4 になります!!!

そして、交換後のS君も (交換後のT君も) 相変わらず「基」のままなのです!!!つまり、

 最初S君もT君も「基の領域」に居たならば、交換後のS君とT君もやはり「基の領域」にいる!

ということを意味します。
 ここで、そもそも凸って何?というのを思い出しましょう。

 左図が凸集合の例で、右図が凸でない集合の例になります。集合Sが凸集合であるとは、Sに含まれる任意の2点x, yに対して、xとyとを結ぶ線分がSに含まれることを意味します。
 然るにマトロイドの交換公理は、「基の領域」に含まれる任意のS君とT君に対して、S君とT君がお互いに少し内側に歩み寄っても、両者共に「基の領域」に含まれることを意味します。
 こうして見ると、あくまで感覚的な話なのですが、マトロイドが凸構造を持つことが見えて来ます!「感覚的に」でなく「厳密に」、と思いたくもなるのですが、その必要はないのです。何故なら、マトロイドの交換公理自体をマトロイドの定義としたからです。我々としては、上のようなイメージを念頭に置いて、きっとイイことが示せるだろうと目論みつつマトロイドを定義して、凸であれば出て来るであろう「イイ性質」を見出していくことになります。


マトロイドの最適性条件

 さて、いよいよマトロイドのGreedy性を解き明かしましょう。一応、マトロイドの最大重み集合を求める問題を再掲します。(基による定義に合わせて問題文を若干変えています)

Problem Statement

マトロイド(M, B)が与えられる。Mの各要素eについて、0以上の重みw(e)が与えられている。
このとき、Mの基Xのうち、各要素の重みの総和が最大となるモノについて、
その最大値を求めよ。

Examples
0) 
 M = {1, 2, 3}
 B = { {1,2}, {1,3}, {2,3} }
 w = {4, 5, 6}

 Returns : 11 ({2,3}を選んだときが最大です)

マトロイドの交換公理が示しているのは次のようなことです。

  1. あるマトロイド(M, B)の基Sがあったとします。
  2. 一方、最大重みな基を、Tとします。Sが目指すべき理想です。
  3. T^'に、Tをコピーします。
  4. このとき、Sは、T^'の持っている要素eをねだる代わりに、自分の持っているf をT^'に渡します。
  5. こうすることで、Sはより重みをupさせて、理想のTに近づくことができます。

 これぞ、まさに、3. Marathonではお馴染み、ローカルサーチでも示したローカルサーチです!!!ローカル改善を繰り返して、最適解に近づこうとします。但し、実際にアルゴリズムを動かしている最中は理想のT君の姿が分からないので、以下のようにします。

  1. あるSが与えられたとき、Sの持っていないeを取って来る。
  2. そしてSの持っているfに対して、Sからfを捨ててeを取り入れてできるS^'が「基」で、且つ「元のSよりも大きくなる」なら、
  3. SをそのS^'で置き換える。

 つまり、この場合でのローカルサーチの「近傍」とは、「Sからfを捨ててeを取り入れたモノ」ということになります。

 そして、なんと!!!以下のことが成立します!!!

 <マトロイドの最適性条件>
 マトロイド(M, B)に対し、ある基Sが与えられたとすると、以下の2つは同値である。

  • Sが最大重みの基
  • Sの近傍となる基S^'で、Sよりも重みが大きい基は存在しない

 この定理は、まさにマトロイドが凸構造を持つことを示しています!
 Sの近傍にSより重みの大きいヤツはいない状況、つまり、ローカル最適な状況ならば、そのままグローバル最適になっているというわけです。このローカルサーチを少し効率良く実現しようとすると、Greedyアルゴリズムになります。

 なお、5-2. 離散凸解析にて少し触れたいと思いますが、マトロイドはもっと広く、M凸集合と呼ばれる構造に一般化できます。上で述べた最適性条件は、M凸集合でも成立します。



6. おわりに

 マトロイドの凸構造を軸に、様々なことを書き連ねて来ました。少し長くなってしまいましたが、ここまで読んで頂いた方に感謝の気持ちで一杯です。
 僕は、「問題の持つ構造を深く洞察し、それに基づいて問題の解法を考える」という営みが大好きです。マトロイド程のイイ構造を持つモノはそうそうないですが、それでも、色々な問題の構造を活かして計算量を落としていくような作業がとても楽しいです。競プロ世界では、そんな楽しさが満ち溢れていてとても幸せです。
 あと、本記事において、多少誤摩化してしまった部分があることもお詫びします。例えば、一貫して「凸構造」という言葉を使って来ましたが、本来は「凸集合」「凸関数」を明確に区別して考えるべきです。また、3. Marathonではお馴染み、ローカルサーチにて、「上に凸な関数」も凸関数と呼んでしまったのですが、本当は凸関数は「下に凸な関数」のことを指し、上に凸な関数は凹関数と呼ばれます。
 何にせよ、この記事を書くことを通して、色々なことが頭の中で繋がって行ったのでとても楽しかったです。何か1つでも読んで頂いた方の心に残るモノがあったら嬉しいです。

TopCoder SRM 694 DIV1 Hard - SRMDiv0Easy

二段階単体法のデバッグに苦労しました。

問題概要

初期状態が全要素が 0 であるような N 次元ベクトルに対し、
以下のような Q 個の区間クエリを実施して得られる結果が
全要素が等しい N 次元ベクトルとなるようにしたい。

区間クエリ i:
閉区間 [ L[i], R[i] ] に X[i] 以上 Y[i] 以下のいずれかの整数 x[i] を一律に加算する

各区間クエリにおいて加算する x[i] の値を調整して、
得られる N 次元ベクトルの要素の値を最大化せよ。
そのようなものが存在しない場合は -1 をリターンせよ。

(制約)
・1 <= Q, N <= 200

解法 1 (単体法)

(i, j) 成分が L[j] <= i <= R[j] ならば 1, それ以外ならば 0 となるような行列を A,
等しい値を z として

max  z
s.t.
    X[i] <= x[i] <= Y[i]
    x[i] は整数
    Ax = [z, z, …, z]'

これは整数計画問題であるが、
A が完全単模な区間行列なので整数ベクトルな最適解が存在する。
(区間行列は完全単模であることが知られている)

あとは、これを単体法ライブラリの形 (自分のは等式標準形) に直して
ライブラリに投げる。実行時間は topcoder サーバ上で 800ms 程度。

解法 2 (フロー)

解法 1 で登場したような "区間行列" は、
差分をとることで、あるグラフの接続行列になることが知られている。
このことからフロー解が存在することが示唆される。

Ax = [z, z, …, z]'

について、各行について差分をとると、

B[i][j] =
 1 (j = L[i]) 
 -1 (j = R[i]+1)
 0 (それ以外)

となるような行列 B (A が行数 N であるのに対し、B の行数は N+1) に対して、

Bx = [z, 0, 0, …, 0, -z]'

となる。
これは、

・ノード  : (0, 1, 2, …, N)
・エッジ  : L[i] -> R[i]+1 (ここを流れるフローが x[i])
・始点と終点: 0, N
・フロー値: z

となるネットワークのフロー制約に他ならない。
容量制約は、

X[i] <= x[i] <= Y[i]

で与えられ、この容量制約の下でフロー値 z を最大化する最大流問題である。
下限容量制約つきの最大フロー問題は、
最小費用流や、二分探索 + 最大流によって解くことができる。

コード (解法1)

#include <iostream>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <cstring>
#include <string>
#include <vector>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <numeric>
#include <utility>
#include <iomanip>
#include <algorithm>
#include <functional>
using namespace std;
#define COUT(x) cout << #x << " = " << (x) << " (L" << __LINE__ << ")" << endl
template<class T1, class T2> ostream& operator << (ostream &s, pair<T1,T2> P)
{ return s << '<' << P.first << ", " << P.second << '>'; }
template<class T> ostream& operator << (ostream &s, vector<T> P)
{ for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; }

// min cx s.t. Ax = b, x >= 0
template<class T> struct Matrix {
    vector<vector<T> > val;
    Matrix(int n = 1, int m = 1) {val.clear(); val.resize(n, vector<T>(m));}
    Matrix(int n, int m, T x) {val.clear(); val.resize(n, vector<T>(m, x));}
    void init(int n, int m, T x = 0) {val.clear(); val.resize(n, vector<T>(m, x));}
    void resize(int n) {val.resize(n);}
    void resize(int n, int m, T x = 0) {val.resize(n); for (int i = 0; i < n; ++i) val[i].resize(m, x);}
    int size() {return val.size();}
    inline vector<T>& operator [] (int i) {return val[i];}
    friend ostream& operator << (ostream& s, Matrix<T> M) {s << endl;
        for (int i = 0; i < M.val.size(); ++i) s << M[i] << endl; return s;}
};

const double INF = 1LL<<29;
const double EPS = 1e-10;

template<class T> int PreProcess(Matrix<T> &A, vector<T> &b) {
    int rank = 0;
    Matrix<T> M(A.size(), A[0].size()+1);
    for (int i = 0; i < A.size(); ++i) {
        for (int j = 0; j < A[0].size(); ++j) M[i][j] = A[i][j];
        M[i][A[0].size()] = b[i];
    }

    for (int i = 0; i < A[0].size(); ++i) {
        int pivot = rank;
        T max = 0;
        for (int j = rank; j < M.size(); ++j) {
            if (max < abs(M[j][i])) {
                pivot = j;
                max = abs(M[j][i]);
            }
        }
        if (max > EPS) {
            swap(M[pivot], M[rank]);
            T div = M[rank][i];
            for (int j = 0; j < M[0].size(); ++j) {
                M[rank][j] = M[rank][j] / div;
            }
            for (int j = 0; j < M.size(); ++j) {
                if (j != rank && abs(M[j][i]) > EPS) {
                    T fac = M[j][i];
                    for (int k = 0; k < M[0].size(); ++k) {
                        M[j][k] = M[j][k] - M[rank][k] * fac;
                    }
                }
            }
            ++rank;
        }
    }
    A.resize(rank);
    b.resize(rank);
    for (int i = 0; i < rank; ++i) {
        for (int j = 0; j < A[0].size(); ++j) A[i][j] = M[i][j];
        b[i] = M[i].back();
    }
    return rank;
}

enum STATE { OPTIMIZED, INFEASIBLE, UNBOUNDED };
template<class T> T Simplex(Matrix<T> A, vector<T> b, vector<T> c, vector<T> &res, STATE &state) {
    res = vector<T>();
    PreProcess(A, b);                                               // let A be row-fullrank

    // build tableau
    int n = A.size(), m = A[0].size();
    for (int i = 0; i < n; ++i) if (b[i] < 0) {b[i] *= -1; for (int j = 0; j < m; ++j) A[i][j] *= -1; }
    vector<int> base(n), non(m);
    for (int i = 0; i < n; ++i) base[i] = m+i;
    for (int j = 0; j < m; ++j) non[j] = j;

    A.resize(n+2, n+m+1);
    for (int i = 0; i < n; ++i) A[i][m+i] = 1, A[i][n+m] = b[i];
    for (int i = 0; i < n; ++i) { A[n][n+m] += A[i][n+m]; for (int j = 0; j < m; ++j) A[n][j] += A[i][j]; }
    for (int j = 0; j < m; ++j) A[n+1][j] = -c[j];

    // start simplex
    for (int phase = 0; phase < 2; ++phase) {
        while (true) {
            int nn = -1, nb = -1;
            for (int i = 0; i < non.size(); ++i) {
                if (non[i] >= m) continue; // We cannot let slack value move to the base
                if (A[n][non[i]] > EPS) {
                    if (nn == -1) nn = i;
                    else if (non[i] < non[nn]) nn = i; // Bland's smallest subscript rule
                    //if (chmax(FMax, A[n][non[i]])) nn = i; // max coefficient rule
                }
            }
            if (nn == -1) {
                if (phase == 1) break;                              // All done!
                if (A[n][A[0].size()-1] > EPS) {                    // No feasible solution!
                    state = INFEASIBLE;
                    return -1;
                }

                // detail postprocess of phase 0
                bool ok = true;
                for (int i = 0; i < base.size(); ++i) {
                    if (base[i] >= m) {
                        ok = false; nb = i; break;                  // If base doesn't contain slack, go to phase 1
                    }
                }
                if (ok) break;
                for (int i = 0; i < non.size(); ++i) {
                    if (non[i] < m && abs(A[nb][non[i]]) > EPS) {
                        nn = i; break; // slack has base, continue phase 0
                    }
                }
                if (nn == -1) {
                    break;                                          // It can't be happened (A is row-fullrank)
                }
            }
            int col = non[nn];

            if (nb == -1) {
                T min_ratio = INF;
                for (int i = 0; i < base.size(); ++i) if (A[i][col] > EPS) {
                    T tmp = A[i][A[0].size()-1] / A[i][col];
                    if (min_ratio > tmp + EPS) { min_ratio = tmp, nb = i; }
                    else if (min_ratio >= tmp - EPS) {
                        if (nb == -1) nb = i;
                        else if (base[i] < base[nb]) nb = i;        // Bland's smallest subscript rule
                    }
                }
                if (nb == -1) {                                     // It cannot happen at the 1st stage
                    state = UNBOUNDED;
                    return -1;
                }
            }
            int row = nb;

            T piv = A[row][col];
            for (int j = 0; j < A[0].size(); ++j) A[row][j] = A[row][j] / piv;
            for (int i = 0; i < A.size(); ++i) {
                if (i == row) continue;
                T pn = A[i][col];
                for (int j = 0; j < A[0].size(); ++j) A[i][j] = A[i][j] - A[row][j] * pn;
            }
            swap(base[nb], non[nn]);
        }

        if (phase == 0) {
            swap(A[n], A[n+1]);
            A.val.pop_back();
            for (int i = 0; i < n; ++i)
                for (int j = 0; j < A.size(); ++j)
                    A[j].erase(A[j].begin() + m);

            /*
            for (int i = 0; i < base.size(); ++i) {                 // base slack can be removed
                if (base[i] >= m) {
                    A.val.erase(A.val.begin() + i);
                    int eraseID = base[i];
                    base.erase(base.begin() + i);
                    --i;
                    --n;
                }
            } */


            for (int i = 0; i < non.size(); ++i)
                if (non[i] >= m)
                    non.erase(non.begin() + i--);
        }
    }
    res = vector<T>(m, 0);
    for (int i = 0; i < base.size(); ++i) res[base[i]] = A[i].back();
    state = OPTIMIZED;
    return A[n].back();
}

class SRMDiv0Easy {
public:
    int get(int N, vector <int> L, vector <int> R, vector <int> X, vector <int> Y) {
        int Q = L.size();
        Matrix<double> A(Q*2+N, Q*3+1, 0.0);
        vector<double> b(Q*2+N, 0.0);
        vector<double> c(Q*3+1, 0.0);

        for (int i = 0; i < Q; ++i) {
            for (int j = L[i]; j <= R[i]; ++j)
                A[j][i] = -1.0;
            A[N+i][i] = 1.0;
            A[N+i][Q+i] = -1.0;
            A[N+Q+i][i] = 1.0;
            A[N+Q+i][Q*2+i] = 1.0;
            b[N+i] = X[i];
            b[N+Q+i] = Y[i];
        }
        for (int j = 0; j < N; ++j) {
            A[j][Q*3] = 1.0;
            b[j] = 0.0;
        }
        c[Q*3] = -1.0;

        vector<double> x;
        STATE state;
        double res = Simplex(A, b, c, x, state);
        if (state != OPTIMIZED) return -1;

        return (int)(-res + EPS);
    }
};

コード (解法2)

#include <iostream>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <cstring>
#include <string>
#include <vector>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <numeric>
#include <utility>
#include <iomanip>
#include <algorithm>
#include <functional>
using namespace std;
#define COUT(x) cout << #x << " = " << (x) << " (L" << __LINE__ << ")" << endl
template<class T> ostream& operator << (ostream &s, vector<T> P)
{ for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; }
template<class T> ostream& operator << (ostream &s, vector<vector<T> > P)
{ for (int i = 0; i < P.size(); ++i) { s << endl << P[i]; } return s << endl; }


const int MAX_V = 2100;
const int INF = 1LL<<29;

struct Edge {
    int rev, from, to, cap, icap;
    Edge(int r, int f, int t, int c) : rev(r), from(f), to(t), cap(c), icap(c) {}
    friend ostream& operator << (ostream& s, const Edge& E) {
        if (E.cap > 0) return s << E.from << "->" << E.to << '(' << E.cap << ')';
        else return s;
    }
};

struct Graph {
    int V;
    vector<Edge> list[MAX_V];
    
    Graph(int n = 0) : V(n) {for (int i = 0; i < MAX_V; ++i) list[i].clear();}
    void init(int n = 0) {V = n; for (int i = 0; i < MAX_V; ++i) list[i].clear();}
    void resize(int n = 0) {V = n;}
    void reset() {for (int i = 0; i < V; ++i) for (int j = 0; j < list[i].size(); ++j) list[i][j].cap = list[i][j].icap;}
    inline vector<Edge>& operator [] (int i) {return list[i];}
    
    Edge &redge(Edge e) {
        if (e.from != e.to) return list[e.to][e.rev];
        else return list[e.to][e.rev+1];
    }
    
    void addedge(int from, int to, int cap) {
        list[from].push_back(Edge(list[to].size(), from, to, cap));
        list[to].push_back(Edge(list[from].size()-1, to, from, 0));
    }
    
    void addbiedge(int from, int to, int cap) {
        list[from].push_back(Edge(list[to].size(), from, to, cap));
        list[to].push_back(Edge(list[from].size()-1, to, from, cap));
    }
    
    friend ostream& operator << (ostream& s, const Graph& G) {
        s << endl; for (int i = 0; i < G.V; ++i) {s << i << " : " << G.list[i] << endl;}return s;
    }
};

Graph G;


int level[MAX_V];
int iter[MAX_V];

void dibfs(Graph &G, int s) {
    memset(level, -1, sizeof(level));
    level[s] = 0;
    queue<int> que;
    que.push(s);
    while (!que.empty()) {
        int v = que.front();
        que.pop();
        for (int i = 0; i < G[v].size(); ++i) {
            Edge &e = G[v][i];
            if (level[e.to] < 0 && e.cap > 0) {
                level[e.to] = level[v] + 1;
                que.push(e.to);
            }
        }
    }
}

int didfs(Graph &G, int v, int t, int f) {
    if (v == t) return f;
    for(int &i = iter[v]; i < G[v].size(); ++i) {
        Edge &e = G[v][i], &re = G.redge(e);
        if (level[v] < level[e.to] && e.cap > 0) {
            int d = didfs(G, e.to, t, min(f, e.cap));
            if (d > 0) {
                e.cap -= d;
                re.cap += d;
                return d;
            }
        }
    }
    return 0;
}

int Dinic(Graph &G, int s, int t) {
    int res = 0;
    while (true) {
        dibfs(G, s);
        if (level[t] < 0) return res;
        memset(iter, 0, sizeof(iter));
        int flow;
        while ((flow = didfs(G, s, t, INF)) > 0) {
            res += flow;
        }
    }
}

int demand[1000];

class SRMDiv0Easy {
public:
    int get(int N, vector <int> L, vector <int> R, vector <int> X, vector <int> Y) {
        for (int i = 0; i < 1000; ++i) demand[i] = 0;
        for (int i = 0; i < L.size(); ++i) {
            demand[L[i]] += X[i];
            demand[R[i]+1] -= X[i];
        }
        
        int low = -1, high = INF;
        while (high - low > 1) {
            int mid = (low + high) / 2;
            demand[0] -= mid; demand[N] += mid;
            G.init(N+3);
            int s = N+1, t = N+2;
            for (int i = 0; i < L.size(); ++i) G.addedge(L[i], R[i]+1, Y[i]-X[i]);
            int flow = 0;
            for (int i = 0; i <= N; ++i) {
                if (demand[i] > 0) G.addedge(i, t, demand[i]), flow += demand[i];
                else if (demand[i] < 0) G.addedge(s, i, -demand[i]);
            }
            G.addedge(N, 0, INF);
            int res = Dinic(G, s, t);
            
            if (res < flow) high = mid;
            else low = mid;
            
            demand[0] += mid; demand[N] -= mid;
        }
        
        return low;
    }
};

TopCoder SRM 452 DIV1 Hard - IncreasingNumber (本番 0 人)

SRM 埋めを始めたいと思う! ところでこの問題、こんなの気付かないよ...!! まあ、Petr と tourist も参加したコンテストで誰も AC してない問題なので...。

問題概要

10 進法表記で最高次数から順に広義単調増加であるような整数を Increasing Number と呼ぶ。

整数  D, K が与えられる。

 D 桁の整数であって (最高次の係数は 0 でない)、 K の倍数であるようなものの個数を 1000000007 で割ったあまりを求めよ。

制約

  •  1 \le D \le 10^{18}
  •  1 \le K \le 500

考えたこと

安直に考えると、次のような DP が良さそうに思えた。

  • dp[i][j][v] i 桁の整数であって、1 の位の値が  j であって、 K で割って  v あまるような整数の個数

このままだと、 G = 10 として  O(DKG^{2}) の計算量となってしまう。ちょっとでも改善したいと思うと、 i のところは行列累乗を持ち出すことで  O(K^{3}G^{3} \log D) に改善される。でも微妙に間に合わない...

天才的な言い換え

Increasing Number というものを天才的な言い換えをする。天才すぎる... D 桁以下の Increasing Number ( D 桁以下のものから  D-1 桁以下のものを引く方針でやる) とは

 1 \times x_{1}
 + 11 \times x_{2}
 + \dots
 + 11...1 \times x_{D}

という形であって、

  •  x_{i} \ge 0
  •  x_{1} + x_{2} + \dots + x_{D} \le 9

を満たすものとして特徴付けられるのだ。あとは、 K の倍数であるようなものの個数を求めればよい。

DP へ

ここまで来たら、もう、すぐ!!!

  • num[v] ← 1, 11, 111, ... のうち、 K で割って  v あまるものの個数

とする。ここからは次のような DP でできる。

  • dp[i][j][v] K で割って  0, 1, \dots, i-1 あまるもののみをちょうど合計  j 個だけ用いて足して、全体のあまりが  v となるようなものの個数

遷移は次のようにできる (ただし num[i] = 0 のときは dp[i+1] = dp[i])。

dp[i+1][j+k][(v+i*k)%K] += dp[i][j][v] * com(k + num[i] - 1, k);

二項係数の部分は「num[i] 種類のものから k 個選ぶ」という重複組合せ。

計算量は  O(K^{2}G^{2}) まで改善する。

コード

#include <bits/stdc++.h>
using namespace std;

const int MOD = 1000000007;
long long modinv(long long a, long long mod) {
    long long b = mod, u = 1, v = 0;
    while (b) {
        long long t = a/b;
        a -= t*b; swap(a, b);
        u -= t*v; swap(u, v);
    }
    u %= mod;
    if (u < 0) u += mod;
    return u;
}

long long com(long long N, long long R) {
    if (N < 0 || R < 0 || R > N) return 0;
    long long ue = 1, shita = 1;
    for (int k = 0; k < R; ++k) {
        ue = ue * ((N-k) % MOD) % MOD;
        shita = shita * (k+1) % MOD;
    }
    return ue * modinv(shita, MOD) % MOD;
}

long long solve(long long D, long long K) {
    if (D == 0) return 1;
    deque<long long> num(K, 0), syu, used(K, 0);
    long long v = 1 % K;
    for (long long i = 0; i < D; ++i) {
        if (used[v]) {
            while (!syu.empty() && syu[0] != v) {
                --D;
                ++num[syu[0]];
                syu.pop_front();
            }
            break;
        }    
        syu.push_back(v);
        used[v] = 1;
        v = (v * 10 + 1) % K;
    }

    long long q = D / syu.size(), r = D % syu.size();
    for (auto v: syu) num[v] += q;
    for (int i = 0; i < r; ++i) ++num[syu[i]];

    vector<vector<long long>> dp(10, vector<long long>(K, 0));
    dp[0][0] = 1;
    for (int i = 0; i < K; ++i) {
        if (!num[i]) continue;
        vector<vector<long long>> nex(10, vector<long long>(K, 0));

        vector<long long> coms(10);
        for (int k = 0; k <= 9; ++k) coms[k] = com(k+num[i]-1, k);
        for (int j = 0; j <= 9; ++j) {
            for (int v = 0; v < K; ++v) {
                if (dp[j][v] == 0) continue;
                for (int k = 0; j+k <= 9; ++k) {
                    int nv = (v + i * k) % K;
                    nex[j+k][nv] += dp[j][v] * coms[k] % MOD;
                    nex[j+k][nv] %= MOD;
                }
            }
        }
        swap(dp, nex);
    }
    long long res = 0;
    for (int i = 0; i <= 9; ++i) res = (res + dp[i][0]) % MOD;
    return res;
}

class IncreasingNumber {
public:
    long long countNumbers(long long D, int K) {
        long long res = (solve(D, K) - solve(D-1, K) + MOD) % MOD;
        return res;
    }
};

TopCoder SRM 402 DIV1 Hard - IncreasingSequence (本番 2 人)

詰め切るの大変だった!

問題概要

'0'〜'9' からなる長さ  N の文字列  S が与えられる。

これらの文字列をいくつかの連続する部分文字列に分ける。次の条件を満たす必要がある。

  • 各部分文字列を数値とみなしたとき、strictly に単調増加である
  • leading zero は許容する

このような分け方のうち、末尾の数値が最小となるものを求めよ。複数通りある場合は、辞書順最大のものを求めよ。そして、求められた分け方について、区切られた数値の積を 1000000003 で割った余りを答えよ。

制約

  •  1 \le N \le 2500

考えたこと

ぱっと見は DP で一瞬に見えた。一旦、計算量を無視して考えることにする。


dp[i] ← 先頭から  i 文字を条件を満たすように区切る方法のうち、末尾を表す数値の最大値


そうして、いつもの DP をすれば良さそうに見える。

for (int i = 1; i <= N; ++i) {
    for (int j = 0; j < i; ++j) {
        if (dp[j] < S[j:i] の表す数) {
            chmin(dp[i], S[j:i] の表す数);
        }
    }
}

という感じだ。また、末尾が決まってからは今度は逆方向に同様の DP を回すことで、辞書順最大のものを求めることができる。計算量を無視すれば解けた。

しかし、dp[i] が最長  N 文字の文字列なので、計算量は  O(N^{3}) となる。このままでは間に合わない。

S[i:j] の序列を求める

僕のとった方法は、文字列の各区間  \lbrack l, r) を小さい順にソートすることだった。

具体的には、各区間  \lbrack l, r) に対して、区間の表す数値の大小関係に対応するように、整数値  f(l, r) を定めてあげることとした。僕は次のようにやった。


  1. leading zero は取り除く (適切な前処理によって  O(1) でできる)
  2. 桁数を  d とする
  3. Suffix Array における、区間  \lbrack l, r) (から leading zero を取り除いた部分) を表す文字列の lower_bound を求めて、 k とする
  4.  f(l, r) = 10000d + k とする

3 について、最初は  k を、Suffix Array のランクを rank として、rank[l] としていた。しかしこれだと、

 S = "242456"

などのケースで、区間 [0, 2) の "24" と、区間 [2, 4) の "24" に異なる値がついてしまう。よって、lower_bound を求めることにした。

先ほどの DP に対して、この  f の値を用いることで、計算量は  O(N^{2} \log N) へと改善できた。

leading zero についての注意

0 がたくさんある場合に注意。たとえば

 S = "10000000000010"

の場合、正解は "1", "0000000000010" と区切ることだが、末尾を "10" で区切ってしまうと詰むことに注意する。

コード

テストコードを含む

#include <bits/stdc++.h>
using namespace std;
using pint = pair<int, int>;
using pll = pair<long long, long long>;
template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; }
template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; }

#define REP(i, n) for (long long i = 0; i < (long long)(n); ++i)
#define REP2(i, a, b) for (long long i = a; i < (long long)(b); ++i)
#define COUT(x) cout << #x << " = " << (x) << " (L" << __LINE__ << ")" << endl
template<class T1, class T2> ostream& operator << (ostream &s, pair<T1,T2> P)
{ return s << '<' << P.first << ", " << P.second << '>'; }
template<class T> ostream& operator << (ostream &s, vector<T> P)
{ for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; }
template<class T> ostream& operator << (ostream &s, deque<T> P)
{ for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; }
template<class T> ostream& operator << (ostream &s, vector<vector<T> > P)
{ for (int i = 0; i < P.size(); ++i) { s << endl << P[i]; } return s << endl; }
template<class T> ostream& operator << (ostream &s, set<T> P)
{ for(auto it : P) { s << "<" << it << "> "; } return s; }
template<class T> ostream& operator << (ostream &s, multiset<T> P)
{ for(auto it : P) { s << "<" << it << "> "; } return s; }
template<class T1, class T2> ostream& operator << (ostream &s, map<T1,T2> P)
{ for(auto it : P) { s << "<" << it.first << "->" << it.second << "> "; } return s; }


// Sparse Table
template<class MeetSemiLattice> struct SparseTable {
    vector<vector<MeetSemiLattice> > dat;
    vector<int> height;
    
    SparseTable() { }
    SparseTable(const vector<MeetSemiLattice> &vec) { init(vec); }
    void init(const vector<MeetSemiLattice> &vec) {
        int n = (int)vec.size(), h = 1;
        while ((1<<h) < n) ++h;
        dat.assign(h, vector<MeetSemiLattice>(1<<h));
        height.assign(n+1, 0);
        for (int i = 2; i <= n; i++) height[i] = height[i>>1]+1;
        for (int i = 0; i < n; ++i) dat[0][i] = vec[i];
        for (int i = 1; i < h; ++i)
            for (int j = 0; j < n; ++j)
                dat[i][j] = min(dat[i-1][j], dat[i-1][min(j+(1<<(i-1)),n-1)]);
    }
    
    MeetSemiLattice get(int a, int b) {
        return min(dat[height[b-a]][a], dat[height[b-a]][b-(1<<height[b-a])]);
    }
};

// SA-IS (O(N))
template<class Str> struct SuffixArray {
    // data
    Str str;
    vector<int> sa;    // sa[i] : the starting index of the i-th smallest suffix (i = 0, 1, ..., n)
    vector<int> rank;  // rank[sa[i]] = i
    vector<int> lcp;   // lcp[i]: the lcp of sa[i] and sa[i+1] (i = 0, 1, ..., n-1)
    SparseTable<int> st;  // use for calcultating lcp(i, j)

    // getter
    int& operator [] (int i) { return sa[i]; }
    const int& operator [] (int i) const { return sa[i]; }
    vector<int> get_sa() { return sa; }
    vector<int> get_rank() { return rank; }
    vector<int> get_lcp() { return lcp; }

    // constructor
    SuffixArray() {}
    SuffixArray(const Str& str_, bool no_limit_elements = false) : str(str_) {
        build_sa(no_limit_elements);
    }
    void init(const Str& str_, bool no_limit_elements = false) {
        str = str_;
        build_sa(no_limit_elements);
    }
    void build_sa(bool no_limit_elements = false) {
        vector<int> s;
        int num_of_chars = 256;
        if (!no_limit_elements) {
            for (int i = 0; i < (int)str.size(); ++i) {
                s.push_back(str[i] + 1);
            }
        } else {
            unordered_map<int,int> dict;
            for (int i = 0; i < (int)str.size(); ++i) {
                if (!dict.count(str[i])) dict[str[i]] = dict.size();
            }
            for (int i = 0; i < (int)str.size(); ++i) {
                s.push_back(dict[str[i]] + 1);
            }
            num_of_chars = (int)dict.size();
        }
        s.push_back(0);
        sa = sa_is(s, num_of_chars);
        build_lcp(s);
        build_sparse_table();
    }

    // SA-IS
    // num_of_chars: # of characters
    vector<int> sa_is(vector<int> &s, int num_of_chars) {
        int N = (int)s.size();
        if (N == 0) return {};
        else if (N == 1) return {0};
        else if (N == 2) {
            if (s[0] < s[1]) return {0, 1};
            else return {1, 0};
        }

        vector<int> isa(N);
        vector<bool> ls(N, false);
        for (int i = N - 2; i >= 0; --i) {
            ls[i] = (s[i] == s[i + 1]) ? ls[i + 1] : (s[i] < s[i + 1]);
        }
        vector<int> sum_l(num_of_chars + 1, 0), sum_s(num_of_chars + 1, 0);
        for (int i = 0; i < N; ++i) {
            if (!ls[i]) ++sum_s[s[i]];
            else ++sum_l[s[i] + 1];
        }
        for (int i = 0; i <= num_of_chars; ++i) {
            sum_s[i] += sum_l[i];
            if (i < num_of_chars) sum_l[i + 1] += sum_s[i];
        }

        auto induce = [&](const vector<int> &lms) -> void {
            fill(isa.begin(), isa.end(), -1);
            vector<int> buf(num_of_chars + 1);
            copy(sum_s.begin(), sum_s.end(), buf.begin());
            for (auto d: lms) {
                if (d == N) continue;
                isa[buf[s[d]]++] = d;
            }
            copy(sum_l.begin(), sum_l.end(), buf.begin());
            isa[buf[s[N - 1]]++] = N - 1;
            for (int i = 0; i < N; ++i) {
                int v = isa[i];
                if (v >= 1 && !ls[v - 1]) {
                    isa[buf[s[v - 1]]++] = v - 1;
                }
            }
            copy(sum_l.begin(), sum_l.end(), buf.begin());
            for (int i = N - 1; i >= 0; --i) {
                int v = isa[i];
                if (v >= 1 && ls[v - 1]) {
                    isa[--buf[s[v - 1] + 1]] = v - 1;
                }
            }
        };
            
        vector<int> lms, lms_map(N + 1, -1);
        int M = 0;
        for (int i = 1; i < N; ++i) {
            if (!ls[i - 1] && ls[i]) {
                lms_map[i] = M++;
            }
        }
        lms.reserve(M);
        for (int i = 1; i < N; ++i) {
            if (!ls[i - 1] && ls[i]) {
                lms.push_back(i);
            }
        }
        induce(lms);

        if (M) {
            vector<int> lms2;
            lms2.reserve(isa.size());
            for (auto v: isa) {
                if (lms_map[v] != -1) lms2.push_back(v);
            }
            int rec_upper = 0;
            vector<int> rec_s(M);
            rec_s[lms_map[lms2[0]]] = 0;
            for (int i = 1; i < M; ++i) {
                int l = lms2[i - 1], r = lms2[i];
                int nl = (lms_map[l] + 1 < M) ? lms[lms_map[l] + 1] : N;
                int nr = (lms_map[r] + 1 < M) ? lms[lms_map[r] + 1] : N;
                bool same = true;
                if (nl - l != nr - r) same = false;
                else {
                    while (l < nl) {
                        if (s[l] != s[r]) break;
                        ++l, ++r;
                    }
                    if (l == N || s[l] != s[r]) same = false;
                }
                if (!same) ++rec_upper;
                rec_s[lms_map[lms2[i]]] = rec_upper;
            }
            auto rec_sa = sa_is(rec_s, rec_upper);

            vector<int> sorted_lms(M);
            for (int i = 0; i < M; ++i) {
                sorted_lms[i] = lms[rec_sa[i]];
            }
            induce(sorted_lms);
        }
        return isa;
    }

    // find min id that str.substr(sa[id]) >= T
    int lower_bound(const Str& T) {
        int left = -1, right = sa.size();
        while (right - left > 1) {
            int mid = (left + right) / 2;
            if (str.compare(sa[mid], string::npos, T) < 0)
                left = mid;
            else
                right = mid;
        }
        return right;
    }

    // find min id that str.substr(sa[id], T.size()) > T
    int upper_bound(const Str& T) {
        int left = -1, right = sa.size();
        while (right - left > 1) {
            int mid = (left + right) / 2;
            if (str.compare(sa[mid], T.size(), T) <= 0)
                left = mid;
            else
                right = mid;
        }
        return right;
    }

    // find min id that sa[id] >= str.substr(l, r-l)
    int lower_bound(int l, int r) {
        int left = -1, right = rank[l];
        while (right - left > 1) {
            int mid = (left + right) / 2;
            if (st.get(mid, rank[l]) < r - l) left = mid;
            else right = mid;
        }
        return right;
    }
    
    // search
    bool is_contain(const Str& T) {
        int lb = lower_bound(T);
        if (lb >= sa.size()) return false;
        return str.compare(sa[lb], T.size(), T) == 0;
    }

    // find lcp
    void build_lcp(const vector<int> &s) {
        int N = (int)s.size();
        rank.assign(N, 0), lcp.assign(N - 1, 0);
        for (int i = 0; i < N; ++i) rank[sa[i]] = i;
        int h = 0;
        for (int i = 0; i < N - 1; ++i) {
            int pi = sa[rank[i] - 1];
            if (h > 0) --h;
            for (; pi + h < N && i + h < N; ++h) {
                if (s[pi + h] != s[i + h]) break;
            }
            lcp[rank[i] - 1] = h;
        }
    }
    
    // build sparse table for calculating lcp
    void build_sparse_table() {
        st.init(lcp);
    }

    // calc lcp of str.sutstr(a) and str.substr(b)
    int get_lcp(int a, int b) {
        return st.get(min(rank[a], rank[b]), max(rank[a], rank[b]));
    }

    // debug
    void dump() {
        for (int i = 0; i < sa.size(); ++i) {
            cout << i << ": " << sa[i] << ", " << str.substr(sa[i]) << endl;
        }
    }
};

// modint
template<int MOD> struct Fp {
    // inner value
    long long val;
    
    // constructor
    constexpr Fp() noexcept : val(0) { }
    constexpr Fp(long long v) noexcept : val(v % MOD) {
        if (val < 0) val += MOD;
    }
    constexpr long long get() const noexcept { return val; }
    constexpr int get_mod() const noexcept { return MOD; }
    
    // arithmetic operators
    constexpr Fp operator - () const noexcept {
        return val ? MOD - val : 0;
    }
    constexpr Fp operator + (const Fp &r) const noexcept { return Fp(*this) += r; }
    constexpr Fp operator - (const Fp &r) const noexcept { return Fp(*this) -= r; }
    constexpr Fp operator * (const Fp &r) const noexcept { return Fp(*this) *= r; }
    constexpr Fp operator / (const Fp &r) const noexcept { return Fp(*this) /= r; }
    constexpr Fp& operator += (const Fp &r) noexcept {
        val += r.val;
        if (val >= MOD) val -= MOD;
        return *this;
    }
    constexpr Fp& operator -= (const Fp &r) noexcept {
        val -= r.val;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr Fp& operator *= (const Fp &r) noexcept {
        val = val * r.val % MOD;
        return *this;
    }
    constexpr Fp& operator /= (const Fp &r) noexcept {
        long long a = r.val, b = MOD, u = 1, v = 0;
        while (b) {
            long long t = a / b;
            a -= t * b, swap(a, b);
            u -= t * v, swap(u, v);
        }
        val = val * u % MOD;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr Fp pow(long long n) const noexcept {
        Fp res(1), mul(*this);
        while (n > 0) {
            if (n & 1) res *= mul;
            mul *= mul;
            n >>= 1;
        }
        return res;
    }
    constexpr Fp inv() const noexcept {
        Fp res(1), div(*this);
        return res / div;
    }

    // other operators
    constexpr bool operator == (const Fp &r) const noexcept {
        return this->val == r.val;
    }
    constexpr bool operator != (const Fp &r) const noexcept {
        return this->val != r.val;
    }
    friend constexpr istream& operator >> (istream &is, Fp<MOD> &x) noexcept {
        is >> x.val;
        x.val %= MOD;
        if (x.val < 0) x.val += MOD;
        return is;
    }
    friend constexpr ostream& operator << (ostream &os, const Fp<MOD> &x) noexcept {
        return os << x.val;
    }
    friend constexpr Fp<MOD> modpow(const Fp<MOD> &r, long long n) noexcept {
        return r.pow(n);
    }
    friend constexpr Fp<MOD> modinv(const Fp<MOD> &r) noexcept {
        return r.inv();
    }
};

// Suffix Array
string S;
SuffixArray<string> sa;

// S[i] 以降の最初の 0 でない index
vector<int> nex;

// S[l:r] が何番目か
long long order(int l, int r, bool debug = false) {
    long long tl = nex[l];
    if (tl >= r) return 0;
    long long len = r - tl;
    long long rank = sa.rank[tl];
    long long res = len * 10000 + sa.lower_bound(tl, r);
    return res;
}

class IncreasingSequence {
public:
    int getProduct(vector <string> digits) {
        S = "";
        for (auto s : digits) S += s;
        int N = S.size();
        nex.assign(N, N);
        for (int i = N-1; i >= 0; --i) {
            if (S[i] != '0') nex[i] = i;
            else if (i < N-1) nex[i] = nex[i+1];
        }
        sa.init(S);

        // forward
        const long long INF = 1LL<<60;
        vector<long long> dp(N+1, INF);
        dp[0] = 0;
        for (int i = 1; i <= N; ++i) {
            for (int j = 0; j < i; ++j) {
                if (order(j, i) > dp[j]) chmin(dp[i], order(j, i));
            }
        }
        
        // backward
        vector<long long> dp2(N+1, -1);
        vector<int> pre2(N+1);
        for (int i = 0; i < N; ++i) if (order(i, N) == dp.back()) {
            dp2[i] = dp.back();
            pre2[i] = N;
        }
        for (int i = N-1; i >= 0; --i) {
            for (int j = i+1; j < N; ++j) {
                if (order(i, j) < dp2[j]) {
                    if (chmax(dp2[i], order(i, j))) pre2[i] = j;
                }
            }
        }
    
        
        const int MOD = 1000000003;
        using mint = Fp<MOD>;
        auto calc = [&](int l, int r) -> mint {
            mint res = 0;
            for (int i = l; i < r; ++i) res = res * 10 + (S[i] - '0');
            return res;
        };
        
        mint res = 1;
        int i = 0;
        while (i != N) {
            assert(i >= 0 && i <= N);
            int i2 = pre2[i];
            res *= calc(i, i2);
            i = i2;
        }
        return res.get();
    }
};



// BEGIN CUT HERE
namespace moj_harness {
int run_test_case(int);
void run_test(int casenum = -1, bool quiet = false) {
    if (casenum != -1) {
        if (run_test_case(casenum) == -1 && !quiet) {
            cerr << "Illegal input! Test case " << casenum << " does not exist." << endl;
        }
        return;
    }
    
    int correct = 0, total = 0;
    for (int i=0; i <= 100; ++i) {
        int x = run_test_case(i);
        if (x == -1) {
            if (i >= 100) break;
            continue;
        }
        correct += x;
        ++total;
    }
    
    if (total == 0) {
        cerr << "No test cases run." << endl;
    } else if (correct < total) {
        cerr << "Some cases FAILED (passed " << correct << " of " << total << ")." << endl;
    } else {
        cerr << "All " << total << " tests passed!" << endl;
    }
}

int verify_case(int casenum, const int &expected, const int &received, clock_t elapsed) {
    cerr << "Example " << casenum << "... ";
    
    string verdict;
    vector<string> info;
    char buf[100];
    
    if (elapsed > CLOCKS_PER_SEC / 200) {
        sprintf(buf, "time %.2fs", elapsed * (1.0/CLOCKS_PER_SEC));
        info.push_back(buf);
    }
    
    if (expected == received) {
        verdict = "PASSED";
    } else {
        verdict = "FAILED";
    }
    
    cerr << verdict;
    if (!info.empty()) {
        cerr << " (";
        for (int i=0; i<(int)info.size(); ++i) {
            if (i > 0) cerr << ", ";
            cerr << info[i];
        }
        cerr << ")";
    }
    cerr << endl;
    
    if (verdict == "FAILED") {
        cerr << "    Expected: " << expected << endl;
        cerr << "    Received: " << received << endl;
    }
    
    return verdict == "PASSED";
}

int run_test_case(int casenum__) {
    switch (casenum__) {
        case 0: {
            string digits[]           = {"12345"};
            int expected__            = 120;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 1: {
            string digits[]           = {"543210"};
            int expected__            = 45150;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 2: {
            string digits[]           = {"20210222"};
            int expected__            = 932400;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 3: {
            string digits[]           = {"1111111111"};
            int expected__            = 1356531;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 4: {
            string digits[]           = {"171829294246"};
            int expected__            = 385769340;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 5: {
            string digits[]           = {"3","235","236"};
            int expected__            = 264320;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
            
        // custom cases
            
        case 8: {
            string digits[]           = {"1000001"};
            int expected__            = 1000001;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 9: {
            string digits[]           = {"10100010"};
            int expected__            = 1000100;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 10: {
            string digits[]           = {"1010100100"};
            int expected__            = 101101000;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 7: {
            string digits[]           = {"1"};
            int expected__            = 1;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        case 6: {
            string digits[]           = {"9596773956300483475666508855401133731667330988123165153538530995247140312823703377028402364265219868355394269265472925005761886019934622351891153935910985710485587114289717177330114225323750943079188779226749966413886200213106627656798322769329706875862123079815668686703415695489789120672880212373087168195357431839993487945184040983792186159431522752650795442357489456776201924733778097867967798921688217922107693269741424949548576155774642897980644206351528320992407082770025463494849143927680168253395870345012699278535340520549234693348797240471489285404715704284727841302969288597478862708462192746815217374796483218319263673088806537715436717462130571938348378206613989414300830616334590918258211128547388661819747609070100497010284217195276141017185085057081499521588845228255302374529171991355017852671740892729689718429851275475360028106304911628983632137809796234351944489333608559581232982470774357548094323633921140412613494611894043721554711712396854464758985630637169680519089012708243823133407555679477513250251998449793365822411778926826821017859113910038694750562848009940490126257627309295980545610756518862358030168388173763333280788352250024664421905922244347812350291463794867872850866559916638317710866887241535598389091467946935682337616957225973930832363037617182339859028911238351304184593963314252177049776990118013585212110748974498916820272725850668876868401314019177653558129245775126137007384278227148344146329199732233107943046160450869190645790276624558714982412721843571345800293864985291272145704303997620887051942740056103871942108695960375161733905287990253586275917465961724174947816351499080020174241119331340745135693307689795584172858305350085499108223208359942994270416812098971637711474826166158338616813425577427239985976918898744712697073760903386433679496951819944726002789838941864946566563602444694269983337108757220422403811240756020886873145003738597200969249145448104228267253529352967933194494585961981521712693697996846030807256067257031185939593346760452603128016471719465713526751107186291711976921980551149169706242486937937793695984548848701358709638843909175786819543212142471472691746022569245643044116704843531643882005720529034576487709022454764632375068881225153769542121472563718364123737562711281974074901827527761158342603574648231789706653647569109181271077937607496121318749791334834036000141509760091120251820561376812959881672143007897364098732830474774870453453351105370493819070043809472713679652851101692175100979449158808113059"};
            int expected__            = 1;
            
            clock_t start__           = clock();
            int received__            = IncreasingSequence().getProduct(vector <string>(digits, digits + (sizeof digits / sizeof digits[0])));
            return verify_case(casenum__, expected__, received__, clock()-start__);
        }
        default:
            return -1;
    }
    }
}
 

int main(int argc, char *argv[]) {
    if (argc == 1) {
        moj_harness::run_test();
    } else {
        for (int i=1; i<argc; ++i)
            moj_harness::run_test(atoi(argv[i]));
    }
}
// END CUT HERE