这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天。
今天看的视频课内容比较多,笔记还没整理好,先发一个学习动态规划的笔记吧,明后天补上今天笔记。
闫式DP法
动态规划问题本质:有限集中最值问题。 动态规划:
-
状态表示 - 化零为整
-
集合 f(i, j)
- 一般描述:所有只考虑前i个物品,且总体积不超过j的选法的集合
-
属性:集合中每个方案的 max / min / count 值
-
-
状态计算 - 化整为零
-
划分为若干个不重复、不遗漏的子集来求解
划分依据:寻找最后一个不同点
-
例题
01背包问题
朴素版本
动态规划过程:
-
状态表示 - 化零为整
-
集合 f(i, j)
- 一般描述:所有只考虑前i个物品,且总体积不超过j的选法的集合
-
属性:max,当前情况下背包内物品价值 所以集合f(i, j)划分为:
- 所有不选第i个物品的方案 -- 1到i-1物品,总体积不超过j。f(i, j) = f(i-1, j)
- 所有选择第i个物品的方案 -- 1到i物品,总体积不超过j。因为1到i-1已经不变了,所以关键是选出第i个使得值最大f(i, j) = f(i-1, j-vi) + wi 即二维数组迭代公式1:f(i, j) = max (f(i-1, j), f(i-1, j-v_i) + w_i)
-
-
状态计算 - 化整为零
- 划分为若干个不重复、不遗漏的子集来求解
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][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 = 0; j <= m; j++) // j表示已经容纳物品的体积<=j
{
f[i][j] = f[i-1][j];
if (j - v[i] >= 0)
f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
优化方法(空间优化)
优化方式:f(i, j)的二维数组可以优化成一维数组 观察式子可以发现,第i行的值只依赖于第i-1行的数据,所以可以采用滚动数组的方式(通过一维数组更新存储数据)。 同时可以发现,第j列的值依赖于第j列和第j-vi列的数据,那么j可以从大到小遍历,这样由于滚动数组更新从大到小进行,所以遍历到j的时候,j-vi列的数据还是第i-1行时的数据。 优化后方程f[j] = max(f[j], f[j-v[i]] + w[i]);
#include<iostream>
using namespace std;
const int N = 1010;
int n, V;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> V;
for(int i = 1; i <= n; ++ i)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; ++ i)
for(int j = V; j >= 0; j--) // j表示已经容纳物品的体积<=j
{
f[j] = f[j];
if (j - v[i] >= 0)
f[j] = max(f[j], f[j-v[i]] + w[i]);
}
cout << f[V] << endl;
return 0;
}
完全背包问题
-
状态表示 - 化零为整
-
集合 f(i, j)
- 一般描述:所有只考虑前i个物品,且总体积不超过j的选法的集合
-
属性:max,当前情况下背包内物品价值 所以集合f(i, j)划分为:
- 所有不选第i个物品的方案 -- 1到i-1物品,总体积不超过j。f(i, j) = f(i-1, j)
- 选择1个第i个物品的方案 -- 1到i物品,总体积不超过j。因为1到i-1已经不变了,所以关键是选出第i个使得值最大f(i, j) = f(i-1, j-vi) + wi
- 选择2个第i个物品的方案
- 选择3个第i个物品的方案
- ...
- 选择k个第i个物品的方案 即二维数组迭代公式2:
-
-
状态计算 - 化整为零
- 划分为若干个不重复、不遗漏的子集来求解
由于存在三重循环,估算可知,可能会TLE,所以需要优化时间。
优化时间
由二维数组迭代公式可以,当j = j - v时,可以得到公式3:
由于,由公式3得到j - k * v_i>0,所以f(i-1, j-(k+1)v_i) + kw_i最后一项不用考虑,因此这个公式变为:
可以发现,他与公式2后半部分只差了一个w_i,所以可以优化公式3得到公式4:
公式表面意思是:划分为两个子集,第一个子集不取第i个物品,第二个子集表示至少取一个第i个物品。
同时近似01背包问题,优化空间以后可以得到最终公式5:
不过,此时的j要从小到大遍历,因为第二项是f(i, j-v_i)即更新后的数。
#include<iostream>
using namespace std;
const int N = 1010;
int n, V;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> V;
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 <= V; j++) // j表示已经容纳物品的体积<=j
{
f[j] = max(f[j], f[j- v[i]] + w[i]);
}
cout << f[V] << endl;
return 0;
}
石子合并问题(区间型DP)
282. 石子合并 - AcWing题库 题目分析:
- 由于所有中合并情况共计!n种方法,即是有限的(虽然很大),而问题求解的是最小代价。所以有限集的最值问题 —>DP
- 由于每次只能合并相邻的两堆石子,所以对于一个区间而言,最后一次合并一定是左右两个区间进行合并,DP要从这里入手。 DP:
-
状态表示 - 化零为整(用一个集合包含非常多的情况)
-
集合 f(i, j)
- 一般描述:所有将[i, j]这个区间内的石子合并成一堆的方案的集合
-
属性:min
-
-
状态计算 - 化整为零(对集合中情况分为不同子集来计算)
-
划分为若干个不重复、不遗漏的子集来求解。以分解点为划分依据:
- 最后合并的两堆:[i, i+1] [i+1, j]
- 最后合并的两堆:[i, i+2] [i+2, j]
- ...
- 最后合并的两堆:[i, j-1] [j-1, j] 即我们的任务是让最后合并的两堆分别求min,然后最后整体min即为其之和。
-
区间型DP问题: 一般来说,两个循环,第一层循环遍历区间长度,第二层循环从区间最左侧元素开始往后遍历。
// https://www.acwing.com/problem/content/description/284/
// 石子合并
#include <climits>
#include <iostream>
using namespace std;
const int N = 340;
int s[N]; // 质量前缀和
int f[N][N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> s[i], s[i] += s[i - 1];
for (int len = 2; len <= n; ++len)
{
for (int i = 1; i + len - 1 <= n; i++)
{
int j = i + len - 1;
f[i][j] = INT_MAX;
// 分界点 k
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]);
// s[i]为前i个元素质量之和 s[j] - s[i-1]可以得到合并 i到k 和 k+1到j 两堆的代价
}
}
}
cout << f[1][n] << endl;
return 0;
}
最长公共子序列
剑指 Offer II 095. 最长公共子序列 - 力扣(LeetCode) (leetcode-cn.com) DP:
-
状态表示 f(i, j)
-
集合 所有A[1到i]与B[1到j]的公共子序列的集合
-
A[i-1]和B[j-1]的最大公共子序列 不包含A的第i个元素和B的第j个元素情况
-
A[i-1]和B[j]的最大公共子序列 不包含A的第i个元素
-
A[i]和B[j-1]的最大公共子序列 不包含B的第j个元素
-
A[i]和B[j]的一定包含A的第i个元素和B的第j个元素的最大公共子序列 if(A[i] ==B[j]) A[i-1]和B[j-1] + 1
- 属性 max
-
-
状态计算