DP[0]

Dynamic Programming I

INDEX

Introduction

Knapsack

Paths on Grids

Introduction

簡介

1. 前綴和

2. 狀態 / 轉移式

Introduction

前綴和

Prefix Sum

將一個陣列在當前索引下所有前方元素的和加起來
3
6
11
13
15
19
27
33
3
3
5
2
2
4
8
6
array
prefix\_sum
3
3 + 3
3 + 3 + 5
3 + 3 + 5 + 2
3 + 3 + 5 + 2 + 2
3 + 3 + 5 + 2 + 2 + 4
3 + 3 + 5 + 2 + 2 + 4 + 8
3 + 3 + 5 + 2 + 2 + 4 + 8 + 6
初始狀態\ pre[0] == 0
1
2
3
4
5
6
7
8
index
1
2
3
4
5
6
7
8
index
轉移式\ pre[i] == pre[i - 1] + a[i]

Introduction

Code

#include <iostream>
#include <vector>
using namespace std;

int main() {
	int n = 8;
    vector<int> a = {0, 3, 3, 5, 2, 2, 4, 8, 6};
    vector<int> pre(n + 1);
    
    for (int i = 1; i <= n; i++) {
    	pre[i] = pre[i - 1] + a[i];
    }
    
    // index = 5 前綴和查詢
    cout << pre[5] << endl;
    
    // 區間 [2, 5] 和查詢 = (5 前綴和) - (1 前綴和)
    cout << pre[5] - pre[1] << endl;
}
總複雜度\ O(n)
n\ 次轉移
O(1)\ 轉移
O(1)\ 查詢

Knapsack

背包問題

1. Minimizing Coins

2. Coin Combinations

3. Money Sums

Knapsack

題目

之前解這問題是用\ greedy\\ 但在硬幣不為特定形式下則無法確保\ greedy\ 一定生成最佳解
根據遞迴(暴搜)方式構成解
利用陣列存儲子問題的解
\rightarrow 每個子問題只需要計算一遍
給定\ n \ 個硬幣,每一個硬幣的值為一正整數\\ 問構成\ x\ 最少需要幾個硬幣
改為使用低批

遞迴

Recursive formulation

定義\ solve(x)\ 為構成\ x\ 所需要的最少硬幣數
舉例來說,如果硬幣=\{1,3, 4\}
solve(0) = 0\\ solve(1) = 1\\ solve(2) = 2\\ solve(3) = 1\\ solve(4) = 1\\ solve(5) = 2\\ solve(6) = 2\\ solve(7) = 2\\ solve(8) = 2\\ solve(9) = 3\\
solve(9) = 3,因為最佳解為\ 3 + 3 + 3
可以使用以下公式計算
solve(x) = min(
solve(x - 1) + 1,\\ solve(x - 3) + 1,\\ solve(x - 4) + 1)
solve(9) = solve(6) + 1 = solve(3) + 2 = solve(0) + 3 = 3
寫成以下遞迴式
solve(x) = \begin{cases} \infty& x<0\\ 0& x=0\\ min_{c\in coins}solve(x - c)+1& x>0 \end{cases}

Knapsack

Code

int solve(int x) {
	if (x < 0) return INF;
    if (x == 0) return 0;
    int best = INF;
    for (int c : coins) {
    	best = min(best, solve(x - c) + 1);
    }
    return best;
}
solve(x) = \begin{cases} \infty& x<0\\ 0& x=0\\ min_{c\in coins}solve(x - c)+1& x>0 \end{cases}
solve()\ 會重複計算,導致指數級增長
利用陣列儲存計算過的值
bool ready[N]; // 是否被計算過
int value[N];  // 被計算過的值

Knapsack

Code II

int solve(int x) {
	if (x < 0) return INF;
    if (x == 0) return 0;
    if (ready[x]) return value[x];
    int best = INF;
    for (int c : coins) {
    	best = min(best, solve(x - c) + 1);
    }
    value[x] = best;
    ready[x] = true;
    return best;
}
或是寫成迭代式(比較常見的)
value[0] = 0;
for (int x = 1; x <= n; x++) {
	value[x] = INF;
    for (int c : coins) {
    	if (x - c >= 0) {
        	value[x] = min(value[x], value[x - c] + 1);
        }
    }
}

