目录
线性动态规划
一般都是对一维数组进行操作,比如求该序列里的上升子序列,下降子序列,或者是求两个序列里的公共子序列,说到底都是线性操作
1.最长上升(下降)子序列(单序列)
以最长上升为例,下降改变符合就行
01.暴力版 O ( n 2 ) O(n^2) O(n2)
我们一般设状态方程是
f
[
i
]
f[i]
f[i] 表示以i结尾的最长上升子序列的长度
状态计算也是暴力,直接找前面小于该数的值中,最长的序列,然后拼接上去就行
最长上升子序列I
核心代码
for(int i=1;i<=n;i++)
{
f[i]=1; // 初始化为一,保证以该数为底的数至少有一个
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
{
// 如果a[j]<a[i] 则把以a[j] 为结尾的数的序列长度+1和本身比较取最大值
f[i]=max(f[i],f[j]+1); // 因为f[i]是用f[j] 更新的,而且i从小到大, // 一定记得要加1!!!!!
// f[i]会记录以该点为最后一位的数的最长长度
}
}
}
02.二分优化 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))
当我们在找的时候会发现一个规律,当长度越长,结尾的数字也越大,有单调性
假如我们要枚举到4时,我们用二分找到第一个小于4的序列,我们可以用4去替代长度为3的序列,因为3结尾的序列长度是2,拼接上4就是3了,4的值比5小,比5更有可能接上其他数。两个数存的长度一样时,最后一位数小的数会更有用,因为阈值变大,可以存更多数
最长上升子序列II
核心代码
int len=0; // 长度
q[0]=-2e9; // 标杆,0长度为无穷小
for(int i=1;i<=n;i++)
{
int l=0,r=len;
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<a[i]) l=mid; // 找小于a[i]的数,且在右侧 该序列是从小到大
// 如果找最长下降子序列是直接 q[mid]>a[i] 因为该序列是从大到小,所以已经翻过了
else r=mid-1;
}
len=max(len,r+1); // 更新len,len就是答案
q[r+1]=a[i]; // 因为q[r] 是小于a[i] 中最大的,q[r+1] 大于 a[i] 所以可以用a[i] 更新q[r+1]
}
2.双序列
这个模型就开始操作两个序列了
01.最长公共子序列
一般给两个序列,问其中最长的公共子序列的多长
状态表示
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示在第一个序列的前i个数字中,在第二个序列的前j个数字中,最长的公共子序列
状态计算
如果
a
i
≠
b
i
a_i \not = b_i
ai=bi
f
[
i
]
[
j
]
f[i][j]
f[i][j]取
f
[
i
−
1
]
[
j
−
1
]
,
f
[
i
−
1
]
[
j
]
,
f
[
i
]
[
j
−
1
]
f[i-1][j-1] , f[i-1][j] , f[i][j-1]
f[i−1][j−1],f[i−1][j],f[i][j−1]中的最大值转移过来
如果
a
i
=
b
i
a_i = b_i
ai=bi
f
[
i
]
[
j
]
f[i][j]
f[i][j]取上面三个状态,和
f
[
i
−
1
]
[
j
−
1
]
+
1
f[i-1][j-1]+1
f[i−1][j−1]+1中的最大值转移过来
核心代码
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
// 计算情况分为四种,a[i],b[j],不相等且和先前的字母也不相同即00,
// a[i]和b[j-1]相同即为10,同理另一个是01,10。
// 01,10,包含了00的情况,所以不用特判。
// 这三种情况都是求他们先前的最大值。两个数不相等和相等是两个无关联的情况。
// 所以先判这个不影响,如果相同再特判
f[i][j]=max(f[i-1][j],f[i][j-1]);
// 这一步才是计数的,当他俩相同时,计数加上1,而且同一层j往后都会保留这个数
// 并且在同一层循环中,数不会在变
// 因为如果再遇见相同的数,都是f[i-1][j-1]+1,数值不变
if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
02.最短编辑距离
给我们两个字符串,让我们修改,可以增,删,改,求,把第一个变成第二个字符串的最小编辑次数
状态表示
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示第一个序列的前i个字母和第二个序列的前j个字母匹配的最短的编辑距离
状态计算
考虑状态转移的时候
先考虑如果我没有进行这个操作应该是什么状态
然后考虑你进行这一步操作之后会对你下一个状态造成什么影响
然后再加上之前状态表示中你决策出来的那个DP属性
这样就可以自然而然地搞出来转移方程啦
1)删除操作:把a[i]删掉之后a[1i]和b[1j]匹配
所以之前要先做到a[1(i-1)]和b[1j]匹配
f[i-1][j] + 1
2)插入操作:插入之后a[i]与b[j]完全匹配,所以插入的就是b[j]
那填之前a[1i]和b[1(j-1)]匹配
f[i][j-1] + 1
3)替换操作:把a[i]改成b[j]之后想要a[1i]与b[1j]匹配
那么修改这一位之前,a[1(i-1)]应该与b[1(j-1)]匹配
f[i-1][j-1] + 1
但是如果本来a[i]与b[j]这一位上就相等,那么不用改,即
f[i-1][j-1] + 0
一篇不错的题解
最短编辑距离
核心代码
for(int i=0;i<=m;i++) f[0][i]=i; // 初始化
for(int i=0;i<=n;i++) f[i][0]=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); // 有三种计算,先按照前两种,删和增算
// 因为这两个和改是没有联系的,就是有改,取他们的最小值
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);
}
}