AcWing 1017 怪盗基德的滑翔翼

题目描述:

怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。

而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。

有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。不得已,怪盗基德只能操作受损的滑翔翼逃脱。

假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。

初始时,怪盗基德可以在任何一幢建筑的顶端。

他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。

因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。

他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。

请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?

输入格式

输入数据第一行是一个整数K,代表有K组测试数据。

每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。

输出格式

对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。

数据范围

1≤K≤100,
1≤N≤100,
0<h<10000

输入样例:

3
8
300 207 155 299 298 170 158 65
8
65 158 170 298 299 155 207 300
10
2 1 3 4 5 6 7 8 9 10

输出样例:

6
6
9

分析:

从本题起进入到最长上升子序列模型,LIS问题动态规划解法见AcWing 895 最长上升子序列,贪心+二分解法见AcWing 896 最长上升子序列 II

本题题意是说一排建筑高低不同,怪盗基德可以选择其中任意一个建筑作为起点,朝一个方向向高度低的建筑物滑翔,求其能经过的建筑物的最大数量。而普通的LIS问题相当于在一排建筑中找一个建筑作为起点,然后向左滑翔的最大长度,也就是说,本题相当于对一个线性序列正向和逆向求最长下降子序列长度。可以注意到逆向的最长下降子序列相当于正向的最长上升子序列,正向的最长下降子序列相当于逆向的最长上升子序列,所以本题简化为一句话就是正向和逆向分别求LIS长度,找出二者中的较大值。

方法一:

使用动态规划求LIS,f[i]表示以a[i]为末字符的最长上升子序列长度,状态转移方程为f[i] = max(f[k]) + 1,k < i且a[k] < a[i]。求完f数组后,最后LIS的长度等于f数组中的最大者。从左往右遍历序列求得的LIS长度为len1,从右往左遍历求得的LIS长度为len2,本题要求的能够滑翔的最大长度为max(len1,len2)。

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

方法二:

贪心+二分求解LIS。长度为len的上升子序列我们只需要保存其中末字符最小的那一串,也就是说,我们只需要找到长度为1,2,3,...的上升子序列的末字符是什么就可以延伸LIS的长度了。之前LIS问题中已经证明了,存放各个长度上升子序列末字符的数组是单调递增的。我们只需要构造出该数组,构造完成后该数组的长度就是LIS的长度。构造f数组的过程可以描述为:在遍历序列的过程中查找f数组中小于当前遍历元素a[i]的最大元素的位置,并将a[i]放到该位置的后一个位置即可。

二分就是要在f数组中找到小于a[i]的最大数的位置l,所以f[mid] < a[i]时,l = mid;否则r = mid - 1,这是为了最终二分终止时l == r,即r的最终位置等于l,都是处于小于a[i]的位置,所以当f[mid] >= a[i]时,r应该等于mid - 1。注意r恰好比l大1时,如果mid = l + r >> 1,则mid = l,若此时f[l] < a[i],则l = mid = l,会导致l与r始终无法重合,故mid设置为l + r + 1 >> 1,即(l + r) / 2的上取整,mid的值等于r时,r总会减小,所以循环必会终止。下面二分代码还有个巧妙的地方在于len = 0时,l == r,此时f[l + 1] = f[1] = a[1],即跳过了f[0]直接为f数组中上升子序列长度为1的末字符赋值。

#include<iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=105;
int a[N],f[N];
int main(){
    int n,T;
    cin>>T;
    while(T--){
        cin>>n;
        for(int i=1;i<=n;i++) cin>>a[i];
        int l,r,len=0,res = 0;
        for(int i = 1;i <= n;i++){
           l = 0,r = len;
           while(l < r){
            int mid = l + r  + 1>> 1;
            if(f[mid] < a[i])   l = mid;
            else    r = mid - 1;
           } 
           len = max(len,l + 1);
           f[l + 1] = a[i];
        }
        res = len,len = 0;
        for(int i=n;i>=1;i--){
            l = 0,r = len;
           while(l < r){
            int mid = l + r  + 1>> 1;
            if(f[mid] < a[i])   l = mid;
            else    r = mid - 1;
           } 
           len = max(len,l + 1);
           f[l + 1] = a[i];
        }
        res = max(res,len);
        cout<<res<<endl;
    }
}
发布了272 篇原创文章 · 获赞 26 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_30277239/article/details/104044955