---
# System prepended metadata

title: '演算法課程題解 - 動態規劃\: 基本不定型'
tags: [SCIST 演算法 題解]

---

# 演算法課程題解 - 動態規劃\: 基本不定型

# Kattis Riječi
## 題目
https://open.kattis.com/problems/rijeci

有一台機器可以顯示字串，當按下按鈕之後會開始顯示字串，並且每次觸發會轉換成新的字串，規律如下
`A` -> `B` -> `BA` -> `BAB` -> `BABBA` -> `BABBABAB` -> ...
也就是每次會將上一個字串接上前一個字串就會升成新的字串

現在告訴你按鈕被觸發了幾次，請問 `A` 以及 `B` 分別共出現了幾次

## 想法 By Koios
因為每次都是將前兩個字串合併行成新的字串
所以 `A` 以及 `B` 出現的次數也會等同前兩個字串數量相加

會影響到答案的只有按鈕觸發的次數，這就是 DP 的狀態
至於轉移，根據上面的推論可以得到 $A_i = A_{i-1} + A_{i-2} \quad B_i = B_{i-1} + B_{i-2}$
初始狀態 $A_0 = 1, A_1 = 0 \quad B_0 = 0, B_1 = 1$
有了狀態、轉移、初始狀態，我們就完成 DP 的要件了
接下來你可以選擇要用 Buttom-up 或是 Top-down，以下示範兩種方式

如果你選擇的是 Top-down，記得上課提到費氏數列的例子，要避免同樣問題重複詢問，需要用陣列紀錄結果

### 程式碼 - Buttom-up
```cpp=
//By Koios1143
#include<iostream> 
using namespace std;
int n, A[50], B[50];
int main(){
	A[0] = 1, A[1] = 0;
	B[0] = 0, B[1] = 1;
	for(int i=2 ; i<50 ; i++){
		A[i] = A[i-1] + A[i-2];
		B[i] = B[i-1] + B[i-2];
	}
	cin>>n;
	cout<<A[n]<<" "<<B[n]<<"\n";
	return 0;
}
```

### 時間複雜度分析
令 $N = 50$
總時間複雜度為 $O(N)$

### 程式碼 - Top-down
```cpp=
//By Koios1143
#include<iostream> 
using namespace std;
int n, dp_A[50], dp_B[50];

int A(int depth){
	if(depth == 0) return 1;
	if(depth == 1) return 0;
	if(dp_A[depth-1] == 0) dp_A[depth-1] = A(depth-1);
	if(dp_A[depth-2] == 0) dp_A[depth-2] = A(depth-2);
	return dp_A[depth-1] + dp_A[depth-2];
}
int B(int depth){
	if(depth == 0) return 0;
	if(depth == 1) return 1;
	if(dp_B[depth-1] == 0) dp_B[depth-1] = B(depth-1);
	if(dp_B[depth-2] == 0) dp_B[depth-2] = B(depth-2);
	return dp_B[depth-1] + dp_B[depth-2];
}

int main(){
	cin>>n;
	cout<<A(n)<<" "<<B(n)<<"\n";
	return 0;
}
```

### 時間複雜度分析
在沒有 DP 的情況下，複雜度最差為 $O(2^n)$
不過現在有了 DP，每個 DP 值最多被更新一次，更新後的詢問時間複雜度降至 $O(1)$
總時間複雜度為 $O(n)$

# UVa 369
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?369

給定 $N, M$ ，求排列組合中的組合數 $C^{N}_{M}$

$5 \leq N, M \leq 100 \\ M \leq N$

$C = \frac{N!}{(N-M)! \times M!}$

## 想法 By Koios
組合的定義是從 $N$ 個物品中取 $M$ 個任意排列，移除掉取的物品相同但排列方式不同的情況

組合有以下的性質
1. $C^{N}_{1} = N$
2. $C^{N}_{N} = 1$
3. $C^{N}_{M} = 0 \ (N < M)$
4. $C^{N}_{M} = C^{N-1}_{M} + C^{N-1}_{M-1}$
	
	:::info
	關於這個性質可以這樣思考
	從 $N$ 個物品取出 $M$ 個組合，那麼最後一個元素可以選擇選或不選
	
	如果最後一個元素要選，那麼剩下的 $N-1$ 個元素還需要選出 $M-1$ 個
	如果最後一個元素不選，那麼剩下的 $N-1$ 個元素還需要選出 $M$ 個
	:::

會影響組合數的就是 $N, M$，這也就是 DP 的狀態
藉由第四個性質可以得到轉移式
藉由前三個性質可以得到初始狀態
有了狀態、轉移、初始狀態，我們就完成 DP 的要件了
接下來你可以選擇要用 Buttom-up 或是 Top-down，以下示範兩種方式

### 程式碼 - Buttom-up
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
long long C[105][105];
int n, m;
int main(){
	for(int i=0 ; i<=100 ; i++){
		for(int j=1 ; j<=100 ; j++){
			if(j==1) C[i][j] = i;
			else if(i==j) C[i][j] = 1;
			else if(i<j) C[i][j] = 0;
			else C[i][j] = C[i-1][j] + C[i-1][j-1];
		}
	}
	while(cin>>n>>m && (n!=0 || m!=0)){
		cout<<n<<" things taken "<<m<<" at a time is "<<C[n][m]<<" exactly.\n";
	}
	return 0;
}
```

### 時間複雜度分析
令 $N = 100$
預處理時間複雜度為 $O(N^2)$
詢問時間複雜度為 $O(1)$，假設總共詢問 $Q$ 次
總時間複雜度為 $O(N^2 + Q)$

### 程式碼 - Top-down
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
long long C[105][105];
int n, m;

int Combine(int i, int j){
	if(C[i][j]) return C[i][j];
	if(j == 1) return i;
	if(i == j) return 1;
	if(i<j) return 0;
	C[i-1][j] = Combine(i-1, j);
	C[i-1][j-1] = Combine(i-1, j-1);
	return C[i-1][j] + C[i-1][j-1];
}

int main(){
	while(cin>>n>>m && (n!=0 || m!=0)){
		cout<<n<<" things taken "<<m<<" at a time is "<<Combine(n, m)<<" exactly.\n";
	}
	return 0;
}
```

