线性 dp--最长上升子序列模型
最长上升子序列
题目链接:
B3637 最长上升子序列 - 洛谷
思路:
用 f[i] 表示所有以 a[i] 结尾的严格单调上升子序列长度,初始时 f[i]=1 ,枚举序列的倒数第二个数,用 j(0<j<i) 来表示,若 a[j]<a[i] 则 f[i]=max(f[i],f[j]+1)。
代码:
cpp
const int N=5500;
int a[N];
int f[N];
void solve()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
f[i]=1;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
{
if(a[i]>a[j])
f[i]=max(f[i],f[j]+1);
}
}
int ans=0;
for(int i=1;i<=n;i++)ans=max(ans,f[i]);
cout<<ans;
}
使用二分查找可以减少时间复杂度,对于新进来的元素 a[i],如果 a[i] 大于 f[cnt],则 f 数组长度加一,将 a[i] 添加进去。反之用 a[i] 替换掉 f 数组中第一个大于或者等于 a[i] 的元素。这样虽然 f 数组中存的不是最长上升子序列,但是长度是相等的。
cpp
const int N=1e5+5;
int a[N];
int f[N];
int cnt;
int find(int x){
int l=1,r=cnt;
while(l<r){
int mid=(l+r)>>1;
if(f[mid]>=x){
r=mid;
}else l=mid+1;
}
return l;
}
void solve()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
f[++cnt]=a[1];
for(int i=2;i<=n;i++)
{
if(f[cnt]<a[i])f[++cnt]=a[i];
else{
int idx=find(a[i]);
f[idx]=a[i];
}
}
cout<<cnt;
}
怪盗基德的滑翔翼
题目链接:
U234151 怪盗基德的滑翔翼 - 洛谷
思路:
这个题其实就是求一个最长下降子序列,可以选任意楼为起点,假如选择 a[i] 为起点,那么最长距离为以 a[i] 为结尾的最长上升子序列。所以可以正向做一次反向再求一次得到答案最大值。
代码:
cpp
const int N=1e5+5;
int a[N];
int f[N];
int b[N];
void solve()
{
int n;
cin>>n;
memset(b,0,sizeof b);
memset(f,0,sizeof f);
for(int i=1;i<=n;i++)
{
cin>>a[i];
f[i]=1;
b[i]=1;//这里用了两个数组可能不好看
}
for(int i=n;i>0;i--)
{
for(int j=n;j>i;j--){
if(a[j]<a[i])
f[i]=max(f[i],f[j]+1);
}
}
int ans=0;
// for(int i=1;i<=n;i++)
// {
// ans=max(ans,f[i]);
// }
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
{
if(a[j]<a[i])b[i]=max(b[i],b[j]+1);
}
}
for(int i=1;i<=n;i++)
{
ans=max(ans,max(f[i],b[i]));
}
cout<<ans<<endl;
}
登山
题目链接:
U247946 登山 - 洛谷
思路:
这里的要求一旦开始下降就不能再上升,相邻两个景点的海拔不能相同。按照编号递增顺序来浏览,那么就是先单调上升再单调下降。这样我们就要在中间选一个点,分别向两边走,取最大值。这样的话就先从左向右做一遍最长上升子序列再从右向左做一遍最长上升子序列,再进行枚举答案取最大值即可。
代码:
cpp
const int N=1e5+5;
int a[N];
int f[N],b[N];
void solve()
{
int n;
cin>>n;
memset(f,0,sizeof f);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
int ans=0;
for(int i=1;i<=n;i++)
{
f[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<a[i])f[i]=max(f[i],f[j]+1);
}
}
for(int i=n;i>0;i--){
b[i]=1;
for(int j=n;j>i;j--){
if(a[j]<a[i])b[i]=max(b[i],b[j]+1);
}
}
for(int i=1;i<=n;i++)
{
ans=max(ans,f[i]+b[i]-1);
}
cout<<ans<<endl;
}
合唱队形
题目链接:
P1091 [NOIP 2004 提高组] 合唱队形 - 洛谷
思路:
这个题目要求最少的出队人数,那么其实和上一题是相似的,我们找到满足这个类型队伍的最大值,用 n 减去即可。
代码:
代码只需要将输出修改为 cout<<n-ans<<endl。
友好城市
题目链接:
P2782 友好城市 - 洛谷
思路:
先把一边排序,另一边的最长上升子序列的长度就是最多的的数量,一边已经确定好顺序了,只要保证另一边也是升序,就一定不会交叉。只用 dp 会超时,用二分优化。
同样的用一个数组 f 来存,如果 a[i] 大于 f 最后一个数那么就可以放进去,长度加一。反之则找 f 数组中第一个大于 a[i] 的数替换。
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define endl '\n'
#define PII pair<int,int>
#define x first
#define y second
const int N=2e5+5;
PII a[N];
int f[N],b[N];
void solve()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].x;
cin>>a[i].y;
}
sort(a+1,a+1+n);
int cnt=0;
f[++cnt]=a[1].y;
for(int i=2;i<=n;i++)
{
if(a[i].y>f[cnt])f[++cnt]=a[i].y;
else{
int idx=upper_bound(f+1,f+cnt+1,a[i].y)-f;
f[idx]=a[i].y;
}
}
cout<<cnt;
}
signed main(){
IOS
int t=1;
while(t--)
{
solve();
}
}
最大上升子序列和
题目链接:
T285024 最大上升子序列和 - 洛谷
思路:
这个题目使用 f[i] 表示所有以 a[i] 结尾的上升子序列和的最大值。
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define endl '\n'
#define PII pair<int,int>
#define x first
#define y second
const int N=2e5+5;
int a[N];
void solve()
{
int n;
cin>>n;
vector<int> f(n+5);
for(int i=1;i<=n;i++)
{
cin>>a[i];
f[i]=a[i];
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
{
if(a[j]<a[i])f[i]=max(f[i],f[j]+a[i]);
}
}
int ans=*max_element(f.begin(),f.end());
cout<<ans;
}
signed main(){
IOS
int t=1;
while(t--)
{
solve();
}
}
导弹拦截
题目链接:
P1020 [NOIP 1999 提高组] 导弹拦截 - 洛谷
思路:
第一问求最长不上升子序列。
第二问考虑贪心:对于每个数,如果现有的子序列的结尾都小于当前数字,那么开一个新的子序列。否则将它放到结尾大于等于它的最小子序列的后面。
不会证明 🥱
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define endl '\n'
#define PII pair<int,int>
#define x first
#define y second
const int N=2e5+5;
int q[N];
int a[N];
int f[N];
int cnt=0;
void solve()
{
int n=0,x;
while(cin>>x){
a[++n]=x;
}
f[++cnt]=a[1];
for(int i=2;i<=n;i++)
{
if(f[cnt]>=a[i])f[++cnt]=a[i];
else{
int idx=upper_bound(f+1,f+1+cnt,a[i],greater<int>())-f;
f[idx]=a[i];
}
}
cout<<cnt<<endl;
memset(q,0x3f,sizeof q);
cnt=1;
for(int i=1;i<=n;i++)
{
if(q[cnt]>=a[i]){
int idx=lower_bound(q+1,q+1+cnt,a[i])-q;
q[idx]=a[i];
}else{
q[++cnt]=a[i];
}
}
cout<<cnt;
}
signed main(){
IOS
int t=1;
while(t--)
{
solve();
}
}
最长公共子序列
题目链接:
897. 最长公共子序列 - AcWing题库
思路:
用 f[i][j] 表示 a 的前 i 个字母,和 b 的前 j 个字母的最长公共子序列长度。
以 a[i] 、b[j] 是否在子序列中划分为四类:
a[i]不在,b[j]不在 此时max = f[i-1][j-1]。a[i]在,b[j]不在 此时max=f[i−1][j]。a[i]不在,b[j]在 此时max=f[i][j-1]。a[i]在,b[j]在 此时max = f[i-1][j-1]+1。
1)情况是包含在 2)3)中的。
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define int long long
#define endl '\n'
const int N=1010;
char a[N],b[N];
int n,m;
int f[N][N];
void solve(){
cin>>n>>m;
cin>>a+1>>b+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);
}
}
cout<<f[n][m];
}
signed main(){
IOS
int t=1;
while(t--){
solve();
}
}
两个排列的最长公共子序列
题目链接:
P1439 两个排列的最长公共子序列 - 洛谷
思路:
这个题目用上面的方法会超时。我们可以把第一个序列中第 i 个数映射为 i,在第二个序列中也进行修改。比如原序列为 A={1,2,5,4,3} B={5,3,4,2,1} 映射后为 A={1,2,3,4,5} B={3,5,4,2,1} 这样将求最长公共子序列问题转为求 B 序列的最长上升子序列。这样就可以用二分优化。
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define int long long
#define endl '\n'
const int N=1e5+5;
int a[N],b[N];
int n;
int f[N];
void solve(){
cin>>n;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
a[x]=i;//这样使用A数组存储映射关系,方便B数组查询。
}
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
b[i]=a[x];
}
int cnt=0;
f[++cnt]=b[1];
for(int i=2;i<=n;i++){
if(b[i]>f[cnt])f[++cnt]=b[i];
else{
int idx=lower_bound(f+1,f+1+cnt,b[i])-f;
f[idx]=b[i];
}
}
cout<<cnt;
}
signed main(){
IOS
int t=1;
while(t--){
solve();
}
}
Missile Defence System
题目链接:
P10490 Missile Defence System - 洛谷
思路:
一个导弹有四种选择:
1)用一个已经建立好的递增拦截装置拦截;
2)用一个已经建立好的递减拦截装置拦截;
3)新建一个递增拦截装置;
4)新建一个递减拦截装置。
考虑深搜 DFS,我们要进行搜索这颗导弹属于上升子序列还是下降子序列,搜索这颗导弹属于哪一个上升/下降子序列。
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define endl '\n'
#define PII pair<int,int>
#define x first
#define y second
const int N=55;
int up[N],down[N];
int a[N];
int ans;
int n;
//前u个导弹\上升系统个数su\和下降系统个数sd
void dfs(int idx,int su,int sd){
if(su+sd>=ans)return;
//最优性剪枝:当目前答案已经大于已知最优答案时,直接回溯
if(idx==n){
ans=su+sd;
return;
}
/*
使用上升系统拦截。
如果当前已有的上升拦截系统的高度都大于第u个导弹高度,则重新开一套系统
否则,就找到当前低于第u个导弹最高拦截系统来负责拦截
*/
int k=0;
while(k<su&&up[k]>=a[idx])k++;
int t=up[k];
up[k]=a[idx];
if(k>=su)dfs(idx+1,su+1,sd);
else dfs(idx+1,su,sd);
up[k]=t;//恢复现场
/*
用下降拦截系统来拦截
如果当前已有的下降拦截系统的高度都小于第u个导弹高度,则重新开一套系统
否则,则由当前大于第u个导弹最低拦截系统来负责拦截
*/
k=0;
while(k<sd&&down[k]<=a[idx])k++;
t=down[k];
down[k]=a[idx];
if(k>=sd)dfs(idx+1,su,sd+1);
else dfs(idx+1,su,sd);
down[k]=t;
}
void solve()
{
while(cin>>n){
if(n==0)break;
for(int i=0;i<n;i++)cin>>a[i];
ans=n;
dfs(0,0,0);
cout<<ans<<endl;
}
}
signed main(){
IOS
int t=1;
while(t--)
{
solve();
}
}
LCIS
题目链接:
P10954 LCIS - 洛谷
思路:
设 f[i][j] 表示 a 的前 i 个数和 b 的前 j 个数并以 b[j] 结尾的最大长度。
当 a[i]=b[j] 时,只需要在前面找到一个可以把 b[j] 接上去的最长公共上升子序列,答案为 max(f[i-1][k])+1。
当 a[i]≠b[j] 时,f[i][j]=f[i-1][j]
代码:
cpp
/*三重循环做法*/
const int N=3100;
int a[N],b[N];
int n;
int f[N][N];
void solve(){
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++)
{
for(int j=1;j<=n;j++)
{
f[i][j]=f[i-1][j];
if(a[i]==b[j]){
int mx=0;
for(int k=1;k<j;k++){
if(b[k]<b[j])
mx=max(mx,f[i-1][k]);
}
f[i][j]=mx+1;
}
}
}
int ans=0;
for(int i=1;i<=n;i++)
{
ans=max(ans,f[n][i]);
}
cout<<ans;
}
/*
可以发现每次循环所得到的mx值满足a[i]>b[k]的f[i-1][k]+1的前缀最大值,所以我们直接将这步提出来。
*/
const int N=3100;
int a[N],b[N];
int n;
int f[N][N];
void solve(){
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 mx=0;
for(int j=1;j<=n;j++)
{
if(a[i]!=b[j])f[i][j]=f[i-1][j];
if(a[i]==b[j])f[i][j]=max(mx,f[i][j])+1;
if(a[i]>b[j])mx=max(mx,f[i-1][j]);
}
}
int ans=0;
for(int i=1;i<=n;i++)
{
ans=max(ans,f[n][i]);
}
cout<<ans;
}