线性DP
原题目均来自:https://www.acwing.com/about/
数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数,表示数字三角形的层数。
接下来行,每行包含若干整数,其中第 行表示数字三角形第 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
思路:
个人感觉这应该是DP里边最简单的一道题了,这里表示的就表示以当前点为结尾的所有路径之和集合,而我们需要的集合属性是,而是从它的左上角和右上角转移而来,即, ,状态转移方程:
- 不要忘了初始化
- 最后对最后一行取,得到需要的最大值
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 1e9;
int a[N][N];
int f[N][N];
int n;
int main()
{
ios::sync_with_stdio(false), cin.tie(0);
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++)
{
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] + a[i][j], 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;
}
最长上升子序列
给定一个长度为的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数。
第二行包含个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
思路:
用表示以当前数字结尾的所有子序列长度的集合,集合的属性就是,怎么进行状态计算呢?
对于,以最长子序列的倒数第二个数分类,而最长子序列的倒数第二个数的下标可以是从中任何一个,用表示,因为在之前的都已经被同样的方式更新过了,所以对于每一个我们从小到大枚举,如果那么就可以被更新,因为表示的是以下标的数字结尾的最长上升子序列长度,所以状态转移方程:
- 的初始值是,因为它最少就只有它自己一个数字
- 最后再枚举一遍, 取
时间复杂度:
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, a[N], f[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[j] < a[i]) {
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;
}
如果想要记录出最长上升子序列是啥,就只需要记录出每一步转移是从什么时候转移的就可以了,最后倒序输出,有点类似与迷宫的记录路径。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, a[N], f[N], g[N]; //g数组保存每个转移是怎么做出来的
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
f[i] = 1;
g[i] = 0; //表示只有一个数
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) {
if(f[i] < f[j] + 1) {
f[i] = f[j] + 1;
g[i] = j; //记录一下当前i是由哪个j转移过来的
}
}
}
}
int k = 1;
for (int i = 1; i <= n; i++) {
if (f[k] < f[i]) {
k = i; //找到最长子序列的末尾
}
}
//倒序输出
for (int i = 0, len = f[k]; i < len; i++) { //f[k] 存的是最长子序列的长度
cout << a[k] << endl;
k = g[k]; //因为k记录了i的转移过程
}
return 0;
}
最长公共子序列
给定两个长度分别为和的字符串和,求既是的子序列又是的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数和。
第二行包含一个长度为的字符串,表示字符串。
第三行包含一个长度为的字符串,表示字符串。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
输入样例:
4 5
acbd
abedc
输出样例:
3
思路:
用表示所有在第一个序列的前个字母中出现,且在第二个序列中的前个字母中出现的子序列,集合属性是,那怎么进行状态计算呢?
我们可以把集合分成四类:
- 不选, ,即表示成状态就是,即从第一个序列的前,第二个序列中前个字母出现的公共子序列。
- 选, ,即,前提是当前,需要特殊判断
还剩下两种情况分别为:不选, 选,和,选, 不选,这两种情况怎么表示呢,它们等价于, 吗,看定义:
- 表示的是所有第一个序列的前个字母出现,且在第二个序列中的前个字母中出现的子序列,所以包含了这里的不选, 选的这种情况,但是好在因为是求最大值,所以我们完全可以用来涵盖这种不选, 选的情况,用分别表示选和不选即:, 那么整个
- 表示与上同理。
要注意的是:
,即它们之间是有交集的,但是只有一个,因此并不影响,还有就是既然有交集,那么可以看出,, 因此在写代码的时候完全可以把省略掉啦!
时间复杂度:
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
char a[N], b[N];
int f[N][N]; //i表示从a串中前i个字母中选的集合, j表示从b串中前j个字母中选的集合
int n, m;
int main() {
cin >> n >> m;
cin >> a >> b;
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]) { //特别判断a[i] == b[j]才会有f[i-1][j-1] + 1
f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
}
}
}
cout << f[n][m] << endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端