【题解】P2501 [HAOI2006] 数字序列 [思维 + dp]
P2501 [HAOI2006] 数字序列 - 洛谷
1.解题前思考
注意到需要先改变的点最少,其次才是代价最小。
既然要改变,肯定是有一些 是固定不变的,作为严格递增的范本。
考虑 和
,如果这两个点都是固定点,那应该满足
。
至此,我们确定做法:
(1)筛出固定点
(2)把两个固定点之间不合法段处理成合法段
2.深度思考
满足 的
和
是一对固定点,我们尽量让固定点更多。
即满足以上条件且长度最长的 中序列,是最优方案。
注意到合法段是严格上升(严格阶梯状),不好计算代价,考虑定义 。
即 中的最长不降序列是最优方案。
这样 中的合法段是不降(一段段平的拼接)的,代价计算方便(看到后面就知道方便在哪)。
考虑将两个固定点之间的不合法段改成合法所需的代价。
这个段内肯定没有在 和
之间的值,不然最长不降序列还能再长。
所以对于一个点来说,最终改成的值不是 就是
,这样代价才最小。
但我们又要求最终合法是不降序列,所以肯定是前一段 ,后一段
。
反证:
如果前一段中靠近后一段的一段,改成 代价比改成
更小,
那么可以证明,这一段改成 代价会更小。
如果后一段中靠近前一段的一段,改成 代价比改成
更小,
那么可以证明,这一段改成 代价会更小。
3.构造代码
中的最长不降序列,可以用二分 / 树状数组 / 线段树
的时间求出来。
我这里选择了二分,同时增加一个合法点 n + 1,照顾到那些结尾不在 n 的最长不降序列。
二分结束后添加一个合法虚拟起点 0,照顾到那些开头不在 1 的最长不降序列。
然后一个个枚举不合法区间,由于最长不降序列可能有多个,
这里选择每个点都求出最长不降序列大小 ,然后把每个点放到对应的
桶中。
每次取区间时就从 的桶中取
小于等与当前
值的点当作
。
再求出 和
两个数组,代表
到
全部改成
的代价。
枚举前一段后一段的临界点 ,求出最小值。
这个最小值只能代表这段不合法段的最小代价,不能代表这道题的答案。
考虑定义 dp 数组,dp[i] 代表从虚拟起点 0 到合法点 i 的最小总代价。
最后输出 dp[n + 1],详细见代码。
时间复杂度最坏 ,但题目说 “随机生成”,可以看作
,玄学可过。
注释代码:
#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N = 35e3 + 10;int a[N], b[N];
int mn_end[N], len; // [i]:长度为 i 的最长不降子序列(LIS)的最小结尾
int L[N]; // [i]:以 i 点为结尾 LIS 的长度
vector<int> G[N]; // LIS 长度桶
LL sumi[N], sumj[N]; // [k]:i 到 k 全部改成 b[i] / b[j] 的代价
LL dp[N]; // [i]:从虚拟起点到合法点 i 的最小总代价 int main () {ios::sync_with_stdio(false);cin.tie(0);int n;cin >> n;for (int i = 1; i <= n; i ++) {cin >> a[i];b[i] = a[i] - i;}b[n + 1] = 1e9; // 不一定所有最长不降子序列(LIS)的结尾都是 n// 为了所有非法段都能被处理到,加入一个所有 LIS 的结尾点 memset(mn_end, 0, sizeof(mn_end));int len = 0; // mn_end 数组的当前长度 for (int i = 1; i <= n + 1; i ++) {int l = 0, r = len, p = 0;while (l <= r) {int mid = (l + r) >> 1;if (mn_end[mid] <= b[i]) {p = mid;l = mid + 1;}else {r = mid - 1;}}if (p == len) {len ++; // 更新全局 LIS 长度 } mn_end[p + 1] = b[i]; // 长度为 p + 1 的 LIS 结尾最小值更新 // 如果 p + 2 也因为 i 可以被更新的更小,导致更新错误怎么办?// 答:不用担心// 假设后面有一个点 x,me[p + 1](new) < x < me[p + 2](new)// 并且 me[p + 2](old) < x < me[p + 3](old)// 这样会导致本应更新 p + 2 的 x 更新了 p + 3// 但很明显,不可能做到同时 < me[p + 2](new) 且 > me[p + 2](old)// 所以不用担心会更新错误 L[i] = p + 1;G[L[i]].push_back(i); }cout << n - len + 1 << "\n"; // 不合法点的数量,因为多算了一个 n + 1 memset(dp, 0x7f, sizeof(dp)); // 初始化最大值 G[0].push_back(0); // 添加虚拟起点 b[0] = -1e9; dp[0] = 0; // 保证所有点都能接它后面 for (int j = 1; j <= n + 1; j ++) {for (int i : G[L[j] - 1]) if (i < j && b[i] <= b[j]){sumi[i] = 0;for (int k = i + 1; k < j; k ++) {sumi[k] = sumi[k - 1] + abs(b[k] - b[i]);}sumj[j] = 0;for (int k = j - 1; k > i; k --) {sumj[k] = sumj[k + 1] + abs(b[j] - b[k]);}for (int k = i; k < j; k ++) {dp[j] = min(dp[j], dp[i] + sumi[k] + sumj[k + 1]);} }}cout << dp[n + 1] << "\n";return 0;
}