---
# System prepended metadata

title: '演算法課程題解 - 動態規劃\: 最大連續和'
tags: [SCIST 演算法 題解]

---

# 演算法課程題解 - 動態規劃\: 最大連續和

# 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 演算法 題解`