动态规划经典模型
什么是动态规划
动态规划 \(Dynamic Programming (DP)\) 是研究多步决策过程最优化问题的一种数学方法。在动态规划中,为了寻找一个问题的最优解(即最优决策过程),将整个问题划分成若干个相应的阶段,并在每个阶段都根据先前所作出的决策作出当前阶段最优决策,进而得出整个问题的最优解。
能采用动态规划求解的问题的一般要具有三个基本条件:
- 重叠子问题
(1)子问题是原大问题的小版本,计算步骤完全一样;
(2)计算大问题的时候,需要多次重复计算小问题。
例如 0/1背包:从小背包扩展到大背包,计算步骤完全一样 - 最优子结构
(1)大问题的最优解包含小问题的最优解;
(2)可以通过小问题的最优解推导出大问题的最优解。
例如 0/1背包:小背包的解用于大背包 - 无后效性:只关心前面的结果,不关心前面的过程
例如 0/1背包:只用到小背包的结果,与小背包如果计算无关
动态规划问题的基本分析步骤
- 状态:依据经验,进行模型转化,定义状态
- 转移:保证数据不会遗漏,方程可不可以转移
- 初始化,边界处理
- 分类依据:选|不选、增加|减少|不变、00|01|10|11、...
- 目标:最后的答案
- 优化:
- 空间优化,状态压缩
- 时间优化,二进制优化、数据结构优化、状态优化
背包问题
01背包
已知现在有 \(n\) 种物品,第 \(i\) 种物品的体积为 \(v_i\),价值为 \(w_i\),数量为 \(1\)。
有一个容量为 \(m\) 的背包,问能装入背包的最大价值是多少,最大体积是多少。
- 状态:\(f_{i,j}\) 表示前 \(i\) 种物品装入剩余容量为 \(j\) 的背包时能获得的最大价值。
- 转移:\(f_{i,j} = max\{ f_{i-1,j},f_{i-1,j-v_i}+w_i \}.\)
- 初始化:\(f_{i,j}=0\)
- 分类依据:第 \(i\) 种物品选或不选
- 目标:\(f_{n,m}\)
int slove1(){
for(int i=1; i<=n; i++){
for(int j=0; j<=m; j++){
f[i][j] = f[i-1][j];
if(j>=v[i]) f[i][j]=max(f[i][j], f[i-1][j-v[i]]+w[i]);
}
}
return f[n][m];
}
- 优化:时间上已经无法优化了,空间上可以使用滚动数组优化或者压缩到一维。
- \(f_{j}=max\{ f_j, f_{j-v_i}+w_i\}\)
// int f[2][M]; 滚动数组优化
int slove2() {
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
f[i & 1][j] = f[(i - 1) & 1][j];
if (j >= v[i])
f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - v[i]] + w[i]);
}
}
return f[n & 1][m];
}
// int f[M]; 状态压缩
int slove3() {
for (int i = 1; i <= n; i++)
for (int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
return f[m];
}
完全背包
已知现在有 \(n\) 种物品,第 \(i\) 种物品的体积为 \(v_i\),价值为 \(w_i\),数量无穷。
有一个容量为 \(m\) 的背包,问能装入背包的最大价值是多少,最大体积是多少。
- 状态:\(f_{i,j}\) 表示前 \(i\) 种物品装入剩余容量为 \(j\) 的背包时能获得的最大价值。
- 转移:\(f_{i,j} = max\{ f_{i-1,j-k*v_i}+k*w_i \}.\)
- 分类依据:第 \(i\) 种物品选 \(k\) 个。
- 目标:\(f_{n,m}\)
int slove1() {
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
for (int k = 0; k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
return f[n][m];
}
- 优化:可以在选择第 i种物品的基础上继续选择,那么也就是需要 \(f_{i,j-v_i}\),所以可以按照 j 递增循环。
- 如果上述描述难以理解,可以看如下公式简化
/* 公式简化
f[i, j] = max{ f[i-1, j-k*v] +k*w };
k=0 k=1 k=2 k=k
f[i, j] = max{ f[i-1, j], f[i-1, j-v] +w, f[i-1, j-2v]+2w, ..., f[i-1, j-kv]+kw };
f[i, j-v] = max{ f[i-1, j-v], f[i-1, j-2v]+w, f[i-1, j-3v]+2w, ..., f[i-1, j-kv]+(k-1)w };
f[i,j] = max{ f[i-1, j], f[i, j-v]+w };
*/
int slove2() {
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++) {
f[i][j] = f[i - 1][j];
if (j >= v[i])
f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
return f[n][m];
}
- 优化:状态压缩
- \(f_j = max\{ f_j, f_{j-v_i}+w_i \}\)
int slove3() {
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= m; j++)
f[j] = max(f[j], f[j - v[i]] + w[i]);
return f[m];
}
多重背包
已知现在有 \(n\) 种物品,第 \(i\) 种物品的体积为 \(v_i\),价值为 \(w_i\),数量为 \(s_i\)。
有一个容量为 \(m\) 的背包,问能装入背包的最大价值是多少,最大体积是多少。
- 状态:\(f_{i,j}\) 表示前 \(i\) 种物品装入剩余容量为 \(j\) 的背包时能获得的最大价值。
- 转移:\(f_{i,j} = max\{ f_{i-1,j-k*v_i}+k*w_i \}.\)
- 分类依据:第 \(i\) 种物品选 \(k\) 个。
- 目标:\(f_{n,m}\)
int slove1() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int a, b, c;
cin >> a >> b >> c;
for (int j = 0; j <= m; j++)
for (int k = 0; k * a <= j && k <= c; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * a] + k * b);
}
return f[n][m];
}
- 优化:二进制拆分,将问题转化为01背包
int f[N], cnt;
int slove2() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int a, b, c, base = 1;
cin >> a >> b >> c;
while (c >= base) { // 二进制拆分
v[++cnt] = a * base, w[cnt] = b * base;
c -= base, base *= 2;
}
if (c) v[++cnt] = a * c, w[cnt] = b * c;
}
// 转为 01背包
for (int i = 1; i <= cnt; i++)
for (int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
return f[m];
}
- 单调队列优化
多重背包基础转移方程:\(f[i][j] = max{f[i-1][j-k*v] + k*w}.\)
可以发现: 每一个状态只能由 \(j-k*v\) 转移过来,也就是说能影响它的点,是模 \(v\) 同余的点。
所以可以按照模 \(v\) 的余数进行分组。
#include <bits/stdc++.h>
using namespace std;
const int N = 20010;
int n, m, q[N], f[N], g[N];
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
int v, w, s;
cin >> v >> w >> s;
memcpy(g, f, sizeof(f));
for (int j = 0; j < v; j++) {
int h = 0, t = -1;
for (int k = j; k <= m; k += v) {
if (h <= t && q[h] < k - s * v) h++;
// while(h<=t && g[k]-(k-j)/v*w >= g[q[t]]-(q[t]-j)/v*w) t--;
while (h <= t && g[k] >= g[q[t]] + (k - q[t]) / v * w) t--;
q[++t] = k;
f[k] = g[q[h]] + (k - q[h]) / v * w;
}
}
}
cout << f[m] << endl;
return 0;
}
混合背包
有 \(n\) 种物品和一个容量是 \(m\) 的背包,第 \(i\) 种物品的体积是 \(v_i\),价值是 \(w_i\),数量是 \(s_i\)。
当物品数量为 \(-1\)时,可以使用无限次,问能装入背包的最大价值是多少。
通过分析发现这是 01背包、完全背包、多重背包的混合。
可以每个单独解决,也可以直接将其转为多重背包。
分组背包
已知现在有 \(n\) 种物品,第 \(i\) 种物品的体积为 \(v_i\),价值为 \(w_i\),数量为 \(1\)。
同时每个物品属于一个组,同组内最多只能选择一个物品。
有一个容量为 \(m\) 的背包,问能装入背包的最大价值是多少,最大体积是多少。
- 状态:\(f_j\) 表示背包容量为 \(j\) 的最大价值
- 转移:\(f_j=max\{ f_j, f_{j-v_{i,k}}+w_{i,k} \}\)
- 例题:洛谷P1757 通天之分组背包
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e3 + 10, INF = 0x3f3f3f3f;
int n, m, v[N][N], w[N][N], cnt[N], f[N];
// v[i][j] - 第i组第j个物品的使用代价
int main() {
cin >> m >> n;
int a, b, c, t = 0;
for (int i = 1; i <= n; i++) {
cin >> a >> b >> c;
int& s = ++cnt[c];
v[c][s] = a, w[c][s] = b, t = max(t, c);
}
for (int i = 1; i <= t; i++) // 第i组
for (int j = m; j >= 0; j--) // 背包容量
for (int k = 1; k <= cnt[i]; k++) // 第k个
if (j >= v[i][k])
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[m];
return 0;
}
二维费用背包
有 \(n\) 种物品和一个容量是 \(m\) 的背包,背包能承受的最大重量是 \(q\);
第 \(i\) 种物品的体积是 \(v_i\),重量是 \(u_i\),价值是 \(w_i\)。
每种物品可以使用一次,问能装入背包的最大价值是多少。
- 状态:\(f_{i,j,k}\) 从前 \(i\)个物品选,费用 1不超过 \(j\),费用 2不超过 \(k\) 的最大价值。
- 状态转移:\(f_{i,j,k}=max\{f_{i-1,j,k}, f_{i−1,j-v1_i, k-v2_i}+w_i \}\)
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 410;
int n, m, f[N][N], V1, V2, v1[N], v2[N], w[N];
int main() {
cin >> V1 >> V2 >> n;
for (int i = 1; i <= n; i++)
cin >> v1[i] >> v2[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = V1; j >= v1[i]; j--)
for (int k = V2; k >= v2[i]; k--)
f[j][k] = max(f[j][k], f[j - v1[i]][k - v2[i]] + w[i]);
cout << f[V1][V2];
return 0;
}
子序列问题
最长上升子序列(Longest Increasing Subsequence - LIS)
给定一个长度为 \(N\) 的数列,求数值严格单调递增的子序列的长度最长是多少。
- 状态:\(f_{i}\) 表示以第 \(i\) 个数结尾的最长上升子序列长度。
- 转移:\(f_i=max\{f_j+1\} , a_i > a_j\)。
- 目标:\(res=max\{f_i\}\)
int lis1() {
int res = 0;
for (int i = 1; i <= n; i++) {
f[i] = 1;
for (int j = 1; j < i; j++)
if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
res = max(res, f[i]);
}
return res;
}
- 优化:最长上升子序列具有单调上升的特性,可以利用二分优化,这时候更像是贪心。
- \(f[i]\) 表示长度为 \(i\) 的 LIS 末尾元素的最小值,\(len\) 表示当前已知 LIS 的长度。发现 \(f[i]\) 单调不降,考虑进来一个元素 \(a[i]\):
- \(a[i]>f[len]\),将该元素插入到 \(f\) 序列的末尾,同时 \(len\)++。
- \(a[i]≤f[len]\),找到 \(f\) 序列中第一个大于等于它的元素,替换它(二分查找)。
初始化:\(f[1]=a[1], len=1\)
目标答案:\(len\),复杂度:\(O(nlogn)\)。
int f[N], len = 0;
int lis2() {
for (int i = 1; i <= n; i++) {
if (i == 1 || f[len] < a[i]) f[++len] = a[i];
else *lower_bound(f + 1, f + 1 + len, a[i]) = a[i];
}
return len;
}
最长不下降子序列,需要修改两个细节。
int f[N], len = 0;
int lis3() {
for (int i = 1; i <= n; i++) {
if (i == 1 || f[len] <= a[i]) f[++len] = a[i];
else *upper_bound(f + 1, f + 1 + len, a[i]) = a[i];
}
return len;
}
最长公共子序列(longest common subsequence - LCS)
给定两个长度分别为 \(N,M\) 的数列 \(A,B\),求两者的最长公共子序列的长度。
- 状态:\(f_{i,j}\) 表示 \(A[1...i],B[1...j]\) 最长公共子序列长度。
- 转移:
- 目标:\(res=f_{n,m}\)
int lcs() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
return f[n][m];
}
- LCS AtCoder - dp_f:求 \(LCS\) 的串是什么?
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 10;
int n, m, f[N][N];
string s, t;
void lcs() { // s[0..n) t[0...m)
n = s.size(), m = t.size(), memset(f, 0, sizeof(f));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
int x = n, y = m;
string res;
while (x > 0 && y > 0) {
if (f[x][y] == f[x - 1][y]) x--;
else if (f[x][y] == f[x][y - 1]) y--;
else res += s[x - 1], x--, y--;
}
reverse(res.begin(), res.end());
cout << res << endl;
}
void lcs2() { // s[1..n] t[1...m]
s = " " + s, t = " " + t;
n = s.size() - 1, m = t.size() - 1, memset(f, 0, sizeof(f));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i] == t[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
int x = n, y = m;
string res;
while (x > 0 && y > 0) {
if (f[x][y] == f[x - 1][y]) x--;
else if (f[x][y] == f[x][y - 1]) y--;
else res += s[x], x--, y--;
}
reverse(res.begin(), res.end());
cout << res << endl;
}
int main() {
while (cin >> s >> t) {
// lcs();
lcs2();
}
return 0;
}
区间DP
P1775 石子合并(弱化版)
- P1775 石子合并(弱化版)
设有 \(N(N \le 300)\) 堆石子排成一排,其编号为 \(1,2,3,\cdots,N\)。每堆石子有一定的质量 \(m_i\ (m_i \le 1000)\)。现在要将这 \(N\) 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。
【分析】区间DP
区间dp是一类通过合并小区间来求解大区间的问题,总的问题是求解整个区间,子问题是求解子区间,状态定义的核心在于如何表示区间。
本题我们可以定义 dp[i][j] 表示区间[i,j]的最优解(即左端点为第i个石子,右端点为第j个石子的区间合并成一堆的最小花费)。
例如区间[1,5]可以有很多种合并方式,如[1,1]+[2,5]、[1,2]+[ 3,5]、[1,3]+[4,5]、[1,4]+[5,5]
选择由代价最小的合并方式来转移。
\(dp[i,k]=min(dp[i,j]+dp[j+1,k]+cost) (i≤j<k)\)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 305, INF = 0x3f3f3f3f;
int t = 1, n, m, a[N];
ll s[N], f[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
memset(f, 0x3f, sizeof f);
for (int len = 1; len <= n; len++) {
for (int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
if (len == 1) f[i][j] = 0;
else for (int k = i; k < j; k++)
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
}
}
cout << f[1][n];
return 0;
}
P1880 [NOI1995] 石子合并
在一个圆形操场的四周摆放 \(N\) 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 \(2\) 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 \(N\) 堆石子合并成 \(1\) 堆的最小得分和最大得分。
\(1\leq N\leq 100\),\(0\leq a_i\leq 20\)。
【分析】区间DP,破换成链,将数据 a[1...n] 拷贝一份给 a[n+1 ... 2*n],那么这时候对于新的数据进行区间DP即可,具体看代码。
#include <bits/stdc++.h>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f, MOD = 1E9 + 7;
int n, w[N], s[N], f[N][N], g[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> w[i], w[i + n] = w[i];
for (int i = 1; i <= 2 * n; i++) s[i] = s[i - 1] + w[i];
memset(f, 0x3f, sizeof f);
memset(g, -0x3f, sizeof g);
for (int len = 1; len <= n; len++)
for (int l = 1; l + len - 1 <= 2 * n + 1; l++) {
int r = l + len - 1;
if (len == 1) f[l][r] = g[l][r] = 0;
else for (int k = l; k < r; k++) {
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
}
}
int mn = INF, mx = -INF;
for (int i = 1; i <= n; i++) {
mn = min(mn, f[i][i + n - 1]);
mx = max(mx, g[i][i + n - 1]);
}
cout << mn << endl << mx;
return 0;
}