Important

【这篇题解放在这里是用来测试Gmeek整体渲染效果的,题解内容是半年前随便写的,正确性、严谨性、规范性没有保证,仅供参考(?)】

题目大意:对于一个数列,求出其最长不上升子数列的长度,以及至少能用几个最长不上升子序列覆盖整个数列。

思路

$O(n^2)$ 朴素做法

对于第一个分任务,考虑线性动态规划,寻找状态方程。

以原数列(长度为 $n$ )第 $i$ 项为结尾的最长不上升子数列的长度为 $dp_{i}$ ,最终目的是求出 $\displaystyle \max_{i = 1,..,n} dp_{i}$ 。

首先考虑 $dp_{i}$ 的值会受到什么影响?

对于 $i$ 前面的所有项 $j$ ,只要 $arr_{i} \leqslant arr_{j}$ ,说明 $arr_{i}$ 可以加在 $arr_{j}$ 后面,构成一个更长的不上升子数列,此时 $dp_{i}$ 有可能受到影响成为 $dp_{j} + 1$ ,也有可能因为目前的 $dp_{i}$ 已经足够大而不受影响。

考虑到这是唯一会改变 $dp_{i}$ 的情况,可以据此得到状态方程:

$$ dp_{i}=\max (dp_{i}, dp_{j}+1) ,\quad \text{for all } j < i \text{ and } arr_{i} \leqslant arr_{j} $$

根据状态方程写出实现第一个任务的代码,时间复杂度为 $O(n^2)$ :

for (int i = 0; i < n; dp[i++] = 1) {}	// 设置每项初始值为 1

for (int i = 1; i < n; i++)
	for (int j = 0; j < i; j++)	// 对于每个 i ,遍历在它之前的所有值
		if (arr[i] <= arr[j])	// true: arr[i] 可以接在以 arr[j] 结尾的不上升子数列后面,长度 + 1
			dp[i] = max(dp[i], dp[j] + 1);	// 长度可能改变为 dp[j] + 1 ,也可能依旧是 dp[i] 自身

对于第二个分任务,事实上可以转化为求最长(递增)子序列的长度。因为每个不上升子数列中的元素在原序列中是不上升的,所以如果我们能找到最长的递增子序列,那么每个递增子序列中的元素必须属于不同的不上升子数列。(严格证明详见 Dilworth 定理 from OI_Wiki ,反正我看不懂)

所以把上述代码的 $arr_{i} \leqslant arr_{j}$ 变个符号( > )就能解决问题了。

$O(n \log n)$ 优化做法

上面的代码只能 $AC$ 前 $50 \%$ 的数据,以下是针对后 $50 \%$ 的数据的 贪心 + 二分 做法(原理写在注释中):

// 最长单调不上升串
vector<int> dp1;	// dp1 是一个不上升栈
for (int num : arr)
{
    if (dp1.empty() || num <= dp1.back())	// 若当前元素 <= dp1 的最后一个元素,则直接添加到栈的末尾
        dp1.push_back(num);
    // 否则【找到第一个小于当前元素的位置】并替换,保持数组的降序特性同时,尽可能使末元素足够小
    // 这样这串序列的长度就更有可能延长,就能够实现最长不上升子序列
    // * 注意 dp1 的内容本身并不是这个最长不上升子序列,它只能表示长度!
    else
    {
        auto it = upper_bound(dp1.begin(), dp1.end(), num, greater<int>());	// 【找】
        *it = num;	//
    }
}
// 由于 dp1 始终保持降序,并且每次操作都尽可能延长序列的长度,因此最终 dp1 的长度就是最长不上升子序列的长度

// 同理找最长严格递增串,只需要改动下面的两处地方:
// if (dp2.empty() || num > dp2.back())
// auto it = lower_bound(dp2.begin(), dp2.end(), num);

这样就得到了完全 AC 代码