经典动态规划思想
最长递增子序列(LIS)是一个非常典型的 偏序型动态规划问题 。最自然的思路是从朴素的状态设计入手:定义 表示 以第 i 个元素结尾 的最长递增子序列长度。那么在计算 时,我们只需要枚举所有 ,如果 ,就可以尝试用 来更新 。这种做法本质上是在所有可能的前驱中寻找最优决策,因此时间复杂度为 。
然而这种朴素做法虽然直观,但效率偏低。进一步观察可以发现:在位置 之前的所有历史状态中,存在大量冗余的状态。如果某个位置 的数值更小且对应的状态值更大,那么对于后续的任何决策而言,位置 显然都比数值更大且状态值更小的位置 更具竞争力。
单调二分优化
基于这一思想,我们不再显式维护每个位置的最优值,而是换一个角度:维护一个数组 ,表示 长度为 len 的递增子序列,其最小可能的结尾元素是多少 。换句话说,我们对 “同一长度的所有递增子序列” 只保留结尾最小的那一个,因为结尾越小,未来可扩展的空间就越大,这显然是更优的代表状态。
这个数组具有一个重要性质:它一定是 单调递增的 。原因在于,长度为 的递增子序列必然是在某个长度为 的递增子序列后面添加一个更大的元素得到的,因此其结尾元素一定严格大于对应长度为 的结尾元素。并且由于 中存储的是 所有长度为 len 的递增子序列中结尾最小的那个值 ,它本身已经是该长度下的最优代表状态,所以无论长度为 的序列是从哪个具体的长度为 的序列转移过来的,其结尾元素都必然大于这个最小结尾。因此必然有:
这一单调性保证了我们可以对其进行二分查找。于是,当遍历到一个新元素 时,我们只需要在 数组中找到 第一个大于等于 的位置,用 去更新它;如果不存在这样的元素,则说明可以扩展最长长度。这样,每个元素只需一次二分查找,时间复杂度从 优化为 。
树状数组优化
回到问题本身,我们也可以从最初的动态规划状态转移出发,对朴素的 枚举过程进行进一步优化。仍然定义 表示 以第 i 个元素结尾的最长递增子序列长度 。根据递增子序列的性质,如果存在 且满足 ,那么就可以从位置 转移到位置 ,因此有转移关系:
在朴素算法中,需要枚举所有满足条件的 ,从而导致 的时间复杂度。进一步观察可以发现,这个转移实际上只关心一件事情:在所有 数值小于 的元素中,最大的 值是多少。也就是说,我们并不关心这些元素具体出现在序列的哪个位置,只需要知道当前所有满足 的状态中的最优值即可。
为了能高效地完成这一查询,我们可以先对序列中的数值进行 离散化 。将所有出现过的数值进行排序并重新编号,使得每个元素 对应一个排名 。这样一来,所有满足 的元素,就等价于排名位于区间 的元素。在此基础上,可以利用 树状数组 来维护这些信息。
树状数组的每个节点存储当前某个数值范围内的最大 值,并支持两种基本操作:一是查询区间 的最大值,二是更新某个位置的最大值。当遍历到元素 时,首先在树状数组中查询区间 的最大值,得到当前所有小于 的元素所对应的最大 值,记为 ,于是:
随后再用 去更新树状数组中位置 的值即可。
由于树状数组的单次查询和更新操作复杂度均为 ,整个算法的时间复杂度可以降低为 。与前面的单调二分优化不同,树状数组优化的核心思想并不是通过维护最优代表状态来压缩状态空间,而是 直接从动态规划转移关系出发,将原本需要枚举的转移过程转化为数据结构上的区间查询问题 。
最长上升子序列
Problem Statement
某国为了防御敌国的导弹袭击,开发了一种导弹拦截系统。但这种系统存在一个缺陷:
- 第一发炮弹可以到达任意高度;
- 之后每一发炮弹的高度 都不能高于前一发炮弹的高度 。
由于系统仍在试用阶段,目前只有 一套拦截系统 ,因此可能无法拦截所有导弹。
现在给出导弹依次飞来的高度序列,要求:
- 一套拦截系统最多能够拦截多少枚导弹 ;
- 如果希望拦截所有导弹,最少需要多少套拦截系统 。
Constraints
Input
输入包含两行:
- 第一行包含一个整数 ,表示数组的长度。
- 第二行包含 个整数,表示数组的元素。
Output
输出包含两行:
- 一套系统最多能拦截的导弹数量
- 拦截所有导弹所需的最少系统数量
Sample Input
389 207 155 300 299 170 158 65Sample Output
62题目要点解析
首先考虑题目的第一个问题:一套拦截系统最多能够拦截多少枚导弹 。根据题目规则,拦截系统发射的炮弹高度必须满足后一发不高于前一发,也就是说拦截的导弹高度序列必须是一个 不上升序列 。因此问题可以直接转化为:在给定的导弹高度序列中,求一个 最长不上升子序列 。这一问题与经典的最长递增子序列问题是完全对称的,可以通过动态规划求解,利用二分优化将复杂度降低到 。
接下来考虑第二个问题:如果要拦截所有导弹,最少需要多少套拦截系统 。每一套拦截系统所拦截的导弹序列都必须满足不上升的性质,因此本质上是在问:如何将整个序列划分成尽量少的若干个 不上升子序列 。从反向的角度来看,如果存在一个严格上升的子序列,那么其中的每一个元素都无法被同一套系统拦截,因为后一枚导弹高度高于前一枚导弹,违背了拦截规则。因此,这个上升序列中的每一个元素都必须由 不同的拦截系统 来处理。
由此可以得到一个重要结论:需要的最少拦截系统数量,恰好等于该序列的 最长严格上升子序列 的长度。换句话说,最长上升子序列中的每一个元素都至少需要一套独立的系统才能完成拦截。这个结论也可以从序列划分的角度理解:将一个序列划分为若干个不上升序列,其最少划分数量等于该序列的最长上升子序列长度。
综上这道题可以拆分为两个经典问题:第一个是求 最长不上升子序列 ,第二个是求 最长严格上升子序列 。二者都可以利用最长递增子序列的标准算法进行求解,整体时间复杂度可以做到 。
# include <bits/stdc++.h>using namespace std;
int main(){
}数组K递增问题
Problem Statement
给你一个下标从 开始包含 个正整数的数组 arr ,和一个正整数 k 。
如果对于每个满足 的下标 都有 ,那么称数组 arr 是 k 递增 的。
- 比方说,
arr = [4, 1, 5, 2, 6, 2]对于 是 k 递增的,因为: - 相反,
arr = [4, 1, 5, 2, 6, 2]对于 不是 k 递增的,因为 。
在一步操作中,你可以选择一个下标 并将 改成 任意 正整数。
请你返回使数组 arr 变成 k 递增 的 最少 操作次数。
Constraints
Input
输入包含两行:
- 第一行包含两个整数 和 。
- 第二行包含 个整数,表示数组 中的元素。
Output
输出一个整数,表示使数组变成 k 递增的最少操作次数。
Sample Input 1
6 15 4 3 2 1 1Sample Output 1
4Sample Input 2
6 24 1 5 2 6 2Sample Output 2
0Sample Input 3
6 34 1 5 2 6 2Sample Output 3
2题目要点解析
取模分组问题
嵌套的信封问题
Problem Statement
给你一个二维整数数组 envelopes ,其中 ,表示第 个信封的宽度和高度。
当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算 最多 能有多少个信封能形成一组 “俄罗斯套娃” 信封(即可以把一个信封放到另一个信封里面)。
注意:不允许旋转信封。
Constraints
Input
输入包含多行:
- 第一行包含一个整数 ,表示信封的数量。
- 接下来 行,每行包含两个整数 和 ,表示每个信封的宽度和高度。
Output
输出一个整数,表示最多能套娃的信封数目。
Sample Input 1
45 46 46 72 3Sample Output 1
3Sample Input 2
31 11 11 1Sample Output 2
1题目要点解析
二维偏序问题
最长数对链问题
Problem Statement
给你一个包含 个数对的数组 pairs ,其中 且 。
现在,我们定义一种 跟随 关系,当且仅当 时,数对 才可以跟在 后面。我们用这种形式来构造 数对链 。
请你返回能够形成的序列链的 最长长度 。
你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
Constraints
Input
输入包含多行:
- 第一行包含一个整数 ,表示数对的数量。
- 接下来 行,每行包含两个整数 和 。
Output
输出一个整数,表示最长数对链的长度。
Sample Input 1
31 22 33 4Sample Output 1
2Sample Input 2
31 27 84 5Sample Output 2
3题目要点解析
带修递增子序列
Problem Statement
给定一个长度为 的整数序列: 。你可以从中选择一个 连续 的区间,该区间的长度为 ,并将该区间内的所有数字全部修改成任意一个相同的整数。
请你返回在进行至多一次上述修改操作后,整个序列的 最长不下降子序列 的长度最大是多少。
最长不下降子序列的定义是:从原序列中按顺序取出若干个数字,使得这些数字满足非递减关系。
Constraints
Input
输入包含两行:
- 第一行包含两个整数 和 。
- 第二行包含 个整数,表示数组 中的各个元素。
Output
输出一个整数,表示在修改后能获得的最长不下降子序列的最大长度。
Sample Input
5 11 4 2 8 5Sample Output
4题目要点解析
使数组严格递增
Problem Statement
给你两个整数数组 arr1 和 arr2 ,返回使 arr1 严格递增所需要的最小操作次数。
每一步操作中,你可以分别从 arr1 和 arr2 中各选出一个索引,分别为 和 ,用 arr2[j] 替换 arr1[i] 。
如果无法让 arr1 严格递增,请返回 。
Constraints
Input
输入包含三行:
- 第一行包含两个整数 和 ,分别表示 和 的长度。
- 第二行包含 个整数,表示 中的元素。
- 第三行包含 个整数,表示 中的元素。
Output
输出一个整数,表示使 严格递增所需的最小操作次数。
Sample Input 1
4 31 5 3 61 3 2Sample Output 1
1Sample Input 2
4 31 5 3 64 3 1Sample Output 2
2Sample Input 3
4 31 5 3 61 6 3 3Sample Output 3
-1