线性dp
线性dp指有线性关系的一类dp,但是有时候很难看出来是线性的。之前说过的迷宫类DP也属于这个范畴,所以当然,它的难度通常属于dp中最低的,放轻松
LIS(最长上升子序列)
I. 怎么解?
dp
Ⅱ. 解题思路?
状态表示:f[i]
集合表示:\(f[i]\)表示从\(1到i\)中的LIS长度
属性:max
状态转移:
for (int i = 1; i <= n; i ++ )
{
f[i] = 1;
for (int j = 1; j <= i; j ++ ) // 判断合法
if(m[j] < m[i])
f[i] = max(f[i], f[j] + 1);
}
完整代码奉上
#include <iostream>
#define INF 0x7f7f7f7f
using namespace std;
const int N = 1010;
int dp[N], a[N], ans = -INF;//DP
int main ()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i], dp[i] = 1;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j < i; j++)
{
if(a[i] > a[j])
{
dp[i] = max(dp[i], dp[j]+1);
}
}
ans = max(ans, dp[i]);
}
cout << ans;
return 0;
}
LCS(最长公共子序列)
I. 怎么解?
dp
Ⅱ. 解题思路?
状态表示:f[i][j]
集合表示:\(f[i][j]\)表示从\(a[i]\)到\(b[j]\)的LCS长度
属性:max
状态转移:分为四种情况:
if(a[i] == b[j])
{
就直接选a[i]和b[j],因此长度就是上次长度加一,
f[i][j] = f[i-1][j-1] + 1
}
else if(a[i] != b[j])
{
再分三种情况:a选b不选,a不选b选,ab都不选,最后取max
1. ab都不选
{
即保持之前的状态
f[i][j] = f[i-1][j-1]
}
2. a选b不选
{
即只考虑a[i]到b[j-1]
f[i][j] = f[i][j-1]
}
3. a不选b选
{
同上
f[i][j] = f[i-1][j]
}
}
实际上ab都不选的情况包含在2和3中,因此ab都不选可以不用考虑
完整代码奉上
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N], n, m;
char a[N], b[N];
int main()
{
scanf("%d%d", &n, &m);
scanf("%s%s", a+1, b+1); // 下标从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] = f[i-1][j-1] + 1;
}
printf("%d", f[n][m]);
return 0;
}
LCIS(最长公共子上升序列)
较难
题目描述
给定两个整数序列,写一个程序求它们的最长上升公共子序列。
当以下条件满足的时候,我们将长度\(N\)的序列\(S\_1,S\_2,...,S\_N\) 称为长度为\(M\)的序列\(A\_1,A\_2,...,A\_M\)的上升子序列:
存在\(1≤i_1\)
输入
每个序列用两行表示,第一行是长度\(M(1≤M≤500)\),第二行是该序列的\(M\)个整数\(A\_i(-2^{31}≤A\_i<2^{31} )\)
输出
在第一行,输出两个序列的最长上升公共子序列的长度\(L\)。在第二行,输出该子序列。如果有不止一个符合条件的子序列,则输出任何一个即可。
输入数据 1
5
1 4 2 5 -12
4
-12 1 2 4
输出数据 1
2
1 4
此题分为两问
Q1
第一问是典型的线性dp,是LIS和LCS的结合,但是状态的表示比较难想
状态表示:
f[i][j]
代表所有a[1 ~ i]
和b[1 ~ j]
中以b[j]
结尾的公共上升子序列的集合;
属性:max
状态转移:
for(i = 1 ~ n, j = 1 ~ m)
if(不包含a[i])
{
f[i - 1][j];
}
else
{
// 根据子序列倒数第二个数的取值继续划分集合
for(k ~ j - 1) // k为子序列倒数的第二个数
f[i - 1][k] + 1;
}
直接写出代码:
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j ++)
{
f[i][j] = f[i - 1][j];
if(a[i] == b[j]) // 是公共的
{
int mx = 1;
for(int k = 1; k <= j - 1; k ++)
if(a[i] > b[k] && mx < f[i - 1][k] + 1) // 是上升的
mx = f[i - 1][k] + 1;
f[i][j] = max(f[i][j], mx);
}
}
}
聪明的你也许发现了,这样需要三层循环,而实际上,我们可以将其优化至两层循环。
不难发现,每轮的mx
实际上都记录的是上一个阶段的,因此等价代换得到正解
for(int i = 1; i <= n; i ++)
{
int mx = 0;
for(int j = 1; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if(a[i] > b[j] && f[i - 1][j] > mx) mx = f[i - 1][j];
else if(a[i] == b[j]) f[i][j] = mx + 1;
}
}
Q2
关于第二问,我们只需记录每个状态的前驱,最后顺着前驱往前找即可
完整代码奉上
#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 500;
typedef long long LL;
// init
struct qaq
{
int num, nx1, nx2;
}f[N][N]; // 存前驱
int a[N], b[N], ans2[N], idx;
int main()
{
// input
int n1, n2;
cin >> n1;
for(int i = 1; i <= n1; i ++) cin >> a[i];
cin >> n2;
for(int i = 1; i <= n2; i ++) cin >> b[i];
swap(a, b);
swap(n1, n2);
// dp
for(int i = 1; i <= n1; i ++)
{
qaq maxv = (qaq){0, 0, 0};
for(int j = 1; j <= n2; j++)
{
f[i][j] = (qaq){f[i - 1][j].num, i - 1, j};
if(a[i] > b[j] && f[i - 1][j].num > maxv.num) maxv = (qaq){f[i - 1][j].num, i - 1, j};
else if(a[i] == b[j]) f[i][j] = (qaq){maxv.num + 1, maxv.nx1, maxv.nx2};
}
}
// ans_1
int ans = 0, temp;
for(int i = 1; i <= n2; i++)
{
if(ans < f[n1][i].num)
{
ans = f[n1][i].num;
temp = i;
}
}
cout << ans << endl;
if(ans == 0) return 0;
// ans_2
int i = n1, j = temp;
while(i && j)
{
qaq t = f[i][j];
if(f[t.nx1][t.nx2].num == t.num - 1)
ans2[++idx] = a[i];
i = t.nx1;
j = t.nx2; // 下一个前驱
}
// output
for(int i = idx; i >= 1; i --) cout << ans2[i] << " ";
puts("");
return 0;
}
最短编辑距离
I. 怎么解?
dp
Ⅱ. 解题思路?
状态表示:f[i][j]
集合表示:a[i]到b[j]的最短编辑距离
属性:min
状态转移:
分四种情况讨论
if(a[i] == b[j])
{
跳过,不用编辑,因此操作次数不用增加
f[i][j] = f[i-1][j-1]
}
else if(a[i] != b[j])
{
再分三种情况讨论,取min
1. 删除
{
如果选择删除,删除的必然是a[i],因为之前的已经匹配
那么a[1 - i-1]和b[1-j]匹配
f[i][j] = f[i-1][j] + 1
}
2. 插入
{
如果选择插入,插入的肯定是b[j]
那么a[1-i]和b[1- j-1]匹配
f[i][j] = f[i][j-1] + 1
}
3. 替换
{
相当于删除+插入,所以二者的方程都要匹配
f[i][j] = f[i-1][j-1] + 1
}
}
完整代码奉上
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define INF 0x3f3f3f3f
const int N = 1010;
int f[N][N];
char a[N], b[N];
int main()
{
int la, lb;
scanf("%d%s", &la, a+1); // 从1开始方便
scanf("%d%s", &lb, b+1);
for(int i = 1; i <= la; i++)
for(int j = 1; j <= lb; j++)
f[i][j] = INF; // 因为是min所以是正INF
for(int i = 1; i <= la; i++) f[i][0] = i; // 从a[1-i]变到空需要i次删除
for(int i = 1; i <= lb; i++) f[0][i] = i; // 从空变到b[1-i]需要i次插入
for(int i = 1; i <= la; i++)
{
for(int j = 1; j <= lb; j++)
{
f[i][j] = min(f[i-1][j] + 1, f[i][j-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);
}
}
printf("%d", f[la][lb]);
return 0;
}
扩展题目
LIS扩展怪盗基德的滑翔翼
LIS扩展合唱队形