- 背包问题
![](https://img-blog.csdnimg.cn/img_convert/1af278c164459aa29ce9ce6fa4e66cb4.png)
(1)01背包问题
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i];
f[j] = max(f[j], f[j - v[i]] + w[i]);
二维写法
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]);
}
一维优化
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]);
可参考这个同学的讲解 https://www.acwing.com/solution/content/116859/
![](https://img-blog.csdnimg.cn/img_convert/56a7b043db39028a19b1573897b21f02.png)
(2)完全背包问题
![](https://img-blog.csdnimg.cn/img_convert/037e449fefc8186e66d52b35b71b9494.png)
![](https://img-blog.csdnimg.cn/img_convert/0f54b4f59e45537f917543007947a978.png)
朴素做法 (会超时)
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 - v[i] * k] + w[i] * k);
二维优化
![](https://img-blog.csdnimg.cn/img_convert/0e468ff27e18ba197678dc3c531f0ff9.png)
![](https://img-blog.csdnimg.cn/img_convert/c7295ebf7d15a4d4886726e88858fecb.png)
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]);
}
一维优化
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]);
(3)多重背包问题
![](https://img-blog.csdnimg.cn/img_convert/30f418bc3552cb8d9767ec538a87d6e0.png)
暴力朴素做法
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
(错解)用完全背包的思路优化
![](https://img-blog.csdnimg.cn/img_convert/102056daeaa7adb17b27da80490131ce.png)
(正解)2^0 + 2^1 + ... + 2^(n - 1) = 2^n
凑出来 0 ~ s 的拼法
![](https://img-blog.csdnimg.cn/img_convert/3abe2d44f7554d34af653c6c9736296d.png)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N];
int f[M];
int main()
{
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; i ++ )
{
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while (k <= s)
{
cnt ++ ;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if (s > 0)
{
cnt ++ ;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
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]);
cout << f[m] << endl;
return 0;
}
(4)分组背包问题
for (int i = 1; i <= n; i ++ )
{
cin >> s[i];
for (int j = 0; j < s[i]; j ++ )
cin >> v[i][j] >> w[i][j];
}
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
for (int k = 0; k < s[i]; k ++ )
if (v[i][k] <= j)
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
- 线性DP
(1)数字三角形
![](https://img-blog.csdnimg.cn/img_convert/aba9af339d460e3f95bbd2b6c44a6111.png)
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
scanf("%d", &a[i][j]);
for (int i = 0; i <= n; i ++ )
for (int j = 0; j <= i + 1; j ++ )
f[i][j] = -INF;
f[1][1] = a[1][1];
for (int i = 2; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);
int res = -INF;
for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
(2)最长上升子序列问题
![](https://img-blog.csdnimg.cn/img_convert/130988aa34e278eb17dcdbd015f50323.png)
朴素解法 O(n^2)
for (int i = 1; i <= n; i ++ )
{
f[i] = 1; // 只有a[i]一个数
for (int j = 1; j < i; j ++ )
if (a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);
如何保存最长子序列
for (int i = 1; i <= n; i ++ )
{
f[i] = 1; // 只有a[i]一个数
g[i] = 0; //
for (int j = 1; j < i; j ++ )
if (a[j] < a[i])
if (f[i] < f[j] + 1)
{
f[i] = f[j] + 1;
g[i] = j;
}
}
int k = 1;
for (int i = 1; i <= n; i ++ )
if (f[k] < f[i])
k = i;
for (int i = 0, len = f[k]; i < len; i ++ )
{
cout << a[k] << ' ';
k = g[k];
}
优化解法(II)
长度相同的只存结尾数字小的
![](https://img-blog.csdnimg.cn/img_convert/7abcbdfdb02a038f344937b73604d9d6.png)
(1)q[] (2)长度是下标 (3)结尾数字大小是 q[] 的值
二分出来小于某个数的最大的数
int len = 0;
for (int i = 0; i < n; i ++ )
{
int l = 0, r = len;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (q[mid] < a[i]) l = mid;
else r = mid - 1;
}// r 是 q[r] 比 a[i] 小的下标
len = max(len, r + 1);
q[r + 1] = a[i];
}
cout << len << endl;
(3)最长上升公共子序列
f[i - 1][j] 包含 00 01
f[i][j - 1] 包含 00 10
![](https://img-blog.csdnimg.cn/img_convert/fe6b94d044c80dc373e8e94e80fe02a1.png)
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
(4)最短编辑距离
// 初始化,如果 a[] 的长度为 0,或者 b[] 的长度为 0
for (int i = 0; i <= m; i ++ ) f[0][i] = i;
for (int i = 0; i <= n; i ++ ) f[i][0] = i;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
编辑距离
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 15, M = 1010;
int n, m;
int f[N][N];
char str[M][N];
int edit_distance(char a[], char b[])
{
int la = strlen(a + 1), lb = strlen(b + 1);
for (int i = 0; i <= lb; i ++ ) f[0][i] = i;
for (int i = 0; i <= la; i ++ ) f[i][0] = i;
for (int i = 1; i <= la; i ++ )
for (int j = 1; j <= lb; j ++ )
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
}
return f[la][lb];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) scanf("%s", str[i] + 1);
while (m -- )
{
char s[N];
int limit;
scanf("%s%d", s + 1, &limit);
int res = 0;
for (int i = 0; i < n; i ++ )
if (edit_distance(str[i], s) <= limit)
res ++ ;
printf("%d\n", res);
}
return 0;
}
3.区间DP
(1)石子合并
每次合并两堆
![](https://img-blog.csdnimg.cn/img_convert/1a21bf02a2130732f2a1ba31b2c66424.png)
前缀和求 i ~ j 的全部重量
for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1]; // 前缀和
for (int len = 2; len <= n; len ++ )
for (int i = 1; i + len - 1 <= n; i ++ )
{
int l = i, r = i + len - 1;
f[l][r] = 1e8;
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]);
}
printf("%d\n", f[1][n]);
每次合并 n 堆 (较复杂,等以后找题写)
4.计数类DP
(1)整数划分
用完全背包问题的思路求解
![](https://img-blog.csdnimg.cn/img_convert/41565d532bb3473f9fce103d51b8da9d.png)
![](https://img-blog.csdnimg.cn/img_convert/7f4d13ad4b7e4d7ebc1421a0466db7b0.png)
f[i][j] = f[i - 1][j] + f[i][j - i];
f[j] = f[j - i];
f[0] = 1;
for (int i = 1; i <= n; i ++ )
for (int j = i; j <= n; j ++ )
f[j] = (f[j] + f[j - i]) % mod;
其他解法
![](https://img-blog.csdnimg.cn/img_convert/ce6fbf7bb1e44295950c0fde0f107a37.png)
f [ i - 1 ][ j - 1 ] 表示把 1 去掉
f [ i - j ][ j ] 表示把每个数减去 1
![](https://img-blog.csdnimg.cn/img_convert/30b0e242563b408d85be4215b60c9bcd.png)
f[1][1] = 1; // f[0][0] = 1;
for (int i = 2; i <= n; i ++ ) // for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;
int res = 0;
for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod;
5.数位统计DP
(1)计数问题
求 a ~ b 中 0 ~ 9 出现的次数
求前缀
![](https://img-blog.csdnimg.cn/img_convert/654b25207f574d9e951470d440b0dcd7.png)
![](https://img-blog.csdnimg.cn/img_convert/bd41142e14084afb9c73b0493488399c.png)
边界问题:
(1)1 出现在第一位
(2)当取 0 时,前三位 从 001 ~ abc - 1
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 10;
/*
001~abc-1, 999
abc
1. num[i] < x, 0
2. num[i] == x, 0~efg
3. num[i] > x, 0~999
*/
int get(vector<int> num, int l, int r) // 求 xxx
{
int res = 0;
for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
return res;
}
int power10(int x) // 求 yyy 的位数
{
int res = 1;
while (x -- ) res *= 10;
return res;
}
int count(int n, int x)
{
if (!n) return 0;
vector<int> num; // 将 n 的每一位存在 num 中
while (n)
{
num.push_back(n % 10); // 低位 --> 高位
n /= 10;
}
n = num.size(); // n = n 的位数
int res = 0; // 出现的次数
// 如果 x == 0 ,那么 0 在最高位不存在,所以从 最高位 - 1 开始计算
for (int i = n - 1 - !x; i >= 0; i -- ) // x 在第 i 位出现的次数
{
if (i < n - 1)
{
res += get(num, n - 1, i + 1) * power10(i); 求 xxx 中出现的次数
if (!x) res -= power10(i); // 如果 x 等于 0
}
if (num[i] == x) res += get(num, i - 1, 0) + 1;
else if (num[i] > x) res += power10(i);
//else res += 0;
}
return res;
}
int main()
{
int a, b;
while (cin >> a >> b , a)
{
if (a > b) swap(a, b); // 小数在前,大数在后
for (int i = 0; i <= 9; i ++ ) // 统计 0 ~ 9 出现的次数
cout << count(b, i) - count(a - 1, i) << ' '; // 用前缀和的思路求解
cout << endl;
}
return 0;
}
6.状态压缩DP(用一个整数表示一个状态,二进制数表示)
(1)蒙德里安的梦想
![](https://img-blog.csdnimg.cn/img_convert/68aacb794f13493796d526e23c5f523b.png)
切割数 = 横向摆放小方格的方法数
![](https://img-blog.csdnimg.cn/img_convert/ec9ff2240e37402aacf3eadaadc33fb5.png)
朴素写法(1000ms)
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << N; // M = 2^N
int n, m;
long long f[N][M]; // 存储状态
bool st[M]; // 判断每列是否合法
int main()
{
while (cin >> n >> m, n || m)
{
for (int i = 0; i < 1 << n; i ++ ) // 预处理 st[] ,判断每列相邻 0 的个数是不是偶数
{ // i < 2^n
int cnt = 0;
st[i] = true;
for (int j = 0; j < n; j ++ )
if (i >> j & 1) // 等价于 i 除以 2^j ,然后与 1 做与运算
{ // 如果最后一位是 1
if (cnt & 1) st[i] = false;// 判断 cnt 是不是偶数
cnt = 0;
}
else cnt ++ ;
if (cnt & 1) st[i] = false;
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= m; i ++ )
for (int j = 0; j < 1 << n; j ++ )
for (int k = 0; k < 1 << n; k ++ )
if ((j & k) == 0 && st[j | k]) // j 和 k 在相邻列是合法的
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/b4a36c65f4dc410283efbabc3ed11bb7.png)
视频中提前将 j 和 k 相邻列合法的情况提取出来
(2)最短Hamilton路径
![](https://img-blog.csdnimg.cn/img_convert/7adf45a8cb4042fd91d64b43e19557e6.png)
#include <cstring> // memset(f, 0x3f, sizeof f)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20, M = 1 << N;
int n;
int w[N][N];
int f[M][N];
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ )
cin >> w[i][j]; // 权重
memset(f, 0x3f, sizeof f); // 初始化,将距离初始化为正无穷
f[1][0] = 0;
for (int i = 0; i < 1 << n; i ++ ) // i 和 j 枚举每种状态
for (int j = 0; j < n; j ++ )
if (i >> j & 1) // 如果从 0 走到 j ,那么 i 里一定要包含 j
for (int k = 0; k < n; k ++ ) // 枚举所有转移的状态,从哪个点转移过来
if (i >> k & 1) // if ((i - (1 << j)) >> k & 1)
// 第 k 位一定要是 1 // 如果想从 k 转移过来,那么 i 除去 j 这个点之后,一定要包含 k
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
cout << f[(1 << n) - 1][n - 1] << endl;
return 0;
}
7.树形DP
(1)没有上司的舞会(深搜)
![](https://img-blog.csdnimg.cn/img_convert/11cdb19003694bfd8a25bb4759fe7188.png)
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 6010;
int n;
int h[N], e[N], ne[N], idx;
int happy[N];
int f[N][2];
bool has_fa[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
f[u][1] = happy[u];
// i != -1
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][1] += f[j][0];
f[u][0] += max(f[j][0], f[j][1]);
}
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &happy[i]);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b;
scanf("%d%d", &a, &b); // b 是上司
add(b, a); // b --> a
has_fa[a] = true; // a 有上司
}
int root = 1;
while (has_fa[root]) root ++ ; // 找根节点
dfs(root);
printf("%d\n", max(f[root][0], f[root][1]));
return 0;
}
8.记忆化搜索(递归)
(1)滑雪
![](https://img-blog.csdnimg.cn/img_convert/dfd2bd4da71c4537b48bf0c6c91ed174.png)
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n, m;
int g[N][N]; // 高度
int f[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; // 偏移量,四个方向
int dp(int x, int y)
{
int &v = f[x][y]; // 简化代码,用 v 来表示这个状态,C++ 特性,等价于 f[x][y]
if (v != -1) return v; // v 算过了,就返回
v = 1; // 初始化,最小值是 1
for (int i = 0; i < 4; i ++ ) // 枚举四个方向
{
int a = x + dx[i], b = y + dy[i];
// 在棋盘范围内 // 高度要下降
if (a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
v = max(v, dp(a, b) + 1);
}
return v;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
scanf("%d", &g[i][j]);
memset(f, -1, sizeof f);
int res = 0;
for (int i = 1; i <= n; i ++ ) // 枚举从每个点出发
for (int j = 1; j <= m; j ++ )
res = max(res, dp(i, j));
printf("%d\n", res);
return 0;
}
记忆化搜索的优点
代码复杂度 低
思路简单