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)\)
但是也 容易被卡,但是本題中其 均攤複雜度是正確的,不必擔心
入門題 CF915E Physical Education Lessons
現在我們將 不用改的部分 忽略後 每次暴力單點修改 就行了
繼續 線上二維數點,樹套樹...
不對,注意到(在某次更新之後)這道題的空間限制只有 \(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;
}