- gf24240 的博客
《梦溪笔谈·故事》卷一:动态规划
- 2025-4-23 21:18:27 @
关于动态规划,这里有我的一些观点:
-
both我&文晶目前(编者注:2025年4月23日)还没学动态规划;
-
动态规划看不懂;
-
但是可E的文晶竟然上个补习班就做了几道dp
这是文晶做的一道题:拦截导弹
太坏了,我堂堂LZH竟然看不懂鸡爪(即文晶)的代码,看了半天只觉得有点二分查找(可是both I and wwenjing还没学)的代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100005
int a[N], d[N], len, h, n;
int main()
{
while(cin >> h)
a[++n] = h;
d[++len] = a[1];
for(int i = 2; i <= n; ++i)
{
if(a[i] <= d[len])
d[++len] = a[i];
else
{
int l = 1, r = len, m;
while(l < r)
{
m = (l+r)/2;
if(d[m] < a[i])
r = m;
else
l = m+1;
}
d[l] = a[i];
}
}
cout << len << endl;
len = 0;
d[++len] = a[1];
for(int i = 2; i <= n; ++i)
{
if(a[i] > d[len])
d[++len] = a[i];
else
{
int l = 1, r = len, m;
while(l < r)
{
m = (l+r)/2;
if(d[m] >= a[i])
r = m;
else
l = m+1;
}
d[l] = a[i];
}
}
cout << len << endl;
return 0;
}
感谢DeepSeek的讲解。。。
代码功能分析
这段代码实现了导弹拦截问题的两个关键计算:
- 第一行输出:最多能拦截的导弹数量(最长不上升子序列长度)
- 第二行输出:拦截所有导弹最少需要的系统数量(等于最长严格上升子序列长度)
代码详解
1. 输入处理
while(cin >> h) a[++n] = h;
- 读取所有输入的导弹高度,存储在数组
a
中n
记录导弹的总数量2. 计算最长不上升子序列(第一问)
d[++len] = a[1]; for(int i = 2; i <= n; ++i) { if(a[i] <= d[len]) d[++len] = a[i]; else { int l = 1, r = len, m; while(l < r) { m = (l+r)/2; if(d[m] < a[i]) r = m; else l = m+1; } d[l] = a[i]; } } cout << len << endl;
- 使用贪心+二分法优化计算最长不上升子序列
d
数组维护当前可能的子序列- 当新导弹高度 ≤
d
数组末尾时,直接扩展序列- 否则,二分查找合适位置进行替换
3. 计算最长严格上升子序列(第二问)
len = 0; d[++len] = a[1]; for(int i = 2; i <= n; ++i) { if(a[i] > d[len]) d[++len] = a[i]; else { int l = 1, r = len, m; while(l < r) { m = (l+r)/2; if(d[m] >= a[i]) r = m; else l = m+1; } d[l] = a[i]; } } cout << len << endl;
- 同样使用贪心+二分法,但改为计算严格上升子序列
- 根据Dilworth定理,最少系统数等于最长严格上升子序列长度
时间复杂度分析
- 两种计算都使用了O(nlogn)的算法:
- 外层循环遍历所有导弹:O(n)
- 内层使用二分查找:O(logn)
- 总时间复杂度:O(nlogn)
示例验证
输入样例:
389 207 155 300 299 170 158 65
执行过程:
- 最长不上升子序列计算:
维护的d数组变化:
[389]
[389,207]
[389,207,155]
[389,300,155] (300替换207)
[389,300,299]
[389,300,299,170]
[389,300,299,170,158]
[389,300,299,170,158,65]
最终长度:6
- 最长严格上升子序列计算:
维护的d数组变化:
[389]
[207] (替换389)
[155] (替换207)
[155,300]
[155,299] (替换300)
[155,170] (替换299)
[155,158] (替换170)
[65,158] (替换155)
最终长度:2
输出:
6 2
代码优化建议
- 可以合并两个计算过程的公共代码,减少重复
- 添加输入验证,确保输入数据的有效性
- 使用更清晰的变量名,如用
maxNonIncreasingLen
代替len
关键点说明
- 贪心+二分的正确性:
- 维护的d数组并不一定是真实的子序列,但长度是正确的
- 通过替换操作保证了后续数字的扩展可能性
- Dilworth定理的应用:
- 将导弹序列视为偏序集
- 最少链划分等于最长反链长度
- 边界处理:
- 代码正确处理了n=1的情况
- 二分查找的边界条件处理得当
总结
这段代码高效地解决了导弹拦截问题,通过:
- 贪心算法维护可能的子序列
- 二分查找优化查找过程
- 两次遍历分别解决两个子问题
- 正确应用了组合数学中的Dilworth定理
最终实现了O(nlogn)时间复杂度的解决方案,能够高效处理大规模输入。
好吧。DeepSeek又跟我讲了半天终于弄出了动态规划的解法:
#include <iostream>
using namespace std;
int n = 1, hei[100005], dp1[100005], dp2[100005];
int main()
{
while (cin >> hei[n++])
{
dp1[n - 1] = 1; // 初始化dp1
dp2[n - 1] = 1; // 初始化dp2
}
n -= 2;
// 计算最长不上升子序列(dp1)
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < i; j++)
{
if (hei[j] >= hei[i])
dp1[i] = max(dp1[i], dp1[j] + 1);
}
}
int ans1 = 0;
for (int i = 1; i <= n; i++)
ans1 = max(ans1, dp1[i]);
// 计算最长严格上升子序列(dp2)
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < i; j++)
{
if (hei[j] < hei[i])
dp2[i] = max(dp2[i], dp2[j] + 1);
}
}
int ans2 = 0;
for (int i = 1; i <= n; i++)
ans2 = max(ans2, dp2[i]);
cout << ans1 << "\n" << ans2;
return 0;
}