文章目录
DP专题
一、线性DP——数字三角形模型
1~5题具体请看: 动态规划——数字三角形模型(线性DP)_塔塔开!!!的博客-优快云博客
1.数字三角形
【代码实现】
注意看题目:−10000≤三角形中的整数≤10000
所有要初始化边界!!!
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int a[N][N];
int f[N][N];
int 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 ++ )
for (int j = 0; j <= i + 1; j ++ )// 注:j 从 0 到 i + 1(下一层最后一个位置上放的状态来自此时i + 1)
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], f[i - 1][j]) + a[i][j];
// 遍历最后一层求最大值
int res = -INF;
for(int j = 1; j <= n; j ++)
res = max(res, f[n][j]);
cout << res;
return 0;
}
DFS深搜:
从起点深向下或者向右下搜到最后一层求解最大和,毫无疑问会超时
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int a[N][N];
int f[N][N];
int n;
int ans = -INF;
void dfs(int x, int y, int sum)
{
// 递归出口
if(x > n)
{
ans = max(ans, sum);
return;
}
// 向下走或者右下走
dfs(x + 1, y, sum + a[x + 1][y]);
dfs(x + 1, y + 1, sum + a[x + 1][y + 1]);
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
dfs(1, 1, a[1][1]);
cout << ans;
return 0;
}
记忆化搜索(DP的另一种实现方式)
- 求解每一个点的值,先判断该点的值是否曾经求解过,如果曾经求解过,直接拿过来使用;如果没求解过,递归求解,并存储该解!
- 将计算过的值存储到一个数组中(当用到时直接取用,避免大量重复计算)
- 如何判断是否求解过呢?——做标记判断
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int a[N][N];
int f[N][N];// 存储记录计算过的结果
int n;
int dfs(int x, int y)
{
// 最后一层答案即为自己(递归的初始必要已知条件)
if(x == n) return f[x][y] = a[x][y];
// 之前计算过了,直接用
if(f[x][y] != -1) return f[x][y];
//求解(x,y)点走到底层经过的数字和的最大值,并存储
int t = a[x][y] + max(dfs(x + 1, y), dfs(x + 1, y + 1));
return f[x][y] = t;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
memset(f, -1, sizeof f);// 标记
int ans = dfs(1, 1);
cout << ans;
return 0;
}
2.摘花生
3.最低通行费
4.方格取数
5.地宫取宝
6.蓝桥杯 数字三角形
【题目链接】数字三角形 - 蓝桥云课 (lanqiao.cn)
本题的限制:
数字三角形变形,多了限制条件,向左下走的次数与向右下走的次数相差不能超过 1。
DFS深搜:
我们可以统计向下和向右下走的次数,在递归出口特判即可。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int a[N][N];
int n;
int ans;
// l_cnt:往左下走的次数
// r_cnt:往右下走的次数
void dfs(int x, int y, int l_cnt, int r_cnt, int sum)
{
if(x == n)
{
if(abs(l_cnt - r_cnt) <= 1)
ans = max(ans, sum);
return ;
}
dfs(x + 1, y, l_cnt + 1, r_cnt, sum + a[x + 1][y]);
dfs(x + 1, y + 1, l_cnt, r_cnt + 1, sum + a[x + 1][y + 1]);
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
cin >> a[i][j];
dfs(1, 1, 0, 0, a[1][1]);
cout << ans;
return 0;
}
DP:
数字三角形上的数都是 0 至 100 之间的整数。
因此可以不用初始化边界为-INF
向左下走的次数与向右下走的次数相差不能超过 1,这个条件如何满足?
不妨进行模拟,在满足条件下行走的方案情况:
当n
为奇数时:在满足条件的情况下,最后一步必定落在位置:[n][n / 2 + 1]
当n
为偶数时:在满足条件的情况下,最后一步必定落在位置:[n][n / 2]
或者[n][n / 2 + 1]
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, INF = 0x3f3f3f3f;
int f[N][N];
int a[N][N];
int n;
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
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], f[i - 1][j - 1]) + a[i][j];
int res = -INF;
if(n % 2 == 0)
res = max(f[n][n / 2], f[n][n / 2 + 1]);
else
res = f[n][n / 2 + 1];
cout << res;
return 0;
}
7.蓝桥杯 跳跃
【题目链接】跳跃 - 蓝桥云课 (lanqiao.cn)
dfs深搜:
本题限制:他不能走到行号比 r 小的行,也不能走到列号比 c 小的列。同时,他一步走的直线距离不超过 3。即他每一次跳跃可以选择多种不同跳跃方式从而到达某一可行点!
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int w[N][N];
int n, m;
int ans;
int dx[6] = {0, 0, 0, 1, 2, 3};
int dy[6] = {1, 2, 3, 0, 0, 0};
void dfs(int x, int y, int sum)
{
sum += w[x][y];
if(x == n && y == m)
{
ans = max(ans, sum);
return ;
}
// 枚举所有可能情况
for(int i = 0; i < 6; i ++)
{
int a = x + dx[i], b = y + dy[i];
if(a <= n && y <= m)
dfs(a, b, sum);
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
cin >> w[i][j];
dfs(1, 1, 0);
cout << ans;
return 0;
}
DP:
状态表示:
g[i][j]
表示到达此点的最大权值。
状态转移方程:
(a,b)表示由上一个点(i,j)到此时(a,b)点的多种可能情况
f[a][b] = max(f[a][b], f[i][j] + w[a][b])
表示求能够到达此点的所有点的权值最大值。
注:这题与第一、二的明显区别是,从起点走到(a,b)点而不再是从起点走到(i,j)点
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int w[N][N];
int f[N][N];
int n, m;
int ans;
int dx[6] = {0, 0, 0, 1, 2, 3};
int dy[6] = {1, 2, 3, 0, 0, 0};
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
cin >> w[i][j];
memset(f, -0x3f, sizeof f);
f[1][1] = w[1][1];
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
for(int k = 0; k < 6; k ++)
{
int a = i + dx[k];
int b = j + dy[k];
if(a >= 1 && a <= n && b >= 1 && b <= m)
{
f[a][b] = max(f[a][b], f[i][j] + w[a][b]);
}
}
cout << f[n][m];
return 0;
}
二、线性DP——LIS模型
1.最长上升子序列
【题目链接】895. 最长上升子序列 - AcWing题库
【代码实现】
时间复杂度:O(n * n)
#include<iostream>
using namespace std;
const int N = 1010;
int f[N];// f[i]:以a[i]为结尾的所有子序列的集合
int a[N];
int n;
int main()
{
cin >> 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;
return 0;
}
2.最长上升子序列 II
【题目链接】896. 最长上升子序列 II - AcWing题库
【代码实现】
思路:贪心 + 二分
时间复杂度:O(nlongn)
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int f[N];// 存放上升子序列长度为i的上升子序列的末尾数值最小为几
int a[N];
int n;
int len;
int find(int x)
{
int l = 1, r = len;
while(l < r)
{
int mid = l + r >> 1;
if(f[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
f[1] = a[1];
len = 1;
for(int i = 2; i <= n; i ++)
{
if(a[i] > f[len]) f[++ len] = a[i];// 如果a[i]大于f[]中的最后一个数,直接拼接,长度加1
else
{
int idx = find(a[i]);// 在f[]数组中找到第一个大于等于a[i]的数的位置
f[idx] = a[i];// 然后替换
}
}
cout << len;
return 0;
}
3.怪盗基德的滑翔翼
【题目链接】1017. 怪盗基德的滑翔翼 - AcWing题库
【代码实现】
求倒V
形某一边的最长上升子序列。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int f[N];
int a[N];
int main()
{
int T;
cin >> T;
while(T --)
{
int n;
cin >> 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]);
}
memset(f, 0, sizeof f);
for(int i = n; i >= 1; i --)
{
f[i] = 1;
for(int j = n; 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;
}
4.登山
【题目链接】1014. 登山 - AcWing题库
求倒V
形两边的上升子序列的最大(和)。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N];
int g[N];
int a[N];
int n;
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
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);
}
for(int i = n; i >= 1; i --)
{
g[i] = 1;
for(int j = n; j > i; j --)
if(a[i] > a[j])
g[i] = max(g[i], g[j] + 1);
}
int res = 0;
for(int i = 1; i <= n; i ++) res = max(res, f[i] + g[i] - 1);// 记得减去重合的顶点
cout << res;
return 0;
}
5.合唱队形
【题目链接】482. 合唱队形 - AcWing题库
思路:求最长上升子序列的扩展应用题,以每一个数为中心,求取它各个数的左右两边的最长递增子序列是多少,剩下的就是要筛掉的最少人数。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int a[N];
int f[N], g[N];
int main()
{
int n;
cin >> 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);
}
for(int i = n; i >= 1; i -- )
{
g[i] = 1;
for(int j = n; j > i; j --)
if(a[i] > a[j])
g[i] = max(g[i], g[j] + 1);
}
for (int i = 1; i <= n; i ++ ) res = max(res, f[i] + g[i] - 1);
cout <<n - res;
return 0;
}
6.最大上升子序列和
【题目链接】1016. 最大上升子序列和 - AcWing题库
f[i]
表示前i
个数中的最大子序列和
状态转移:
对于每一个小于a[i]
的a[j] (j < i)
f[i] = max(f[i], f[j] + a[i])
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int a[N];
int f[N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
int res = 0;
for (int i = 1; i <= n; i ++ )
{
f[i] = a[i];
for (int j = 1; j < i; j ++ )
if (a[i] > a[j])
f[i] = max(f[i], f[j] + a[i]);
res = max(res, f[i]);
}
cout << res;
return 0;
}
7.蓝桥杯 游园安排
三、线性DP——LCS模型
1.最长公共子序列
【题目链接】897. 最长公共子序列 - AcWing题库
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N];
char a[N], b[N];
int n, m;
// f[i][j]:a序列中前i个数与b序列中前j个数的公共序列的所有集合
// 属性:max
int main()
{
cin >> n >> m;
cin >> a + 1 >> b + 1;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
if(a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
else
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
cout << f[n][m];
return 0;
}
2.蓝桥杯 密码脱落
【题目链接】密码脱落 - 蓝桥云课 (lanqiao.cn)
思路:将原串翻转,然后求原串与新串的最长公共子序列,那么剩下的即为要脱落的个数。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N];
char a[N], b[N];
int main()
{
cin >> a + 1;
int n = strlen(a + 1);
for(int i = 1, j = n; i <= n; i ++, j --) b[i] = a[j];// 翻转
for (int i = 1; i <= n; i ++ )
for(int j = 1; j <= n; j ++)
{
if(a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
else
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
cout << n - f[n][n];
return 0;
}
3.蓝桥杯 蓝肽子序列
【题目链接】蓝肽子序列 - 蓝桥云课 (lanqiao.cn)
思路:
最最长公共子序列的长度,但元素不再是单个元素,而是由字符串看成一个元素,预处理出来然后LCS即可。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N];
string a[N], b[N];
int cnt1, cnt2;
int main()
{
string s1, s2;
cin >> s1;
cin >> s2;
int len1 = s1.size(), len2 = s2.size();
// 预处理字符串————使得每一个子钛为一个元素
for (int i = 0; i < len1; i ++ )
{
if(s1[i] >= 'A' && s1[i] <= 'Z')
{
int j = i + 1;
while(s1[j] >= 'a' && s1[j] <= 'z') j ++;
string str = s1.substr(i, j - i);
a[++ cnt1] = str;
}
}
for (int i = 0; i < len2; i ++ )
{
if(s2[i] >= 'A' && s2[i] <= 'Z')
{
int j = i + 1;
while(s2[j] >= 'a' && s2[j] <= 'z') j ++;
string str = s2.substr(i, j - i);
b[++ cnt2] = str;
}
}
// for(int i = 1; i <= cnt1; i ++) cout << a[i] << ' ';
// puts("");
// for(int i = 1; i <= cnt2; i ++) cout << b[i] << ' ';
// 求a数组与b数组的LCS
for (int i = 1; i <= cnt1; i ++ )
for (int j = 0; j <= cnt2; j ++ )
{
if(a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
else
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
cout << f[cnt1][cnt2];
return 0;
}
四、其它
1.最短编辑距离
【题目链接】902. 最短编辑距离 - AcWing题库
思路:
有三个操作,因此有三个子集!
状态表示
dp[i][j]
- 集合 : 所有吧a中的前i个字母 变成 b中前j个字母的集合的操作集合
- 属性 : 所有操作中操作次数最少的方案的操作数
状态计算
状态划分 以对a中的第i个字母操作不同划分
在该字母之后添加
- 添加一个字母之后变得相同,说明没有添加前a的前i个已经和b的前j-1个已经相同
- 即 :
dp[i][j] = dp[i][j-1] + 1
删除该字母
- 删除该字母之后变得相同,说明没有删除前a中前i-1已经和b的前j个已经相同
- 即 :
dp[i][j] = dp[i-1][j] + 1
替换该字母
- 替换存在两种情况:结尾字母不同;结尾字母相同;
- 若
a[i] == b[j]
,啥也不做,即:dp[i][j] = dp[i-1][j-1]
- 若应结尾字母不相同,直接替换即可
即:dp[i][j] = dp[i-1][j-1] + 1
时间复杂度:O(n*n)
【代码实现】
#include <iostream>
#include <cstring>
#include<cstdio>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N][N];// dp[i][j] :a中1~i与b中1~j相等需要的最少操作次数
char a[N], b[N];
int n, m;
int main()
{
scanf("%d%s", &n, a + 1);// 字符串从下标1开始输入
scanf("%d%s", &m, b + 1);
//边界:当只有字符串b或者a时,只能通过插入操作完成
for(int i = 0; i <= m; i ++) dp[0][i] = i;
for(int i = 0; i <= n; i ++) dp[i][0] = i;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
if(a[i] == b[j]) dp[i][j] = min(dp[i][j], dp[i - 1][j - 1]);
else dp[i][j] = min(dp[i][j], dp[i - 1][j - 1] + 1);
}
printf("%d", dp[n][m]);
return 0;
}
参考文献:
1.acwing算法基础课、提高课
2.蓝桥杯题库