背景:

小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 数组中的值尽可能小,以便后续元素更容易扩展。
  • 更新规则
    1. 如果 a[i] > d[len],直接扩展:d[++len] = a[i]
    2. 否则,用 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
      • 直接替换 54,得到 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)。两者解决的问题相同,但贪心+二分更适合大规模数据。