# TOJ 470
## 題目
https://toj.tfcis.org/oj/pro/470/

你憑藉著極大的熱情與努力，獲得了學科能力競賽的選手資格
現在你想要利用盡量多的公假來練習，但是你的老師並不同意
儘管你不願意，你還是決定要敷衍一下，請公假時讓使魔擬態
不過老師畢竟是老師，只要連續兩天都讓使魔擬態就會被發現
現在告訴你每天訓練分別會有多少訓練成效，求最大成效總和

## 想法 By Koios
每天都可以選擇是否要請公假訓練
那麼如果今天選擇不訓練，最大成效就會是前一天訓練成效最大值
反之如果今天選擇要訓練，最大成效就會是前一天不訓練的訓練成效最大值，加上今日訓練的成效值

定義 $dp[i][j]$ 表示第 $i$ 天選擇是否要訓練 $(j = 1 \texttt{ or } 0)$ 的最大訓練成效
那麼我們可以得出這樣的轉移式
$$
\begin{cases}
dp[i][0] = max(dp[i-1][0], dp[i-1][1]) \\
dp[i][1] = max(dp[i-1][0], dp[i][1]) + arr[i]
\end{cases}
$$

並且
$$
dp[0][0] = 0 \\
dp[0][1] = arr[0]
$$

如此一來就完成 DP 的要素了

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n, arr[1000010], dp[1000010][2];
int main(){
	while(cin>>n){
		for(int i=0 ; i<n ; i++){
			cin>>arr[i];
			dp[i][0] = dp[i][1] = 0;
		}
		dp[0][0] = 0;
		dp[0][1] = arr[0];
		for(int i=1 ; i<n ; i++){
			dp[i][0] = max(dp[i-1][0], dp[i-1][1]);
			dp[i][1] = max(dp[i][1], dp[i-1][0] + arr[i]);
		}
		cout<<max(dp[n-1][0], dp[n-1][1])<<"\n";
	}
	return 0;
}
```

## 優化
從上面的 DP 轉移式當中會發現到，實際上我們需要的資訊只有前一個 DP 值，所以我們並不需要前一個以前的 DP 值

那麼 $dp[i][j]$ 的 $i$ 值就只需要 $0, 1$ 即可
如果當前取的 $i$ 值是 $0$，那麼前一個 DP 值就會存在 $i$ 值為 $1$ 的位置
反之，如果當前取的 $i$ 值是 $1$，那麼前一個 DP 值就會存在 $i$ 值為 $0$ 的位置

現在 DP 式改為
$$
\begin{cases}
dp[i\%2][0] = max(dp[(i-1)\%2][0], dp[(i-1)\%2][1]) \\
dp[i\%2][1] = max(dp[(i-1)\%2][0], dp[i\%2][1]) + arr[i]
\end{cases}
$$

最後答案為 $max(dp[(n-1)\%2][0], dp[(n-1)\%2][1])$

最後的最後，我們還可以把 dp 的計算都整理進輸入的迴圈內

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n, tmp, dp[2][2];
int main(){
	while(cin>>n){
		dp[0][0] = dp[0][1] = dp[1][0] = dp[1][1] = 0;
		cin>>dp[0][1];
		for(int i=1 ; i<n ; i++){
			cin>>tmp;
			dp[i%2][0] = max(dp[(i-1)%2][0], dp[(i-1)%2][1]);
			dp[i%2][1] = max(dp[i%2][1], dp[(i-1)%2][0] + tmp);
		}
		cout<<max(dp[(n-1)%2][0], dp[(n-1)%2][1])<<"\n";
	}
	return 0;
}
```

### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(n)$
每筆測資建表時間複雜度為 $O(n)$
每筆測資總時間複雜度為 $O(n)$

# TOJ 540
## 題目
https://toj.tfcis.org/oj/pro/540/

在一場鐵人三項的比賽當中，每個選手都必須依序進行**游泳**、**腳踏車**、**跑步**，不過不限制各路段的使用方式，也就是說你可以在柏油路上游泳、在水上跑步

我們已經是先探勘好場地，知道在每個路段上使用三種運動各需要消耗多少時間，問最短消耗時間為何

## 想法 By Koios
在每個路段上選擇使用某運動，則上一段只可能是上一種活動或是同一種活動
舉例來說，這個路段要選擇**騎腳踏車**，那麼上一段只可能是**游泳**或是**腳踏車**

定義 $dp[i][j]$ 表示在第 $i$ 個路段上使用 $j$ 運動所消耗的最短時間
定義 $arr[j][i]$ 表示在第 $i$ 個路段上使用 $j$ 運動所消耗的時間
其中 $j$ 依序表示**游泳**、**腳踏車**、**跑步**

可以得到轉移式
$$
\begin{cases}
dp[i][0] = dp[i-1][0] + arr[0][i] \\
dp[i][1] = min(dp[i-1][1], dp[i-1][0]) + arr[1][i] \quad (i \geq 1)\\
dp[i][2] = min(dp[i-1][2], dp[i-1][1]) + arr[2][i] \quad (i \geq 2)
\end{cases}
$$

並且
$$
dp[0][0] = arr[0][0] \\
dp[0][1] = dp[0][2] = dp[1][2] = \infty
$$

可以直接令 $10^9$ 是無限大

而最後答案為 $dp[n-1][2]$

