线性DP之 LIS_LCS_LCIS
引言
一、LIS问题(最长上升子序列)
L I S ( L o n g e s t − I n c r e a s i n g − S u b s e q u e n c e ) \mathcal{LIS(Longest- Increasing -Subsequence)} LIS(Longest−Increasing−Subsequence)
1,朴素做法
- 状态表示:
f[i]
表示从第一个数字开始算,以w[i]
结尾的最大的上升序列。(以w[i]
结尾的所有上升序列中属性为最大值的那一个) - 状态计算(集合划分): j ∈ ( 0 , 1 , 2 , . . , i − 1 ) \mathcal{j∈(0,1,2,..,i-1)} j∈(0,1,2,..,i−1) 在 w [ i ] > w [ j ] \mathcal{w[i] > w[j]} w[i]>w[j]时, f [ i ] = m a x ( f [ i ] , f [ j ] + 1 ) \mathcal{f[i] = max(f[i], f[j] + 1)} f[i]=max(f[i],f[j]+1)
- 有一个边界,若前面没有比
i
小的,f[i]
为1
(自己为结尾)。 - 最后在找
f[i]
的最大值
2,贪心做法(更优)
- 状态表示:
f[i]
表示长度为i
的最长上升子序列,末尾最小的数字
(长度为i
的最长上升子序列所有结尾
中,结尾最小的) 即长度为i的子序列末尾最小元素是什么 - 特性:随着i的递增,f[i]必然是递增的,这个性质不证自明
- 接下来我们对我们需要的对每一个
a[i]
尝试去接入一个序列,当然,结尾必然比它小,但是我还想长度尽可能长 f[i]
具有单调性,直接二分就可以找到第一个结尾小于或者等于a[i]
的序列了,好的,接特!!- 接完了,新的序列长度是
接入序列的长度+1
,取原来这么长的序列的结尾的数和这次的新构序列的最小值就成(后面还接嘞)
int n, a[N], f[N];
int lis()
{
scanf("%d", &n); for (int i = 0; i < n; i ++) scanf("%d", &a[i]);
int len = 0;
f[0] = -2e9;
长度是0的结尾值是-2e9(表示长为0的子序列不存在)
for (int i = 0; i < n; i ++)
{
f[len]表示长度是len的最长(严格)上升子序列,其结尾的值是 f[len]
找到最接近但小于(小于等于)a[i] 的 f[k], 其中k 在 0 ~ i - 1, 这里的k表示子序列的长度, 结尾的值是f[k]
也就是f[k] < a[i], f[k]表示子序列倒数第二个数
int l = 0, r = len;
while (l < r)
{
int mid = (l + r + 1) >> 1;
if (f[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = max(len, r + 1);
这一步很费解,当找到可以接入的序列时,r只能停留在已知的贪心序列中间,+1也不影响已知序列的长度
如果贪心序列全都很符合要求的,说明贪心数组长度又增1,`r+1`新添一个元素已知的贪心序列的长度也加上1
长度是r + 1的(严格)上升子序列是以 a[i] 结尾的
f[r + 1] = a[i];
}
return len;
}
模式化:
二、LCS问题(最长公共子序列)
L
C
S
(
L
o
n
g
e
s
t
−
C
o
m
m
o
n
−
S
u
b
s
e
q
u
e
n
c
e
)
\mathcal{LCS(Longest- Common-Subsequence)}
LCS(Longest−Common−Subsequence)
特征:是指求出两个序列的最长公共子序列的问题
状态划分:
f
[
i
,
j
]
\mathcal{f[i,j]}
f[i,j]即以a[i] , b[i]
结尾的子串的最大长度
状态转移:很明显,对于a[i] , b[j]
来讲,只有选,或者不选这两种可能,用状态机的思路,0为选,1为不选
分四种情况:
1,00,都不选,转移自
f
[
i
−
1
,
j
−
1
]
\mathcal{f[i-1,j-1]}
f[i−1,j−1]
2,01,选b[j]
不选a[i]
,来自
f
[
i
−
1
,
j
]
\mathcal{f[i-1,j]}
f[i−1,j]
3,10,不选b[j]
选a[i]
,来自
f
[
i
,
j
−
1
]
\mathcal{f[i,j-1]}
f[i,j−1]
4,特殊情况,11,都能选,仅当b[j]==a[i]
才可以,来自
f
[
i
−
1
,
j
−
1
]
+
1
\mathcal{f[i-1,j-1]+1}
f[i−1,j−1]+1
注意:以及可能这三种情况具有交集,因为还是没用状态机,不过我们只要最值,多算几次没啥
int lcs()
{
cin>>n>>m;
scanf("%s \n %s", a+1, b+1);
小细节注意一下,scanf读的char从0开始带数据,如果用在转移上,(0-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]=max(f[i][j],f[i-1][j-1]+1);
}
return f[n][m];
}
三、LCIS问题(最长公共上升子序列)
参考:y总超强分析,绝绝子,速速读下
思路:考虑直接把两个问题并起来
回顾:这两个子问题的状态表示:
1,LCS,
f
[
i
]
表
示
以
i
\mathcal{f[i]表示以 i }
f[i]表示以i结尾的上升串的最大长度
2,LIS,
f
[
i
,
j
]
表
示
在
a
[
1
,
i
]
,
b
[
1
,
j
]
\mathcal{f[i,j]表示在a[1,i] , b[1,j] }
f[i,j]表示在a[1,i],b[1,j]中出现的最长公共串的长度
如果两个串是公共的的话,那么求a的上升子串和求b的上升子串是一样的
所有我们需要把末尾元素确定下来,人为规定一下就好
得出这次的状态:
f
[
i
,
j
]
表
示
在
a
[
1...
i
]
和
b
[
1...
j
]
\mathcal{f[i,j]表示在a[1...i]和b[1...j]}
f[i,j]表示在a[1...i]和b[1...j]中公共的且以b[j]结尾
的上升串的最大长度
状态转移:
1,不包含a[i]
,说明以b[j]
结尾的公共串在扩入a[i]
后,结尾数字没变,长度也没变,那么最长就是是f[i-1][j]
了,传承下来就行
2,包含a[i]
,说明公共串长度和末尾数值全变了,那就有必要处理一下了
- 尝试接入b串中
b[j]
前的所有元素之后
1,朴素代码(生硬套模板)
int lcis()
{
int ans=-1e9;
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= n; j ++ )
{
f[i][j]=f[i-1][j]; 不管如何,不选a[i]都是可以的
if(a[i]==b[j]) 当串更新了,我们处理一下
{
maxv=1;
决定选b[j]那最起码有一个了
for (int k = 1; k < j; k ++ )
if(b[k]<a[i])maxv=max(f[i-1][k]+1,maxv); 依次尝试接入
f[i][j]=max(f[i][j],maxv);
ans=max(ans,f[i][j]);
}
}
}
return ans;
}
2,前缀和思路优化
- 冗余就在最内层尝试接串的循环上
b[k]
比对的对象始终不变,那么把k
循环和j
循环合并,maxv
可以继承下来其实就行了
int lcis()
{
int ans=-1e9;
for (int i = 1; i <= n; i ++ )
{
maxv=1;
for (int j = 1; j <= n; j ++ )
{
f[i][j]=f[i-1][j];
if(a[i]==b[j])f[i][j]=max(f[i][j],maxv),ans=max(ans,f[i][j]);
if(b[j]<a[i])maxv=max(f[i-1][j]+1,maxv);
}
}
return ans;
}
tips – hack:
最长公共上升子序列抽串在判断的数据失误,最长公共子序列不唯一
like this:
6 5 8 7 6 3 1
8 6 7 6 3 1