P8425 笔记
有点离谱的一个题。
题目大意
有点复杂不写了。
思路
首先找一下区间 \([L, R]\) 合法的充要条件,如果不合法,最小值和最大值都不是 \(P_L, P_R\),于是合法当且仅当 \(P_L\) 或 \(P_R\) 是区间最值。
考虑求解答案的过程,先从 \([1, N]\) 开始,若最小最大值都不在 \(1, N\),就要放一个到 \(P_1\) 或 \(P_N\),从而递归到 \([2, N]\) 或 \([1, N - 1]\)...
于是可以区间 DP,设 \(dp(l, r, x)\) 表示区间 \([l, r]\),值域是 \([x, x + (r - l)]\) 的答案,DP 时如果 \(P_l\) 或 \(P_r\) 是最小/最大就能节省 \(1\) 的步数,总时间复杂度 \(O(N^3)\)。
优化上述 DP 是没有前途的,因为状态数已经是 \(O(N^3)\) 了,至少我们需要一个状态数为 \(O(N^2)\) 的算法。
对于上面 DP 的转移,假如把 \(dp(l, r, x)\) 看作一个 \(x \in [l, r], y \in [x, x + r - l]\) 的正方形,转移相当于选择正方形的一个角,并把该角的两条边往里“缩”;再把 \((i, P_i)\) 这 \(N\) 个点画在坐标轴上,则转移的代价为角上是否存在点。
所以我们的目标就是在正方形缩减的过程中,使角上出现的点最多。
于是就可以优化状态了,设 \(dp(i, j, 0/1/2/3)\) 表示点 \((i, P_i)\) 在长度为 \(j\) 的正方形的某个角(通过第 3 维记),能最多选几个点。状态数 \(O(N^2)\),转移 \(O(N)\) 或 \(O(\log N)\),因为可以用二维偏序优化。
看看还能优化什么,突然发觉题目还有一个性质没用到:序列是随机的!这启示我们答案大约是 \(O(\sqrt N)\) 级别的并且还能写妙妙乱搞了。
交换定义域和值域:\(dp(i, j, 0/1/2/3)\) 表示点 \((i, P_i)\) 在正方形的某个角,选了 \(j\) 个点,边长最短是多少,转移同样可以二维偏序。这样总时间复杂度就变成了 \(O(N \sqrt{N} \log N)\),可以通过。
但这题还并未结束,因为存在一种乱搞的解法:首先在最外层枚举 \(j\),接着在循环内转移,而每次选出边长最短的 \(500\) 个状态转移就行了,这样甚至可以写 \(O(N)\) 转移。不会证,但数据随机,就当它是对的了!(因为你也没法卡。。。)
代码异常好写(写的 push 没写 pull,状态定义略有出处):
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
cin >> N;
vector<int> P(N);
vector<array<int, 4>> dp(N);
for (int i = 0; i < N; ++i) {
cin >> P[i];
--P[i];
dp[i][0] = min(N - i - 1, N - P[i] - 1);
dp[i][1] = min(i, N - P[i] - 1);
dp[i][2] = min(i, P[i]);
dp[i][3] = min(N - i - 1, P[i]);
}
int ans = 0;
while (true) {
vector<array<int, 3>> V;
for (int i = 0; i < N; ++i) {
for (int j = 0; j < 4; ++j) {
if (~dp[i][j]) {
V.push_back({i, j, dp[i][j]});
}
}
}
if (V.empty()) {
break;
}
++ans;
sort(V.begin(), V.end(), [&](const auto &x, const auto &y) {
return x[2] > y[2];
});
int t = min<int>(V.size(), 500);
for (int i = 0; i < N; ++i) {
for (int j = 0; j < 4; ++j) {
dp[i][j] = -1;
}
}
for (int _ = 0; _ < t; ++_) {
auto [i, j, v] = V[_];
int l, r, x, y;
if (j < 2) {
x = P[i] + 1;
y = P[i] + v;
} else {
x = P[i] - v;
y = P[i] - 1;
}
if (j == 1 || j == 2) {
l = i - v;
r = i - 1;
} else {
l = i + 1;
r = i + v;
}
for (int k = l; k <= r; ++k) {
if (P[k] >= x && P[k] <= y) {
dp[k][0] = max(dp[k][0], min(r - k, y - P[k]));
dp[k][1] = max(dp[k][1], min(k - l, y - P[k]));
dp[k][2] = max(dp[k][2], min(k - l, P[k] - x));
dp[k][3] = max(dp[k][3], min(r - k, P[k] - x));
}
}
}
}
cout << N - ans << "\n";
return 0;
}
这题确实 push 会好写很多,二维偏序做法需要分讨,以上述写法为例。考虑对 \(dp(k, 0)\) 转移,只需要更新 \(k \ge l \land P_k \ge x\) 的所有 \(k\) 的 \(\min r\) 和 \(\min y\)。其它 \(1, 2, 3\) 也类似处理就行了。

浙公网安备 33010602011771号