0% found this document useful (0 votes)
330 views36 pages

Algorithm Notes - Dynamic Programming

这篇文档介绍了动态规划的概念和方法。它通过阶乘和爬楼梯问题的例子,说明如何将复杂问题分解成子问题,并通过记忆化搜索避免重复计算,从而提高效率。文中还详细说明了动态规划四个主要步骤:子问题重叠、状态空间、计算顺序和初始值,以及两种实现方式。

Uploaded by

karthikuppuraj
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
330 views36 pages

Algorithm Notes - Dynamic Programming

这篇文档介绍了动态规划的概念和方法。它通过阶乘和爬楼梯问题的例子,说明如何将复杂问题分解成子问题,并通过记忆化搜索避免重复计算,从而提高效率。文中还详细说明了动态规划四个主要步骤:子问题重叠、状态空间、计算顺序和初始值,以及两种实现方式。

Uploaded by

karthikuppuraj
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 36

Dynamic Programming

長江後浪催前浪,一替新人趲舊人。《張協狀元》

資之深,則取之左右逢其原。《孟子》

Dynamic Programming

先透過一個簡單的例子,感受一下動態規劃吧!

範例:階乘( Factorial )

1 × 2 × 3 × ⋯ × N 。整數 1 到 N 的連乘積。 N 階乘。 N! 。

N! 源自 (N-1)! ,如此就遞迴分割問題了。

陣列的每一格對應每一個問題。設定第一格的答案,再以迴圈依序計算其餘答案。

1. const int N = 10;


2. int f[N];
3.
4. void factorial()
5. {
6. f[0] = 0;
7. f[1] = 1;
8. for (int i=2; i<=N; ++i)
9. f[i] = f[i-1] * i;
10. }

1. const int N = 10;


2.
3. void factorial()
4. {
5. int f = 1;
6. for (int i=2; i<=N; ++i)
7. f *= i;
8. }
UVa 623 568 10220 10323

時間複雜度

總共 N 個問題,每個問題花費 O(1) 時間,總共花費 O(N) 時間。

空間複雜度

求 1! 到 N! :總共 N 個問題,用一條 N 格陣列儲存全部問題的答案,空間複雜度為 O(N)


只求 N! :用一個變數累計乘積,空間複雜度為 O(1) 。

Dynamic Programming:
recurrence

Dynamic Programming
= Divide and Conquer + Memoization

動態規劃是分治法的延伸。當遞迴分割出來的問題,一而再、再而三出現,就運用記憶法
儲存這些問題的答案,避免重複求解,以空間換取時間。

動態規劃的過程,就是反覆地讀取數據、計算數據、儲存數據。

1. 把原問題遞迴分割成許多更小的問題。(recurrence)
1-1. 子問題與原問題的求解方式皆類似。(optimal sub-structure)
1-2. 子問題會一而再、再而三的出現。(overlapping sub-problems)
2. 設計計算過程:
2-1. 確認每個問題需要哪些子問題來計算答案。(recurrence)
2-2. 確認總共有哪些問題。(state space)
2-3. 把問題一一對應到表格。(lookup table)
2-4. 決定問題的計算順序。(computational sequence)
2-5. 確認初始值、計算範圍。(initial states / boundary)
3. 實作,主要有兩種方式:
3-1. Top-down
3-2. Bottom-up

1. recurrence

遞迴分割問題時,當子問題與原問題完全相同,只有數值範圍不同,我們稱此現象為
recurrence ,再度出現、一再出現之意。
【註: recursion 和 recurrence ,中文都翻譯為「遞迴」,然而兩者意義大不相同,讀者切
莫混淆。】

此處以爬樓梯問題當作範例。先前於遞歸法章節,已經談過如何求踏法,而此處要談如何
求踏法數目。

踏上第五階,只能從第四階或從第三階踏過去。因此「爬到五階」源自兩個子問題:「爬
到四階」與「爬到三階」。

「爬到五階」的踏法數目,就是總合「爬到四階」與「爬到三階」的踏法數目。寫成數學
式子是「 f(5) = f(4) + f(3) 」,其中「 f(‧) 」表示「爬到某階之踏法數目」。

依樣畫葫蘆,得到「 f(4) = f(3) + f(2) 」、「 f(3) = f(2) + f(1) 」。

「爬到兩階」與「爬到一階」無法再分割、沒有子問題,直接得到「 f(2) = 2 」、「 f(1)


= 1 」。

整理成一道簡明扼要的遞迴公式:

f(n) =
{ 1 , if n = 1
{ 2 , if n = 2
{ f(n-1) + f(n-2) , if n >= 3 and n <= 5

爬到任何一階的踏法數目,都可以藉由這道遞迴公式求得。 n 代入實際數值,遞迴計算即
可。

為什麼分割問題之後,就容易計算答案呢?因為分割問題時,同時也分類了這個問題的所
有可能答案。分類使得答案的規律變得單純,於是更容易求得答案。

2-1. recurrence

f(n) =
{ 1 , if n = 1
{ 2 , if n = 2
{ f(n-1) + f(n-2) , if n >= 3
2-2. state space

想要計算第五階的踏法數目。

全部的問題是「爬到一階」、「爬到二階」、「爬到三階」、「爬到四階」、「爬到五階
」。

至於「爬到零階」、「爬到負一階」、「爬到負二階」以及「爬到六階」、「爬到七階」
沒有必要計算。

2-3. lookup table

建立六格的陣列,儲存五個問題的答案。

表格的第零格不使用,第一格是「爬到一階」的答案,第二格是「爬到二階」的答案,以
此類推。

如果只計算「爬完五階」,也可以建立三個變數交替使用。

2-4. computational sequence

因為每個問題都依賴「階數少一階」、「階數少二階」這兩個問題,所以必須由階數小的
問題開始計算。

計算順序是「爬到一階」、「爬到二階」、 …… 、「爬到五階」。

2-5. initial states / boundary

最先計算的問題是「爬到一階」與「爬到二階」,必須預先將答案填入表格、寫入程式碼
,才能繼續計算其他問題。心算求得「爬到一階」的答案是 1 ,「爬到二階」的答案是 2

最後計算的問題是原問題「爬到五階」。
為了讓表格更順暢、為了讓程式碼更漂亮,可以加入「爬到零階」的答案,對應到表格的
第零格。「爬到零階」的答案,可以運用「爬到一階」的答案與「爬到兩階」的答案,刻
意逆推而得。

最後可以把初始值、尚待計算的部份、不需計算的部分,統整成一道遞迴公式:

f(n) =
{ 0 , if n < 0 [Exterior]
{ 1 , if n = 0 [Initial]
{ 1 , if n = 1 [Initial]
{ f(n-1) + f(n-2) , if n >= 2 and n <= 5 [Compute]
{ 0 , if n > 5 [Exterior]

UVa 11069

3. 實作

直接用遞迴實作,而不使用記憶體儲存各個問題的答案,是最直接的方式,也是最慢的方
式。時間複雜度是 O(f(n)) 。問題一而再、再而三的出現,不斷呼叫同樣的函式求解,效
率不彰。剛接觸 DP 的新手常犯這種錯誤。

1. int f(int n)
2. {
3. if (n == 0 || n == 1)
4. return 1;
5. else
6. return f(n-1) + f(n-2);
7. }

正確的 DP ,是一邊計算,一邊將計算出來的數值存入表格,以後便不必重算。這裡整理
了兩種實作方式,各有優缺點:

1. Top-down
2. Bottom-up

3-1. Top-down

1. int table[6]; // 表格,儲存全部問題的答案。


2. bool solve[6]; // 記錄問題是否已經計算完畢
3.
4. int f(int n)
5. {
6. // [Initial]
7. if (n == 0 || n == 1) return table[n] = 1;
8.
9. // [Compute]
10. // 如果已經計算過,就直接讀取表格的答案。
11. if (solve[n]) return table[n];
12.
13. // 如果不曾計算過,就計算一遍,儲存答案。
14. table[n] = f(n-1) + f(n-2); // 將答案存入表格
15. solve[n] = true; // 已經計算完畢
16. return table[n];
17. }
18.
19. void stairs_climbing()
20. {
21. for (int i=0; i<=5; i++)
22. solve[i] = false;
23.
24. int n;
25. while (cin >> n && (n >= 0 && n <= 5))
26. cout << "爬到" << n << "階," << f(n) << "種踏法";
27. }

1. int table[6]; // 合併 solve 跟 table,簡化程式碼。


2.
3. int f(int n)
4. {
5. // [Initial]
6. if (n == 0 || n == 1) return 1;
7.
8. // [Compute]
9. // 用 0 代表該問題還未計算答案
10. // if (table[n] != 0) return table[n];
11. if (table[n]) return table[n];
12. return table[n] = f(n-1) + f(n-2);
13. }
14.
15. void stairs_climbing()
16. {
17. for (int i=0; i<=5; i++)
18. table[i] = 0;
19.
20. int n;
21. while (cin >> n && (n >= 0 && n <= 5))
22. cout << "爬到" << n << "階," << f(n) << "種踏法";
23. }

這個實作方式的好處是不必斤斤計較計算順序,因為程式碼中的遞迴結構會迫使最小的問
題先被計算。這個實作方式的另一個好處是只計算必要的問題,而不必計算所有可能的問
題。

這個實作方式的壞處是程式碼採用遞迴結構,不斷呼叫函式,執行效率較差。這個實作方
式的另一個壞處是無法自由地控制計算順序,因而無法妥善運用記憶體,浪費了可回收再
利用的記憶體。

UVa 10285 10446 10520

3-2. Bottom-up

訂定一個計算順序,然後由最小的問題開始計算。特色是程式碼通常只有幾個迴圈。這個
實作方式的好處與壞處與前一個方式恰好互補。

首先建立表格。

1. int table[6];

1. int table[5 + 1];

心算「爬到零階」的答案、「爬到一階」的答案,填入表格當中,作為初始值。分別填到
表格的第零格、第一格。

1. table[0] = 1;
2. table[1] = 1;

尚待計算的部份就是「爬到兩階」的答案、 …… 、「爬到五階」的答案。通常是使用迴
圈,按照計算順序來計算。

計算過程的實作方式,有兩種迥異的風格。一種是「往回取值」,是常見的實作方式。
往回取值

1. int table[6];
2.
3. void dynamic_programming()
4. {
5. // [Initial]
6. table[0] = 1;
7. table[1] = 1;
8.
9. // [Compute]
10. for (int i=2; i<=5; i++)
11. table[i] = table[i-1] + table[i-2];
12. }

另一種是「往後補值」,是罕見的實作方式。

往後補值

1. int table[6];
2.
3. void dynamic_programming()
4. {
5. // [Initial]
6. for (int i=0; i<=5; i++) table[i] = 0;
7. table[0] = 1;
8. // table[1] = 1; // 剛好可以被算到
9.
10. // [Compute]
11. for (int i=0; i<=5; i++)
12. {
13. if (i+1 <= 5) table[i+1] += table[i];
14. if (i+2 <= 5) table[i+2] += table[i];
15. }
16. }

計算完畢之後,最後印出答案。

1. void stairs_climbing()
2. {
3. dynamic_programming();
4.
5. int n;
6. while (cin >> n && (n >= 0 && n <= 5))
7. cout << "爬到" << n << "階," << f(n) << "種踏法";
8. }

UVa 495 900 10334

總結

第一。先找到原問題和其子問題們之間的關係,寫出遞迴公式。如此一來,便可利用遞迴
公式,用子子問題的答案,求出子問題的答案;用子問題的答案,求出原問題的答案。

第二。確認可能出現的問題全部總共有哪些,這樣才能知道要計算哪些問題,才能知道總
共花多少時間、多少記憶體。

第三。有了遞迴公式之後,就必須安排出一套計算的順序。大問題的答案,總是以小問題
的答案來求得的,所以,小問題的答案是必須先算的,否則大問題的答案從何而來呢?

一個好的安排方式,不但使程式碼容易撰寫,還可重複利用記憶體空間。

第四。記得先將最小、最先被計算的問題,心算出答案,儲存入表格,內建於程式碼之中
。一道遞迴公式必須擁有初始值,才有辦法計算其他項。

第五。實作 DP 的程式時,會建立一個表格,在表格存入所有大小問題的答案。安排好每
個問題的答案在表格的哪個位置,這樣計算時才能知道該在哪裡取值。

切勿存取超出表格的元素,產生溢位情形,導致答案算錯。計算過程當中,一旦某個問題
的答案出錯,就會如骨牌效應般一個影響一個,造成很難除錯。

範例:不重複組合( Combination )

兩個子問題的組合數目加起來,就是原問題的組合數目。遞迴公式就是著名的巴斯卡公式
( Pascal's Formula ):

c(n, m) =
{ c(n-1, m-1) + c(n-1, m) , if n > 1 and m > 1 and n >= m
{ n , if m = 1
{ 1 , if n = 1

範例:河內塔( Tower of Hanoi )

f(n) =
{ f(n-1) + 1 + f(n-1) , if n > 1
{ 1 , if n = 1

Dynamic Programming:
counting / optimization

範例:樓梯路線( Staircase Walk ),計數問題

一個方格棋盤,從左上角走到右下角,每次只能往右走一格或者往下走一格。請問有幾種
走法?

對於任何一個方格來說,只可能「從左走來」或者「從上走來」,答案是兩者相加。

「從左走來」是一個規模更小的問題,「從上走來」是一個規模更小的問題,答案是兩者
相加。

二維陣列的每一格對應每一個問題。設定第零行、第零列的答案,再以迴圈依序計算其餘
答案。

時間複雜度分析:令 X 和 Y 分別是棋盤的長和寬。計算一個問題需要 O(1) 時間(兩個子


問題答案相加的時間)。總共 XY 個問題,所以計算所有問題需要 O(XY) 時間。

空間複雜度分析:總共 XY 個問題,所以需要 O(XY) 空間,簡單來說就是二維陣列啦!


如果不需要儲存所有問題的答案,只想要得到其中一個特定問題的答案,那只需要一維陣
列就夠了,也就是 O(min(X,Y)) 空間。

1. const int X = 8, Y = 8;
2. int c[X][Y];
3.
4. void staircase_walk()
5. {
6. // [Initial]
7. for (int i=0; i<X; i++) c[i][0] = 1;
8. for (int j=0; j<Y; j++) c[0][j] = 1;
9.
10. // [Compute]
11. for (int i=1; i<X; i++)
12. for (int j=1; j<Y; j++)
13. c[i][j] = c[i-1][j] + c[i][j-1];
14.
15. // 輸出結果
16. cout << "由(0,0)走到(7,7)有" << c[7][7] << 種走法;
17. // cout << "由(0,0)走到(7,7)有" << c[X-1][Y-1] << 種走法;
18.
19. int x, y;
20. while (cin >> x >> y)
21. cout << "由(0,0)走到(x,y)有" << c[x][y] << 種走法;
22. }

節省記憶體是動態規劃當中重要的課題!

如果只打算求出一個問題,那麼只需要儲存最近算出來的問題答案,讓計算過程可以順利
進行就可以了。

兩條陣列輪替使用,就足夠儲存最近算出來的問題答案、避免 c[i-1][j] 超出陣列範圍。

1. const int X = 8, Y = 8;
2. int c[2][Y]; // 兩條陣列,儲存最近算出來的問題答案。
3.
4. void staircase_walk()
5. {
6. // [Initial]
7. for (int j=0; j<Y; ++j) c[0][j] = 1;
8.
9. // [Compute]
10. for (int i=1; i<X; i++)
11. for (int j=1; j<Y; j++)
12. // 只是多了 mod 2,
13. // 外觀看起來就像兩條陣列輪替使用。
14. c[i % 2][j] = c[(i-1) % 2][j] + c[i % 2][j-1];
15.
16. // 輸出結果
17. cout << "由(0,0)走到(7,7)有" << c[7 % 2][7] << 種走法;
18. // cout << "由(0,0)走到(7,7)有" << c[(X-1) % 2][Y-1] << 種走法;
19. }
事實上,一條陣列就夠了。也不能再少了。

1. const int X = 8, Y = 8;
2. int c[Y]; // 一條陣列就夠了
3.
4. void staircase_walk()
5. {
6. // [Initial]
7. for (int j=0; j<Y; ++j) c[j] = 1;
8.
9. // [Compute]
10. for (int i=1; i<X; i++)
11. for (int j=1; j<Y; j++)
12. c[j] += c[j-1];
13.
14. // 輸出結果
15. cout << "由(0,0)走到(7,7)有" << c[7] << 種走法;
16. // cout << "由(0,0)走到(7,7)有" << c[Y-1] << 種走法;
17. }

1. const int X = 8, Y = 8;
2. int c[Y]; // 一條陣列就夠了
3.
4. void staircase_walk()
5. {
6. // [Initial]
7. c[0] = 1; // 部分步驟移到[Compute]
8.
9. // [Compute]
10. for (int i=0; i<X; i++) // 從零開始!
11. for (int j=1; j<Y; j++)
12. c[j] += c[j-1];
13.
14. // 輸出結果
15. cout << "由(0,0)走到(7,7)有" << c[7] << 種走法;
16. // cout << "由(0,0)走到(7,7)有" << c[Y-1] << 種走法;
17. }

如果某些格子上有障礙物呢?把此格設為零。

如果也可以往右下斜角走呢?添加來源 c[i-1][j-1] 。
如果可以往上下左右走呢?不斷繞圈子,永遠不會結束,走法無限多種。

UVa 10599 825 926 ICPC 4787

遞迴公式

若瞭解遞迴關係,就不必強記遞迴公式。若瞭解圖片意義,就不必強記數學符號。

count(i, j) =
{ 0 , if i < 0 or j < 0 [Exterior]
{ 1 , if i = 0 [Initial]
{ 1 , if j = 0 [Initial]
{ count(i-1, j) + count(i, j-1) , if i > 0 and i < 8 [Compute]
{ and j > 0 and j < 8 [Compute]
{ 0 , if i >= 8 or j >= 8 [Exterior]

count(i, j):從格子 (0, 0) 走到格子 (i, j) 的走法數目。

遞歸方向

這個問題雙向都可以遞歸。對於任何一個方格來說,只可能「向右走出」或者「向下走出
」。

範例:樓梯路線( Staircase Walk ),極值問題

動態規劃的問題,可以分為「計數問題」和「極值問題」。方才介紹「計數問題」,現在
介紹「極值問題」。

一個方格棋盤,格子擁有數字。從左上角走到右下角,每次只能往右走一格或者往下走一
格。請問總和最小的走法?(或者總和最大的走法?)

1. const int X = 8, Y = 8;
2. int a[X][Y];
3. int c[X][Y];
4.
5. void staircase_walk()
6. {
7. // [Initial]
8. c[0][0] = a[0][0];
9. for (int i=1; i<X; i++)
10. c[i][0] = c[i-1][0] + a[i][0];
11. for (int j=1; j<Y; j++)
12. c[0][j] = c[0][j-1] + a[0][j];
13.
14. // [Compute]
15. for (int i=1; i<X; i++)
16. for (int j=1; j<Y; j++)
17. c[i][j] = max(c[i-1][j], c[i][j-1]) + a[i][j];
18.
19. // 輸出結果
20. cout << "由(0,0)走到(7,7)的最小總和" << c[7][7];
21. // cout << "由(0,0)走到(7,7)的最小總和" << c[X-1][Y-1];
22.
23. int x, y;
24. while (cin >> x >> y)
25. cout << "由(0,0)走到(x,y)的最小總和" << c[x][y];
26. }

想要印出路線,另外用一個陣列,記錄從哪走來。

1. const int X = 8, Y = 8;
2. int a[X][Y];
3. int c[X][Y];
4. int p[X][Y];
5. int out[X+Y-1];
6.
7. void staircase_walk()
8. {
9. // [Initial]
10. c[0][0] = a[0][0];
11. p[0][0] = -1; // 沒有源頭
12. for (int i=1; i<X; i++)
13. {
14. c[i][0] = c[i-1][0] + a[i][0];
15. p[i][0] = 0; // 從上走來
16. }
17. for (int j=1; j<Y; j++)
18. {
19. c[0][j] = c[0][j-1] + a[0][j];
20. p[0][j] = 1; // 從左走來
21. }
22.
23. // [Compute]
24. for (int i=1; i<X; i++)
25. for (int j=1; j<Y; j++)
26. if (c[i-1][j] < c[i][j-1])
27. {
28. c[i][j] = c[i-1][j] + a[i][j];
29. p[i][j] = 0; // 從上走來
30. }
31. else if (c[i-1][j] > c[i][j-1])
32. {
33. c[i][j] = c[i][j-1] + a[i][j];
34. p[i][j] = 1; // 從左走來
35. }
36. else /*if (c[i-1][j] == c[i][j-1])*/
37. {
38. // 從上走來、從左走來都可以,這裡取左。
39. c[i][j] = c[i][j-1] + a[i][j];
40. p[i][j] = 1;
41. }
42.
43. // 反向追蹤路線源頭
44. int n = 0; // out size
45. for (int i=X-1, j=Y-1; i>=0 && j>=0; )
46. {
47. out[n++] = p[i][j];
48. if (p[i][j] == 0) i--;
49. else if (p[i][j] == 1) j--;
50. }
51.
52. // 印出路線
53. for (int i=n-1; i>=0; --i)
54. cout << out[i];
55. }

額外介紹一個技巧。為了避免減一超出邊界,需要添補許多程式碼。整個棋盤往右下移動
一格,就能精簡許多程式碼。

1. const int X = 8, Y = 8;
2. int a[X+1][Y+1]; // 整個棋盤往右往下移動一格
3. int c[X+1][Y+1]; // 全域變數,將自動初始化為零。
4. int p[X+1][Y+1];
5. int out[X+Y-1];
6.
7. void staircase_walk()
8. {
9. // [Initial]
10.
11. // [Compute]
12. for (int i=1; i<=X; i++)
13. for (int j=1; j<=Y; j++)
14. if (c[i-1][j] < c[i][j-1])
15. {
16. c[i][j] = c[i-1][j] + a[i][j];
17. p[i][j] = 0; // 從上走來
18. }
19. else /*if (c[i-1][j] >= c[i][j-1])*/
20. {
21. c[i][j] = c[i][j-1] + a[i][j];
22. p[i][j] = 1; // 從左走來
23. }
24.
25. // 反向追蹤路線源頭
26. int n = 0; // out size
27. for (int i=X, j=Y; i>0 && j>0; )
28. {
29. out[n++] = p[i][j];
30. if (p[i][j] == 0) i--;
31. else if (p[i][j] == 1) j--;
32. }
33.
34. // 印出路線
35. for (int i=n-1; i>=0; --i)
36. cout << out[i];
37. }

1. const int X = 8, Y = 8;
2. int a[X+1][Y+1]; // 整個棋盤往右往下移動一格
3. int c[X+1][Y+1]; // 全域變數,將自動初始化為零。
4. int p[X+1][Y+1];
5. int out[X+Y-1];
6.
7. void staircase_walk()
8. {
9. // [Initial]
10.
11. // [Compute]
12. const int x[2] = {1, 0};
13. const int y[2] = {0, 1};
14.
15. for (int i=1; i<=X; i++)
16. for (int j=1; j<=Y; j++)
17. {
18. if (c[i-1][j] < c[i][j-1]) p[i][j] = 0;
19. else p[i][j] = 1;
20.
21. int& d = p[i][j];
22. c[i][j] = c[i-x[d]][j-y[d]] + a[i][j];
23. }
24.
25. // 反向追蹤路線源頭
26. int n = 0; // out size
27. for (int i=X, j=Y; i>0 && j>0; )
28. {
29. int& d = p[i][j];
30. out[n++] = d;
31. i -= x[d]; j -= x[d];
32. }
33.
34. // 印出路線
35. for (int i=n-1; i>=0; --i)
36. cout << out[i];
37. }

範例:樓梯路線( Staircase Walk ),極值問題

節省記憶體是動態規劃當中重要的課題!

方才的分割方式:分割最後一步,窮舉最後一步從哪走來;方才的實作方式:由小到大的
迴圈。問題答案 c[i][j] ,可以精簡成一維陣列。路線來源 p[i][j] ,無法精簡成一維陣列。

想讓路線來源精簡成一維陣列,必須採用另一種分割方式:從地圖中線分割,窮舉穿過中
線的所有地點;同時採用另一種實作方式:由大到小的遞迴。

Dynamic Programming:
state / DAG
( Under Construction! )

State / DAG

以 State 和 DAG 的觀點,重新看待動態規劃。


動態規劃得類比成「狀態 State 」:「問題」變「狀態」,「全部問題」變「狀態空間」
,「遞迴關係」變「狀態轉移函式」。

動態規劃得類比成「有向無環圖 DAG 」:既然遞迴關係不能循環,顯然就是 DAG 。「


問題」變「點」,「遞迴關係」變「邊」,「計算順序」變「拓撲順序」。

即便讀者不懂 State 和 DAG 也沒關係,只要抓住兩個要點:每個小問題各是一個狀態,


只有數值範圍不同;狀態之間是單行道,依序求解,不能循環。

ICPC 5104

範例

http://algorithms.tutorialhorizon.com/

Maximum Subarray
1D p-Center Problem
Longest Increasing Subsequence
Longest Common Subsequence
Longest Palindrome Substring
0/1 Knapsack Problem
Shortest Path

範例:巴斯卡三角形( Pascal's Triangle )

巴斯卡三角形左右對稱,可以精簡掉對稱部分。巴斯卡三角形逆時針轉 45˚ ,視覺上就可


以一一對應至表格。

時間複雜度為 O(N²) ,空間複雜度為 O(N²) 。

UVa 369 485 10564

範例:矩陣相乘次序( Matrix Chain Multiplication )

一連串矩陣相乘,無論從何處開始相乘,計算結果都一樣,然而計算時間卻有差異。兩個
矩陣,大小為 a x b 及 b x c ,相乘需要 O(abc) 時間(當然還可以更快,但是此處不討論
)。那麼一連串矩陣相乘,最少需要多少時間呢?
一連串矩陣,從最後一次相乘的地方分開,化作兩串矩陣相乘。考慮所有可能的分法。

f(i, k) = min { f(i, j) + f(j+1, k) + r[i] ⋅ c[j] ⋅ c[k] }


i≤j< k

f(i, k):從第 i 個矩陣乘到第 k 個矩陣,最少的相乘次數。


r[i]:第 i 個矩陣的 row 數目。
c[i]:第 i 個矩陣的 column 數目。

1. int f[100][100];
2. int r[100], c[100];
3.
4. void matrix_chain_multiplication()
5. {
6. memset(array, 0x7f, sizeof(array));
7. for (int i=0; i<N; ++i)
8. array[i][i] = 0;
9.
10. for (int l=1; l<N; ++l)
11. for (int i=0; i+l<N; ++i)
12. {
13. int k = i + l;
14. for (int j=i; j<k; ++j)
15. f[i][k] = min(f[i][k], f[i][j] + f[j+1][k] + r[i] * c[j] * c[k]);
16. }
17. }

可以調整成 online 版本。

1. for (int k=1; k<N; ++k)


2. for (int i=k-1; i>=0; --i)
3. for (int j=k-1; j>=i; --j)
4. // for (int j=i; j<k; ++j)
5. f[i][k] = min(f[i][k], f[i][j] + f[j+1][k] + r[i] * c[j] * c[k]);

同類型的動態規劃問題:

Matrix Chain Multiplication


Optimal Binary Search Tree
Hu-Tucker Compression
Minimum Weight Triangulation of Convex Polygon
Cocke-Younger-Kasami Algorithm
UVa 348 442

範例: Longest Increasing Subsequence

把解答編入狀態之中。

詳見「 Longest Increasing Subsequence 」。

範例: Weighted Interval Scheduling Problem

有了權重之後 greedy 就不管用了。

範例: Word Wrap

一大段英文,適度換行,讓文字不超過紙張邊界,美化版面。

窮舉行數,再窮舉一行擠入多少字數。自行定義留白的代價。

UVa 709 848 400

範例: Bitonic Euclidean TSP

ICPC 4791

範例:二進位數字

ICPC 4833 5101

範例: Sequence Combination 【尚無正式名稱】

逐步消去一連串同色彩珠,找到步驟最少的消除方式。

UVa 10559 11523

範例:節省記憶體

ICPC 6435

範例: ???

Problem J: Subway Timing


http://www.csc.kth.se/~austrin/icpc/finals2009solutions.pdf
ICPC 4454

http://codeforces.com/blog/entry/13007

ICPC 6669

Dynamic Programming:
bitset

bitset

bitset 是一個二進位數字,每一個 bit 分別代表每一件東西, 1 代表開啟, 0 代表關閉。例


如現在有十個燈泡,編號設定為零到九,其中第零個、第三個、第九個燈泡是亮的,剩下
來的燈泡是暗的。我們用一個 10 bit 的二進位數字 1000001001 ,表示這十個燈泡的亮暗
狀態。

建立一個大小為 2¹⁰ 的陣列,便囊括了所有可能的狀態。陣列的每一格,就代表一種燈泡


開關的狀態。

1. int array[1<<10];
2. array[521] = 想記錄的數字;
3. /* 1000001001(2 進位) = 521(10 進位) */

當狀態數量呈指數成長,可以利用 bitset 作為狀態。

UVa 10952 ICPC 4794

範例: Maximum Matching

以線相連的兩物,可以配對在一起。求最大配對數目暨配對方式。

「 Maximum Matching 」有多項式時間演算法,可是很難實作;動態規劃雖然慢了些,是


指數時間演算法,但是容易實作。移除匹配成一對的點,就得到遞迴公式。

f[S+{i}+{j}] = max { f[S] + adj[i,j] } i,j∉S


f[S] = max { f[S-{i}-{j}] + adj[i,j] } i,j∉S
使用 bitset ,已匹配標成 1 ,未匹配標成 0 。時間複雜度為 O(2ᴺ N²) ,空間複雜度為
O(2ᴺ) 。

這個方法需要大量記憶體,所以無法計算 N 很大的情況,何況編譯器也不准我們建立太
大的陣列, N=28 就是極限了。這個方法同時也需要大量時間,以現在的個人電腦來說,
N=17 就已經要花上幾分鐘才能求出答案了。

1. // top-down DP
2. const int N = 10;
3. int adj[N][N]; // adjacency matrix。連線為 1,否則為 0。
4. int dp[1<<N]; // dp table
5. bool ok[1<<N]; // dp table 是否已存值
6. //int p[1<<N][2]; // 記錄匹配方式,此處省略。
7.
8. bool f(int s)
9. {
10. if (s == 0) return true;
11. if (ok[s]) return dp[s];
12.
13. for (int i=0; i<N; ++i)
14. for (int j=i+1; j<N; ++j)
15. if (s & ((1<<i) | (1<<j)))
16. {
17. // ss = s - {i} - {j};
18. int ss = s ^ (1<<i) ^ (1<<j);
19. dp[s] = max(dp[s], f(ss) + adj[i][j]);
20. }
21.
22. ok[s] = true;
23. return dp[s];
24. }
25.
26. int maximum_matching()
27. {
28. memset(dp, 0, sizeof(dp));
29. memset(ok, false, sizeof(ok));
30. return f((1<<N)-1);
31. }

這個演算法可以再修正,讓時間複雜度成為 O(2ᴺ N) 。各位可以試試看。

UVa 10888 10911 11439 10296 11156

範例: Hamilton Path


找到一條路徑,剛好每一個點都去過一次。有可能找不到。

「 Hamilton Path 」尚無多項式時間演算法。直覺的解法是 backtracking ,窮舉所有點的各


種排列方式,一種排列方式當作一條路徑,判斷是不是 Hamilton Path 。

運用動態規劃,可以減少計算時間。拆掉一條路徑的最後一條邊,就得到遞迴公式。需要
額外維度,記錄路徑終點。

path[S+{j},j] = or_all { path[S,i] && adj[i,j] } i∈S, j∉S


path[S,j] = or_all { path[S-{j},i] && adj[i,j] } i∈S, j∉S

時間複雜度為 O(2ᴺ N²) ,空間複雜度為 O(2ᴺ) 。

1. const int N = 10;


2. bool adj[N][N]; // adjacency matrix
3. bool dp[1<<N][N]; // dp table
4. bool ok[1<<N][N]; // dp table 是否已存值
5.
6. bool path(int s, int s_size, int j)
7. {
8. if (s_size == 1) return true; // s 只有一個位元是 1
9. if (ok[s][j]) return dp[s][j];
10. ok[s][j] = true;
11.
12. for (int i=0; i<N; ++i)
13. if (i != j && s & (1<<i))
14. if (path(s ^ (1<<j), s_size-1, i) && adj[i][j])
15. return dp[s][j] = true;
16.
17. return dp[s][j] = false;
18. }
19.
20. bool Hamilton_Path()
21. {
22. memset(dp, false, sizeof(dp));
23. memset(ok, false, sizeof(ok));
24.
25. for (int i=0; i<N; ++i)
26. if (path((1<<N)-1, N, i))
27. return true;
28. return false;
29. }

UVa 216 10068 10496 10818 10937 10944 10605 10890 265

範例:不重複路線( Self-avoiding Walk )

先前介紹過樓梯路線( Staircase Walk )。樓梯路線問題,只能往兩個方向走,可以簡單


的遞迴分割,得到多項式時間演算法。不重複路線問題,可以往四個方向走,無法簡單的
遞迴分割,只有指數時間演算法。儘管如此,不重複路線還是可以使用動態規劃。

http://blog.sina.com.cn/s/blog_51cea4040100gmky.html

中文網路稱為「插头 DP 」或「轮廓线 DP 」。

UVa 10572 10531 ICPC 4789 4793 Timus 1519

範例: Domino Tiling

https://github.com/indy256/olymp-docs/raw/master/dp2.pdf

http://www.math.ntu.edu.tw/~shing_tung/PDF/4th/04Jiang.pdf

UVa 11741

Dynamic Programming:
stack / deque

概論

F[n] = min/max { F[i] ⋅ W[i] + C[i] }


0<=i< n

F[n] 是未知數, F[i] W[i] C[i] 是已知數。計算到 F[n] 時, F[i] 早已計算完畢,因此 F[i]
是已知數。

這種形式的 recurrence ,直接計算是 O(N²) 。此處介紹更快的演算法。

stack

括號配對極值。 stack 保持嚴格遞增(嚴格遞減),以便即時獲取過往最大值(最小值)


、即時移除已處理數值。

maximize problem
keep monotone increasing ---->
---------------------------------------
stack | F[2] | F[4] | F[6] | ... | F[10]
---------------------------------------
^^^^^^^ extract maximum
clean tops until monotonicity
then push new F[i] at top

F[i] 放入尾端。放入前,先清除尾端數值,使得 F[i] 放入尾端之後, stack 呈嚴格遞增。


最大值從尾端取得。

範例: Largest Empty Rectangle

詳見「 Largest Empty Rectangle 」。

範例: All Nearest Smaller Values

http://en.wikipedia.org/wiki/All_nearest_smaller_values

deque

滑動視窗極值。 deque 保持嚴格遞增(嚴格遞減),以便即時獲取過往最小值(最大值)


、即時移除已處理數值。

minimize problem
keep monotone increasing ---->
--------------------------------------
deque F[2] | F[4] | F[6] | ... | F[10]
--------------------------------------
^^^^^^ ^^^^^^^
extract minimum clean tails until monotonicity
then push new F[i] at end

F[i] 放入尾端。放入前,先清除尾端數值,使得 F[i] 放入尾端之後, deque 呈嚴格遞增。


放入後,再清除頭端數值,使得元素個數符合滑動視窗大小。最小值從頭端取得。

中文網路稱為「单调队列优化」。

ICPC 4327

範例: Maxium Sum Subarray

詳見「 Maxium Sum Subarray 」。

範例: Maximum Average Subarray

詳見「 Maxium Average Subarray 」。

由於斜率是關鍵,因此中文網路稱為「斜率优化」。
Dynamic Programming:
convex hull

概論

F[n] = min/max { F[i] ⋅ W[n] + C[i] }


0<=i< n

W[i] 換成 W[n] 。前章節 W[i] 不隨時間 n 而變化,但是各有不同;本章節 W[n] 隨著時間


n 而變化,但是一律相同。

envelope

直線 y = F[i] x + C[i] 、鉛直線 x = W[n] ,交點 Y 座標是 F[n] 。

最小(大)值對應最低(高)交點。

最小(大)值位於下(上)包絡線。

convex hull

如果討厭包絡線,可透過點線對偶,從包絡線變成凸包。

直線穿過點 (F[i], -C[i]) ,斜率 W[n] , Y 軸截距是 -F[n] 。

垂直方向翻轉,讓最小(大)值對應最低(高)截距。

直線穿過點 (F[i], C[i]) ,斜率 -W[n] , Y 軸截距是 F[n] 。

最小(大)值位於下(上)凸包的切線,跟 Y 軸的交點。

如果不熟悉點線對偶,可透過移項推導,得到相同結論:

http://www.cnblogs.com/Rlemon/p/3184899.html
According to the nature of F[i] and W[n], the time complexity changes accordingly:

First, W[n] is the same: you don't need to maintain the convex hull, just maintain the order of
each point in the "vertical direction of the line of slope -W[n]", as if "monotonic queue
optimization". A common example is W[n] = 1 . O(N).

Second, F[i] and W[n] are monotonous: Andrew's monotone chain maintains the convex hull
(when the minimum is found). The tangent slope -W[n] is decremented/increased. Start from the
last cut point and find the new cut point to the left/right. O(N).

Third, F[i] Monotony: Andrew's monotone chain maintains the convex hull (when the minimum
is found). The tangent slope -W[n] will change. The three-point search finds the tangent point, or
the binary search for the convex hull slope finds the tangent point. O(NlogN).

Fourth, no special nature: dynamic convex hull data structure. O(NlogNlogN).

1. Http : //blog.csdn.net/madaidao/article/details/40823207

UVa 12524 ICPC 5133

Example: 1D p-Median Problem

See " p-Median Problem " for details .

Https://algnotes.wordpress.com/2013/10/25/p-median/

Example: Bounded Knapsack Problem

See " Bounded Knapsack Problem " for details .

Dynamic Programming:
unimodal function
Introduction

F[n] = min/max { M[i] } M[i] is monotone/unimodal


0<=i< n

According to the nature of M[i], the time complexity changes accordingly:

First, the sub-problems of F[n], the answer M[0]...M[n-1] happens to be a monotonic function:
there is no good calculation at all, and the best solution is obviously the first (last) child.
problem. O(N).

Second, the sub-problems of F[n], the answer M[0]...M[n-1] happens to be a unimodal function:
a three-point search for a mountain, or a binary search slope. O(NlogN).

Third, the unimodal function of each problem, the mountain position just increases (moving to
the right): use the same scan line to find the mountain. O(N).

Unimodal function

When will it be a unimodal function? Give two examples.

F[n] = min { max(F[i], G[i]) + 5 }


0<=i< n

When F is incremented and G is decremented, max(F[i], G[i]) + 5 is a unimodal function. But


this example is a bit stupid, and the valley just doesn't move forever.

F[n] = min { F[i] + G[i] }


0<=i< n

FG is a convex function, then F[i] + G[i] is a convex function, which is a unimodal function. But
this example is also a bit stupid, the valley just happens to never move.

Example: Egg Drop

A pile of eggs, the known endurance is the same, I do not know how much stamina. Try to
endure.

Endurance is measured by floor: larger than a certain floor, it must be broken and cannot be
reused; less than or equal to a certain floor, it will not break if it falls, it will not be damaged at
all, and it can be reused.
There are many variations on this issue, and here is the case where the experimental site is n-
floor:

1. At least a few eggs are broken? Need to prepare a few eggs in advance?

A: An egg. From the first floor, fall down and gradually go upstairs until it breaks.

Second, an infinite number of eggs, when the luck is not good, at least a few times?

A: Two-point search. F[0] = 0, F[1] = 1, F[n] = F[ceil((n-1)/2)] + 1 .

Third, an egg, when the luck is not good, at least a few times?

A: n times. The endurance is unfortunately the n floor, starting from the first floor, to fall n
times.

Four or two eggs, when the luck is not good, at least a few times?

A: The starting lineup is in the i building. If it breaks, there is an egg left, so I have to start
testing from the first floor; in the worst case, the endurance is i-1 floor, I have to fall i-1 times. If
it is not broken, it is still two eggs, the problem is still the same, the scope is reduced to ni floor.
In both cases, the maximum value is taken. Exhaustive i, the least number of people found.

F[n] = 1 + min { max(i-1, F[ni]) }


1<=i<=n
F[n] = 1 + min { max(i, F[n-1-i]) } Adjust the index value
0<=i< n

i is incremented, F[n-1-i] is decremented, so max(i, F[n-1-i]) is a unimodal function, and the


valley continues to move to the right, which can be solved using scan lines. There are also
mathematical formula solutions:

https://www.ptt.cc/bbs/Prob_Solve/M.1398152375.A.C05.html

Five, k eggs, when the luck is not good, at least a few times?

A: Leave it to the reader.

UVa 10934 882 ICPC 4554

Example: Isotonic Regression

Https://algnotes.wordpress.com/2015/01/28/isotonic-regression/

Http://stackoverflow.com/questions/10460861/
Dynamic Programming:
totally monotone matrix

Introduction

F[n] = min/max { M[i][n] } M[i][n] is monotone/totally monotone


0<=i< n

According to the nature of M[i][n], the time complexity changes accordingly:

First, M[i][n] is the upper triangle monotone matrix: there seems to be no particularly fast
algorithm, still O(N2).

Second, M[i][n] is the upper triangular totaltone matrix: two algorithms, O(NlogN) and
O(Nα(N)). The following is only the first one, divided into two cases.

Third, M[i][n] is the upper triangle Monge matrix: There is still no proprietary algorithm, and
everyone is following the algorithm of totally monotone matrix.

The Monge matrix is more common. The common form is:

F[n] = min/max { F[i] ⋅ W[i] + C[i][n] }


0<=i< n

(1) F[i] is non-negative


(2) W[i] is non-negative
(3) C[i][n] is Monge
Thus M[i][n] = F[i] ⋅ W[i] + C[i][n] is Monge (non-negative linearity)

Interval view: This chapter is the tail end interval C[i,n], and the previous chapter is the head end
interval C[0,i] (omitted 0).

Time-varying point of view: This section C[i][n] changes with time n (C[i][n] is rewritten as
C[n][i] ), and the previous section C[i] does not change with time n .

The Monge matrix's inequality used to be called quadrangle inequality, so the Chinese network is
called quadrilateral inequality optimization.

Convex totally monotone matrix

General: The minimum position of the straight bar is decremented upwards. Use stack.

First draw C[i][n] , an upper triangular matrix. The picture omits the actual value.

Calculate F[1] : F[0] ⋅ W[0] is added to C[0][1].


Calculate F[2] : F[0] ⋅ W[0] is added to C[0][2] , F[1] ⋅ W[1] is added to C[1][2], and straight C[:
][2] Minimum value.

And so on. The time complexity is O(N2).

Re-reflecting the calculation process and improving the algorithm:

1. After F[i] is calculated, F[i] ⋅ W[i] is added to the corresponding horizontal bar, which is more
convenient. Since F[i] ⋅ W[i] is non-negative, the result is still a convex Monge matrix, which is
also a convex monotone matrix!

Second, calculate F[i], that is, take the minimum value of the straight bar. Record the minimum
position of each straight bar at any time, and add F[i] ⋅ W[i] to the corresponding horizontal bar
at any time to update the minimum position.

Get an algorithm that is easier to interpret. The time complexity is still O(N2).

Convex monotone matrix (straight version), each submatrix is a convex monotone matrix, the
minimum position of the straight bar is always decreasing upwards!

After F[i] is calculated, the minimum position is updated from left to right. The minimum
position may become i and may not change. There is only one dividing line between change and
invariance, and the left side is always changed to i, and the right side is always unchanged to
satisfy the decreasing nature.

Although it is possible to break early, the time complexity is still O(N2).

Those with the same position are combined into one interval and up to N intervals. Whenever the
minimum position is updated, the right end position of each interval is judged from left to right
until a constant position is encountered. Then search for the interval and find the dividing line.

Using stack implementation, a data has two parameters: the straight bar number at the right end
of the interval, and the minimum position. Pop some intervals per round, do a binary search,
push an interval. The time complexity is O(NlogN).

Concave totally monotone matrix

Intention: The minimum position of the straight bar is incremented. Use deque.
Change to update the minimum position from right to left. The time complexity is O(NlogN).

Example: Word Wrap

Https://algnotes.wordpress.com/2013/10/26/word-wrap/

Dynamic Programming:
interval

Knuth's optimization

F[i,j] = min { F[i,k] + F[k,j] + C[i,j] }


i<=k<=j

C[i,j] satisfies "monge matrix" and "sorted matrix".


(upper trianglar) (toward ↗)

Interval coverage: C[a,b] <= C[c,d] when [a,b] ⊆ [c,d]


Interval coverage: C[a,b] <= C[c,d] when c<=a<=b<=d
Sorted matrix: ← <= → and ↓ <= ↑
(toward ↗)

Quadrilateral inequalities, resulting in a monotonic left boundary. The matrix has been sorted,
resulting in a monotonic right border.

The questions are sorted by size, and the best segmentation points for the problem are exactly the
same. The Chinese network is called "decision monotony."

http://www.quora.com/What-is-Knuths-optimization-in-dynamic-programming

Example: Optimal Binary Search Tree

N pen data, want to create a " Binary Search Tree ". And predicted the number of searches for
each data.

What is the shape of the Binary Search Tree in order to minimize the number of nodes visited? In
other words, the "depth" of each node is multiplied by the number of "search times", and the sum
is the smallest.

The recursive formula is similar to Matrix Chain Multiplication and is a record interval.
Exhausted the roots, divided into two subtrees and left. The sub-problems are a total of O(N2),
and a sub-problem is exhaustive of O(N) kinds of division points, so the time complexity is
O(N3).

1. // Add one space to the left and right of the array boundary to save the trouble of judging the
boundaries of the array.
2. Int freq [ 8 + 2 ] = { 0 , 4 , 2 , 1 , 2 , 3 , 1 , 2 , 1 };
3.
4. Int pre [ 8 + 2 ]; // cumulative sum
5. Int c [ 8 + 2 ][ 8 + 2 ]; // Implement the array used by DP.
6. Int p [ 8 + 2 ][ 8 + 2 ]; // Record the root of the tree, which is the split point.
7.
8. // interval and
9. Int sum ( int i , int j ) { return pre [ j ] - pre [ i - 1 ];}
10.
11. Void optimal_binary_search_tree ()
12. {
13. For ( int i = 1 ; i <= 8 ; ++ i )
14. Pre [ i ] = pre [ i - 1 ] + freq [ i ];
15.
16. For ( int i = 1 ; i <= 8 ; ++ i )
17. {
18. Dp [ i ][ i ] = freq [ i ];
19. p [ i ][ i ] = i ;
20. }
21.
22. / / Calculation order is online version
23. For ( int k = 2 ; k <= 8 ; ++ k ) // interval end point
24. For ( int i = k - 1 ; i >= 1 ; -- i ) // starting point of the interval
25. {
26. c [ i ][ k ] = 1e9 ;
27. For ( int j = i ; j <= k ; ++ j ) // split point
28. If ( c [ i ][ j - 1 ] + c [ j + 1 ][ k ] + sum ( i , k ) < c [ i ][ k ])
29. {
30. c [ i ][ k ] = c [ i ][ j - 1 ] + c [ j + 1 ][ k ] + sum ( i , k )
31. p [ i ][ k ] = j ;
32. }
33. }
34.
35. Cout << "Number of nodes visited in total" << c [ 1 ][ 8 ];
36. }
Since the sum(i,k) of the second layer loop maintains the fixed value, it does not affect the
judgment result of the maximum value, so it can be moved outside the loop, reducing the number
of additions and reducing the execution time.

1. For ( int k = 2 ; k <= 8 ; ++ k ) // interval end point


2. For ( int i = k - 1 ; i >= 1 ; -- i ) // starting point of the interval
3. {
4. c [ i ][ k ] = 1e9 ;
5. For ( int j = i ; j <= k ; ++ j ) // split point
6. If ( c [ i ][ j - 1 ] + c [ j + 1 ][ k ] < c [ i ][ k ])
7. {
8. c [ i ][ k ] = c [ i ][ j - 1 ] + c [ j + 1 ][ k ];
9. p [ i ][ k ] = j ;
10. }
11. // Move outside the circle and add it at the end.
12. c [ i ][ k ] += sum ( i , k );
13. }

A total of O(N2) sub-problems, each sub-question must exhaust O(N) split points, so the time
complexity is O(N3).

It is no different from ordinary Dynamic Programming. The next step is to go further.

Every time you calculate a sub-question, you always have to exhaust all the split points.
However, some of the segmentation points are obviously wrong, especially those that are close to
the boundary of the interval. It is really unlikely that the two subtrees will be evenly divided and
the sum will be minimized.

Similar sub-problems, the division points are also very similar. The sub-question [a,b] attempts
to remove a piece of data from the right end into a sub-question [a, b-1]; the sub-questions [a, b],
[a, b-1] have similar points.

In order to make the left and right subtrees uniform, the division point of [a,b] must be greater
than or equal to the division point of [a,b-1] to reduce the sum. Less than the division point of [a,
b-1], there is no need for exhaustiveness, the tree will only be more unbalanced, the sum will
only be bigger and not smaller!

The subproblem [a+1,b] is also very similar and will not be repeated.

That is to say, the division point of the sub-problem [a, b] must be located between the division
points of the smaller sub-problems [a, b-1] and [a+1, b]. To calculate a sub-problem, you don't
have to exhaust all the split points.
1. For ( int j = p [ i ][ k - 1 ]; j <= p [ i + 1 ][ k ]; ++ j ) // split point

Observe the split point table, [a, b-1] is the left square, and [a+1, b] is the lower square. To
calculate a split point, the exhaustive range is the value of the left square to the value of the
lower grid. That is to say, each grid will be greater than or equal to the left square and less than
or equal to the lower square.

For each upper left and lower right slash, the upper left minimum is 0, the lower right maximum
is N-1, and each diagonal is at most 2N = O(N) division points.

In addition to the initial values, a total of N-1 slashes require an exhaustive number of division
points totaling O(N2), so the time complexity is reduced to O(N2).

The UVa 10304 10003 12057 12809 ICPC 7464

Example: 1D p-Center Problem

See " p-Center Problem " for details .

Hu's optimization

F[i,j] = min { F[i,k] + F[k,j] + C[i]⋅C[j]⋅C[k] }


i<=k<=j

Matrix Chain Multiplication can be accelerated to O(NlogN).

I have not studied.

references

https://www.ptt.cc/bbs/Prob_Solve/M.1458211168.A.907.html

[slope]
Using geometric techniques to improve dynamic programming algorithms
For the economic lot-sizing problem and extensions

[totally monotone matrix 1]


Dynamic programming with convexity, concavity and sparsity

[totally monotone matrix 2]


An almost linear time algorithm for generalized matrix searching

[Knuth's optimization]
Optimum binary search trees
[Hu's optimization]
Computation of matrix chain products, part I, part II
◀ Index

Dynamic Programming

Dynamic Programming:
recurrence

Dynamic Programming:
counting / optimization

Dynamic Programming:
state / DAG
(Under Construction!)

Dynamic Programming:
bitset

Dynamic Programming:
stack / deque

Dynamic Programming:
convex hull

Dynamic Programming:
unimodal function

Dynamic Programming:
totally monotone matrix

Dynamic Programming:
interval

Original text
Contribute a better translation

You might also like