# 演算法課程題解 - 動態規劃\: 最大連續和 # UVa 10684 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10684 在一個賭場當中有個人在研究賭客的輸贏情形 已知每個賭局最大可以贏得多少錢,以正數與負數表示表示贏錢與輸錢 請問在連續的賭局當中最多可以贏得多少錢 ## 想法 By Koios 題目的意思等價於求一個序列中子序列的最大連續和 那麼每個數字有兩種情況,一種是被包含在一個已存在的連續序列當中; 另一種是它是新的連續子序列的開頭 定義 $dp[i][j]$ 表示第 $i$ 項在兩種情況當中前 $i$ 項的最大連續和 其中 $j=0$ 表示第 $i$ 項包含在已存在的連續序列當中 而 $j=1$ 表示第 $i$ 項維新的連續序列的開頭 則有轉移式 $$ \begin{cases} dp[i][0] = max(dp[i-1][0], dp[i-1][1]) + arr[i] \\ dp[i][1] = arr[i] \end{cases} $$ 並且 $$ \begin{cases} dp[0][0] = 0 \\ dp[0][1] = arr[0] \end{cases} $$ 可以發現到每項 $dp$ 值都只跟前一項有關,並且每次每項 $dp$ 值都會完全被覆蓋,所以可以使用模運算優化記憶體使用量 同時,每項 $arr$ 都只會使用一次,所以不需要陣列,可以改用一個變數取代,在輸入同時計算 $dp$ 值 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int n, ans, m, dp[2][2]; int main(){ while(cin>>n && n){ ans = 0; dp[0][0] = dp[0][1] = dp[1][0] = dp[1][1] = 0; for(int i=0 ; i<n ; i++){ cin>>m; if(i == 0) dp[0][1] = m; else{ dp[i%2][0] = max(dp[(i-1)%2][0], dp[(i-1)%2][1]) + m; dp[i%2][1] = m; } ans = max(ans, max(dp[i%2][0], dp[i%2][1])); } if(ans == 0){ cout<<"Losing streak.\n"; } else{ cout<<"The maximum winning streak is "<<ans<<".\n"; } } return 0; } ``` ### 時間複雜度分析 每筆測資時間複雜度為 $O(n)$ # UVa 108 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?108 給一個 $n \times n$ 的序列,請找出最大子區域和 一個子區域是在 $n \times n$ 序列中任意大小的矩形 子區域合的定義為該區域內所有元素的和 ## 想法 By Koios 如果我們現在**只看其中一列**,我們會怎麼計算它的最大總和呢? 對於每個數字都有兩種選擇 1. 跟前面的數字組合 2. 自己作為新組合的開頭 定義 $dp[i]$ 表示前 $i$ 行的最大區間和 則有轉移式 $$ dp[i] = max(dp[i], dp[i-1] + arr[i]) $$ 並且 $dp$ 的初始值是序列中的第一個元素 不過如果說第一個元素就已經小於零了,那麼不取它必定會拿到更好的解 $$ dp[0] = max(0, arr[i]) $$ 看完如何計算一行的最大區間和之後,接下來看二維 --- 如果給定一個 $n \times n$ 的矩形區域,要求寬度同樣是 $n$ 的最大子區域和要怎麼求呢? 也就是說現在矩形的**寬是固定的,只有高變動** 因為寬是固定的,那麼當我們選擇某一列,那一列的全部元素都必須要被加起來 所以每一列**只需要考慮加總後的結果** 最終,我們可以把這個問題轉化成前面提到的一維最大區間和! 現在 $arr[i]$ 表示的是第 $i$ 列的總和 定義 $dp[i]$ 表示前 $i$ 列的最大區間和 則有轉移式 $$ dp[i] = max(dp[i], dp[i-1] + arr[i]) $$ 同樣的 $$ dp[0] = max(0, arr[0]) $$ --- 接下來把寬也考慮進來 我們現在已經可以做到**固定寬的矩形最大子區域和** 那麼,我們可以考慮直接枚舉左界以及右界,這樣就可以得到所有子區域的最大和了! ``` 尋找固定寬的矩形最大子區域和所需的時間複雜度為 O(n) 枚舉左界以及右界時間複雜度為 O(n^2) 這樣做的總時間複雜度為 O(n^3),在可以接受的範圍內 ``` 前面的 $arr[i]$ 表示的是第 $i$ 列區間 $[L, R]$ 的總和 如果每次重算就還需要花一個 $O(n)$ 這裡可以用前綴和先優化 定義 $arr[i][j]$ 表示原矩形的陣列 定義 $cnt[i][j]$ 表示第 $i$ 列前 $j$ 行的元素總和 $$ \begin{cases} cnt[i][j] = arr[i][j] \quad \texttt{ if }j=0 \\ cnt[i][j] = cnt[i][j-1] + arr[i][j] \quad \texttt{else} \end{cases} $$ 那麼要得到第 $i$ 列的區間 $[L, R]$ 總和就可以用 $cnt[i][R] - cnt[i][L-1]$ 取得 定義 $dp[i]$ 表示在固定矩形寬度在區間 $[L, R]$ 的情況下,前 $i$ 列最大區間和 $$ dp[i] = max(dp[i], dp[i-1] + (cnt[i][R]-cnt[i][L-1])) $$ $$ dp[0] = max(0, (cnt[i][R]-cnt[i][L-1])) $$ 針對每個 $L, R$ 都找出一個區間最大和,其中的最大值就是本題答案 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; int n, arr[105][105], cnt[105][105], dp[105]; int main(){ cin>>n; for(int i=1 ; i<=n ; i++){ for(int j=1 ; j<=n ; j++){ cin>>arr[i][j]; // 先計算出前綴和 cnt[i][j] = cnt[i][j-1] + arr[i][j]; } } int ans = 0; for(int L=1 ; L<=n ; L++){ int cur=0; for(int R=L ; R<=n ; R++){ // 這邊固定好長寬了,可以用一維 dp 找到區間最大矩形和 dp[0] = max(0, cnt[0][R]-cnt[0][L-1]); for(int i=1 ; i<=n ; i++){ int sum = cnt[i][R]-cnt[i][L-1]; dp[i] = max(sum, dp[i-1] + sum); cur = max(cur, dp[i]); } ans = max(ans, cur); } } cout<<ans<<"\n"; return 0; } ``` ### 時間複雜度分析 輸入以及初始化前綴和時間複雜度為 $O(n^2)$ DP 每種狀態轉移時間複雜度維 $O(1)$,總共有 $n$ 種狀態,每次 DP 時間複雜度為 $O(n)$ DP 總共需要做 $n^2$ 次,總時間複雜度為 $O(n^3)$ 總時間複雜度為 $O(n^3)$ # UVa 10755 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10755 跟 UVa 108 類似,不過這次進化成 3 維的問題 做法相同,我們會做 DP 的部分仍然是只有一維 我們要枚舉長方體的高、寬分別是從哪裡到哪裡 固定好長寬之後,就可以把在這個高度中的每個二維矩陣加起來,變成一個新的二維矩陣 二維矩陣加的方向可以參考下圖 ![](https://i.imgur.com/oKBDKfC.png) 接下來就又回到 UVa 108 的二維最大區間和了! 關於二維的做法可以參考 UVa 108 的題解 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> using namespace std; const long long MinN = -9223372036854775807; int t, A, B, C; bool out=false; long long ans, arr[25][25][25], arr2[25][25], cnt[25][25], dp[25]; int main(){ cin>>t; while(t--){ if(out) cout<<"\n"; else out = true; cin>>A>>B>>C; for(int i=1 ; i<=A ; i++){ for(int j=1 ; j<=B ; j++){ for(int k=1 ; k<=C ; k++){ cin>>arr[i][j][k]; } } } ans = MinN; for(int st=1 ; st<=A ; st++){ // 清空 arr2 for(int i=1 ; i<=B ; i++){ for(int j=1 ; j<=C ; j++){ arr2[i][j] = 0; } } for(int ed=st ; ed<=A ; ed++){ // 固定高了 // 計算新的二維陣列 for(int i=1 ; i<=B ; i++){ for(int j=1 ; j<=C ; j++){ // 等同於輸入二維陣列的每個值 arr2[i][j] += arr[ed][i][j]; // 計算前綴和 cnt[i][j] = cnt[i][j-1] + arr2[i][j]; } } for(int L=1 ; L<=C ; L++){ long long cur = MinN; for(int R=L ; R<=C ; R++){ // 固定寬了 // 這次不需要跟 0 取 max ,因為至少要取一個 dp[0] = cnt[0][R] - cnt[0][L-1]; for(int i=1 ; i<=B ; i++){ long long sum = cnt[i][R] - cnt[i][L-1]; dp[i] = max(sum, dp[i-1] + sum); cur = max(cur, dp[i]); } ans = max(ans, cur); } } } } cout<<ans<<"\n"; } return 0; } ``` ### 時間複雜度分析 輸入時間複雜度為 $O(ABC)$ DP 每種轉移時間複雜度為 $O(1)$,總共有 $B$ 種狀態 總共需要做 $A^2C^2$ 次 DP ,總時間複雜度為 $O(A^2BC^2)$ 總時間複雜度為 $O(t(A^2BC^2))$ ###### tags: `SCIST 演算法 題解`