### 程式碼
```cpp=
//By Koios1143
#include<iostream> 
using namespace std;
int n, arr[4][200005];
long long dp[200005][4];
int main(){
    cin>>n;
    for(int i=0 ; i<n ; i++){
        for(int j=0 ; j<3 ; j++){
        	cin>>arr[j][i];
        }
    }
    dp[0][0] = arr[0][0];
    dp[0][1] = dp[0][2] = dp[1][2] = 1e9+10;
    for(int i=1 ; i<n ; i++){
    	dp[i][0] = dp[i-1][0] + arr[0][i];
    	if(i >= 1)
    		dp[i][1] = min(dp[i-1][1], dp[i-1][0]) + arr[1][i];
    	if(i >= 2)
    		dp[i][2] = min(dp[i-1][2], dp[i-1][1]) + arr[2][i];
	}
    cout<<dp[n-1][2]<<"\n";
    return 0;
}
```

## 優化
從上面的程式碼中可以發現到
1. 輸入的 $arr[j][i]$ 實際上各只會用到一次
2. $dp$ 的每一項只跟上一項有關係

由第一點可以發現 $dp$ 的計算可以直接在輸入階段跟著做，而且不需要 $arr$ 陣列了，只需要一個變數即可
由第二點可以發現 $dp$ 不需要那麼大的空間，所以可以用取餘數的技巧來簡化空間使用
另外，在計算每個運動項目的 $dp$ 值，前提條件都是 $i$ 必須至少 $\geq j$ ，所以可以再化簡一點程式碼

### 程式碼
```cpp=
//By Koios1143
#include<iostream> 
using namespace std;
int n, m;
long long dp[2][4];
int main(){
	cin>>n;
	// 為了避免減1後出現負數，從 1 開始
	for(int i=1 ; i<=n ; i++){
		for(int j=1 ; j<=3 ; j++){
			dp[i%2+1][j] = 1e9+10;
			cin>>m;
			if(j == 1)
				dp[i%2+1][j] = dp[(i-1)%2+1][j] + m;
			else if(i >= j)
				dp[i%2+1][j] = min(dp[(i-1)%2+1][j-1], dp[(i-1)%2+1][j]) + m;
		}
	}
	cout<<dp[n%2+1][3]<<"\n";
	return 0;
}
```

### 時間複雜度分析
輸入以及 DP 時間複雜度皆為 $O(n)$
總時間複雜度為 $O(n)$

# TOJ 508
## 題目
https://toj.tfcis.org/oj/pro/508/

定義一條完美河道是由高度 $9$ 每次移動一格只遞減 $1$，並且最終高度為 $0$
現在給你 $N \times M$ 的長方形區域，只由 $0$~$9$ 的數字組成請問其中有多少條完美河道

## 想法 By Koios
跟爬樓梯的問題很類似，假如看做是從 $0$ 爬到 $9$，那麼每個高度 $h$ 都是從高度 $h-1$ 轉移過來
為了避免重複詢問相同的問題，我們可以利用 dp 的方式把已知的答案儲存起來，下次詢問的時候就直接回答

定義 $dp[i][j]$ 表示有多少條從 $0$ 開始每次遞增 $1$ 到 $(i, j)$ 這格的河道
定義 $arr[i][j]$ 表示在 $(i, j)$ 的數字

則有轉移式
$$
\begin{cases}
dp[i][j] = dp[i-1][j] + 1 \quad \texttt{if arr[i][j] = arr[i-1][j] + 1} \\
dp[i][j] = dp[i][j-1] + 1 \quad \texttt{if arr[i][j] = arr[i][j-1] + 1} \\
dp[i][j] = dp[i+1][j] + 1 \quad \texttt{if arr[i][j] = arr[i+1][j] + 1} \\
dp[i][j] = dp[i][j+1] + 1 \quad \texttt{if arr[i][j] = arr[i][j+1] + 1}
\end{cases}
$$

並且
$$
dp[i][j] = 1 \quad \texttt{if arr[i][j] = 0}
$$

這裡使用 Top-down 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n, m, ans, dp[1005][1005];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, -1, 0, 1};
char arr[1005][1005];

int dfs(int x, int y){
	if(arr[x][y] == '0') return dp[x][y] = 1;
	// 記得有找過答案就直接回傳
	if(dp[x][y]) return dp[x][y];
	int res = 0;
	for(int i=0 ; i<4 ; i++){
		int nx = x + dx[i];
		int ny = y + dy[i];
		if(nx < 0 || nx >= n || ny < 0 || ny >=m || arr[nx][ny] + 1 != arr[x][y]) continue;
		res += dfs(nx, ny);
	}
	return dp[x][y] = res;
}

int main(){
	cin>>n>>m;
	for(int i=0 ; i<n ; i++){
		for(int j=0 ; j<m ; j++){
			cin>>arr[i][j];
		}
	}
	for(int i=0 ; i<n ; i++){
		for(int j=0 ; j<m ; j++){
			if(arr[i][j] == '9')
				ans += dfs(i, j);
		}
	}
	cout<<ans<<'\n';
	return 0;
}
```

### 時間複雜度分析
輸入時間複雜度為 $O(NM)$
DFS 每個格子最多被拜訪一次，時間複雜度為 $O(NM)$
總時間複雜度為 $O(NM)$

# UVa 10198
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10198

有個人知道怎麼做算數，現在正在學習寫數字
他已經學會怎麼寫 $1, 2, 3, 4$ 了，不過他還沒發現到 $1$ 和 $4$ 的差別，所以他認為 $4$ 是 $1$ 的另一種寫法
現在他正在玩數字求和的遊戲，他會把幾個 $1$~$4$ 的數字相加，組成新的數字(新的數字可以包含 $1$~$4$ 以外的數字)
現在給你一個正整數 $N$，請問在只用 $1$~$4$ 這四個數字的情況下，你可以有多少種寫來表示數字 $N$?

例如 $2$ 可以用 $1+1$, $1+4$, $4+1$, $4+4$, $2$ 這五種寫法

## 想法 By Koios
$N$ 可以用很多 $1$~$4$ 相加而成
要問 $N$ 有多少種表示方式，可以看看最後一個選取的數字是 $1$~$4$ 的哪一個
選取完最後一個數字，就得到了子問題
例如最後一個數字選擇 $1$，那麼子問題就是 $N-1$ 有多少的組成方式

定義 $dp[i]$ 表示數字 $i$ 有多少表示方式
則有轉移式
$$
dp[i] = dp[i-1] + dp[i-2] + dp[i-3] + dp[i-1]
$$

並且
$$
\begin{cases}
dp[i] = 0 \quad \texttt{if i <= 0} \\
dp[1] = 2 \\
dp[2] = 5 \\
dp[3] = 13
\end{cases}
$$

這裡使用 Top-down 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n;
long long dp[1005];

long long sol(int n){
	if(n <= 0) return 0;
	if(dp[n]) return dp[n];
	return dp[n] = sol(n-1)*2 + sol(n-2) + sol(n-3);
}

int main(){
	dp[1] = 2; dp[2] = 5; dp[3] = 13;
	while(cin>>n){
		cout<<sol(n)<<"\n";
	}
	return 0;
}
```

