Luogu P4690 [Ynoi2016] 镜中的昆虫
Luogu P4690 [Ynoi2016] 镜中的昆虫
本质就是 区间修改,区间数颜色
弱化一下,先考虑不带修的情况,也就是 P1972 [SDOI2009] HH的项链
其难点在于 区间颜色的去重,需要想一个办法 让区间内一个颜色只被数一次
我们可以去维护一个 \(Nxt\) 数组,表示 下一个与当前位置颜色相同的位置
若 当前位置 \(i\) 是该颜色出现的 最后一个位置,序列长为 \(N\),则 \(Nxt_i = N + 1\)
容易发现 区间 \([L, R]\) 中 每种颜色 的最后一个位置 \(P_i\) 将满足 \(Pos_{P_i} > R\)
这样每种颜色 恰好对应一个位置,不重不漏
于是我们可以考虑 计数上面这个东西,也就是在 区间中 \(Nxt\) 值大于 \(R\) 的位置个数
转化到平面上 进行 二维数点 即可
然后考虑 单点修改,注意到其实 颜色本身 已经没啥用了
我们关心修改之后对应的 \(Nxt\) 数组的变化
令人高兴的是,单点修改 只会对 修改点 以及 与 修改前 / 后 同色的 前驱点 的 \(Nxt\) 值造成影响
就是 每次 \(Nxt\) 数组只有 \(O(1)\) 的修改,所以 暴力修改,询问时 在线二维数点 即可
回到这道题的 区间修改,感觉就很难做
如果 单点修改 \(O(1)\),区间 \(O(N)\),那这玩意儿 复杂度就炸了,火大
但显然并 没有什么很好的方法可以批量快速修改,于是仔细思考性质
发现区间 \([L, R]\) 修改后,\([L, R - 1]\) 的 \(Nxt\) 都将指向 其对应的下一个位置
就是 \(\forall i \in [L, R - 1], Nxt_i = i + 1\)
而且无论修改成什么颜色这个性质都将存在
换言之,如果我将 \([L, R]\) 的颜色先修改成 \(C_1\),再修改成 \(C_2\)
中间 \([L, R - 1]\) 的 \(Nxt\) 值都 不会改变,于是我们可以考虑 摊还分析
设若初始时 \(\forall i \in [1, N],Nxt_i = i + 1\)
那么我们每次修改时,至多有 \(R\),与 \(L\) 修改前后同色 的 三个点 的 \(Nxt\) 会被改变
于是我们总的修改就不是 \(O(NM)\) 而是 \(O(N + M)\) 的了
从 \(Nxt_i = i + 1\) 的状态变成 输入状态 相当于 修改了 \(O(N)\) 次
于是我们就可以 直接做 \(—— \textsf{z}\color{red}\textsf{hicheng}\)
但是这里 与 \(L\) 修改前后同色的前驱点 还得考虑一下
在我们可以像 单点修改 时 那样记录每个点 前驱和后缀 来找,但是这就意味着 修改常数巨大
所以我们可以 引入一些 \(ODT\) 来维护 每种颜色的区间,同时再用一棵 \(ODT\) 维护整个序列
\(ODT\) 又称 珂朵莉树 \((Chtholly-Tree)\),多用在 维护区间颜色相关的问题
本质上就是一个
std::set
,它将 每个相同颜色的段 缩成一个点 用 \(L, R\) 描述同时可以有一些 附加权值或信息,其 核心操作 是
Split
即 将一个节点(区间) \([L, R]\) 分成 \([L, x - 1],[x, R]\) 两个节点,并返回 后一个节点的指针
一般情况下,我们会 暴力的对树上的区间操作,其复杂度在 随机数据下 为 \(O(N \log\log N)\)
但是也 容易被卡,但是本题中其 均摊复杂度是正确的,不必担心
现在我们将 不用改的部分 忽略后 每次暴力单点修改 就行了
继续 在线二维数点,树套树...
不对,注意到(在某次更新之后)这道题的空间限制只有 \(64 ~ MB\)
而 二维树套树 的空间复杂度是 严格 \(O(N \log N)\) 的,过不了一点
但是咱又不想写 \(CDQ\)...
于是,我们直接对 序列分块维护,注意到 修改 \(M\) 次,原序列 \(N\) 个数
一共仅有 \(N + M\) 个 可能权值,于是我们可以对 所有权值离散化 一下
然后对于 序列上每个块,内部使用 值域分块 维护
\(O(\sqrt {N})\) 修改(个数 \(+1\)),\(O(1)\) 查询 后缀和(也就是 大于某个数的个数)
虽然分块这玩意儿吧,空间理论上是 \(O(N \sqrt N)\) 的,比 树套树还大
但是这只是理论,我们可以通过 调整块长 获得 更小的空间,然后卡过去
用 时间换空间,赢!
在时间上,分块 \(O(N \sqrt N)\) 的复杂度在 \(10 ^ 5\) 的数据下不比其余算法 \(O(N \log ^ 2 N)\) 的理论上劣
而且 常数较小,实际上 跑得飞快
#include <bits/stdc++.h>
const int MAXN = 200005;
const int MAXB = 505;
const int RELB = 80;
using namespace std;
int Nxt[MAXN];
int N, M, P, Cnt;
int Blk2[MAXN];
struct Block {
int BL[MAXB], BR[MAXB];
int *Blk = Blk2;
int b;
short SufA[MAXN], SufB[MAXB];
inline void Init () {
b = 400;
for (int i = 1; i <= N + 1; ++ i) {
Blk[i] = i / b + 1;
if (Blk[i] != Blk[i - 1]) BL[i / b + 1] = i, BR[i / b] = i - 1;
} BR[N / b + 1] = N + 1;
}
inline void Add (const int x) {
for (int i = 1; i <= Blk[x]; ++ i) ++ SufB[i];
for (int i = BL[Blk[x]]; i <= x; ++ i) ++ SufA[i];
}
inline void Del (const int x) {
for (int i = 1; i <= Blk[x]; ++ i) -- SufB[i];
for (int i = BL[Blk[x]]; i <= x; ++ i) -- SufA[i];
}
inline short GetSuf (const int x) {
return SufB[Blk[x] + 1] + SufA[x];
}
} B[RELB];
int Blk[MAXN], BL[RELB], BR[RELB];
namespace Chtholly_Tree {
struct Node {
int L, R, C;
inline bool operator < (const Node &a) const {
return L < a.L;
}
};
set <Node> S, T[MAXN];
inline auto Split_1 (const int x) {
if (x > N) return S.end ();
auto it = -- S.upper_bound ({x, 0, 0});
if (it -> L == x) return it;
Node k1 = {it -> L, x - 1, it -> C}, k2 = {x, it -> R, it -> C};
return S.erase (it), S.insert (k1), S.insert (k2).first;
}
inline auto Split_2 (const int x, const int c) {
if (x > N) return T[c].end ();
auto it = -- T[c].upper_bound ({x, 0, 0});
if (it -> L == x) return it;
Node k1 = {it -> L, x - 1, it -> C}, k2 = {x, it -> R, it -> C};
return T[c].erase (it), T[c].insert (k1), T[c].insert (k2).first;
}
inline void Change (const int x, const int v) {
B[Blk[x]].Del (Nxt[x]), Nxt[x] = v, B[Blk[x]].Add (Nxt[x]);
}
inline void Modify (const int L, const int R, const int x) {
auto itr = Split_1 (R + 1), itl = Split_1 (L), it = itl;
Split_2 (L, itl -> C), Split_2 (R + 1, itr -> C);
for (it = itl; it != itr; ++ it) {
auto tmp = T[it -> C].lower_bound ({it -> L, 0, 0});
T[it -> C].erase (tmp);
tmp = T[it -> C].lower_bound ({it -> L, 0, 0}); auto itx = tmp;
if (tmp == T[it -> C].end () && T[it -> C].size ()) -- tmp, Change (tmp -> R, N + 1);
else if (tmp != T[it -> C].begin ()) -- tmp, Change (tmp -> R, itx -> L);
Change (it -> L, it -> L + 1);
if (it -> L == it -> R) continue ;
Change (it -> R, it -> R + 1);
}
S.erase (itl, itr), S.insert ({L, R, x}), T[x].insert ({L, R, x});
itl = T[x].lower_bound ({L, 0, 0});
if (itl != T[x].begin ()) -- itl, Change (itl -> R, L);
itr = T[x].upper_bound ({R, 0, 0});
if (itr != T[x].end ()) Change (R, itr -> L);
else Change (R, N + 1);
}
}
using namespace Chtholly_Tree;
inline int Query (const int L, const int R) {
int Ret = 0;
if (Blk[L] == Blk[R]) {
for (int i = L; i <= R; ++ i) if (Nxt[i] > R) ++ Ret;
} else {
for (int i = L; i <= BR[Blk[L]]; ++ i) if (Nxt[i] > R) ++ Ret;
for (int i = Blk[L] + 1; i < Blk[R]; ++ i) Ret += B[i].GetSuf (R + 1);
for (int i = BL[Blk[R]]; i <= R; ++ i) if (Nxt[i] > R) ++ Ret;
}
return Ret;
}
unordered_map <int, int> Mp;
int Num[MAXN], Val[MAXN], Pos[MAXN], Rnk;
short Opt[MAXN];
int L[MAXN], R[MAXN], x[MAXN];
int main () {
cin >> N >> M, P = N / 78;
for (int i = 1; i <= N; ++ i) cin >> Num[i];
for (int i = 1; i <= M; ++ i) {
cin >> Opt[i] >> L[i] >> R[i];
if (Opt[i] == 1) cin >> x[i];
}
memcpy (Val, Num, sizeof Num), Cnt = N;
for (int i = 1; i <= M; ++ i) Val[++ Cnt] = x[i];
sort (Val + 1, Val + Cnt + 1);
for (int i = 1; i <= N; ++ i) {
Blk[i] = i / P + 1;
if (Blk[i] != Blk[i - 1])
BL[i / P + 1] = i, BR[i / P] = i - 1, B[i / P + 1].Init ();
} BR[N / P + 1] = N;
for (int i = 1; i <= Cnt; ++ i) Mp[Val[i]] = i;
for (int i = 1; i <= N; ++ i) T[Mp[Num[i]]].insert ({i, i, Mp[Num[i]]}), S.insert ({i, i, Mp[Num[i]]});
for (int i = N; i >= 1; -- i) {
if (Pos[Mp[Num[i]]]) Nxt[i] = Pos[Mp[Num[i]]], B[Blk[i]].Add (Nxt[i]);
else Nxt[i] = N + 1, B[Blk[i]].Add (N + 1);
Pos[Mp[Num[i]]] = i;
}
for (int i = 1; i <= M; ++ i) {
if (Opt[i] == 1) Modify (L[i], R[i], Mp[x[i]]);
if (Opt[i] == 2) cout << Query (L[i], R[i]) << '\n';
}
return 0;
}