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] 的计算同理。
- 如果第 i 天游泳,前一天就只能爬山或写作业,在这两种选择中取最大值
-
边界
- 从第 1 天开始有活动,第 0 天没有快乐值
dp[0][0] = dp[0][1] = dp[0][2] = 0;
- 从第 1 天开始有活动,第 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],考虑最后一步,也就是选或不选 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 长度。
- 贪心 1
合唱队形
-
知识点: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() ,要自己写二分。