概念上是對了，但是因為這題的數字會很大，大到超過 long long 的大小，所以需要大數運算
這裡只是希望大家練習 DP 的想法，所以不詳細說明如何進行大數運算，這裡推薦一個模板讓大家使用
http://sunmoon-template.blogspot.com/2015/01/big-interger.html

直接把程式碼貼上去，把 dp 陣列的 long long 改成 bigN 型態即可

# UVa 11067
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?11067

大家應該都看過小紅帽的故事，如果當初他知道大野狼所在的位置，接下來就不會有那麼多麻煩事出現
今天有一個 $W \times H$ 的網狀方格地圖，地圖上有 $(W+1) \times (H+1)$ 個點
小紅帽的家位於地圖左下角 $(0, 0)$ 的位置，而奶奶家位在地圖右上角 $(W, H)$ 的位置

小紅帽要從家裡前往奶奶家，並且希望用最少步數(也就是只能向右或是向上走一步)
告訴你大野狼在森林中可能出現的位置有哪些，請問在使用最少步數且避開大野狼可能出現的地點的前提下，小紅帽有幾種到達奶奶家的方式

## 想法 By Koios
這題地圖的 $(0, 0)$ 雖然是在左下角，但是其實上下顛倒也對答案沒有影響，在陣列使用上也比較直觀
所以不妨想成小紅帽在左上角，奶奶在右下角，每次只能向右或是向下走

對於每個點，只能從左邊以及上面轉移過來
定義 $dp[i][j]$ 表示 $(i, j)$ 的移動方式
則有轉移式
$$
dp[i][j] = dp[i-1][j] + dp[i][j-1]
$$

並且
$$
\begin{cases}
dp[0][0] = 1 \\
dp[i][j] = 0 \quad \texttt{if (i, j) has wolf}
\end{cases}
$$

這裡用 Top-down 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int w, h, n;
long long dp[105][105];

long long sol(int x, int y){
	if(dp[x][y] > 0) return dp[x][y];
	if(dp[x][y] < 0) return 0;
	long long res=0;
	// left
	if(y-1 >= 0) res += sol(x, y-1);
	// down
	if(x-1 >= 0) res += sol(x-1, y);
	return dp[x][y] = res;
}

int main(){
	while(cin>>w>>h && (w!=0 || h!=0)){
		// 注意格子點數是 (W+1)*(H+1)
		for(int i=0 ; i<w+1 ; i++){
			for(int j=0 ; j<h+1 ; j++){
				dp[i][j] = 0;
			}
		}
		dp[0][0] = 1;
		cin>>n;
		for(int i=0, x, y ; i<n ; i++){
			cin>>x>>y;
			dp[x][y] = -1;
		}
		long long ans = sol(w, h);
		if(ans == 0)
			cout<<"There is no path.\n";
		else if(ans == 1)
			cout<<"There is one path from Little Red Riding Hood's house to her grandmother's house.\n";
		else
			cout<<"There are "<<ans<<" paths from Little Red Riding Hood's house to her grandmother's house.\n";
	}
	return 0;
}
```

# UVa 10081
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10081

定義一個數字是**緊密的**，唯有每個相鄰的數字之間不會差距超過 $1$
現在給定正整數 $k, n$， $0 \leq k \leq 9$ ，請問在使用 $0$~$k$ 這幾個數字組出長度為 $n$ 的數字中，有多少比例的數字是緊密的

## 想法 By Koios
要計算比例就需要知道總共能組出多少數字，總共有多少組是緊密的數字
全部的組數可以用排列組合的觀念來想，每個位數都有 $0$~$k$ 這 $k+1$ 種數字，總共有 $(k+1)^{n}$ 種

至於計算緊密數字，可以想成最後一個數字要放多少，如此一來就可以找到子問題
例如最後選擇放 $m$，那麼上個數字只能是 $m-1, m, m+1$ 這三種，長度 $n-1$ 的子問題就出現了

定義 $dp[i][j]$ 表示第 $i$ 個數字放 $j$ 的方法數
則有轉移式
$$
dp[i][j] = dp[i-1][j-1] + dp[i-1][j] + dp[i-1][j+1]
$$

並且
$$
dp[0][i] = 1 \quad 0 \leq i \leq k
$$

這裡使用 Buttom-up 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<iomanip>
#include<cmath>
using namespace std;
int k, n;
long long dp[105][10];

int main(){
	while(cin>>k>>n){
		for(int i=0 ; i<105 ; i++){
			for(int j=0 ; j<10 ; j++){
				if(i == 0)
					dp[i][j] = 1;
				else
					dp[i][j] = 0;
			}
		}

		for(int i=1 ; i<n ; i++){
			for(int j=0 ; j<=k ; j++){
				if(j-1 >= 0)
					dp[i][j] = dp[i][j] + dp[i-1][j-1];
				dp[i][j] = dp[i][j] + dp[i-1][j];
				if(j+1 <= k)
					dp[i][j] = dp[i][j] + dp[i-1][j+1];
			}
		}
		long long cnt = 0, tot=pow(k+1, n);
		for(int i=0 ; i<=k ; i++){
			cnt = cnt + dp[n-1][i];
		}
		cout<<fixed<<setprecision(5)<<100*((double)(cnt)/(double)(tot))<<"\n";
	}
	return 0;
}
```

