秦九韶アルゴリズムを用いて最大部分配列和問題を解く

WARNING

この記事ChatGPTによって中国語から翻訳されたものでいくつかのりがまれているかもしれません。不正確部分があればご了承ください

問有兩尖田一段其尖長不等兩大斜三十九步兩小斜二十五步中廣三十步欲知其積幾何

術曰以少廣求之翻法入之。置半廣自乘為半冪與小斜冪相減相乘為小率 以半冪與大斜冪相減相乘為大率。以二率相減餘自乘為實併二率倍之為從上廉以一為益隅開翻法三乘方得積。

人々、関数型プログラミング軽視しています。彼らは、関数型プログラミング無意味抽象化やすだけでその代償パフォーマンス低下だとじるかもしれませんこのような見解人々おそらくRichard Bird著書 PEARLS OF FUNCTIONAL ALGORITHM DESIGN んだことがないでしょうこの内容かに難解、私一度完全したことはありません

幸運なことにRichard Bird1989にわずか5ページ論文 Algebraic identities for program calculation いていますこの論文では、最大部分配列和1Maximum subarray problemという有名問題いて、関数型プログラミング思考アルゴリズム問題する独特理解見事しています最大部分配列和問題秦九韶アルゴリズムくことができます

最大部分配列和問題

最大部分配列和問題目的、数列一次元方向連続した部分配列つけその部分配列最大になるようにすることです。例えば、数列 [-2, 1, -3, 4, -1, 2, 1, -5, 4] して、連続した部分配列最大つのは [4, -1, 2, 1] その 6 です2

らかに、私たちは平凡アルゴリズムっていますすなわちすべての部分配列列挙、最大値めます

1def mss(arr):
2    r = 0
3    for end in range(len(arr) + 1):
4        for start in range(0, end):
5            s = 0
6            for k in range(start, end):
7                s += arr[k]
8            r = max(s, r)

このプログラムHaskellくとシンプルになります

1import Data.List (tails, inits)
2
3mss = maximum . map sum . segs
4segs = concat . map tails . inits

ここで tails 「接尾辞」、inits 「接頭辞」意味します

1tails [1, 2, 3] = [[1, 2, 3], [2, 3], [3], []]
2inits [1, 2, 3] = [[], [1], [1, 2], [1, 2, 3]]

らかに、「すべての接頭辞のすべての接尾辞」、またはすべての接尾辞のすべての接頭辞」リストのすべての部分リストになります

さてアルゴリズム自体りましょうこの単純アルゴリズム時間複雑度 O(n3)O(n^3) ですすべての部分配列列挙するのに O(n2)O(n^2) 、各部分配列めるのに O(n)O(n) 必要なので、結果として O(n3)O(n^3) になります

実際には、最れたアルゴリズム O(n)O(n) 時間複雑度でこの問題解決できますBird論文、単純アルゴリズムから出発して、最れたアルゴリズム__方法について議論していますまずアルゴリズム変形します

1def mss(arr):
2    m = 0
3    for end in range(len(arr) + 1):
4        m = max(max_tails(arr, end), m)
5
6def max_tails(arr, end):
7    m = 0
8    for start in range(0, end):
9        s = 0
10        for k in range(start, end):
11            s += arr[k]
12        m = max(m, s)
13    return m

これは単純変形のようにえますが、私3つのループ内側2つをしただけですmax_tails 関数、配列 arr [0, end) 範囲最大接尾辞します。最大値関数 \uparrow すとmax_tails 関数バージョンまたは数学的バージョンのようにけます

arr[0:end][x1,x2,,xn]m=(i=1nxi)(i=2nxi)(xn)0 \begin{aligned} \textsf{arr[0:end]} &\equiv [ x_1, x_2, \cdots, x_n ] \\ m &= (\sum_{i=1}^n x_i) \uparrow (\sum_{i=2}^n x_i) \uparrow \cdots \uparrow (x_n) \uparrow 0 \end{aligned}

これは一見すると非常難解えますが、以下説明しますまずmax_tails([1, 2, 3], 3)つまり [1, 2, 3] のすべての接尾辞最大値えてみましょう

max_tails 計算のようにけます

1(1 + 2 + 3) ↑ (2 + 3) ↑ (3) ↑ 0

つまりmax_tails 各接尾辞計算それらの最大値めます。最大値二項関数 \uparrow としてくとそれを使って各接尾辞をつなげて最終的構成することが自然になります

朴素アルゴリズムでは、接尾辞最大値めるために O(n2)O(n^2) 複雑さが必要です。以下では、秦九韶アルゴリズムいてこの複雑さを O(n)O(n) らします

秦九韶アルゴリズム

秦九韶著作「数術九章」、高次方程式近似解める方法提案しましたその一部現代研究者によって「秦九韶アルゴリズムばれています

nn 次多項式えてみましょう

