P4690 [Ynoi2016] 鏡中的昆蟲

FAKUMARER發表於2024-03-25

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;
}

相關文章