前言
鉴于本蒟蒻的dp学的实在是一坨答辩,临近lqb开始重新学习一遍dp,在acwing和LIS搏斗两天之后,写一篇总结,加深一下印象。
模板
题意与大致思路
题意:
给一个数组,找数组的一个子序列,该序列严格递增且长度最大
状态表示:
dp[i] 表示 以a[i]为结尾的最长上升子序列的长度
状态转移:
对于每个i,遍历1~i,如果找到一个a[j] < a[i],那么ai就可以接到以aj为结尾的LIS后面,此时就可以更新 dp[i] = max(dp[j] + 1 , dp[i]) ,时间复杂度 O (n^2)
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int a[maxn],dp[maxn];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],dp[i] = 1; //dp[i]初始化为1,因为显然a[i]本身即为LIS
int res=0;
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);
res = max(res , dp[i]); //维护dp[i]的最大值
}
cout<<res<<endl;
return 0;
}
接下来上题
Acwing 1017 - 怪盗基德的滑翔翼
Think
该题可以选择向左或者向右选择一条严格递减的序列,往右走就是最长下降子序列,而往左走实际上为最长上升子序列,直接dp并维护最大值即可
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e2+10;
int a[maxn],dp_up[maxn],dp_down[maxn];
int main()
{
int T;
cin>>T;
while(T--)
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
dp_up[i]=1;
dp_down[i]=1;
}
for(int i=1;i<=n;i++)
for(int j=0;j<i;j++)
if(a[i]>a[j])
dp_down[i]=max(dp_down[i],dp_down[j]+1);
else if(a[i] < a[j])
dp_up[i]=max(dp_up[i],dp_up[j]+1);
int res=0;
for(int i=1;i<=n;i++) {
res=max(res,dp_down[i]);
res=max(res,dp_up[i]);
}
cout<<res<<endl;
}
return 0;
}
Acwing 482-合唱队列
Think
不难想到把问题转化为找一个以i为终点的最长不下降子序列和一个以i为起点的最长不上升子序列,如何得到以i为起点的LIS呢,只需要反向跑一个最长不下降子序列就可以了
//
// main.cpp
// 合唱团
//
// Created by 77777 董昊鹏 on 2022/3/4.
//
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e2+10;
int a[maxn],dp_up[maxn],dp_down[maxn];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
dp_up[i]=1;
dp_down[i]=1;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
if(a[i]>a[j]) dp_up[i]=max(dp_up[i],dp_up[j]+1);
}
for(int i=n;i>=1;i--)
for(int j=n;j>i;j--)
if(a[i]>a[j]) dp_down[i]=max(dp_down[i],dp_down[j]+1);
int res=0;
for(int i=1;i<=n;i++)
res=max(res,dp_up[i]+dp_down[i]-1);
cout<<n-res<<endl;
}
Acwing 1012-友好城市
Think
该题目要求两边都无交叉,显然对于两边的城市都要保证一个严格递增,按照其中一边排序后,对于另一边跑LIS就可以得到答案。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+10;
pair<int,int> a[maxn];
int dp[maxn];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].first>>a[i].second,dp[i] = 1;
sort(a+1,a+1+n);
int res=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
if(a[j].second < a[i].second)
dp[i] = max(dp[j] + 1,dp[i]);
res= max(res,dp[i]);
}
cout<<res<<endl;
}
Acwing 1010-拦截导弹
Think
LIS的究极经典例题,对于第一问无需多谈,重点在于第二问,可以贪心去做,也可以用到一个偏序集相关的定理,dilworth定理:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。人话就是:如果让你求最少的不上升子序列,这几个序列中包含整个数组的所有元素,即求最大不下降子序列的长度,严格证明可以看tofu佬的:
偏序集,哈斯图与Dilworth定理 - Tofu 的博客 - 洛谷博客 (luogu.org)
所以第二问再跑一个最长不下降子序列即可
#include<bits/stdc++.h>
using namespace std;
int main()
{
string s,x;
getline(cin,s);
stringstream ss(s);
int n=0,a[1010],dp[1010];
while(ss>>x)
{
a[++n] = stoi(x);
dp[n] = 1;
}
int res=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
if(a[i] <= a[j])
dp[i] = max(dp[j] + 1,dp[i]);
res = max(dp[i],res);
}
cout<<res<<endl;
for(int i=1;i<=n;i++) dp[i] = 1;
res=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
if(a[j] < a[i])
dp[i] = max(dp[j] + 1,dp[i]);
res = max(res,dp[i]);
}
cout<<res<<endl;
}
Acwing 896-最长上升子序列 Ⅱ
Think
这次n变为1e5,n^2的算法肯定没法通过,需要进行优化。考虑原本的dp算法,转移的过程是找每个ai接在哪里使得答案最优, 由于n较小可以直接枚举位置并dp取最大值。现在我们来直接讨论这个ai接的位置。
状态表示与状态转移
我们维护一个dp[i],表示长度为i的子序列尾部的值。然后遍历ai,考虑对于每个ai可以接在哪个序列后面,比如两个序列,他的结尾值分别是1和1000,显然接在1000绝对不会比接在1后面更差,因为1比较小,后面还有很多数能接。也就是说结尾的数越大越好,我们可以去找第一个小于等于ai的dp[i],而这个dp是一定单调的,证明如下:
考虑反证:如果res[i] >= res[j] 且i < j,也就是说,更短的子序列有更大的末尾值,这个显而易见是不成立的,比如你在res[j]删除几个元素,使得其长度变为i,此时的末尾值res[k]一定是小于res[j]的,与前提矛盾。
那么我们找第一个小于等于ai的dpi这个过程就可以用二分来解决,总的时间复杂度就可以优化到O(nlogn)。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
int a[maxn] , dp[maxn];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
int res=0;
dp[++res] = a[1];
for(int i=2;i<=n;i++)
{
//cout<<i<<" "<<dp[res]<<endl;
if(a[i] > dp[res]) dp[++res] = a[i];
else
{
int pos = lower_bound(dp + 1,dp + res + 1,a[i]) - dp;
dp[pos] = a[i];
}
}
cout<<res<<endl;
}
Acwing 187-导弹拦截系统
Think
船新的导弹拦截,在这个题里面,既可以是最长上升也可以是最长下降,我们可以采取上题的思路,用一个up[i]和一个down[i]分别表示每个序列的末尾数。然后我们可以直接按照上一题的贪心思路,即:一直接在第一个小于等于a[i]的序列后面。进行一波搜索剪枝,更多具体细节看代码注释
#include<bits/stdc++.h>
using namespace std;
int a[60],up[60],down[60],res,n;
void dfs(int now,int cnt_u,int cnt_d) //cnt_u和cnt_d分别表示目前以后的上升子序列和下降子序列的数量。
{
if(cnt_u + cnt_d >= res) return ; //如果答案不会更优,直接剪枝
if(now == n + 1)
{
res = min(res , cnt_u+cnt_d); //更新答案。
return ;
}
int i=1;
for(;i<=cnt_u;i++)
if(up[i] < a[now])
break;
//如果按这个贪心思路接下去,up数组一定是单调的,直接遍历到第一个满足条件的即可。
int info = up[i];
up[i] = a[now]; //把a接在后面
dfs(now + 1 , max(cnt_u , i) , cnt_d); //这里如果没有找到满足条件的up得到的i会是cnt_u+1,也就是加了一个新的序列。
up[i] = info;
for(i=1;i<=cnt_d;i++)
if(down[i] > a[now])
break;
info = down[i];
down[i] = a[now];
dfs(now + 1 , cnt_u , max(cnt_d , i));
down[i] = info;
}
int main()
{
while(cin>>n&&n)
{
for(int i=1;i<=n;i++) cin>>a[i];
res = 100;
dfs(1,0,0);
cout << res << endl;
}
}
Acwing 272-最长公共上升子序列
Think
直接类比公共子序列列出dp状态:dp[i][j]表示a[1 ~ i] 和b[1~j]中以b[j]结尾的最长公共上升子序列的长度,接下来考虑转移式。
状态转移:
按照是否包含a[i]划分
1.如果不包含a[i],dp[i][j] = dp[i-1][j]
2.如果包含a[i],我们进行进一步划分,考虑序列的倒数第二个元素b[]是哪个值k
如果只有b[1],则dp[i][j] = 1
如果k = 1,则dp[i][j] = dp[i-1][1] + 1
如果k = 2,则dp[i][j] = dp[i-1][2] + 1
.....
如果k = j - 1,则dp[i][j] = dp[i-1][j-1] + 1
要模拟上述结果,需要三层for循环,过不了
优化:
我们在枚举k时,可以发现我们实际上是在计算当a[i] > b[k] 时dp[i-1][k] 的前缀最大值,这样我们就可以把这层枚举取掉了。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e3 + 10;
int a[maxn] , b[maxn] , dp[maxn][maxn];
int main()
{
int n;
cin>>n;
for(int i = 1; i <= n;i ++)
cin >> a[i];
for(int i = 1; i <= n;i ++)
cin >> b[i];
for(int i = 1;i <= n;i ++)
{
int MAX = 1;
for(int j = 1;j <= n; j++)
{
dp[i][j] = dp[i-1][j];
if(a[i] == b[j]) dp[i][j] = max(dp[i][j] , MAX);
if(a[i] > b[j]) MAX = max(MAX , dp[i-1][j] + 1);
}
}
int res=0;
for(int i=1;i<=n;i++)
res = max(res , dp[n][i]);
cout << res << endl;
}