ch09 部分题目解法

ch09 部分题目解法

先自己思考,多角度尝试后解决不了再看解法。

快乐的假期

  • 知识点:基础 DP

  • 思路:

    • dp[i][0/1/2]:分别表示钦定第 i 天游泳/爬山/写作业,前 i 天的最大快乐值。

    • 状态转移

      • 如果第 i 天游泳,前一天就只能爬山或写作业,在这两种选择中取最大值 dp[i][0] = max(dp[i-1][1], dp[i-1][2]) + a[i];
      • dp[i][1] 和 dp[i][2] 的计算同理。
    • 边界

      • 从第 1 天开始有活动,第 0 天没有快乐值 dp[0][0] = dp[0][1] = dp[0][2] = 0;

数列分段 I

  • 知识点:基础 DP
  • 思路:
    • dp[i]:前 i 个数字最多分成 dp[i] 段
    • 状态转移
      • 前 i 个数字也有可能无法分段,先给 dp[i] 赋初始值 -1 表示无法分段。
      • 考虑第 i 个数字所在的这最后一段范围,记前 j 个数字是之前分好段的,第 j+1 ~ i 个数字作为最后一段。
      • 前 j 个数字最多分成 dp[j] 段,加上最后这一段,共有 dp[j] + 1 段,所以 dp[i] 的一个取值选择是 dp[i] = dp[j] + 1。
        • 前提1:第 j+1 ~ i 个数字的和要 >= 0,可以预处理前缀和,快速计算这一段的和。
        • 前提2:前 j 个数字要能够分段,不能无解。
      • 枚举 j ,取最大的 dp[j] + 1 就是 dp[i] 的值。
    • 边界:
      • 0 个数字分成 0 段,dp[0] = 0。

数列分段 II

  • 知识点:基础 DP
  • 思路:
    • dp[i]:给前 i 个数字分段的最小总代价。
    • 状态转移:
      • 与“数列分段 I”类似,枚举 j,计算最后这一段的代价,dp[i] = min(dp[i], dp[j] + 最后一段代价)。
      • 每个状态因为要取 min,先给一个极大的初始值,dp[i] = 1e18。
    • 边界:
      • 0 个数字不用分段,代价为 0,dp[0] = 0。
    • 注意代价是有可能超过 int 范围的。

数列取数 I

  • 知识点:基础 DP
  • 思路:
    • dp[i]:前 i 个数能取出的最大总和。
    • 状态转移:
      • 对于 dp[i],考虑最后一步,也就是选或不选 a[i]
        • 不选 a[i]:相当于在前 i - 1 个数字中选,dp[i-1]
        • 选 a[i]:不能选 a[i-1],可以在前 i - 2 个数字中选,dp[i-2] + a[i]
      • 所以 dp[i] = max(dp[i-1], dp[i-2] + a[i])
    • 边界
      • 递推计算 dp[i] 要用到 dp[i-1] 和 dp[i-2],可以状态 0 和 1 作为边界,dp[0] = 0, dp[1] = a[1],从 2 开始递推。

数列取数 II

  • 知识点:基础 DP
  • 思路:
    • 与“数列取数 I”的区别是本题不能同时选第 1 个和第 n 个,“数列取数 I”则没有这个要求。处理方式很简单,做两次 DP。
    • 第一次 dp:限制不选 a[n],即按照“数列取数 I”的方式算出 dp[n-1]
    • 第二次 dp:限制不选 a[1],可以将 a[1] 赋值为极小的值,这样肯定不会选到 a[1],然后按照“数列取数 I”的方式算出 dp[n]
    • 两次算出的结果取最大值为答案。

导弹拦截

  • 知识点:LIS 变形问题
  • 思路:
    • 第一问求最长不升子序列长度,与 LIS 解法类似。
    • 第二问解法是贪心
      • 贪心 1
        • 对于每一发来袭的导弹,应该尽量用之前的拦截系统,不要新开一套拦截系统。
        • 能不新开一套拦截系统的情况下去用新的系统,不会令结果更优。
      • 贪心 2
        • 记已经用了 m 套拦截系统,第 j 套系统最后拦截的导弹高度是 b[j]。
        • 对于下一次来袭的导弹高度 a[i],考虑应该用之前哪套系统拦截,还是不得不新开一套系统。
        • 对于 b[j] < a[i] 的系统 j,拦截不了更高的导弹,不能用。
        • 所以在 b[j] >= a[i] 的系统中选,贪心策略是选择最小的 b[j] 。因为 b[j] 更大的系统下一次拦截导弹的范围更广,留着之后用能发挥更大的效果。
      • 如果把代码写出来可以发现这个解法跟 LIS 的贪心解法是一样的,所以第二问等同于在问 LIS 长度。

合唱队形

  • 知识点:LIS

  • 思路:

    • 观察到在队形 t 1 < ⋯ < t i > t i + 1 > ⋯ > t k t_1< \cdots <t_i>t_{i+1}> \cdots >t_k t1<<ti>ti+1>>tk 中,最高的 t i t_i ti 是一个特殊点,只要知道 t i t_i ti 左边的最长上升子序列长度,右边的最长下降子序列长度,就能算出这个队形的长度。以此为切入点设计 DP 状态。
    • dp1[i]:以 a[i] 结尾的最长上升子序列长度
    • dp2[i]:以 a[i] 开头的最长下降子序列长度
      • 从前往后的最长下降,也就是从后往前的最长上升,从后往前计算 dp2[] 即可。
    • 计算出 dp1[] 和 dp2[] 之后,枚举合唱队中身高最高的同学 i,取 dp1[i] + dp2[i] - 1 的最大值就是合唱队的最多人数。
    • 最少出列人数 = 总人数 - 合唱队最多人数。



扩展

  • 最长上升子序列(LIS)有 O ( n log ⁡ n ) O(n\log n) O(nlogn) 的解法,这个方法不是 DP,而是利用序列本身的特征进行贪心。

  • 维护一个数组 b[],len 表示 b[] 数组当前长度。b[i] 表示长度为 i 的 LIS 的结尾最小值,可以想到 b[] 数组是严格单调递增的。

  • 从前往后不断加入新的 a[i] 迭代更新 b[] 数组。考虑 a[i] 可以作为长度多少的 LIS 的结尾最小值?

    • 只需要在 b 数组中找到第一个 >= a[i] 的数据位置,记为 p,即 b[p] 是第一个 >= a[i] 的,此处可以二分查找。
    • 因为 b[p-1] < a[i],所以 a[i] 可以接在 b[p-1] 后面形成长度为 p 的 LIS,所以 a[i] 可以作为长度 p 的LIS 的结尾最小值,更新 b[] 数组 b[p] = a[i];
    • 可以举一些例子模拟下面代码的执行过程,更好地理解这个算法。例如以 int a[] = {4, 8, 9, 5, 6, 7} 为例。
  • 代码:

    const int N = 1010;
    int dp[N], a[N], b[N];
    int LIS(int n) {
    	int len = 0;
    	for (int i = 1; i <= n; i++) {
    		int p = lower_bound(b + 1, b + len + 1, a[i]) - b;
    		if (p == len + 1) len++;
    		dp[i] = p; // 记录以a[i]结尾的LIS长度
    		b[p] = a[i];
    	}
    	return len;
    }
    
  • 拓展:思考求最长不下降子序列、最长下降子序列、最长不上升子序列应该怎么修改?

    • 求最长不下降子序列应该把 lower_bound() 改 upper_bound(),思考为什么。
    • 求最长下降/不上升子序列,b[] 数组是从大到小有序的,不能用 lower_bound() 或 upper_bound() ,要自己写二分。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值