7

最长上升子序列模型练习

 2 years ago
source link: https://blog.csdn.net/csdnwqy030429/article/details/122769407
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

怪盗基德的滑翔翼

题目链接:怪盗基德的滑翔翼
分析:这道题还是比较简单的,我们只需要求一遍最长上升子序列(倒着看就是怪盗基德从最后的一个点滑翔到最前面的一个点),再求一遍最长下降子序列,这两者中的最小值就是答案。
代码实现:
数据范围较小,朴素做法就能过

#include<iostream>
using namespace std;
const int N=110;
int a[N],f[N],g[N];
int main(){
    int t;
    cin>>t;
    while(t--){
        int res=0;
        int n;
        cin>>n;
        for(int i=1;i<=n;i++)   cin>>a[i];
        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=1;i<=n;i++){
            res=max(res,f[i]);
        }
        for(int i=1;i<=n;i++){
            g[i]=1;
            for(int j=1;j<i;j++){
                if(a[j]>a[i])
                    g[i]=max(g[i],g[j]+1);
            }
        }
        for(int i=1;i<=n;i++){
            res=max(res,g[i]);
        }
        cout<<res<<endl;
    }
    return 0;
}
newCodeMoreWhite.png

贪心做法:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=110;
int a[N],n,t;
int main(){
    cin>>t;
    while(t--){
        int res=0;
        cin>>n;
        for(int i=1;i<=n;i++)   cin>>a[i];
        vector<int> ans;
        for(int i=1;i<=n;i++)
            if(ans.empty()||a[i]>ans.back()) ans.push_back(a[i]);
            else *(lower_bound(ans.begin(),ans.end(),a[i]))=a[i];
        res=ans.size();
        ans.clear();
        for(int i=1;i<=n;i++)
            if(ans.empty()||a[i]<ans.back()) ans.push_back(a[i]);
            else *(lower_bound(ans.begin(),ans.end(),a[i],greater<int>()))=a[i];//这里是在不上升的序列里查找小于等于a[i]的第一个数的位置的写法
        res=max(res,(int)ans.size());//这里ans.size()需要强转为int
        cout<<res<<endl;
    }
    return 0;
}

题目链接:登山
分析:题目的意思很明显,就是求一个先递增后递减的最长序列,对于每个点,我们求出以它为终点的最长上升子序列和以它为起点的最长下降子序列,求出总长,其中最大的一个就是答案。
代码实现:
这里数据范围较小,我就只写朴素版本的了

#include<iostream>
using namespace std;
const int N=1010;
int a[N],f[N],g[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)   cin>>a[i];
    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;i--){//这里要注意,我们平常求的都是以某点结束的最长升/降子序列,这里要我们求以某点开始的,因此我们倒着求最长上升子序列就好了,倒序中以i结尾的最长上升子序列就是正序中以i开头的最长下降子序列
       g[i]=1;
        for(int j=n;j>i;j--){
            if(a[j]<a[i]){
                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);//注意,i这个点被包含了两次,要去掉一次
    cout<<res<<endl;
    return 0;
}
newCodeMoreWhite.png

题目链接:友好城市
分析:看完这个题,整个人还是有点懵逼的,这个时候就需要我们多画几组样例模拟一下,找找其中的规律。
在这里插入图片描述
图中每对形状相同的图形就是一对友好城市,我们发现,我们按照上方城市的坐标从小到大排序后,下方城市的最长上升子序列就是答案,因为一旦选择了有下降的序列,就必然会出现相交的情况!因此,我们只需对所有友好城市对按上方的坐标排序,然后对该序列的下方城市求最长上升子序列就是答案了!
代码实现:
同样地,本题数据范围较小,我就懒得写优化版本了

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 5010;
pair<int, int> a[N];
int f[N];
int main() {
	int n;
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> a[i].first >> a[i].second;
	}
	sort(a, a + n);
	int res = 0;
	for (int i = 0; i < n; i++) {
		f[i] = 1;
		for (int j = 0; j < i; j++) {
			if (a[i].second > a[j].second) {
				f[i] = max(f[i], f[j] + 1);
			}
		}
		res = max(res, f[i]);
	}
	cout<<res<<endl;
	return 0;
}

最大上升子序列和

题目链接:最大上升子序列和
分析:和最大上升自序列差不多,我们只需把状态表示的含义改为以i结尾的最大上升子序列和,再在代码中稍作修改就可以了。这里应该就不能用贪心来优化了吧?(本人只是稍微想了想感觉不可以)这里我想到了某人的一句话:一般时间复杂度越高的算法越全能。本题就用最朴素的方法来写。
代码实现:

#include<iostream>
using namespace std;
const int N=1e3+10;
int a[N],f[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)   cin>>a[i];
    int res=0;
    for(int i=1;i<=n;i++){
        f[i]=a[i];//最小也是它自己
        for(int j=1;j<i;j++){
            if(a[j]<a[i]){//如果可以加到以j结尾的上升子序列后面
                f[i]=max(f[i],f[j]+a[i]);//尝试更新
            }
        }
        res=max(res,f[i]);//更新答案
    }
    cout<<res<<endl;
    return 0;
}

