本节目录
- 线性DP
- 区间DP
1. 线性DP
递推顺序是线性的
898. 数字三角形
分析
见寒假每日一题
code
#include <iostream>
using namespace std;
const int N = 510;
int n, a[N][N];
int main(){
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
cin >> a[i][j];
for (int i = n - 1; i >= 1; i -- )
for (int j = 1; j <= i; j ++ )
a[i][j] += max(a[i + 1][j], a[i + 1][j + 1]);
cout << a[1][1] << endl;
return 0;
}
895.最长上升子序列
分析
code
#include <iostream>
using namespace std;
const int N = 1010;
int a[N], n;
int f[N];
int main(){
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
cin >> a[i];
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]);
}
cout << res << endl;
return 0;
}
将最长上升子序列保存下来
用g[N]
保存下, 每个转移是怎么转移过来的
code(带方案)
#include <iostream>
using namespace std;
const int N = 1010;
int a[N], n;
int f[N];
int g[N];
int main(){
scanf("%d", &n);
for (int i = 1; i <= n; i ++ )
cin >> a[i];
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])
if (f[j] + 1 > f[i]){
f[i] = f[j] + 1;
g[i] = j; // g[i] 保存i是从哪个状态转移过来的
}
}
// 找出最优方案
int k = 1;
for (int i = 1; i <= n; i ++ )
if (f[k] < f[i])
k = i;
cout << f[k] << endl;
for (int i = 1, len = f[k]; i <= len; i ++ ){
// 因为最优方案长度为f[k], 所以建个变量len, 而且k在循环中会发生变化, 一定要新建变量
cout << a[k] << ' '; // f[k]的定义就是以a[k]结尾的最长上升子序列, 先输出a[k]
k = g[k]; // 再考虑k是从哪个方案转移过来的, 令k = g[k];
}
return 0;
}
896. 最长上升子序列 II (习题课)
分析
7
3 1 2 1 8 5 6
我们之前求的时候, 比如求以8结尾的最长上升子序列的长度, 记成f(5)
然后求的时候, 枚举下倒数第2个数是哪个数(以倒数第2个数为分类依据)
分别求下每一类的最大值, 然后对每一类取max
然后考虑下每次求的时候, 没有没冗余
比如 3 1两个数, 后面的每一个数, 如果可以接到3的后面, 那么肯定可以接到1的后面, 比方说8可以接到3的后面, 8也可以接到1的后面, 因为1比3更小
所以可以发现3开头的这个上升子序列, 就没有必要存了, 1比3更好(1比3的适用范围更大)
假设当前求到了第i个数, 前面所有的最长上升子序列可以按长度分类,
长度是1的上升子序列, 可以存一个结尾最小的
长度是2的…, 可以存一个结尾最小的
因为可以接到大的数后面的话, 一定可以接到小的数后面, 因为小的数适用范围更广
受此启发, 可以将前面每种最长上升子序列结尾最小值存到数组里去
所有不同长度的最长上升子序列, 结尾值可以存下来
猜测: 随着长度增加, 结尾的值一定单调增加
也就是猜测: 长度越长的话, 结尾的最小值一定越大
证明:
如果长度是6的最长上升子序列和长度是5的最长上升子序列, 结尾最小值一样的话; 更有甚者, 或者说长度是6的结尾更小
看下, 长度是6的上升子序列, 第5个数一定比当前结尾值要小, 也就是我们找到了长度是5的最长上升子序列的结尾, 比我们当前存的长度是5的最长上升子序列结尾值要小, 那就矛盾了, 因为我们存的是长度是5的最长上升子序列的最小值
因此, 我们证明了长度是6的最长上升子序列结尾比长度是5的最长上升子序列结尾值要大
综上所述, 如果存在前面一个长度比较小的上升子序列结尾值比后面要大, 就会矛盾
因此整个序列是严格单调上升的
那么以a[i]
为结尾的最长上升子序列的长度, 应该怎么求呢
首先a[i]
可以接到比自己小的数的末尾的, 要想长度比较长的话, 将a[i]
接到最大的
<
a
[
i
]
<a[i]
<a[i]的数的后面数就可以啦
(简而言之, 就是找距离a[i]最近的, 且末尾值<a[i]的上升子序列)
比方说q[4]是最大的小于a[i]的上升子序列, 那么我们找到的接了a[i]后, 长度是5的最长上升子序列
并且q[4]是最大的<a[i]的数, q[5]>=a[i], 也就是说长度是5的最长上升子序列结尾一定>=a[i], 所以a[i]一定不能接到长度>=5的最长上升子序列后面
因此以a[i]为结尾的最长上升子序列就是4 + 1 = 5
如果找出<a[i]的最大的数呢, 可以用二分, 算完之后, 直接将a[i] 更新到q[5]
时间复杂度: 首先先要找到当前元素可以接到哪个上升子序列后面O(logn)
更新的话是O(1)
一共n个数, 每个数都要二分一下, 所以O(nlogn)
数据范围10,000, 总的也就10, 000 * log(100, 000) = 100, 000~200, 000
完全ok
code
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], n;
int q[N];
int main(){
cin >> n;
for (int i = 0; i < n; i ++ ) cin >> a[i];
q[0] = -1e9;
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;
}
len = max(len, r + 1);// 这里找到了左边最大的小于a[i]的数f[r], 此时的序列长度是原来的长度r 加上 1
q[r + 1] = a[i]; //长度是r + 1的严格上升子序列是以a[i]结尾的
}
cout << len << endl;
return 0;
}
897. 最长公共子序列
分析
00 : 表示不选a[i], 不选b[j]
11 : 选择a[i], 选择b[j]
01 : 不选a[i], 选b[j]
注意⚠️: f[i - 1][j]的定义: 所有在第1个子序列的前i个字母中出现过, 且在第2个字母的前j个字母中出现过
前j个字母中出现过, 并不代表b[j]一定出现
所以f[i - 1][j]严格包含01的情况, 而且f[i - 1, j]的所有情况一定包含在f[i][j]的情况中
即以下关系
01
⊂
f
[
i
−
1
]
[
j
]
⊂
f
[
i
]
[
j
]
01 \subset f[i - 1][j] \subset f[i][j]
01⊂f[i−1][j]⊂f[i][j]
其他情况类似, 因此对
f
[
i
−
1
]
[
j
−
1
]
,
f
[
i
−
1
]
[
j
]
,
f
[
i
]
[
j
−
1
]
,
f
[
i
−
1
]
[
j
−
1
]
+
1
f[i - 1][j - 1], f[i - 1][j], f[i][j - 1], f[i - 1][j - 1] + 1
f[i−1][j−1],f[i−1][j],f[i][j−1],f[i−1][j−1]+1取最大值就是
f
[
i
]
[
j
]
f[i][j]
f[i][j]的最大值
因为以上四种情况包括了f[i][j]的所有情况, 所以取max是正确的
就比如以下
求最大值分成的子集可以重复, 但是求数量分成的子问题, 一定不能重复
解释:代码中一般只分了3种情况的原因
虽然分成了4种情况, 但是在看最长公共子序列代码的时候, 第1种情况f[i - 1][j - 1]一般都不写, 因为f[i - 1][j - 1]这个子序列表示在a的前i - 1个子序列中出现, 在b的前j - 1个子序列中出现的子序列, 这些一定包含在了f[i - 1][j] 和f[i][j - 1] 中
f
[
i
−
1
]
[
j
−
1
]
⊂
f
[
i
−
1
]
[
j
]
f
[
i
−
1
]
[
j
−
1
]
⊂
f
[
i
]
[
j
−
1
]
f[i - 1][j - 1] \subset f[i - 1][j]\\ f[i - 1][j - 1] \subset f[i][j - 1]\\
f[i−1][j−1]⊂f[i−1][j]f[i−1][j−1]⊂f[i][j−1]
所以一般情况下, 看最长公共子序的代码的时候, 只有3种情况
code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int f[N][N];
char a[N], b[N];
int n, m;
int main(){
scanf("%d%d", &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][j], f[i - 1][j - 1] + 1);
}
cout << f[n][m] << endl;
return 0;
}
282. 石子合并
分析
区间dp, 状态表示的时候, 表示的是某一个区间
code
#include <iostream>
using namespace std;
const int N = 310, INF = 0x3f3f3f3f;
int f[N][N];
int w[N], s[N];
int n, m;
int main(){
cin >> n;
for (int i = 1; i <= n; i ++){
cin >> w[i];
s[i] = s[i - 1] + w[i];
}
for (int len = 2; len <= n; len ++)//区间dp首先枚举区间长度
for (int l = 1; l + len - 1 <= n; l ++){ //枚举左端点
int r = l + len - 1;
f[l][r] = INF;
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;
}
902. 最短编辑距离(习题课)
分析
删: 删掉a[i], 那么a[1~i] 与 b[1~j]匹配, 那么在此之前a[1~i - 1]与b[1~j]匹配, f[i - 1][j]
因此f[i][j] = min(f[i][j], f[i - 1][j] + 1);
增: 因为添加完a[i]后, a[i]和b[j]匹配, 所以添加的这个a[i]一定是b[j], 那么在添加a[i]之前, a[1 ~ i]一定已经匹配b[1 ~ j - 1], 所以前面一步是f[i][j - 1]
因此f[i][j] = min(f[i][j], f[i][j - 1] + 1);
改: 改完之后a[1 ~ i] 变成b[1~j], 那么改一定是a[i]变成b[j], 改的话可以分两种情况, 如果a[i]和b[j]相等了, 可以不用变, 那么其实不需要变; 如果不相等, 需要增加一步更改操作, 将a[i]变成b[j]
先不看最后一步操作, 应该让状态达到哪种程度, 应该先让a[1 ~ i - 1] 与 b[1 ~ j - 1]匹配上, 因此需要f[i - 1, j - 1]
做完之后, 再让a[i]变成b[j]
因此改的步骤f[i - 1][j - 1] + 1/0(视a[i] 是否= b[j])
code
#include <iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int n, m;
char a[N], b[N];
int main(){
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
for (int i = 1; i <= n; i ++ ) f[i][0] = i; // 将a[1 ~ i] 变成b[0]
for (int i = 1; i <= m; i ++ ) f[0][i] = i; // 将b[1 ~ i] 变成a[0]
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); // 注意 + 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);
}
cout << f[n][m] << endl;
return 0;
}
899. 编辑距离(习题课)
分析
就是应用上面的函数
code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
char str[N][N];
int n, m;
int f[N][N];
int edit_distance(char a[], char b[]){
int n = strlen(a + 1), m = strlen(b + 1);
for (int i = 1; i <= n; i ++ ) f[i][0] = i;
for (int i = 1; i <= m; i ++ ) f[0][i] = 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);
f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
}
return f[n][m];
}
int main(){
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ )
scanf("%s", str[i] + 1);
while (m -- ){
int cnt = 0, limit;
char c[1010];
scanf("%s %d", c + 1, &limit);
for (int i = 0; i < n; i ++ )
if (edit_distance(str[i], c) <= limit) cnt ++;
cout << cnt << endl;
}
return 0;
}
900. 整数划分(习题课)
分析
不考虑顺序的组合
4 = 1 + 2 + 1 = 2 + 1 + 1 = 1 + 1 + 2
这3种都是一样的
1.看成背包问题, 背包容量n, 物品的体积分别是1, 2, 3, …, n
求恰好装满背包的方案数, 每种物品用无限次, 因此是完全背包问题
因为不考虑顺序, 每种背包的选法都对应一种数字的划分方式
每种数字的划分方式都对应一种背包的选法
一一对应的, 所以方案是一样的
状态数量n^2, 转移数量O(n), 总时间复杂度不是O(n^3)
当体积是1的时候 s = 1000/ 1
2, s = 1000/ 2
…
1000, s = 1000/1000
1/1 + 1/2 + 1/3 + … + 1/n = log(n)
code
#include <iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N];
int n;
int main(){
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i ++ ) // i表示第i个物品的体积
for (int j = i; j <= n; j ++ ) // j 表示总体积
f[j] = (f[j] + f[j - i]) % mod;
cout << f[n] << endl;
return 0;
}
分析(按最小值为1划分)
f[i][j] : 所有总和是i, 并且恰好表示成j个数的和的方案
如果最小值是1, 那么可以将这个1去掉, f[i][j] 由 f[i- 1][j - 1]转化过来, 即:
和是i - 1, 并且恰好表示成j - 1个数的和
所有和是i - 1, 并且恰好表示成j - 1个数的和 + 1个1, 就是总和是i, 并且表示成j个数, 最小值是1的方案,
所以这两个集合是一一对应的
对于最小值 > 1的, 令每个数-1, 那么总和-j, 就变成f[i - j][j]
答案需要将f[n][1] + … f[n][n]
code
#include <iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N][N];
int main(){
scanf("%d", &n);
f[0][0] = 1;
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;
cout << res << endl;
return 0;
}
Week8 习题课 01:20:00
高精度压位