题目描述:
怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。
而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。
有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。不得已,怪盗基德只能操作受损的滑翔翼逃脱。
假设城市中一共有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;
}
}