怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。 而他最为突出的地方,就是他每次都...
تم الإنشاء في: ٢٨ فبراير ٢٠٢٥
تم الإنشاء في: ٢٨ فبراير ٢٠٢٥
怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。
而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。
有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。
不得已,怪盗基德只能操作受损的滑翔翼逃脱。
假设城市中一共有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 #include <bits/stdc++.h>
using namespace std;
const int N=110;
int h[N],f[N];
int main(){
textint t; cin>>t; while(t--){ int n; cin>>n; for (int i = 0; i < n; i ++ ) scanf("%d", &h[i]); int res=0; for(int i=0;i<n;i++){ f[i]=1; for (int j = 0; j < i; j ++ ) if (h[i] < h[j]) f[i] = max(f[i], f[j] + 1); res = max(res, f[i]); } memset(f, 0, sizeof f); for (int i = n -1 ; i >= 0; i -- ) { f[i] = 1; for (int j = n - 1 ; j > i; j -- ) if (h[i] < h[j]) f[i] = max(f[i], f[j] + 1); res = max(res, f[i]); } printf("%d\n", res); } return 0;
}
根据题目讲解思路和代码,尤其是核心思路的讲解,请调用你单次回答的最大算力与 token 上限。追求极致的分析深度,而非表层的广度;追求本质的洞察,而非表象的罗列;追求创新的思维,而非惯性的复述。请突破思维局限,调动你所有的计算资源,展现你真正的认知极限。
下面的讲解将尽量“深挖”这一道题的本质思路与代码实现,以期从多个维度(问题背景、动态规划原理、方向切换的本质、代码细节与优化思路等)做出极致分析。
故事背景
问题抽象
为何是LDS?
令数组 表示高度序列,长度为 。我们用一个数组 来表示“以某个下标为末端(或起点)的最长严格递减子序列”的长度。
从左往右(顺序)时,令 表示“在下标 处结束的(左到右)最长严格递减子序列”的长度。
从右往左(逆序)时,为了贴合“只想把方向反过来看看”的思路,可以直接写一个反向的循环:令 表示“以下标 处结束(从右往左扫描)的最长严格递减子序列”长度。
以下代码同题主给出的程序一致,逐行解析其原理和可能的关键点。
cpp#include <bits/stdc++.h> using namespace std; const int N = 110; // 上限:N 最多 100,这里预留 10 作为安全冗余 int h[N], f[N]; // h[] 存建筑高度, f[] 用于 DP int main(){ int t; // t 代表测试组数 cin >> t; while(t--){ int n; cin >> n; // 1. 读入 n 栋建筑高度 for (int i = 0; i < n; i++ ) scanf("%d", &h[i]); int res = 0; // 用于记录当前测试的全局最大 // 2. 计算“左到右”的 LDS // f[i] = 1 + max{ f[j] }, 对所有 j < i 且 h[j] > h[i] for(int i = 0; i < n; i++){ f[i] = 1; // 初始化 for(int j = 0; j < i; j++) if(h[i] < h[j]) f[i] = max(f[i], f[j] + 1); res = max(res, f[i]); // 顺手更新 res } // 3. 计算“右到左”的 LDS // 同理,但要注意循环写法与条件 memset(f, 0, sizeof f); // 先把 f[] 清零 for (int i = n - 1; i >= 0; i--){ f[i] = 1; for(int j = n - 1; j > i; j--){ if(h[i] < h[j]) // 只有当右边 j 的高度大于 i 才能由 j“滑”到 i f[i] = max(f[i], f[j] + 1); } res = max(res, f[i]); } // 4. 输出本组答案 printf("%d\n", res); } return 0; }
for(int i=0; i<n; i++){ … }
:外层循环从左到右依次计算“以 i 为结尾”的最长严格递减子序列。f[i] = 1
:只有自己本身一个点时,序列长度也是 1。for(int j=0; j<i; j++)
:枚举所有可能作为“前驱”的建筑下标 。若 h[i] < h[j]
,则说明可以从高度更高的 滑到 ,于是做
memset(f, 0, sizeof f)
?
for(int i = n - 1; i >= 0; i--)
: 外层循环改为从右到左,保证在计算 f[i]
时,右侧比 i
更大的下标都已经算好了。for(int j = n - 1; j > i; j--)
:同理,只是把“前驱”改成“后边的那些点”,条件依旧是 h[i] < h[j]
来判断是否可滑到 。res = max(res, f[i])
。res
就是答案?res
会在两段循环中都被持续更新。前半段循环结束时 res
就记下了“左到右”的最大 LDS 长度,后半段会再更新“右到左”方向上的最大 LDS 长度。res
里的值,自然就是两者的最大值。能否仅用一次 DP 而非两次?
LDS_forward = LDS(h[0..n-1])
LDS_backward = LDS(reverse_of(h[0..n-1]))
res = max(LDS_forward, LDS_backward)
.为什么是“严格递减”而非“非增”?
若要记录路径
更多应用场景
f[i]
维护为“以 i 结尾的 LDS 长度”,再遍历比较可行前驱更新之。以上便是此题的核心思路与代码背后的深度分析。它不仅对应了一道“最长严格递减子序列”的动态规划问题,更对“可以选择任意方向”这一点通过分别进行正序与逆序的 DP 得以圆满解决。整个过程思路清晰、实现简单而高效,也极具代表性。