517编程7段第五课T2
大秦为您打开题目传送门(
3/1 题目大意
题目的表述十分清楚,他就是让你求字典序最小的最长上升子序列,如果不存在输出 orz
。
注:在某人jdt1010的提示下发现不存在的情况是当 $n=0$ 的时候分值为两分多一点。
3/2 解题思路
方法1:$dp[i]$ 表示以 $i$ 结尾的最长上升子序列长度
我们阅读题目发现,这道题 $n$ 的数据范围非常大,传统的 $O(n^2)$ 算法肯定是用不了。所以开始思考换一种思路。
由于题目数据范围是 $n\le 10^5$ ,所以不难想到使用 $O(n)$ 或者 $O(n\log n)$ 的算法。
我们开始思考:对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。
所以,我们新建立一个 $st$ 数组,表示以 $i$ 结尾的最长上升子序列的结尾最小值,显然 $st$ 数组严格单调递增。
那么我们实际上就只需要维护这个 $st$ 数组,从而去求出 $dp$ 值。
那么问题就变成了:如何维护这个 $st$ 数组呢?
- 如果这个 $a[i]$ 直接可以接到我们的LIS后面,那么就直接放到 $st$ 数组后面。
- 否则,就把 $st$ 数组中第一个大于等于 $a[i]$ 的数字换成 $a[i]$ 。这样就可以使得 $st$ 数组单调递增。
那么我们就来证明一下这个算法的正确性:
- 最优子结构: 算法在每一步都选择当前最优的状态,即通过比较 $a[i]$ 与 $st$ 中的元素来决定加入 $st$ 的位置。这确保了每个位置 $i$ 上的 $dp[i]$ 是以 $i$ 结尾的最长递增子序列的长度。
- 子问题的最优解是全局最优解的一部分: 通过遍历整个数组,对每个位置 $i$ 计算以 $i$ 结尾的最长递增子序列的长度。最终 $dp$ 数组中的最大值即为整个数组的最长递增子序列长度。
- 状态转移的正确性: 根据 $a[i]$ 与 $st$ 中最后一个元素的比较,更新 $st$ 和 $dp$ 数组的过程保证了递增子序列的正确性。维护 $st$ 数组的目的是确保以 $i$ 结尾的递增子序列的结尾元素的最小值,以便在后续元素中能够更好地扩展。
说人话:这个算法可以通过这道题
那么已经求出了最长上升子序列的长度,还要输出字典序最小的最长上升子序列。(不会已经有人忘记这道题题目问的是什么了罢)
我们既然状态定义的是以 $i$ 结尾的最长上升子序列长度,那么从后往前 for
一遍绝对是最适合这个算法的了。
那么,怎么做到从后往前 for
一遍实现这个算法呢?
- 首先,输出要满足是最长上升子序列,所以,当前节点的 $dp$ 值一定要等于当前的 $len$
- 从后往前第一个等于当前 $len$ 的一定是最小的数字(一会证明)
- 每找到一个满足条件的数,接着要找的数字个数减一,即为:$len=len-1$ 。
那么为什么,从后往前第一个等于当前 $len$ 的一定是最小的数字呢?
证明:
我们假设按照此算法得出的最长上升子序列是:$a_{i_1},\dots,a_{i_x} ,a_{k_x}, \dots,a_{i_{len}}$
但是,真正的答案应该是:$a_{j_1},\dots,a_{j_y} ,a_{k_y}, \dots,a_{j_{len}}$这两个序列的前面 $[i_1,i_x]$ 和 $[j_1,j_y]$ 完全一样,但是 $k_x$ 和 $k_y$ 不一样。
那么如果
- $a_{k_x}>a_{k_y}$ 则正确答案应该会更长,而如果$a_{i_t-1}\ge a_{j_t}$ ,不符合题意
- $a_{k_x}<a_{k_y}$ 则正确答案应该也会更长,而如果$a_{i_t-1}\le a_{j_t}$ ,也不符合题意
- 相等更不用说了
所以此算法正确
方法2:$dp[i]$ 表示以 $i$ 开头最长上升子序列长度
那么,都反着写过了,正着呢?当然也是可以的。
我们把方法1中的定义全都反过来,就变成了:
$dp[i]$ 表示以 $i$ 开头最长上升子序列长度。
$st$ 数组,表示以 $i$ 开头的最长上升子序列的结尾最大值,显然 $st$ 数组严格单调递减。因为为了让LIS更好延续,开头越小越好。
维护 $st$ 数组就变成了:
- 如果这个 $a[i]$ 直接可以接到我们的LIS后面,那么就直接放到 $st$ 数组后面。
- 否则,就把 $st$ 数组中第最个小于等于 $a[i]$ 的数字换成 $a[i]$ 。这样就可以使得 $st$ 数组单调递减。
同理可证该算法正确
3/3 题目代码
方法1:
1 |
|
方法2
1 |
|