Knapsack

AC Code

#include <iostream>
#include <vector>
using namespace std;
 
const int INF = 1e6 + 1;
 
int main() {
	int n, x;
	cin >> n >> x;
	vector<int> coins(n);
	for (int i = 0; i < n; i++) {
    	cin >> coins[i];
    }
 
	vector<int> dp(x + 1, INF);
	dp[0] = 0;
	for (int i = 1; i <= x; i++) {
		for (int c : coins) {
			if (i - c >= 0) {
				dp[i] = min(dp[i], dp[i - c] + 1);
			}
		}
	}
 
	cout << (dp[x] == INF ? -1 : dp[x]) << '\n';
}
複雜度\ O(nk)
用\ k\ 種硬幣湊成\ n

Knapsack

輸出最佳解

Constructing a solution

如果還要知道用了哪些硬幣
int first[N]; // 對於和為 i, 選擇硬幣 first[i] 為最佳解
value[0] = 0;
for (int x = 1; x <= n; x++) {
	value[x] = INF;
    for (int c : coins) {
    	if (x - c >= 0 && value[x - c] + 1 < value[x]) {
        	value[x] = value[x - c] + 1;
            first[x] = c;
        }
    }
}
輸出
while (n > 0) {
	cout << first[n] << endl;
    n -= first[n];
}

Knapsack

題目

舉例來說,如果硬幣=\{2, 3, 5\},x=9,總共有\ 8\ 種方法
定義\ solve(x)\ 為構成\ x\ 的方法數,列出式子
solve(x) = (
solve(x - 2) +\\ solve(x - 3) +\\ solve(x - 5))\ \
給定\ n \ 個硬幣,每一個硬幣的值為一正整數\\ 請輸出有幾種方法可以構成\ x
\bullet\ 2 + 2 + 5\\ \bullet\ 2 + 5 + 2\\ \bullet\ 5 + 2 + 2\\ \bullet\ 3 + 3 + 3\\
\bullet\ 2 + 2 + 2 + 3\\ \bullet\ 2 + 2 + 3 + 2\\ \bullet\ 2 + 3 + 2 + 2\\ \bullet\ 3 + 2 + 2 + 2

Knapsack

Code

count[0] = 1;
for (int x = 1; x <= n; x++) {
	for (int c : coins) {
    	if (x - c >= 0) {
        	count[x] += count[x - c];
        }
    }
}
solve(x) = \begin{cases} 0& x<0\\ 1& x=0\\ \sum_{c\in coins}solve(x - c)& x>0 \end{cases}
通常解的數量會很大,題目會要求對答案取模(如\ mod = 10^9 + 7)\\ \bf{必須在每次相加後都取模,不然會溢位}
count[x] += count[x - c];
count[x] %= mod;

Knapsack

AC Code

#include <iostream>
#include <vector>
using namespace std;
 
const int mod = 1e9 + 7;
 
int main() {
	int n, x;
	cin >> n >> x;
	vector<int> coins(n);
	for (int i = 0; i < n; i++) {
    	cin >> coins[i];
    }
	
	vector<int> dp(x + 1);
	dp[0] = 1;
	for (int i = 1; i <= x; i++) {
		for (int c : coins) {
			if (i - c >= 0) {
				dp[i] = (dp[i] + dp[i - c]) % mod;
			}
		}
	}
 
	cout << dp[x] << '\n';
}
複雜度\ O(nk)
用\ k\ 種硬幣湊成\ n

Knapsack

題目

vector<bool> dp(x + 1);
dp[0] = true;
for (int i = 0; i <= x; i++) {
	for (int c : coins) {
		if (dp[i]) dp[i + c] = true;
	}
}
如果用之前的寫法
假設有\ \{2, 5\}\ 兩個硬幣,sum=7
0
1
0
1
1
1
1
x
dp
0
1
2
3
4
5
6
7
1
給定\ n \ 個硬幣,找出所有可構成的金額(不得重複使用)
輸出:2\ 4\ 5\ 6\ 7
重複

Knapsack

Solution

