- gf24240 的博客
《梦溪笔谈·C++》卷十:贪心+二分和动态规划的联系
- 2025-4-23 21:46:25 @
背景:
小L同学(就是我)在自以为有点弄懂DP时去做【提高】最长上升子序列LIS(2),结果。。。
Time Exceeded!!!
#include <iostream>
using namespace std;
int n, a[1000005], dp[1000005], ans;
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
dp[i] = 1;
scanf("%d", &a[i]);
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < i; j++)
{
if (a[i] > a[j])
dp[i] = max(dp[i], dp[j] + 1);
}
}
for (int i = 1; i <= n; i++)ans = max(ans, dp[i]);
printf("%d", ans);
return 0;
}
不是,这都超时那什么不超时啊
还是要感谢DeepSeek。。。
贪心+二分查找与动态规划(DP)的联系
贪心+二分查找和动态规划都是解决 最长上升子序列(LIS) 问题的有效方法,但它们的核心思想和优化方式不同。以下是它们的联系与区别:
1. 动态规划(DP)解法
基本思想:
- 定义状态:
dp[i]
表示以a[i]
结尾的最长上升子序列的长度。 - 状态转移:
- 对于每个
i
,遍历所有j < i
,如果a[j] < a[i]
,则dp[i] = max(dp[i], dp[j] + 1)
。
- 对于每个
- 初始化:
dp[i] = 1
(每个元素本身就是一个长度为1的子序列)。 - 结果:
max(dp[1..n])
。
时间复杂度:
- O(n²)(对于每个
i
,需要遍历所有j < i
)。
缺点:
- 当
n
很大(如n = 1e5
)时,O(n²) 的复杂度会超时。
2. 贪心+二分查找解法
基本思想:
- 维护一个数组
d
:d[len]
表示长度为len
的上升子序列的最小末尾元素。- 目标是让
d
数组中的值尽可能小,以便后续元素更容易扩展。
- 更新规则:
- 如果
a[i] > d[len]
,直接扩展:d[++len] = a[i]
。 - 否则,用
a[i]
替换d
数组中第一个≥ a[i]
的元素(通过二分查找找到位置)。- 这样可以保证
d
数组仍然有序,且后续元素更容易扩展。
- 这样可以保证
- 如果
时间复杂度:
- O(nlogn):
- 遍历数组:O(n)。
- 每次二分查找:O(logn)。
优点:
- 高效处理大规模数据(
n ≤ 1e6
也能轻松通过)。
3. 贪心+二分查找与DP的联系
(1)贪心+二分是对DP的优化
- DP 解法中,
dp[i]
依赖于所有dp[j] (j < i)
,导致 O(n²) 的时间复杂度。 - 贪心+二分通过维护
d
数组,避免了重复计算:d
数组的本质是对 DP 状态的压缩,只保留关键信息(每个长度的最小末尾元素)。- 通过二分查找,快速定位
a[i]
应该插入的位置,从而减少不必要的比较。
(2)d
数组与 dp
数组的关系
d
数组的索引len
对应 DP 中的子序列长度。d[len]
是 DP 中所有长度为len
的子序列中的最小末尾元素。
(3)为什么贪心+二分是正确的?
- 关键点:用较小的数替换
d
数组中的数,不会影响最终的最大长度,但能让后续的数更容易接上。- 例如,如果当前
d = [1, 3, 5]
,新数是4
:- 直接替换
5
为4
,得到d = [1, 3, 4]
。 - 这样如果后面出现
4.5
,就可以接上,而原来的5
无法接4.5
。
- 直接替换
- 例如,如果当前
4. 对比总结
方法 | 时间复杂度 | 空间复杂度 | 核心思想 |
---|---|---|---|
动态规划(DP) | O(n²) | O(n) | 计算以每个元素结尾的 LIS 长度,依赖之前所有状态。 |
贪心+二分查找 | O(nlogn) | 维护 d 数组,用二分查找快速更新,避免重复计算。 |
5. 代码实现对比
(1)动态规划(O(n²))
for (int i = 1; i <= n; i++) {
dp[i] = 1;
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
ans = *max_element(dp + 1, dp + n + 1);
(2)贪心+二分(O(nlogn))
int len = 0;
d[0] = -INF; // 初始化为极小值
for (int i = 1; i <= n; i++) {
if (a[i] > d[len]) {
d[++len] = a[i];
} else {
int pos = lower_bound(d + 1, d + len + 1, a[i]) - d;
d[pos] = a[i];
}
}
ans = len;
6. 如何选择方法?
- 如果
n ≤ 1e4
,可以用 DP(代码简单,容易理解)。 - 如果
n ≥ 1e5
,必须用贪心+二分(否则会超时)。
7. 为什么贪心+二分更高效?
- DP 的瓶颈在于内层循环(遍历所有
j < i
),而贪心+二分通过维护d
数组和二分查找,将内层循环的 O(n) 优化为 O(logn)。 - 本质上是用空间(
d
数组)换时间(避免重复计算)。
总结
贪心+二分查找是对动态规划的优化,通过维护一个有序的 d
数组和二分查找,将时间复杂度从 O(n²) 降为 O(nlogn)。两者解决的问题相同,但贪心+二分更适合大规模数据。