动态规划「非典型」
我们通过「典型」的动态规划题学习理解动态规划,但我们还需要解决其他「非典型」的动态规划夯实 DP 技巧。本篇文章将讨论两道「非典型」动态规划题,之前 UVa11450 Wedding Shopping 已经在这篇文章中讲解过了,如有需要回顾的,请点击这里:动态规划「入门」。
1. UVa 10943 How do you add?
problem
一个正整数 n n n,关于 k k k 个数字相加等于 n n n 的方法共有多少种?
constraints
1 ≤ n , k ≤ 100 1\leq n, k\leq 100 1≤n,k≤100
sample input
20 2
sample output
21
sample explain
0+20
1+19
2+18
3+17
4+16
5+15
…
18+2
19+1
20+0
解析
首先,根据问题有几个参数来分析决定问题的状态。问题只有两个参数, n n n 和 k k k。那么将有 4 4 4 种可能:
如果不选择任何参数,无法表示状态如果只选择了 n n n,那么无法得知多少个数字已经选择了如果只选择了 k k k,那么无法得知目标之和 n n n- 因此,该问题的状态应该为一对 ( n , k ) (n,k) (n,k),至于选择的数字顺序无关紧要。
接下来,观察基础情况,看起来 k = 1 k=1 k=1 的时候非常简单,无论 n n n 是多少,只有一种方法,选择一个和 n n n 一样的数字。
对于其余情况,状态 ( n , k ) , k > 1 (n, k), k\gt 1 (n,k),k>1,我们可以将 n n n 拆分为两个数字 x x x 和 n − x n-x n−x,也就是 n = x + ( n − x ) n=x + (n-x) n=x+(n−x)。这样做我们得到了子问题(子状态): ( n − x , k − 1 ) (n - x, k - 1) (n−x,k−1),即一个整数 n − x n-x n−x,关于 k − 1 k - 1 k−1 个数字相加等于 n − x n-x n−x 的方法共有多少种?我们可以通过以下方法得到结果:
- w a y s ( n , 1 ) = 1 ; ways(n, 1) = 1; ways(n,1)=1; 我们只可以使用一个 n n n 数字本身,相加等于 n n n
- w a y s ( n , k ) = ∑ x = 0 n w a y s ( n − x , k − 1 ) ; ways(n, k) = \sum_{x=0}^{n}ways(n-x, k-1); ways(n,k)=∑x=0nways(n−x,k−1); 递归地求出所有方案数量
该问题具有重叠子结构,状态共有 n × k n\times k n×k 个,计算每个状态所需 O ( n ) O(n) O(n),因此时间复杂度是 O ( n 2 k ) O(n^2k) O(n2k)。需要注意该问题需要对 1000000 1 000 000 1000000 取模结果。
code
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k;
while (cin >> n >> k, n + k != 0) {
// dp[i][j] 第 i 个数字构成 j 之和的方案数量
vector<vector<int>> dp(k + 1, vector<int>(n + 1));
// 当只有 1 个数字时,方案数量只有 1 种
for (int i = 0; i <= n; i++) {
dp[1][i] = 1;
}
// 在第 i 个数字
for (int i = 2; i <= k; i++) {
// 不同 j 之和的状态下,这里 j = 0 需要取到,或者可以在初始化的时候将所有列为 0 设置为 1
for (int j = 0; j <= n; j++) {
int sum{0};
// 可以选择的数字 x 有 0 ~ j
for (int x = 0; x <= j; x++) {
sum = (sum + dp[i - 1][j - x]) % 1000000;
}
dp[i][j] = sum;
}
}
cout << dp[k][n] << "\n";
}
return 0;
}
2. UVa 10003 Cutting Sticks
problem
给定一段长 l ( 1 ≤ l ≤ 1000 ) l(1\leq l\leq 1000) l(1≤l≤1000) 的木块和 n ( 1 ≤ n ≤ 50 ) n(1\leq n\leq 50) n(1≤n≤50) 处切割点,范围在 [ 0 , l ] [0, l] [0,l] 之间。每次切割的花费是切割下的两端木块长度之和。你的任务是找出一种切割顺序使得总花费最小。
sample input
100
3
25 50 75
10
4
4 5 7 8
sample output
The minimum cutting is 200.
The minimum cutting is 22.
smaple explain
对于第一个样例,可以按照以下顺序切割木块。
- 第一次切割 50 50 50,花费 s u m = ( 50 + 50 ) = 100 sum = (50 + 50) = 100 sum=(50+50)=100
- 第二次切割 25 25 25,花费 s u m = 100 + ( 25 + 25 ) = 150 sum = 100 + (25 + 25) = 150 sum=100+(25+25)=150
- 第三次切割 75 75 75,花费 s u m = 150 + ( 25 + 25 ) = 200 sum = 150 + (25 +25) = 200 sum=150+(25+25)=200
解析
使用端点来表示每根木块。通过引入两个额外的坐标值,即
0
0
0 和
l
l
l,将木块的端点描述为 {0,原始切割端点,l},状态为需要切割木块 cut(left, right);
,表示木块从 left 端至 right 端需要切割:
- 如果
left+1 = right
,即木块段不需要进一步切割,则cut(i-1, i) = 0
,第 i − 1 i-1 i−1 端点和第 i i i 端的切割费用为 0 0 0。 - 否则,通过尝试所有可能的切割点来找到最佳切割方案,得到
cut(left, right) = min(cut(left, i) + cut(i, right) + (A[right]-A[left]))
∀ i ∈ [ l e f t + 1 , r i g h t − 1 ] \forall i\in[left + 1, right -1] ∀i∈[left+1,right−1]。
可以通过计算 cut(0, n+1)
得到。
在这里存在重叠子问题,实际搜索空间并不是很大。所有可能的左/右索引数为 O ( n 2 ) O(n^2) O(n2),并且可以进行记忆化。由于每个状态的计算时间为 O ( n ) O(n) O(n),因此自顶向下的动态规划总时间复杂度为 O ( n 3 ) O(n^3) O(n3)。
综上所述,该算法是可行的。
code
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int wood, n, sec;
vector<int> w;
vector<vector<int>> memo;
int dp(int L, int R) {
if (L + 1 == R) return 0;
if (memo[L][R] != -1) return memo[L][R];
int ans = INF;
for (int i = L + 1; i < R; i++) {
ans = min(ans, dp(L, i) + dp(i, R) + (w[R] - w[L]));
}
return memo[L][R] = ans;
}
int main() {
while (cin >> wood, wood) {
cin >> n;
w.assign(n + 2, 0);
for(int i = 1; i <= n; i++) {
cin >> w[i];
}
w[0] = 0;
w[n + 1] = wood;
memo.assign(n + 2, vector<int>(n + 2, -1));
cout << "The minimum cutting is " << dp(0, n + 1) << ".\n";
}
return 0;
}
转变为自底向上动态规划。
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int main() {
int wood, n, sec;
while (cin >> wood, wood) {
cin >> n;
vector<int> w(n + 2);
// dp[i][j] 表示第 i 个切割点到第 j 个切割点构成的木块花费最小值
vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
for(int i = 1; i <= n; i++) {
cin >> w[i];
}
w[0] = 0;
w[n + 1] = wood;
// 自底向上的动态规划
// 枚举 [L, R] 的所有状态,对于长度为 1 忽略 默认设置为了花费 0
for (int len = 2; len <= n + 1; len++) {
for (int L = 0; L + len <= n + 1; L++) {
int R = L + len;
dp[L][R] = INF;
for (int i = L + 1; i < R; i++) {
dp[L][R] = min(dp[L][R], dp[L][i] + dp[i][R] + (w[R] - w[L]));
}
}
}
cout << "The minimum cutting is " << dp[0][n + 1] << ".\n";
}
return 0;
}