P1439 【模板】最长公共子序列
传送门:
P1439 【模板】最长公共子序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路
第一次遇见绿色的dp板子,可能还是太菜了
刚开始觉得挺容易的跟最长上升子序列有点像
最长上升子序列贴码
for(int i=1; i<=n; i++)
for(int j=1; j<i; j++)
if(a[j] < a[i])
dp[i] = max(dp[i], dp[j]+1);
for(int i=1; i<=n; i++)
ans = max(ans, dp[i]);
但是最长公共子序列与之不同的是找两个序列中可以取出来的排列,这样说有点抽象,举个例子
in:
5
3 2 1 4 5
1 2 3 4 5
out:3
这里的最长公共子序列可以是 345 145 245
举完例子大概对题目的意思已经理解了吧
那我们来往下一步找状态转移方程
一般像我这种高手蒟蒻,都先采用打个表看看
这里dp[i][j]表示a序列中1-i个数和b序列中1-j个数的最长公共子序列
易得状态转换,如果当前a[i]!=b[j]那么dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]),当前a[i]==b[j]那么dp[i][j]=dp[i-1][j-1]+1
来分析一下这两个状态转移方程
-
当前a[i]!=b[j]那么dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]),如果两个数列的当前位置不相等,说明最长公共子序列的长度不会增加,那么他的值来自于a的上一个数和b的这个数的最长公共子序列和a的这个数和b的上一个属的最长公共子序列的较大值,这句话是较为关键的,其实就是暴力打表,然后数列的每一种数列长度情况都列出来然后暴力查找,因为没有后效性(就是说后面的数影响不到当前情况和前面的情况),所以只要继承前面的情况就行
-
当前a[i]==b[j]那么dp[i][j]=dp[i-1][j-1]+1,跟前面其实有点相似,这时候前面找的是[a-1][b]和[a][b-1],这里因为当前值是相等的所以这两个数列都要回到前一个位置[a-1][b-1],如果还回到[a-1][b]和[a][b-1],那这里是必然不相等的,就算有题目里是有重复的字符正好相等了,那当前的值没有加一反而还漏考虑了当前位置(这里当前位置就是如果max取了[a-1][b]那么a位置就掠过了,就算暴力能循环到,那当前值可能会少1)
是不是有点套娃的感觉,就好像这句话表达了如果不这样写,写别的就错了;上面这句话只是为了辅助理解,我感觉dp还是得用自然语言描述出来,这里转换出来就是我那两个数列的前一个位置的最长公共子序列和当前相等的(就是两个数列中暴搜过去多了一个相等的)那原最长公共子序列+1,因为左侧和上面的4已经考虑过了如果我dp[i][j]=dp[i-1][j]+1,那相当于我左侧的位置考虑了两次;这个需要think think think!
这里时间复杂度显然是O(N2),那么对于题目1e5的数据显然是过不去的,
插一句:这里如果想要降低空间复杂度可以用滚动数组,但是这里和背包的滚动数组有所不同,因为背包的滚动数组只要考虑上一次的情况所以可以优化到一维数组,而这里需要考虑上一次和这一次的前面的情况所以需要用二维数组,dp[2][n],可以采用memcpy的方式滚动下去;这里不展开讲述;
但是这样做可以解决数据范围较小且有重复字符出现的题目,如下题:
P1435 [IOI2000] 回文字串 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这题就不再展开了,依照上面这套板子和一点点关于回文字符串的理解应该可以秒杀
广告:有共同学习需求的可以加入洛谷团队:https://www.luogu.com.cn/team/66731
优化
ok,既然上面这套板子不能完全解决,那我们开动优化!
先直接贴码了
// Problem:
// P1439 【模板】最长公共子序列
//
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1439
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
//先看代码!!
/*
看之前拓展一下:
lower_bound(a):返回第一个大于等于a的数的迭代器
upper_bound(a):返回第一个大于a的数的迭代器
哈哈哈哈我老是会搞错先贴在这
*/
#include<iostream>
#include<algorithm>
//#include<cstdio>
#include<set>
#define ll long long
#define endl '\n'
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define N 100010 //1e6+100
using namespace std;
int n;
int a[N],b[N];
int mp[N];
set<int>dp;
set<int>::iterator it;
int main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
rep(i,1,n){
cin>>a[i];
mp[a[i]]=i;
}
rep(i,1,n) cin>>b[i];
dp.insert(mp[b[1]]);
rep(i,2,n){
it=dp.lower_bound(mp[b[i]]);
if(it!=dp.end()) dp.erase(it);
dp.insert(mp[b[i]]);
}
cout<<dp.size();
return 0;
}
//看完代码是不是感觉码写的很简单,但是这里的思路不简单!
代码的意思就是存储a数列每个数出现的位置,然后b数列按照顺序一个个查,如果集合中有比b放进去对应a的位置靠后(就是值较大的)的位置就把这个位置弹出去,然后放入当前的位置
码是这个意思,介于薛定谔的猫(感觉好懂但好像很难)
拆开来思考
- 首先我们把a数列中元素的位置放在他数所在地方,即mp的下标表示a[i],mp[]表示a[i]所在的位置
rep(i,1,n){
cin>>a[i];
mp[a[i]]=i;
}
- 初始化set,因为无论答案再小也不可能小过1
dp.insert(mp[b[1]]);
- 关键三句话
rep(i,2,n){//第一个b[i]在a数列中的位置已经放进去了
it=dp.lower_bound(mp[b[i]]);
if(it!=dp.end()) dp.erase(it);
dp.insert(mp[b[i]]);
}
在以此把b[i]在a数列中的位置放进去的过程发现了之前已经放过靠后的位置了,
那么就要把那个靠后的位置弹出,
因为一直插入较小的位置可以确保我找到最长的序列,这里有贪心的思想
有人回文如过当前我的set是4 5我要插1进去是不是要把4删掉,这时候最长公共子序列这个长度是不变的,
因为虽然我替换了子序列,但是实质上我的长度因为1的插入是不会改变的就好像我1替换了4的位置,不影响后面插入比4的位置,因为这里比4小了后面只要比4大的就不会弹出1
那又有人疑惑了,那替换比4小的原来应该走出4的,但是现在走出了5或1,如果是走出了1就是替换了1的位置,如果是走出了5就是替换了5的位置;再往回思考
这里就是不断的替换位置,但替换完的位置不受到后面的数影响,这里如果能理解这个无后效性,这道题就差不多解了;
说到底有点像贪心+二分但是这里的关键点还是这个无后效性的替换
有问题请评论或私信我,谢谢!