半環上の最大部分配列問題とKadane's algorithm
この記事はSunrise Advent Calendar 2019の9日目の記事です。
内容はごった煮ということなので、アルゴリズムと代数について書きます。
TL; DR
最大部分配列問題
最大部分配列問題(maximum subarray problem)とは、与えられた配列に対して、その連続部分配列のうち和が最大となるものの値を求める問題のことです。
形式化すると次のようになります(以下、添字は0-basedとします)。
- 入力:大きさ の数列
- 出力:
Kadane's algorithm
Kadane's algorithmは最大部分配列問題をで解くDPの一種です。以前記事を書いたので、こちら↓を見てください。
よく見ると、最大部分配列問題もKadane's algorithmもトロピカル半環上で議論できることがわかるので、それについて本記事で解説します。
半環
半環(rig*1)とは、以下を満たす加法と乗法の2項演算 、 をもつ集合のことである:
- は単位元 をもつ可換モノイド(commutative monoid, abelian monoid)*2:
- (結合律)
- (単位元の存在)
- (可換)
- は単位元 をもつモノイド(monoid):
- (結合律)
- (単位元の存在)
- 分配律(distributive law):
- absorption/annihilation law:
参考:https://ncatlab.org/nlab/show/rig
以下では、半環の構造を明示するときは のように表記するものとします。
トロピカル半環
トロピカル半環(tropical rig)とは、半環 のことである。max-plus algebraとも呼ばれる*3。ここで、、は次のように定義される:
が半環であることは、半環の定義の をそれぞれ に置き換えると明らかです。
関連記事:
トロピカル半環上の最大部分配列問題
最大部分配列問題はトロピカル半環を用いて記述できることを示します。
表記
写像 を次のように定義する:
- に対して
- 任意に を一つ選び、 とする。
は可換かつ結合律が成り立つので、の演算結果は一意に定まり写像 はwell-definedである。
最大部分配列問題
上記の表記を用いると、最大部分配列問題は次のように書き直せる:
- 入力:大きさ の数列
- 出力:
以下では
\begin{align} \displaystyle x := \bigoplus \left( \left\{\bigotimes_{k=i}^j a_k \mathrel{}\middle|\mathrel{} 0 \leq i \leq j \lt n \right\} \right) \end{align}
とします。
トロピカル半環上のKadane's algorithm
Kadane's algorithmがトロピカル半環上の最大部分配列問題に対しても適用できることを示します(これの証明を書き換えただけです)。
まず、 の計算の順序は問われないので
\begin{align} x = \bigoplus_{j=0}^{n-1} \bigoplus_{i=0}^j \bigotimes_{k=i}^j a_k \end{align}
と表せます。ここで とおくと、 となります。また、 です。 さらにに対して次の関係式が成り立ちます。
\begin{align} s_{j} &= \bigoplus_{i=0}^j \bigotimes_{k=i}^j a_k \\ &= \left( \bigoplus_{i=0}^{j-1} \bigotimes_{k=i}^j a_k \right) \oplus \left( \bigoplus_{i=j}^j \bigotimes_{k=i}^j a_k \right) \\ &= \left( \left( \bigoplus_{i=0}^{j-1} \bigotimes_{k=i}^{j-1} a_k \right) \otimes a_j \right) \oplus a_j & \because \text{分配律} \\ &= \left( s_{j-1} \otimes a_j \right) \oplus a_j \end{align}
つまり
- に対して
が成り立つので、トロピカル半環の半環としての性質のみでKadane's algorithmを使えることが示されました。
半環上のKadane's algorithm
これまでの議論から一般の半環についてもKadane's algorithmが適用できることがわかります。
実装すると次のようになります(D言語):
T kadane( T, // 型 alias plusFun, // 加法の2項演算 T plusIdentity, // 加法の単位元 alias multFun, // 乗法の2項演算 T multIdentity // 乗法の単位元 )(T[] as) { alias _plusFun = binaryFun!plusFun; alias _multFun = binaryFun!multFun; T res = plusIdentity; T s = multIdentity; foreach(a; as) { s = _plusFun(_multFun(s, a), a); res = _plusFun(res, s); } return res; }
これを用いて通常の最大部分配列問題を解く例です:
応用例
いくつか応用できそうな例を上げます。
半環
半環 上の"最大部分配列問題"は
\begin{align} \sum_{0 \leq i \leq j \lt n} \prod_{k=i}^j a_k \end{align}
を解く問題になります。上の関数kadane
を用いると
long ans = kadane!(long, "a+b", 0, "a*b", 1)(as);
で で求まります*4。
半環
は半環です。ここで
を表すものとします。この半環上の"最大部分配列問題"は
\begin{align} \bigoplus_{0 \leq i \leq j \lt n} \bigwedge_{k=i}^j a_k \end{align}
を解く問題になります。上の関数kadane
を用いると
long ans = kadane!(long, "a^b", 0, "a&b", (1L<<B)-1)(as);
で で求まります。
半環
は半環です。ここで
- は、アルファベット全体の集合(有限集合)
- は、 上の言語、つまり 上の有限列全体の集合
- は、 の冪集合
- は、集合の意味での和集合
- は、各文字列のペアを連結(concatenate)してできる文字列全体の集合を求める演算:
- は、空文字列
を表すものとします。この半環上の"最大部分配列問題"は、列 に対して
\begin{align} \left\{ a_i a_{i+1} \cdots a_j \mathrel{}\middle|\mathrel{} 0 \leq i \leq j \lt n \land (a_i, \ldots, a_j) \in \prod_{k=i}^j A_k \right\} \end{align}
を解く問題になります。つまり、文字列集合の配列に対して、連続した各要素内の文字列を順番に連結したときに生じるすべての文字列を列挙する問題となります。
bool[string][] As = [ ["abc": true], ["": true], ["d": true, "e": true], ["test": true] ];
で文字列集合の配列が与えられたとき、関数kadane
を用いると
kadane!( bool[string], (A, B) { bool[string] res; foreach(a; A.keys) res[a] = true; foreach(b; B.keys) res[b] = true; return res; }, null, (A, B) { bool[string] res; foreach(a; A.keys) foreach(b; B.keys) { res[a~b] = true; } return res; }, ["": true] )(As).keys.sort.writeln;
によって
["", "abc", "abcd", "abcdtest", "abce", "abcetest", "d", "dtest", "e", "etest", "test"]
と出力されます。この計算量は、各2項演算の計算量が ではないので、全体で にはならないことに注意してください。この問題に関しては Kadane's algorithm を単純に適用するより効率的な方法がある可能性があります。
感想
アルゴリズムの対象を抽象化することによって、ひとつのアルゴリズムが様々な問題に適用できる例(ここではKadane's algorithm)を見つけることができました。
最後の応用例のように、元の問題とは大きくかたちが異なる問題に対しても同一アルゴリズムを適用できるのは、抽象化のたのしいところだと思います。
代数はいいぞ。
思いついたものを書いただけなので、参考文献は特にありません。