y总的讲解视频 https://www.bilibili.com/medialist/play/watchlater/BV1X741127ZM
关键词
- 从集合角度来分析
- 有限集中的最优化问题(最大值/最小值/个数/存在与否)
- 自然的思路是指数级的,需要优化
- 先化零为整,将一些有共同特征的元素化为一个子集,用 特定的状态 来表示,考察集合是什么,表示集合的元素存的内容是什么(一般情况就是题目问的对象)
- 再化整为零,即 状态计算, 寻找最后一个不同点,将集合划分为若干个子集,保证不遗漏,不重复(视情况而定)
- 始终抓住 集合的定义
0-1背包问题
//朴素写法 时间O(n^2) 空间O(n^2)
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N]; //f[i][j]表示所有只考虑前i个物品且总体积不超过j的方案中价值的最大值
//递推公式
//f[i][j] = max( f[i-1][j], (f[i - 1][j - v[i]] + w[i]) )
//分成两个子集:1.包含最后一个物品; 2.不包含最后一个物品
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> 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]); //在背包容量大于v[i]的前提下,才能递推
}
}
cout << f[n][m] << endl; //最大价值是考虑所有n个物品且总体积不超过m的方案中的最大价值
return 0;
}
//优化空间复杂度写法 时间O(n^2) 空间O(n)
//滚动数组,去掉第一维,因为每次计算当前层时,只需要用到上一层的结果
//dp问题的所有优化,都是对代码做等价变形
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i ++ ) {
for(int j = m; j >= v[i]; j -- ) { //这儿j从大到小循环,保证了下面求递推时,f[j - v[i]]是上一层循环计算出来的值,即f[i - 1][j - v[i]],在这层还未被更新
if(j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
完全背包问题
/*
1. 01背包: f[i][j] = max(f[i - 1][j], f[i - 1][j - vi] + wi)
2. 完全背包:f[i][j] = max(f[i - 1][j], f[i][j - vi] + wi);
*/
//朴素写法是需要一个二维矩阵来表示状态的,但是通过观察分析模拟过程,我们发现空间是可以复用的,很早之前的状态后续不再需要
//我们只需要保留后续计算还需要的值,可以用一个“滚动数组”来替代二维数组
//背包问题如果用二维矩阵记录状态,第二层的循环顺序不受限制,如果优化成滚动数组,则01背包从大到小循环,完全背包从小到大
//具体来讲,考察当前计算所需的那个状态需不需要更新
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N]; // 在考虑边界和状态转移时始终抓住这点定义!!!
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i ++ ) {
for(int j = v[i]; j <= m; j ++ ) { //与0-1背包唯一一句区别
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
石子合并(区间dp)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n;
int s[N];
int f[N][N]; //f[i][j]表示将[i, j]合并成一堆的方案的集合中的最小代价
int main() {
cin >> n;
for(int i = 1; i <= n; i ++ ) cin >> s[i];
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] = 2e9;
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]);
}
cout << f[1][n] << endl;
return 0;
}
最长公共子序列
//求数量要求不重不漏,求最值得时候可以重复
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N]; //f[i][j]表示所有 a[1 - i]与 b[1 - j]的公共子序列的集合当中的最大长度
int main() {
cin >> n >> m;
scanf("%s%s", a + 1, b + 1);
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 - 1][j - 1] + 1, f[i][j]);
}
}
cout << f[n][m] << endl;
return 0;
}