f(x)=i=0naixi f(x) = \sum_{i=0}^{n} a_i x^{i}

特定 tt してf(t)f(t) める方法はいくつかあります。最単純方法、各項個別めてから合計することですこのアルゴリズムでは O(n2)O(n^2) 乗算必要ですしかし、秦九韶アルゴリズムのように注意しています

anxn+an1xn1++a0=((anxn1)+(an1xn2)++a1)x+a0==(((anx+an1)x+)x+a1)x+a0 \begin{aligned} & a_n x^n + a_{n-1} x^{n-1} + \cdots + a_0 \\ = & ((a_n x^{n - 1}) + (a_{n-1} x^{n - 2}) + \cdots + a_{1}) x + a_0 \\ = & \cdots \\ = & (((a_n x + a_{n - 1}) x + \cdots) x + a_{1})x + a_0 \end{aligned}

これは反復形式くことができます

s0=ansi+1=sit+ani1 \begin{aligned} s_0 &= a_n \\ s_{i + 1} &= s_i t + a_{n - i - 1} \end{aligned}

このようにsn=f(t)s_n = f(t) となりますこの計算では、乗算 nn 回、加算 nn 回行われます

一見すると、秦九韶アルゴリズムなる 数値計算 アルゴリズムぎませんそれは max_tails 関数関係があるのでしょうかここでは、「一般化」された秦九韶アルゴリズム形式える必要があります

まずこのアルゴリズム入力多項式である必要はありません。以下秦九韶アルゴリズムえることができます

x1x2x3+x2x3+x3+1=(((x1+1)x2)+1)x3+1 \begin{aligned} & x_1x_2x_3 + x_2x_3 + x_3 + 1 \\ =& (((x_1 + 1) x_2) + 1) x_3 + 1 \end{aligned}

、秦九韶アルゴリズム考慮する演算 (+,)(+, \cdot) である必要はありませんこのアルゴリズム公因数抽出するとき、実際には乗法分配法則いてえています。任意つの関数 (,)(\oplus, \odot) えるとそれらが以下条件3たす、秦九韶アルゴリズム適用することができます

  • \odot \oplus して分配します
    ac+bc=(a+b)c(ac)(bc)=(ab)c \begin{aligned} a \cdot c + b \cdot c &= (a + b) \cdot c \\ (a \odot c) \oplus (b \odot c) &= (a \oplus b) \odot c \\ \end{aligned}
  • \odot 単位元存在しますつまりすべての xx して ax=xa \odot x = x となる aa 存在します

以下にとります

((x1x2)x3)(x2x3)(x3)(1)=((((x11)x2)1)x3)1 \begin{aligned} & ((x_1 \odot x_2) \odot x_3) \oplus (x_2 \odot x_3) \oplus (x_3) \oplus (1) \\ = & ((((x_1 \oplus 1) \odot x_2) \oplus 1) \odot x_3) \oplus 1 \end{aligned}

ここで 11 \odot 左単位元です

Kadaneアルゴリズム

ほどべたようにmax_tails のようにくことができます

arr[0:end][x1,x2,,xn]m=(i=1nxi)(i=2nxi)(xn)0 \begin{aligned} \textsf{arr[0:end]} &\equiv [ x_1, x_2, \cdots, x_n ] \\ m &= (\sum_{i=1}^n x_i) \uparrow (\sum_{i=2}^n x_i) \uparrow \cdots \uparrow (x_n) \uparrow 0 \end{aligned}

ここでの \uparrow \oplus なすことができ++ \odot なすことができます。加法には単位元 00 存在、分配性らかです

(ab)+c=(a+c)(b+c) (a \uparrow b) + c = (a + c) \uparrow (b + c)

けて [1, 2, 3] にとります

1(1 + 2 + 3) ↑ (2 + 3) ↑ (3) ↑ 0
2= ((1 + 2) ↑ 2 ↑ 0) + 3 ↑ 0
3= (((1 ↑ 0) + 2) ↑ 0) + 3 ↑ 0
4= ((((0 + 1) ↑ 0) + 2 ↑ 0) + 3) ↑ 0

この性質づいて、秦九韶アルゴリズムいて max_tails えます

1def step(s, a):
2    # (s + a) ↑ 0
3    return max(s + a, 0)
4
5def max_tails(arr, end):
6    s = 0
7    for i in range(end):
8        s = step(s, arr[i])

らかに、書えた max_tails 時間複雑度 O(n) ですさらに、次のことに注意します

max_tails(arr,end+1)= step(step(,arr[end1]),arr[end])= step(max_tails(arr, end),arr[end]) \begin{aligned} & \textsf{max\_tails}(\textsf{arr}, \textsf{end} + 1) \\ =\ & \textsf{step}(\textsf{step}(\cdots,\textsf{arr}[\textsf{end} - 1]), \textsf{arr}[\textsf{end}]) \\ =\ & \textsf{step}(\textsf{max\_tails(arr, end)}, \textsf{arr}[\textsf{end}]) \end{aligned}