概念上是對了，但是因為這題的數字會很大，大到超過 long long 的大小，所以需要大數運算
這裡只是希望大家練習 DP 的想法，所以不詳細說明如何進行大數運算，這裡推薦一個模板讓大家使用
http://sunmoon-template.blogspot.com/2015/01/big-interger.html

直接把程式碼貼上去，把 long long 改成 bigN 型態並且自己實作快速冪(qpow) 即可

# UVa 10450
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10450

在一場比賽當中，粉絲會使用氣笛來加油，不過氣笛只要連續吹 $2$ 秒就會壞掉
現在給你一個正整數 $n$ ，請問在長度 $n$ 的0/1字串當中，可以有多少安排方式使得不會出現任何兩個 $1$ 相鄰的情況

## 想法 By Koios
我們可以看第 $n$ 個字要放 $0$ 或是 $1$，這樣就得到了子問題: 第 $n-1$ 個字要放什麼

定義 $dp[i][j]$ 表示第 $i$ 格選擇放 $j$ 的方法數
則有轉移式
$$
\begin{cases}
dp[i][0] = dp[i-1][0] + dp[i-1][1] \\
dp[i][1] = dp[i-1][0]
\end{cases}
$$

並且
$$
\begin{cases}
dp[0][0] = 1 \\
dp[0][1] = 1
\end{cases}
$$

同樣的，因為發現到 $dp$ 實際上只跟前一項有關，所以可以利用取餘數優化記憶體使用量

這裡以 Buttom-up 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int t, n;
long long dp[2][2];

int main(){
	cin>>t;
	for(int Case=1 ; Case<=t ; Case++){
		cin>>n;
		dp[0][0] = dp[0][1] = 1;
		for(int i=1 ; i<n ; i++){
			dp[i%2][0] = dp[(i-1)%2][0] + dp[(i-1)%2][1];
			dp[i%2][1] = dp[(i-1)%2][0];
		}
		cout<<"Scenario #"<<Case<<":\n"<<dp[(n-1)%2][0]+dp[(n-1)%2][1]<<"\n\n";
	}
	return 0;
}
```

# UVa 10036
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10036

給一個長度 $n$ 的序列，你要在兩兩數字之間插入 `+` 或 `-`
請問是否有機會使得計算結果模一個正整數 $k$ 為 $0$

## 想法
因為問的是能不能整除，所以我們**只需要考慮餘數**即可
如果在第 $i$ 個數字可以餘 $m$，那麼第 $i+1$ 個數字就可以餘 $(m+arr[i+1])%k$ 以及 $(m-arr[i+1])%k$

定義 $dp[i][j]$ 表示前 $i$ 個數字是否可以餘 $j$
為了將餘數為負數也考慮進來，一律將餘數加上 $100$
則有轉移式
$$
dp[i][j] = dp[i-1][(m - arr[i] + 100)] \texttt{ || } dp[i-1][(m + arr[i] + 100)] \quad -k \leq m \leq k
$$

並且
$$
dp[0][(arr[0]+100)\%k] = 1
$$

這裡以 Buttom-up 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int t, n, k, m;
bool dp[10005][205];

int main(){
	cin>>t;
	while(t--){
		for(int i=0 ; i<10005 ; i++){
			for(int j=0 ; j<205 ; j++){
				dp[i][j] = false;
			}
		}
		cin>>n>>k;
		for(int i=0 ; i<n ; i++){
			cin>>m;
			m%=k;
			if(i == 0) dp[i][m+100] = true;
			else{
				for(int j=0 ; j<=k+100 ; j++){
					if(dp[i-1][j]){
						dp[i][(j-100+m)%k+100] = true;
						dp[i][(j-100-m)%k+100] = true;
					}
				}
			}
		}
		if(dp[n-1][100]) cout<<"Divisible\n";
		else cout<<"Not divisible\n";
	}	
	return 0;
}
```

# UVa 116
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?116

有一個 $m \times n$ 的方格，每個格子當中都有一個數字表示權重，本題希望找到總權重最小的路徑
每條路徑必定都是從第一行開始，每次從第 $i$ 行走到第 $i+1$ 行
走下一步的方式有三種，如下圖
<center><img src="https://i.imgur.com/IEydiKx.png"></center>

並且第一列以及最後一列可以看做是相鄰的，也就是說可以看做是圓柱的樣子，上下是包起來的

一個路徑的總權重指的是路徑上權重的總和
請找出 $m \times n$ 的方格當中，總權重最小的路徑以及最小的總權重
如果存在相同權重解法，請輸出字典序最小的
路徑請依序輸出各直行選擇了第幾列

## 想法 By Koios
每個方格都有三種移動方式，可以用 DFS 將三個格子都走過一遍，去找出哪個總權重最小，如果相同則留下字典序最小的
至於 DFS 的方向，因為考慮到字典序都是從前面判斷到後面，所以 DFS 方向也是從前到後應該比較直觀

每個方格經過 DFS 後就可以找到最小總權重，無論如何都不會改變，所以記得要把答案儲存起來，下次詢問就直接回答

定義 $dp[i][j]$ 表示第 $(i, j)$ 格向右走的最小總權重
則有轉移式
$$
dp[i][j] = min(dp[i-1][j+1], dp[i][j+1], dp[i+1][j+1])
$$

若 $i-1 < 0$ 則改為 $m-1$
若 $i+1 \geq m$ 則改為 $0$