题目链接:拦截导弹
分析:很经典的一题了,对于求最多拦截的导弹数,我们只需求最长不上升子序列即可,而对于求需要的系统数,我们可以枚举每一个导弹的同时记录每一个系统拦截的最低高度的导弹,如果新的导弹比当前记录的所有系统拦截的最低导弹还要高,就新开一个系统并记录,否则就找到大于等于该导弹高度中高度最小的一个(可以理解留下高度较大的来防范后面的导弹)更新为此导弹的高度。等等!这个熟悉的计算过程,和求最长上升子序列一毛一样!
代码实现

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 1e5 + 10;
int n = 1;
int a[N];
int main() {
	while (cin >> a[n])	n++;//这样读取后最后n的大小比数字个数大1
	vector<int> ans;
	for (int i = 1; i < n; i++) {//这里求最长不上升子序列,和最长下降子序列有一定区别
		if (ans.empty() || a[i] <= ans.back()) ans.push_back(a[i]);//如果新的导弹高度等于ans.back(),也要加入
		else *(upper_bound(ans.begin(), ans.end(), a[i], greater<int>())) = a[i];//查找第一个小于a[i]的数改为a[i]
	}
	cout << ans.size() << endl;
	ans.clear();
	for (int i = 1; i < n; i++) {//求最长上升子序列
		if (ans.empty() || a[i] > ans.back()) ans.push_back(a[i]);
		else *(lower_bound(ans.begin(), ans.end(), a[i])) = a[i];
	}
	cout << ans.size() << endl;
	return 0;
}

导弹防御系统

题目链接:导弹防御系统
分析:这题一看就感觉很恶心,但数据范围很小,因此我们考虑爆搜,对每一个导弹,先考虑将它放进上升子序列,恢复现场后再考虑将它放进下降子序列。
代码实现:

#include<iostream>
using namespace std;
int n,ans;
const int N=55;
int a[N],up[N],down[N];//up数组记录每一个上升子序列的结尾值
void dfs(int x,int u,int d){//第一个参数是当前考虑的数的下标,第二个参数是当前已有的上升子序列的个数,第三个参数是当前已有的下降子序列的个数
    if(u+d>=ans)    return ;//如果当前u和d大小之和已经大于ans了,就没必要考虑了
    if(x==n){//如果考虑完了所有数,就更新答案并返回
        if(u+d<ans) ans=u+d;
    }
    int k=0;
    while(k<u&&up[k]>=a[x]) k++;//找到大于a[x]的第一个数,数据范围很小,懒得二分了,如果没有大于a[x]的数,k就是新的上升子序列的下标
    if(k<u){//如果没开辟新的上升子序列
        int t=up[k];//保留现场
        up[k]=a[x];//更新up数组
        dfs(x+1,u,d);//搜下一个点
        up[k]=t;//恢复现场
    }
    else{//开辟了新的上升子序列
        up[k]=a[x];//直接更新
        dfs(x+1,u+1,d);//这里上升子序列数多了一个
        //这里不用恢复现场,因为对后面没有影响(u,d,down数组的值都没变)
    }
    k=0;
    while(k<d&&down[k]<=a[x]) k++;//同上
    if(k<d){
        int t=down[k];
        down[k]=a[x];
        dfs(x+1,u,d);
        down[k]=t;
    }
    else{
        down[k]=a[x];
        dfs(x+1,u,d+1);
    }
}
int main(){
    while(cin>>n,n){
        for(int i=0;i<n;i++)   cin>>a[i];
        ans=n;//先将答案置为n,最多也就一个导弹占用一个
        dfs(0,0,0);
        cout<<ans<<endl;//输出答案
    }
    return 0;
}
newCodeMoreWhite.png

最长公共上升子序列

题目链接:最长公共上升子序列
分析:同一看就很恶心的题目,也是此专题最后一题。按照dp问题的思路分析一遍:
在这里插入图片描述
代码实现:

#include<iostream>
using namespace std;
int n;
const int N=3010;
int a[N],b[N];
int f[N][N];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)   scanf("%d",a+i);
    for(int i=1;i<=n;i++)   scanf("%d",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]){//如果这两者相等才有下面的,此时a[i]=b[j]
                f[i][j]=max(f[i][j],1);//第二个序列只有j这一个数的情况
                for(int k=1;k<j;k++){//枚举第二个序列的倒数第二个数
                    if(b[k]<b[j]){//满足上升,因为a[i]=b[j],所以b[j]可以换为a[i]
                        f[i][j]=max(f[i][j],f[i-1][k]+1);//更新答案
                    }
                }
            }
        }
    }
    int res=0;
    for(int i=1;i<=n;i++)   res=max(res,f[n][i]);//找出答案
    printf("%d",res);
    return 0;
}

但这么交TLE了,我们再观察发现有一重循环是可以优化掉的。
代码:

#include<iostream>
using namespace std;
int n;
const int N=3010;
int a[N],b[N];
int f[N][N];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)   scanf("%d",a+i);
    for(int i=1;i<=n;i++)   scanf("%d",b+i);
    for(int i=1;i<=n;i++){
        int maxt=1;//maxt记录f[i-1][1~j-1]+1的最大值,也就是说,j=k时,maxt记录的是f[i-1][1~k-1]+1中的最大值
        for(int j=1;j<=n;j++){
            f[i][j]=f[i-1][j];
            if(a[i]==b[j]) f[i][j]=max(f[i][j],maxt);
            if(b[j]<a[i]) maxt=max(maxt,f[i-1][j]+1);//只有当b[j]<a[i]才用更新,因为只有b[j]=a[i]时才会用到maxt,相当于等于a[i]的b[j]用之前小于此时b[j]的状态更新
        }
    }
    int res=0;
    for(int i=1;i<=n;i++)   res=max(res,f[n][i]);
    printf("%d",res);
    return 0;
}

值得一提的是,直接想出来优化后的写法是不大可能的,应该是写出朴素的写法后观察得到优化方法,不要强迫自己直接去理解优化后的代码。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK