洛谷 P9770 [HUSTFC 2023] 逆 KMP 题解
线下菜比选手来写一篇题解
感觉这题很需要选手通过手玩一些例子发现性质,所以希望大家在阅读这篇题解的同时手玩一下我举出的例子来增进理解。
容易发现若给的是严格的 kmp 算法求出的前缀函数
然而给的
我们发现事实上
容易发现另一个限制
使数字的种类变多,就是让重复数字变少。
所以我们就要在以上两个限制下,找到一个尽可能小的
举个例子:
8
0 0 0 0 3 0 0 2
容易还原出
0 0 1 2 3 0 1 2
然后意识到,可能会出现两段重合的例子,比如:
9
0 0 0 0 0 3 0 3 0
其
0 0 0 1 2 3 2 3 0
但是实际上不对,你尝试一下把答案还原出来就会发现端倪。
在位置
显然要取个
所以真正的
0 0 1 1 2 3 2 3 0
当然,不一定仅仅只有两个重合,但是多个重合的情况大体上差不多。这时容易想到一个大力并查集的暴力:
对于每个
举个例子吧:
10
0 0 0 0 4 0 0 4 0 6
大家自己尝试一下把答案求出来。
这个暴力还是不够优秀,我们需要更多的性质和结论优化它。
把结论扩展:
假设某位置 i 被后面的 a 给出了
则有
又有:若
容易发现
我们用大根堆和并查集维护这个长串。
假设某个堆里维护着位置
用完之后,这个堆接着就应当由
现在问题在于,
接下来这个堆去往何方?堆里的次次大值吗?事实上
但是我们发现,这不影响堆里比
所以我们用并查集和堆启发式合并来维护这个过程。我们发现堆里的总元素个数是
参考代码
#include <queue>
#include <cstdio>
#include <cctype>
#include <cstdint>
#include <iostream>
#include <algorithm>
using i32 = int32_t;
using i64 = int64_t;
#define read std::cin
const int N = 2e5 + 10;
int n;
int vis[N];
int nex[N], fa[N];
std::priority_queue<int> heap[N];
int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
void merge(i32 x, i32 y)
{
x = find(x), y = find(y);
if (x == y) return;
if (x < y) std::swap(x, y);
if (heap[x].size() > heap[y].size()) std::swap(heap[x], heap[y]);
fa[x] = y;
while (heap[x].size()) {
heap[y].emplace(heap[x].top());
heap[x].pop();
}
}
int main(void)
{
//Think twice, code once!
std::ios::sync_with_stdio(false);
read >> n;
for (i32 i = 1; i <= n; ++i) {
fa[i] = i;
heap[i].emplace(i);
read >> nex[i];
}
for (i32 i = n; i >= 1; --i) {
auto& hp = heap[find(i)];
while (hp.size() && hp.top() >= i) hp.pop();
if (nex[i]) {
hp.emplace(nex[i]);
}
if (nex[i + 1] > 1) {
hp.emplace(nex[i +1] - 1);
}
nex[i] = hp.size() ? hp.top() : 0;
merge(i, nex[i]);
}
for (int i = 1; i <= n; ++i) {
fa[i] = i;
}
i32 res = 1;
for (i32 i = 1; i <= n; ++i) {
if (nex[i]) fa[find(i)] = find(nex[i]);
}
for (i32 i = 1; i <= n; ++i) {
i32 f = find(i);
printf("%d ", vis[f] ? vis[f] : (vis[f] = res++));
}
puts("");
return 0;
}