並且
$$
dp[i][n-1] = arr[i][j] \quad 0 \leq i < m
$$

因為這題需要回朔路徑，需要紀錄每個點會轉移到下一行哪一列上
定義 $to[i][j]$ 表示 $(i, j)$ 轉移到 $j+1$ 行的哪一列
$$
to[x][y] = (nx, ny) \quad (dp[nx][ny] + arr[x][y] < dp[x][y] \texttt{ || } (dp[nx][ny] + arr[x][y] == dp[x][y] \texttt{ && } to[x][y] > nx))
$$

這裡以 buttom-up 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
const int MaxN = 2147483647;
int n, m, arr[15][105], dp[15][105], to[15][105];
int dx[3] = {-1, 0, 1};

int DFS(int x, int y){
	if(dp[x][y] != MaxN)
		return dp[x][y];
	for(int i=0, nx, ny ; i<3 ; i++){
		nx = x + dx[i];
		ny = y + 1;
		
		if(nx < 0) nx = m - 1;
		else if(nx >= m) nx = 0;
		
		int res = DFS(nx, ny) + arr[x][y];
		// res 比當前紀錄答案還小
		// 或是答案相同，但字典序可以更小
		if(res < dp[x][y] || (res == dp[x][y] && to[x][y] > nx)){
			dp[x][y] = res;
			to[x][y] = nx;
		}
	}
	return dp[x][y];
}

int main(){
	while(cin>>m>>n){
		// 初始化
		for(int i=0 ; i<15 ; i++){
			for(int j=0 ; j<105 ; j++){
				dp[i][j] = MaxN;
				to[i][j] = -1;
			}
		}
		
		for(int i=0 ; i<m ; i++){
			for(int j=0 ; j<n ; j++){
				cin>>arr[i][j];
				if(j == n-1)
					dp[i][j] = arr[i][j];
			}
		}
		
		// 因為可以保證起點的字典序只會越來越大，不必判斷 res==ans 的情況
		int res, ans = MaxN, x, y=0;
		for(int i=0 ; i<m ; i++){
			res = DFS(i, 0);
			if(res < ans){
				ans = res;
				x = i;
			}
		}
		
		cout<<x+1;
		while(to[x][y] != -1){
			cout<<" "<<to[x][y]+1;
			x = to[x][y];
			y++;
		}
		cout<<"\n";
		cout<<ans<<"\n";
	}
	return 0;
}
```

### 時間複雜度分析
每次轉移所需的時間複雜度為 $O(1)$
總共有 $n \times m$ 種狀態
每筆測資時間複雜度為 $O(nm)$

# UVa 10401
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10401

類似於八皇后問題，不過這次我們的皇后是受傷的皇后，攻擊範圍如下圖
黑色表示皇后所在的位置，而灰色表示可以攻擊的範圍

![](https://i.imgur.com/ybdLde5.png)

給定一個 $n \times n$ 的棋盤上，每一行的初始狀態，請問皇后有多少種放法，使得任意兩兩皇后之間不會互相攻擊到

初始狀態以一個長度 $n$ 的字串表示，其中 `?` 表示該行初始狀態沒有皇后
否則會以一個數字 $1$~$9$ 或是大寫英文字母 `A`~`Z` 表示該行皇后初始位於哪一列上

## 想法 By Koios
如果皇后要被放在 $(i, j)$ 上，那麼第 $i-1$ 行只有第 $0$~$j-2$ 列以及第 $j+2$~$n$ 列可以有皇后
也就是說只有這些點可以轉移到 $(i, j)$，所以只要將這些點的方法數加起來就會是 $(i, j)$ 的方法數

定義 $dp[i][j]$ 表示在前 $i$ 行，第 $i$ 行放皇后在 $(i, j)$ 的方法數
則有轉移式
$$
dp[i][j] = dp[i-1][k] \quad 0 \leq k \leq j-2 \texttt{ or } j+2 \leq k < n
$$

若第一行初始沒有任何皇后
$$
dp[0][k] = 1 \quad 0 \leq k \leq n-1
$$

若第一行有皇后位在 $(0, k)$
$$
dp[0][k] = 1
$$

以下以 Buttom-up 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
string s;
long long dp[20][20];
int to_int(char c){
	if('1' <= c && c <= '9')
		return c-'0'-1;
	else if('A' <= c && c <= 'Z')
		return c-'A' + 10-1;
}

int main(){
	while(cin>>s){
		// 初始化
		for(int i=0 ; i<20 ; i++){
			for(int j=0 ; j<20 ; j++){
				dp[i][j] = 0;
			}
		}
		
		// dp 初始值
		if(s[0] == '?'){
			for(int j=0 ; j<s.size() ; j++){
				dp[0][j] ++;
			}
		}
		else{
			int tmp = to_int(s[0]);
			dp[0][tmp] ++;
		}
		
		// 轉移
		for(int i=1 ; i<s.size() ; i++){
			if(s[i] == '?'){
				for(int j=0 ; j<s.size() ; j++){
					for(int k=0 ; k<=j-2 ; k++){
						dp[i][j] += dp[i-1][k];
					}
					for(int k=j+2 ; k<s.size() ; k++){
						dp[i][j] += dp[i-1][k];
					}
				}
			}
			else{
				int j = to_int(s[i]);
				for(int k=0 ; k<=j-2 ; k++){
					dp[i][j] += dp[i-1][k];
				}
				for(int k=j+2 ; k<s.size() ; k++){
					dp[i][j] += dp[i-1][k];
				}
			}
		}
		
		long long ans = 0;
		for(int i=0 ; i<s.size() ; i++){
			ans += dp[s.size()-1][i];
		}
		cout<<ans<<"\n";
	}
	return 0;
}
```

### 時間複雜度分析
輸入時間複雜度為 $O(n)$
DP 每個狀態轉移時間複雜度為 $O(n)$，總共有 $n$ 種狀態，DP 時間複雜度為 $O(n^3)$
計算答案時間複雜度為 $O(n)$
每筆測資總時間複雜度為 $2n + n^3$，計為 $O(n^3)$

