【算法基础】动态规划[二]——线性&区间dp(闫式dp分析法)
目录
- 一、线性DP
-
- 1、AcWing 898. 数字三角形
-
- 算法 1
- 算法2
- 2、AcWing895. 最长上升子序列 [DP]
- 3、AcWing 897. 最长公共子序列
- 4、最短编辑距离
- 5、编辑距离
- 二、区间DP
-
- AcWing 282. 石子合并
- 三、总结
前言
欢迎关注我的专栏,想写完整个基础课的题解hh,开干🚀🚀🚀
一、线性DP
1、AcWing 898. 数字三角形
题目链接
如果涉及到i-1
的下标,一般考虑从1
开始循环,保证数组不会越界。
算法 1
//思路一#include using namespace std;const int N = 510, INF = 1e9;int n;int f[N][N], a[N][N];int main ( ){ cin >> n; for (int i = 1; i <= n; i ++) { for (int j = 1; j <= i ; j ++){ cin >> a[i][j]; } } for (int i = 0; i <= n; i ++) { //需要多初始化俩个节点 0 开始到 i + 1 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 ++) {//涉及i-1 保证数组从1开始 for (int j = 1; j <= i; j++) { f[i][j] = max(f[i - 1][j - 1], f[i - 1][j ]) + a[i][j]; } } int res = -INF; //遍历每个尾结点 for (int i = 1; i <= n;i ++) { res = max(res, f[n][i]); } cout << res << endl; return 0;}
算法2
//思路二#include using namespace std;const int N = 510;int n;int f[N][N];int main ( ){ cin >> n; for (int i = 1; i <= n; i ++) { for (int j = 1; j <= i ; j ++){ cin >> f[i][j];//直接读入 相当于已经初始化了 } } for (int i = n - 1; i > 0; i--) { for (int j = 1 ; j <= i; j++) { f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + f[i][j]; } } cout << f[1][1] << endl; return 0;}
2、AcWing895. 最长上升子序列 [DP]
补知识点
- 子序列:可以不连续,但是相对位置得保持
- 最长公共子序列
(LCS)
给出"ABCD" 和 “EDCA”,这个LCS是 “A” (或 D或C),长度1
给出 “ABCD” 和 “EACB”,这个LCS是"AC"长度2
- 最长上升连续子序列(
LICS
)可以定义为从右到左
或从左到右
的序列。
eg.给定 [5, 4, 2, 1, 3], 其最长上升连续子序列为 [5, 4, 2, 1], 长度 4.
给定 [5, 1, 2, 3, 4], 其最长上升连续子序列为 [1, 2, 3, 4], 长度4. - 最长上升子序列:数值严格单调递增的子序列的长度最长
#include using namespace std;const int N = 1010;int f[N];int a[N];int main(){ int n; cin >> n; for (int i = 1; i <= n; i ++) { cin >> a[i]; } for (int i = 1; i <= n; i ++) { f[i] = 1; //初始化 只有a[i]一个 for (int j = 1; j < i; j ++) { if (a[i] > a[j]) { f[i] = max(f[i], f[j] + 1); } } } int res = 0; for (int i = 1; i <= n; i ++) { res = max(res, f[i]); } cout << res << endl; return 0;}
3、AcWing 897. 最长公共子序列
题目链接
- 如何 理解 01 和 10 ??(上图)
00
的情况涵盖在01
与10
中,因此不考虑f[i - 1 ][j - 1]
,最值不需要考虑重复,只需保证不漏即可。 - 涉及到
i -1
所以a
下标从1
开始 a+1
表示什么??
+1
表示a的地址往后移动1
个字符位的地址 [指针]- 时间复杂度
O(nm)
#include using namespace std;const int N = 1010;int f[N][N]; //开二维的 别问 问就是经验char a[N],b[N];int main(){ int n,m; cin >> n >> m >> a + 1>> b + 1 ;//i - 1 问题 +1 防止数组越界 //防溢出;也可以使用"' '+str" 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;}
4、最短编辑距离
- 集合 : 所有把
a
中的前i个字母 变成b
中前j
个字母的集合的操作集合 - 状态划分 以对
a
中的第i
个字母操作不同划分 - 添加
dp[i][j] = dp[i][j-1] + 1
(前a的前i个已经和b的前j-1
个已经相同) - 删除
dp[i][j] = dp[i-1][j] + 1
(a中前i-1已经和b的前j个已经相同) - 替换
dp[i][j] = dp[i-1][j-1] + 1
(前i-1
个已经相等,判断最后一个字母即可)
时间复杂度O(n2)
#include using namespace std;const int N = 1010;int n,m;char a[N],b[N];int f[N][N];int main () {scanf("%d%s", &n, a+1);scanf("%d%s", &m, b+1);for(int i = 0; i <= n; i ++) f[i][0] = i;//初始化 a for(int j = 0; j <= m; j ++) f[0][j] = j;//初始化 bfor(int i = 1; i <= n; i ++) {for(int j = 1; j <= m; j ++) {f[i][j] = min(f[i - 1][j] ,f[i][j - 1]) + 1; //取删or增的最优解 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;printf("%d\n", f[n][m]);return 0;}
5、编辑距离
题目链接
思路同上题一样
#include using namespace std;const int N = 20,M = 1010;char str[M][N];int f[N][N];int fz(char a[],char b[]) { int l1 =strlen(a + 1), l2 = strlen(b + 1);//注意 + 1 for (int i = 0; i <= l1; i ++ ) f[i][0] = i; for (int j = 0; j <= l2; j ++ ) f[0][j] = j; for (int i = 1; i <= l1; i ++ ){for (int j = 1; j <= l2; 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[l1][l2];}int main () { int n,m;cin >> n >> m;for (int i = 0; i < n; i ++) { cin >> (str[i] + 1); //注意}while (m --) { char a[N]; int b; cin >> (a + 1) >> b;//注意 int res = 0; for(int i = 0; i < n ; i ++) { if(fz(str[i],a) <= b) { res ++; } } cout << res << endl;}return 0;}
二、区间DP
AcWing 282. 石子合并
题目链接
石子合并
注意题目是每次只能合并相邻两堆, 与果子和并不同 [贪心]
前缀和:s[i] = s[i] + s[i - 1]
#include using namespace std;const int N = 310;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 ++) { //枚举长度(小>>大) 长度应为2开始枚举 for (int l = 1; l + len - 1 <= n; l ++) {//枚举 左端点 int r = l + 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]) ; } } } cout << f[1][n]; return 0;}
三、总结
- DP状态计算的依据就是寻找最后一步不同点
- 区间DP = DP + 前缀和
本文参考———y总的算法基础课