要如何讓他每一個硬幣最多只用一次?
vector<bool> dp(x + 1);
dp[0] = true;
for (int c : coins) {
	for (int i = x; i >= 0; i--) {
    	if (dp[i]) dp[i + c] = true;
    }
}
假設有\ \{2, 5\}\ 兩個硬幣,sum=7
1
1
1
x
dp
0
1
2
3
4
5
6
7
1
c = 2,\ i = 0
c = 5,\ i = 2
c = 5,\ i = 0
0
0
0
0
輸出:2\ 5\ 7

Knapsack

AC Code

#include <iostream>
#include <vector>
using namespace std;
 
int main() {
	int n;
	cin >> n;
	vector<int> coins(n);
	int sum = 0;
	for (int i = 0; i < n; i++) {
		cin >> coins[i];
		sum += coins[i];
	}
 
	vector<bool> dp(sum + 1);
	dp[0] = true;
	for (int i = 0; i < n; i++) {
		for (int j = sum; j >= 0; j--) {
			if (dp[j]) dp[j + coins[i]] = true;
		}
	}
 
	vector<int> ans;
	for (int i = 1; i <= sum; i++) {
		if (dp[i]) ans.push_back(i);
	}
 
	cout << ans.size() << '\n';
	for (int a : ans) cout << a << ' ';
	cout << '\n';
}
 
複雜度\ O(nx)
n\ 個硬幣總和\ x

Knapsack

題單

Knapsack

Paths On Grids

路徑

1. Grid Paths

2. Edit Distance

Paths on Grids

題目

1
假設沒有任何障礙物
1
2
1
列出轉移式
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
定義\ dp[i][j]\ 為有幾種方法走到\ (i,j)
給定一個\ n \times n\ 的網格,其中有一些障礙物\\ 從左上走到右下有幾種方式
1
1
1
1
3
3
4
4
6
10
10
20

AC Code

#include <iostream>
#include <vector>
using namespace std;
 
const int mod = 1e9 + 7;
 
int main() {
    int n;
    cin >> n;
 
    vector<vector<int>> dp(n + 1, vector<int>(n + 1));
    dp[0][1] = 1;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            char c;
            cin >> c;
            if (c == '.') {
                dp[i][j] = (dp[i - 1][j] + dp[i][j - 1]) % mod;
            }
        }
    }
 
    cout << dp[n][n] << '\n'; 
}
複雜度\ O(n^2)

Paths on Grids

題目

定義\ dis(a, b)\ 為\ x[0...a]\ 和\ y[0...b]\\ 所需要的操作次數
dis(a, b) = min(
dis(a, b-1) + 1,\\ dis(a - 1, b) + 1,\\ dis(a - 1, b-1)
+ cost(a, b))
寫出轉移式
如果\ x[a] = y[b],cost(a, b) = 0
反之\ cost(a, b) = 1
1
4
5
2
3
1
3
4
2
2
2
3
2
3
2
1
\text{M}
\text{O}
\text{V}
\text{I}
\text{E}
\text{O}
\text{V}
\text{E}
\text{L}
0
1
1
2
2
2
2
3
4
5
3
3
4
4
把它畫成網格會長這樣
給定兩個字串,你可以插入/移除/修改一個字元\\ 問最少需要幾次操作
\text{MOVIE 和 LOVE 需要兩次操作,先將 L 換成 M,然後插入 I}
整條線就是修改路徑

Paths on Grids

AC Code

#include <iostream>
#include <string>
using namespace std;
 
int main() {
    string a, b;
    cin >> a >> b;
    int n = a.length();
    int m = b.length();
 
    int dp[n + 1][m + 1];
    dp[0][0] = 0;
    for (int j = 1; j <= m; j++) dp[0][j] = j;
 
    for (int i = 1; i <= n; i++) {
        dp[i][0] = i;
        for (int j = 1; j <= m; j++) {
            dp[i][j] = min(min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),
                            dp[i - 1][j - 1] + (a[i - 1] != b[j - 1]));
        }
    }
 
    cout << dp[n][m] << '\n';
}
複雜度\ O(nm)

Paths on Grids

題單

Paths on Grids

掰掰

Dynamic Programming I

By keaucucal

Dynamic Programming I

  • 295