luogu上刷到的P1020 [NOIP1999 提高组] 导弹拦截和P1439 【模板】最长公共子序列 有感
LIS:Longest Increasing Subsequence,最长递增子序列
给定一个字符串,求出最长递减序列
这个题问的是下降,上升情况反过来就好了
只考虑第一问,由于O(n*n)会爆T(不解释了),考虑压缩时间
还记得在网上看到的一句话
如果需要对dp进行时间优化,不妨交换状态参数和状态量
基于这句话的启发,这个题思路就若隐若现了
步骤一:首先我们很容易想到 dp[ i ] 来表示:前i个数中以第i个数结尾的最长递减序列
这句话中我理解的状态参数就是(以第i个数结尾)状态量就是(最长递减序列)
我们不妨构造 f 数组,f [ i ] 代表长度为 i 的递减序列的最后一个数的最大值
(为什么要怎么做,等会解释,先理解成最大值后面才能放更多小的数使序列更长)
这个时候状态参数就是(长度为 i 的递减序列)状态量就是(结尾的最大值)
这样我们就实现了递减序列和结尾数字的交换(前面可以再看一遍)
步骤二:需要一个前提证明,f 数组是单调递减的(其实是单调不递增,为了不拗口先这么叫):
反证一下,如果 f [x+1] > f [x] 也就是说长度为x+1的递减序列的末尾会大于长度为x的,那就不对了,由于序列递减,这个长度为x+1的序列的第x个数(称作a)一定>=第x+1个数(称作b),那长度为x的序列结尾的数至少就会跟这个a相等,也就是发 f [ x ] >=a,由于a>=b,b=f [ x+1 ],则 f [x] > f [x+1]得证。要是被绕了就看看这个例子:
长度为4的递减序列:9 6 3 2 ,如果长度为3的递减序列末尾比2还小就会冲突,因为这个序列至少可以取到9 6 3,这个最大值至少比3还大,也就不可能小于2了
步骤三:dp [ i ] 和 f [ i ] 同步进行计算,相互更新,计算dp[ i ] 需要对于每一个 i 满足:
想要用第i 个数结尾,前面的结尾必须比第i个数还大,但是前面满足的递减序列又要尽可能长,也就是i尽可能大,但是 f [ i ] 要比第i个数小,直接二分找到答案。(f 数组求dp数组)
每次计算完 dp [ i ] ,设这个长度为k, 也就是dp [ i ]=k,那么 f [ k ] 长度为k的递减序列的末尾就要尝试更新为第 i 个数。(dp 数组更新 f 数组)
最后遍历看看以谁结尾的最长递减序列最长就好了
AC代码如下(fun2函数是第二问的请忽略,后面的循环和ans数组也是):
//动态规划的时间优化
#include<bits/stdc++.h>
#define N 100050
using namespace std;
typedef long long ll;
vector<ll>ans;
ll h[N],dp[N],f[N];
ll fun(ll i){
ll l=0,r=N-1;
while(l<r-1){
ll mid=(l+r)/2;
if(f[mid]>=h[i]){//等于的情况要特殊考虑!!!
l=mid;
}else{
r=mid;
}
}
if(f[r]>=h[i] && h[i]){
return r;
}else{
return l;
}
}
void fun2(ll i){
if(ans.size()==0){
ans.push_back(i);
}else if(ans.size()==1){
if(ans[0]>=i){
ans[0]=i;
}else{
ans.push_back(i);
}
}else{
ll l=0,r=ans.size()-1;
while(l<r-1){
ll mid=(l+r)/2;
if(ans[mid]>i){
r=mid;
}else{
l=mid;
}
}
if(ans[l]>=i){
ans[l]=i;
}else if(ans[r]>=i){
ans[r]=i;
}else{
ans.push_back(i);
}
}
}
int main(){
ll n=1;
while(cin>>h[n]){
n++;
}
n--;
dp[1]=1;
f[1]=h[1];
for(int i=2;i<=n;i++){
ll k=fun(i);
dp[i]=k+1;
f[dp[i]]=h[i];
}
ll temp_max=0;
for(int i=1;i<=n;i++){
temp_max=max(temp_max,dp[i]);
}
cout<<temp_max<<endl;
for(int i=1;i<=n;i++){
fun2(h[i]);
}
cout<<ans.size();
return 0;
}
LCS:Longest Common Subsequence,最长公共子序列
最长公共子序列模板(nlogn优化)
要想压缩成log想到二分,则需要单调数组
联想到LIS优化,想办法转化为最长上升序列问题:
由于a,b数组都是1~n的全排列 ,开出map记录映射情况
记录每个元素在a数组中的位置,将b的每个元素改成它在a中的位置
b数组中的升序序列就是a数组中的升序序列
则求出b的最长上升子序列就是b中含有的a的最长上升序列
这段序列就是最长公共子序列
解释:b数组中有一些数
这些数在原本字符串b中本就从前往后
现在它为升序,说明这些元素在a中也是从前往后
这样就好了,这些数对应的元素在a,b中的顺序一模一样
那这些数对应的元素就是a,b的公共序列,序列长度等于这些升序数的个数
所以最长上升序列的数目就是公共串的长度
举个例子:
A数组有这些数:a b c d e 映射到map里面就是 1 2 3 4 5
B数组有这些数:c e a b d 用map还原回来就可以得到 3 5 1 2 4
这个最长公共序列的答案就是 3 ,也就是序列 a b d 的长度,这样就可以套LIS了
这样就很好理解了,数字本就在a中顺序排列,能在b组里面升序就说明这些在a中的顺序排列也可以在b中顺序排列,这些数又是它本身,就变成公共序列了
#include<bits/stdc++.h>
#define N 100005
typedef long long ll;
using namespace std;
ll b[N],dp[N],f[N];
ll n;
map<ll,ll>M;
ll fun(ll k){
ll l=0,r=N-1;
while(l<r-1){
ll mid=(l+r)/2;
if(f[mid]<=k){
l=mid;
}else{
r=mid;
}
}
if(f[r]<=k){
return r;
}else{
return l;
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
ll x;
cin>>x;
M[x]=i;
}//map记录每一个数在A中的位置
for(int i=1;i<=n;i++){
ll x;
cin>>x;
b[i]=M[x];
}
for(int i=1;i<N;i++){
f[i]=2*N;
}
for(int i=1;i<=n;i++){
dp[i]=fun(b[i])+1;
f[dp[i]]=min(b[i],f[dp[i]]);
}
ll mm=0;
for(int i=1;i<=n;i++){
mm=max(mm,dp[i]);
}
cout<<mm;
return 0;
}
注:只能求最长公共串的长度,不能求出串本身
如果想要知道这个串得从f数组倒推:
设串长为5,则原串为 f[1] 到 f[5] 的依次连接
因为他们是每一段的结尾