mss [1, end + 1) max_tails める必要があるため、改写後時間複雑度 O(n2)O(n^2) となりますしかし、上記発見からmax_tails(arr, i) max_tails(arr, i - 1) からめることができますこれをすぐに O(n)O(n) mss 関数るために使用することができます

1def step1(state, a):
2    (s, m) = state
3    s_next = step(s, a)
4    return (s_next, max(m, s_next))
5
6def mss(arr):
7    state = (0, 0)
8    for a in arr:
9        state = step1(state, a)
10    return state[1]

間違いなくこれは大名鼎鼎 Kadane アルゴリズムです

推論

Bird 教授研究つの段落にまとめています

、明確だが非効率的関数型プログラムつまり手元問題仕様として機能するプログラム、等式推論いてより効率的なものを計算するという特定タスク興味がありました4

つまりBird 教授シンプル明確だが効率的でない関数型プログラムから出発、一般的ルールいてプログラム最適化より効率的プログラムることをんでいますBird 教授直感的プログラムから出発、一歩一歩推論めることでKMP アルゴリズムのような有名アルゴリズムることができますBird 教授論文では、最大部分配列和問題する推論のようになっています5

1mss = { by definition }                                     
2      max . map sum . segs                                  O(n³)
3    = { by definition of segs }
4      max . map sum . concat . map tails . inits            O(n³)
5    = { map promotion }
6      max . concat . map (map sum) . map tails . inits      O(n³)
7    = { definition of max and fold promotion }               
8      max . map max . map (map sum) . map tails  . inits    O(n³)
9    = { map distributivity }
10      max . map (max . (map sum) . tails) . inits           O(n³)
11    = { Horner's Rule }
12      max . map (foldl (⊗) 0) . inits                       O(n²)
13    = { scan theorem }
14      max . scanl (⊗) 0                                     O(n)
15    = { fold-scan fusion }
16      fst . foldl (⊙) (0, 0)                                O(n)
17    where a ⊗ b = (a + b) ↑ 0
18          (u, v) ⊙ t = (w ↑ u, w), w = v ⊗ t

たった8つの等式最終的アルゴリズムることができその技術驚異的えますさらに価値あることは、秦九韶アルゴリズム最終的最適化、推論過程直接的定理としてされていることですこれらの定理、関数型プログラミングでよく使われる関数mapfoldlconcatなど等式するものです。例えば、秦九韶アルゴリズムのように直接表現することができます

1fold (⊕) b . map (fold (⊗) a) . tails = fold (⊙) a
2u ⊙ v = (u ⊗ v) ⊕ a

ではなぜ関数型プログラミングはこのようなプログラム推論容易にするのでしょうかそれは、関数型プログラミングたちにプログラム 代数的 性質により注意けさせるからですBird 教授はまたThe algebra of programmingといういています

可愛さの裏側

読者上記コード LeetCode 提出しようとするといくつかのテスト失敗するかもしれませんこれはもちろんアルゴリズム間違っているわけではなく、最大部分配列和問題表現なるからです

  • (LeetCode): 配列えられたときその最大部分配列和出力します
  • (Programming Pearls): 配列えられたときその最大部分配列和出力しますもし配列最大部分配列和であれば0します

たちの問題 Programming Pearls バージョン、最大部分配列和になることはありません

LeetCode バージョン問題処理するには、以下のような簡単修正可能です

1mssNeg l
2  | r == 0    = max l
3  | otherwise = r
4    where r = mss l

この方法たいじがしますBird 教授導出過程修正して、負数ケース処理できるアルゴリズム導出することは可能でしょうかこの問題して、私理想的解決策提供していませんなぜならBird 教授最初間違っているからです

1--sum [] = 0
2mss = maximum . map sum . segs

segs 結果にはリスト存在そのリスト sum 評価すると0られますしたがってこの最大和ケース処理することはできませんまたここでの秦九韶アルゴリズムでは0 + 単位元であるため、負数処理できるアルゴリズム修正するには、完全からやり必要があるかもしれません

これは、関数型プログラム導出可愛らしさの裏側かもしれません。関数型プログラム導出プログラム代数的性質依存していますがこの代数的性質として脆弱、問題修正するだけで、元代数的性質消失することがあります。命令型プログラムにとってはそれ自体がこれらの代数的性質依存していないため、些細変更がすぐに機能するかもしれませんが、関数型、特関数型プログラム導出から導出されたプログラムにとってはこのような変更些細なものではありません

Bird 教授メール意見いてみてもいいですか

それもいいかもしれませんしかしBird 教授202244永遠たちをりました。天国手紙ることができる郵便局があるのでしょうか……