# UVa 11258
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?11258

給一個最長長度為 $200$ 的數字字串 $s$
你可以任意把 $s$ 切成好幾段子字串，但是任何一段的數值都不能超過 int 最大值 $2147483647$
請問切割出來後的數字總和最大可以是多少

## 想法 By Koios
如果枚舉每個切割點，時間複雜度大概是在 $n!$，顯然太慢

那麼我們慢慢從最後一個切割點來看
整個序列編號是從 $i$ 到 $j$，假設最後一次切割在 $k$ 點的位置
那麼題目就是在問能不能找到一個 $k$ 點，使得剩下的兩個子區間 $[i, k]$ 以及 $[k, j]$ 切割後的數值總和相加要是最大的
同樣的問題可以再推廣到這兩個之後的子序列

於是我們有個想法
定義 $dp[i][j]$ 表示區間 $[i, j]$ 切割後的最大數值總和
可以得到轉移式
$$
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j]) \quad i \leq k \leq j
$$

不過實際上這樣做還不夠快
仔細觀察一下轉移式會發現到，轉移時間複雜度約為 $O(n)$，而總共有 $n^2$ 種狀態，DP 時間複雜度約為 $O(n^3)$

再回到上一步重新思考一下要怎麼把一個 $n$ 壓掉
我們剛剛推得，最後題目在問的就是能不能找到一個 $k$，使得剩下的兩個子區間 $[i, k]$ 以及 $[k, j]$ 切割後的數值總和相加要是最大的

那麼如果想成後面的 $[k, j]$ 不要再切割呢?
也就是說，這一段就用剩下來的數字，但是要確保這段數字不會超過 $2147483647$
這樣剛剛的 $dp$ 式中 $dp[k][j]$ 就可以看做是一個常數

咦!剩下的只有 $max(dp[i][j], dp[i][k] + constant)$
發現到 $[i]$ 都是相同的

重新定義 $dp[i]$ 表示前 $i$ 個數字能切割出的最大數字總和
定義 $sum[i][j]$ 表示區間 $[i, j]$ 的數值，當超過 $2147483647$ 時為 $0$

$$
dp[i] = max(dp[i], dp[k] + sum[k+1][i]) \quad 0 \leq k < i
$$

初始值
$$
dp[i] = sum[0][i]
$$

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
const long long MaxN = 2147483647;
long long dp[205], sum[205][205];
string s;
int n;

int main(){
	cin>>n;
	while(n--){
		for(int i=0 ; i<205 ; i++){
			for(int j=0 ; j<205 ; j++){
				sum[i][j] = 0;
			}
		}
		
		cin>>s;
		
		// 建立 sum 表格
		for(int i=0 ; i<s.size() ; i++){
			long long res = 0;
			for(int j=i ; j<s.size() ; j++){
				res = res * 10 + s[j] - '0';
				if(res > MaxN) break;
				sum[i][j] = res;
			}
		}
		
		for(int i=0 ; i<s.size() ; i++){
			dp[i] = sum[0][i];
			for(int j=1 ; j<=i ; j++){
				dp[i] = max(dp[i], dp[j-1] + sum[j][i]);
			}
		}
		cout<<dp[s.size()-1]<<"\n";
	}

	return 0;
}
```

### 時間複雜度分析
令 $s$ 字串長度為 $n$
預處理時間複雜度為 $O(n^2)$
DP 每種狀態轉移時間複雜度為 $O(n)$，總共有 $n$ 種狀態，時間複雜度為 $O(n^2)$
每筆測資總時間複雜度為 $O(n^2)$

# UVa 10721
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10721

Bar-Code 是由很多亮/暗的 unit 組成，相同的顏色排在一起稱為一個 bar

$BC(n, k, m)$ 表示長度 $n$ 個 units ，並且由 $k$ 個 bar 組成，每個 bar 不超過 $m$ 個 units，並且第一個 unit 是黑色的 Bar-Code 個數

給定 $n, k, m$ ，求 $BC(n, k, m)$

## 想法 By Koios
對於每個 unit 我們都只有兩種選擇
1. 跟前面的 unit 組成 bar
2. 跟包括自己以後的組成 bar

從最後一個 unit 來看
- 如果選擇第一種，那麼問題就會變成詢問剩下的 $n-1$ 條 units 有 $k$ 個 bar ，而且最後一個 bar 不能超過 $m-1$ 個 units
- 如果選擇第二種，那麼問題就會變成詢問剩下的 $n-1$ 條 units 有 $k-1$ 個 bar，而且最後一個 bar 不能超過 $m$ 個 units
	(會補上不能超過 $m$ 個 units 是為了讓問題跟第一種切出來的子問題相同)

發現到這兩個子問題跟母問題是一樣的，並且這兩個子問題的答案相家就會是母問題的解

定義 $dp[i][j][k]$ 表示 $i$ 個 units 有 $j$ 個 bar ，而且最後一個 bar 不能超過 $k$ 個 units 的 Bar-Code 共有多少種
則有轉移式
$$
dp[i][j][k] = dp[i-1][j][k-1] + dp[i-1][j-1][min(m, i-1)]
$$

會使用 $min(m, i-1)$ 是因為如果 $m$ 如果超過 $i-1$，那麼我們根本用不到 $m$ 那麼多，因為總長度也只有 $i-1$ 而已

並且因為要求第一條要是黑色，所以只有一個 bar 的時候只要長度 $i$ 小於等於限制 bar 的最大長度 $k$ 就有一個解
$$
dp[i][1][k] = 1 \quad i \leq k
$$

最後所有 $i, j, k$ 有任一個為 $0$ 都是沒有解的
$$
dp[i][j][k] = 0 \quad \texttt{i=0 || j=0 || k=0}
$$

以下以 Top-Down 實作
為了避免重複計算答案是 $0$ 的情況， $dp$ 初始值給不可能出現的 $-1$

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int N, K, M;
long long dp[55][55][55];

long long sol(int n, int k, int m){
	if(n < 0 || k < 0 || m < 0) return 0;
	if(dp[n][k][m] != -1) return dp[n][k][m];
	return dp[n][k][m] = sol(n-1, k, m-1) + sol(n-1, k-1, min(M, n-1));
}

int main(){
	while(cin>>N>>K>>M){
		for(int i=0 ; i<=N ; i++){
			for(int j=0 ; j<=K ; j++){
				for(int k=0 ; k<=M ; k++){
					if(i == 0 || k == 0 || j == 0)
						dp[i][j][k] = 0;
					else if(j == 1 && i <= k)
						dp[i][j][k] = 1;
					else
						dp[i][j][k] = -1;
				}
			}
		}
		cout<<sol(N, K, M)<<"\n";
	}
	return 0;
}
```

