动态规划
LIS 和LCS 问题
首先是这两个名词
LIS:Longest Increasing Subsequence,最长递增子序列LCS:Longest Common Subsequence,最长公共子序列
然后先说一下LIS问题
lis O(n^2) 版本
放一个板子题 https://www.luogu.com.cn/problem/B3637
对于 6 5 1 4 2 7
这个的LIS 很明显就是 1 4 7 长度为三 当然这个是不固定的
最简单的方法O(n^2)的算法,dp[i] 当前以i结尾能构造的LIS dp[i]=max(1, max(dp[1]~dp[i-1])+1) //[1,i-1] a[j]< a[i] 这样子的写就行了
void solve(){ int n;cin>>n; ve<int>a(n+1); for(int i=1;i<=n;i++) cin>>a[i]; ve<int>dp(n+1); for(int i=1;i<=n;i++) { dp[i]=1; for(int j=1;j<=i-1;j++) { if(a[j]<a[i]) { dp[i]=max(dp[i],dp[j]+1); } } } cout<<dp[n]<<'\n';}lis O(nlogn)版本
二分优化
还有一种 二分的方法,跟树状数组的方法实现这个代码
二分的方法 dp[i]:代表当前长度为i的最长上升,对于当前已经来到第j个元素 [1,j] 中最小的那个能满足的元素
二分 dp 更新找到第一个大于a[j]的dp[i] (dp[i]<=a[j]) 更新 dp[i+1]=a[j];
void solve(){ int n;cin>>n; ve<int>a(n+1); for(int i=1;i<=n;i++) cin>>a[i]; ve<int>dp(n+1); int len=1; dp[1]=a[1]; for(int i=2;i<=n;i++) { if(a[i]>dp[len]) dp[++len]=a[i]; else { int id=lower_bound(&dp[1],&dp[len],a[i])-&dp[1]; if(dp[id]!=a[i]) id++; dp[id]=a[i]; } // for(int j=1;j<=n;j++) cout<<dp[j]<<" \n"[j==n]; }
cout<<len<<'\n';}树状数组优化
树状数组也能处理这个问题,思路是 如果当前已经遍历到了第i个元素, 树状数组维护了前[1,a[i]-1] 个元素的情况所能组成的最长公共子序列。 因为 在 [1,a[i]-1] 这个范围下肯定会小于a[i]。
这个方法需要注意下离散化行为,因为我们维护的是权值。
int f[N];void add(int x,int val){ while(x>0&&x<N) { f[x]=max(f[x],val); x+=(x&-x); }}int query(int x){ int ans=0; while(x) { ans=max(ans,f[x]); x-=(x&-x); } return ans;}void solve(){ int n;cin>>n; ve<int>a(n+1); for(int i=1;i<=n;i++) cin>>a[i];
int maxa=0; for(int i=1;i<=n;i++) { int x=query(a[i]-1)+1;//长度 maxa=max(maxa,x); add(a[i],x); } cout<<maxa<<"\n";}lcs 问题
最长公共子序列问题:
给出a数组,b数组,找a,b下最长公共子序列
朴素版本O(n^2)
我们可以用dp[i][j]来表示第一个串的前i位,第二个串的前j位的LCS的长度, 那么我们是很容易想到状态转移方程的:
如果当前的A1[i]和A2[j]相同(即是有新的公共元素) 那么 dp[i][j]=max(dp[i][j],dp[i−1][j−1]+1);
如果不相同,即无法更新公共元素,考虑继承: dp[i][j]=max(dp[i−1][j],dp[i][j−1]
lcs O(nlogn)版本
我们可以先对a进行离散化操作。 我们需要按照a的大小准则,对b操作。转换成b按照a准则下的,lis问题 比如a中 [5 3] 得到5<3顺序 则离散化后 5=>1 3=>2 即在b中5大小是1 3大小是2 。这个样子映射后得到的结果
这个方法需要注意重复的情况,在重复的情况需要倒序
数组无重复
int f[N];void add(int x,int val){ while(x<N) { f[x]=max(f[x],val); x+=(x&-x); }}int query(int x){ int ans=0; while(x) { ans=max(ans,f[x]); x-=(x&-x); } return ans;}void solve(){ int n;cin>>n; ve<int>a(n+1),b(n+1); for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=n;i++) cin>>b[i];
map<int,int>mp; for(int i=1;i<=n;i++) mp[a[i]]=i; int ans=0; for(int i=1;i<=n;i++) { int x=query(mp[b[i]]-1)+1; ans=max(ans,x); add(mp[b[i]],x); } cout<<ans<<'\n';}数组有重复
int f[N];void add(int x,int val){ while(x<N) { f[x]=max(f[x],val); x+=(x&-x); }}int query(int x){ int ans=0; while(x) { ans=max(ans,f[x]); x-=(x&-x); } return ans;}void solve(){ int n;cin>>n; int m;cin>>m; ve<int>a(n+1),b(m+1); for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=m;i++) cin>>b[i];
ve<ve<int>>mp(N); for(int i=1;i<=n;i++) mp[a[i]].push_back(i);
ve<int>g; for(int i=1;i<=m;i++) { for(int j=mp[b[i]].size()-1;j>=0;j--) g.push_back(mp[b[i]][j]); }
for(auto x:g) cout<<x<<" "; cout<<"\n-------------\n";
int ans=0; for(int i=0;i<g.size();i++) { int x=query(g[i]-1)+1; ans=max(ans,x); add(g[i],x); } cout<<ans<<'\n';}二分的方法
如果对于有重复的,新数组的构造很可能会超时间复杂度 那么还是二分的方法比较稳定一点