最长上升子序列模型
—— 2023.06,08
核心思想
- 状态转移思想:第i个位置上的数,有多种选择
- 一种是不接在前面0 ~ i - 1上满足条件的数后面 : f[i] = 1;
- 一种是接在0 ~ i - 1上满足条件的数后面 : f[i] = max(f[i],f[j] + 1);
单调性二分优化
- 核心思想是:用q[i]记录长度为i的最长上升子序列的最小结尾数,二分查找q数组中小于等于a[k]的最大一个数的位置pos,f[k] = pos + 1,再更新q[pos + 1] = a[k],len = max(len,pos + 1),最终的答案就是len;
这样为啥是正确的呢?
- 首先我们得数组q中存的数一定满足从小到大的单调性;
反证:
假设存在 i < j, q[i] >= q[j],
因为长度为 j 的子序列以 q[j] 结尾,那么一定存在一个数 k < q[j] 使得以 k 结尾的序列长度为 i, 但此时 q[i] > q[j] > k ,
所以 q[i] 应该存的数是 k,与假设矛盾。所以数组q中存的数一定满足从小到大的单调性
-
其次,因为 q 数组中越大的数,构成的最长上升子序列的长度就更长,以及我们肯定希望a[k]能接在长度最长的子序列后面,所以我们就二分查找q数组中小于等于a[k]的最大一个数的位置pos,f[k] = pos + 1;
-
接着,为什么要更新q[pos + 1] = a[k]呢?
因为我们二分的时候,找的是小于等于a[k]的最大的一个数,所以更新f[k]的长度为 pos + 1后, 为了维护q数组的性质,q[pos + 1] = a[k];
因为q[pos + 1] > a[k],而f[k] = pos + 1;
- 最后更新len的长度应该就很好理解了
题目
最长上身子序列 Ⅰ
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e3+10;
int f[N],a[N];
int n,m;
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],f[i]=1;
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
int mx=0;
for(int i=1;i<=n;i++) mx=max(f[i],mx);
cout<<mx<<endl;
return 0;
}
最长上升子序列 II
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int q[N],a[N],len;
int n;
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++)
{
int l = 0,r = len;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(q[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = max(len,r + 1);
q[r + 1] = a[i];
}
cout << len << endl;
return 0;
}
怪盗基德的滑翔翼
思路
- 很显然这是一个最长子序列的题目
- 题目要求可以选择不同方向,但在同一方向上,只能滑到更低的地方
- 如果选择右方向相当于从右往左找满足递增的最长子序列
- 如果选择左方向相当于从右往左找满足递减的最长子序列
- 所以我们要维护两个数组,一个记录递增的最长子序列,另一个记录递减的最长子序列
代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 110;
int h[N],f[N],g[N]; //f数组统计最长递减子序列,g数组统计最长递增子序列
int t,n;
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> h[i];
f[1] = 1,g[1] = 1;
for(int i = 2; i <= n; i++)
{
f[i] = g[i] = 1;
for(int j = 1; j < i; j++)
if(h[i] < h[j]) f[i] = max(f[i],f[j] + 1);
else if(h[i] > h[j]) g[i] = max(g[i],g[j] + 1);
}
int res = 0;
for(int i = 1; i <= n; i++) res = max(res,max(f[i],g[i])); //统计答案
cout << res << endl;
}
int main()
{
cin >> t;
while(t--) solve();
return 0;
}
登山
思路
- 由题意可知,先上山看风景,再下山看风景,下山后,不再上山;
- 所以我们要找的是(以第i个位置举例)
- 从左到右以第i个位置结尾的最长递增子序列的长度 f[i]
- 从右到左以第i个位置结尾的最长递增子序列的长度 g[i]
- 然后以i为下山位置,浏览风景区的总数为f[i] + g[i] - 1;
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int f[N],g[N];
int h[N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>h[i],f[i]=1,g[i]=1;
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
if(h[i]>h[j]) f[i]=max(f[i],f[j]+1);
for(int i=n-1;i>=1;i--)
for(int j=n;j>i;j--)
if(h[i]>h[j]) g[i]=max(g[i],g[j]+1);
int res=0;
for(int i=1;i<=n;i++) res=max(res,f[i]+g[i]-1);
cout<<res<<endl;
return 0;
}
合唱队形
思路
和登山那个题思路一样的;
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int f[N],g[N];
int h[N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>h[i],f[i]=1,g[i]=1;
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
if(h[i]>h[j]) f[i]=max(f[i],f[j]+1);
for(int i=n-1;i>=1;i--)
for(int j=n;j>i;j--)
if(h[i]>h[j]) g[i]=max(g[i],g[j]+1);
int res=n;
for(int i=1;i<=n;i++) res=min(res,n-f[i]-g[i]+1);
cout<<res<<endl;
return 0;
}
友好城市
思路
- 用pair<int,int>存了每一对城市之后,排个序
- 再找pair<int,int>第二个元素递增的最长子序列
代码
#include<iostream>
#include<algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;
const int N=5010;
int f[N];
PII con[N];
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++) scanf("%d%d",&con[i].x,&con[i].y);
sort(con,con+n);
// for(int i=0;i<n;i++) printf("%d %d\n",con[i].x,con[i].y);
for(int i=0;i<=n;i++)
{
f[i]=1;
for(int j=0;j<i;j++) if(con[i].y>con[j].y) f[i]=max(f[i],f[j]+1);
}
int res=0;
for(int i=0;i<n;i++) res=max(f[i],res);
cout<<res<<endl;
return 0;
}