### 時間複雜度分析
預處理時間複雜度為 $O(NKM)$
DP 轉移時間複雜度為 $O(1)$，總共有 $NKM$ 種狀態，DP 時間複雜度為 $O(NKM)$
每筆測資總時間複雜度為 $O(NKM)$

# TOJ 416
## 題目
https://toj.tfcis.org/oj/pro/416/

你的魔力超級強大，可以輕鬆燒光敵軍，
但你的愛杖『超級智慧魔杖』（簡稱超智杖）卻承受不住你的全力。

你能施展火球術、超炎爆術、以及超大型魔法隕石術三種。
火球術不論施展幾次都沒問題，但超炎爆術只能連續使用兩次。
連續使用三次的話，超智杖就會過熱而毀損。
但只要中間隔一回合冷卻，就能夠再連續使用兩次。

而隕石術由於消耗魔力過大，對超智杖負荷太重，只能使用一次，
不管經過多少回合，只要再次使用，超智杖就會承受不住而折斷。

如果要施展 $n$ 回合的法術，你會有多少種不同的方式可以燒掉敵軍?

## 想法1 (60%) By Koios
我們可以發現到火球術要在甚麼時候施展都無所謂，我們在意的只有會影響未來發展的超炎爆術以及隕石術
我們可以分成 $6$ 種狀態來討論

1. 連續施展超炎爆術 $0$ 次 ; 施展過隕石術 $0$ 次
2. 連續施展超炎爆術 $0$ 次 ; 施展過隕石術 $1$ 次
3. 連續施展超炎爆術 $1$ 次 ; 施展過隕石術 $0$ 次
4. 連續施展超炎爆術 $1$ 次 ; 施展過隕石術 $1$ 次
5. 連續施展超炎爆術 $2$ 次 ; 施展過隕石術 $0$ 次
6. 連續施展超炎爆術 $2$ 次 ; 施展過隕石術 $1$ 次

定義 $dp[i][j][k]$ 表示在第 $i$ 回合最後連續施展 $j$ 次超炎爆術，並且過去施展過隕石術 $k$ 次的方法數

$$
\begin{cases}
dp[i][0][0] = dp[i-1][0][0] + dp[i-1][1][0] + dp[i-1][2][0] \\
dp[i][0][1] = dp[i-1][0][0] + dp[i-1][0][1] + dp[i-1][1][0] + dp[i-1][1][1] + dp[i-1][2][0] + dp[i-1][2][1] \\
dp[i][1][0] = dp[i-1][0][0] \\
dp[i][1][1] = dp[i-1][0][1] \\
dp[i][2][0] = dp[i-1][1][0] \\
dp[i][2][1] = dp[i-1][1][1]
\end{cases}
$$

並且
$$
dp[0][0][0] = dp[0][0][1] = dp[0][1][0] = 1
$$

以下以 Buttom-up 實作

### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n;
long long dp[2][3][2];
const int Mod = 1000000007;

int main(){
	
    while(cin>>n){
        for(int i=0 ; i<2 ; i++){
            for(int j=0 ; j<3 ; j++){
                for(int k=0 ; k<2 ; k++){
                    dp[i][j][k] = 0;
                }
            }
        }
        dp[0][0][0] = dp[0][1][0] = dp[0][0][1] = 1;
        for(int i=1 ; i<n ; i++){
            for(int j=0 ; j<3 ; j++){
                for(int k=0 ; k<2 ; k++){
                    dp[i%2][j][k] = 0;
                }
            }
            // 火球術
            for(int j=0 ; j<3 ; j++){
                for(int k=0 ; k<2 ; k++){
                    dp[i%2][0][k] += dp[(i-1)%2][j][k];
                    dp[i%2][0][k] %= Mod;
                }
            }
            
            // 爆炎術
            for(int j=1 ; j<3 ; j++){
                for(int k=0 ; k<2 ; k++){
                    dp[i%2][j][k] += dp[(i-1)%2][j-1][k];
                    dp[i%2][j][k] %= Mod;
                }
            }
            
            // 隕石術
            for(int j=0 ; j<3 ; j++){
                dp[i%2][0][1] += dp[(i-1)%2][j][0];
                dp[i%2][0][1] %= Mod;
            }
        }
        long long ans=0;
        for(int i=0 ; i<3 ; i++){
            for(int j=0 ; j<2 ; j++){
                ans += dp[(n-1)%2][i][j];
                ans %= Mod;
            }
        }
        cout<<ans<<'\n';
    }
    return 0;
}
```

### 時間複雜度分析
DP 每個狀態轉移時間複雜度可以視為 $O(1)$
總共有 $6n$ 種狀態，DP 時間複雜度計為 $O(n)$

但是在這題， $O(n)$ 還不夠快

:::info
100% 作法請參閱 `SCIST 演算法題解 - 動態規劃: 轉移矩陣`
:::

###### tags: `SCIST 演算法 題解`