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