莫隊演算法

Xeanin發表於2024-08-21

前言

莫隊是由莫濤提出的一種離線演算法,是分塊與雙指標的結合,一般可以在 \(O(n \sqrt n)\) 或者 \(O(n \sqrt m)\) 的複雜度解決一些種類問題。

普通莫隊

SP3267 DQUERY - D-query

給你一個長度為 \(n\) 的數列 \(A\)\(m\) 次詢問,每次詢問區間 \([l,r]\) 內的不同數字的個數。

如果要線上的話,需要用主席樹。

我們考慮離線,把每個操作離線下來,然後掛到序列上。

每一個顏色代表一個詢問。

可以想到用雙指標來做,代表區間 \([l, r]\),如果任意指標移動就更新答案,是 \(O(1)\) 的。

想象是美好的,現實是殘酷的。

如果詢問是這樣子的話:

那麼儘管排完序左端點的移動可以做到 \(O(n)\),但是啊但是,右端點是無序的,就又會把複雜度打會 \(O(nm)\)

那麼這時候,莫隊誕生了。

莫隊在原來的基礎上加了一個分塊與排序。

可以證明覆雜度是 \(O(n\sqrt n)\) 的。

對於左端點,如果每一個塊中有 \(x\) 個詢問的左端點,那麼對於這個塊的最壞複雜度是 \(O(x_i\sqrt n)\)。對於整個塊,跨越一次需要 \(O(\sqrt n)\),最壞會跨整個序列,也就是 \(O(n)\),那麼複雜度就是 $ O (\sum x_i\sqrt n + n\sqrt n) = O (n \sqrt n)$。

對於右端點,因為已經排完序,最壞需要跨整個序列,也就是 \(O(n)\),有 \(O(\sqrt n)\) 塊,所以複雜度就是 \(O(n \sqrt n)\)

所以總複雜度就是 \(O(n \log n + n \sqrt n + n \sqrt n) = O(n \sqrt n)\)

回到這題,我們已經可以基本寫出來了。

int n, m, len, tot;
int a[N], cnt[V];

bool cmp (Query A, Query B) { return A.x / len == B.x / len ? (A.y == B.y ? 0 : ((A.x / len) & 1) ^ (A.y < B.y)) : A.x < B.x; }
bool Cmp (Query A, Query B) { return A.id < B.id; } // 這裡用了兩個 cmp,實際不需要,用一個 ans 陣列記錄即可。

// 移動指標的修改
void add (int x) {
	if (!cnt[a[x]]) tot++;
	cnt[a[x]]++;
}

void del (int x) {
	if (cnt[a[x]] == 1) tot--;
	cnt[a[x]]--;
}

int main () {
	
	len = sqrt (n); // 這句話千萬別漏了
	sort (q + 1, q + m + 1, cmp);
	
	for (int i = 1, l = 1, r = 0; i <= m; ++i) {
		while (l > q[i].x) add (--l);
		while (r < q[i].y) add (++r);
		while (l < q[i].x) del (l++);
		while (r > q[i].y) del (r--);
		
		q[i].ans = tot;
	}
	
	sort (q + 1, q + m + 1, Cmp);
  
	return 0;
}

帶修莫隊

P1903 [國家集訓隊] 數顏色 / 維護佇列

給你一個長度為 \(n\) 的序列 \(A\) 以及 \(m\) 個操作,要你支援單點修改,並且詢問區間顏色種類數。

現在帶修了(惱)。

我們可以把每個詢問抽象成 \((l, r)\) 的一個點對,這樣我們可以構建出一個二維空間。

我們不妨把每一個操作用時間戳來表示,這樣我們就可以多一個維度 \(t\),用來記錄每次詢問的時間維度。

初始化時,只需要找到最近的那個時間維度即可。

再來考慮考慮這個維度該怎麼轉移。

我們假設當前這個區間已經修改了 \(x\) 次,要轉移到修改 \(y\) 次的序列。

如果 \(x \lt y\),那麼我們需要把 \(x + 1\)\(y\) 個修改全都加上。

如果 \(x \gt y\),那麼我們需要把 \(x\)\(y + 1\) 個修改全部還原。

什麼?你問 \(x = y\)

這種情況還需要轉移嗎。。。

於是我們在原來莫隊的基礎上加了一維,成功地實現了帶修莫隊。

int n, m, len, qcnt, rcnt;
int a[N], ans[N], cnt[V], tot;

struct Query {
	int id, l, r, t;
	
	bool operator < (const Query &T) {
		if (l / len != T.l / len) return l < T.l;
		if (r / len != T.r / len) return r < T.r;
		return t < T.t;
	}
} q[N];

void add (int x) {
	if (!cnt[x]) tot++;
	cnt[x]++;
}

void del (int x) {
	if (cnt[x] == 1) tot--;
	cnt[x]--;
}

int main () {

	for (int i = 1; i <= m; ++i) {
		char opt;
		int x, y;
		cin >> opt >> x >> y;
		
		if (opt == 'Q') q[++qcnt] = (Query){qcnt, x, y, rcnt};
		else r[++rcnt] = (Modify){x, y};
	}
	
	len = pow (n, 2.0 / 3.0);
	
	int L = 1, R = 0, now = 0;
	sort (q + 1, q + qcnt + 1);
	for (int i = 1; i <= qcnt; ++i) {
		while (L > q[i].l) add (a[--L]);
		while (R < q[i].r) add (a[++R]);
		while (L < q[i].l) del (a[L++]);
		while (R > q[i].r) del (a[R--]);
		while (now < q[i].t) {
			now++;
			if (L <= r[now].x && r[now].x <= R) {
				add (r[now].y);
				del (a[r[now].x]);
			}
			swap (a[r[now].x], r[now].y);
		}
		while (now > q[i].t) {
			if (L <= r[now].x && r[now].x <= R) {
				add (r[now].y);
				del (a[r[now].x]);
			}
			swap (a[r[now].x], r[now].y);
			now--;
		}
		
		ans[q[i].id] = tot;
	}
	
	return 0;
}

樹上莫隊

眾所周知,莫隊是用於序列上的一種演算法,如果要用到樹上,我們最先想到的就是把樹序列化。

樹上莫隊?

但是一般的 DFS 序這樣肯定是不行的,畢竟要方便統計路徑上權值的種類數。

但是尤拉序很好的滿足了這個要求。

如圖,可以發現尤拉序只有標紅圈的節點沒有算上,而這個標紅圈的節點正是 \(2\)\(6\) 的 Lca。

所以我們只需要在轉移過程中特判一下即可。

SP10707 COT2 - Count on a tree II

給你一棵 \(n\) 個節點的樹,每次詢問路徑 \(u\)\(v\) 上的權值種類數。

典,直接上。

const int N = 2e5 + 5;
int n, m, len; 

int dep[N], fa[N][31], lg[N], eu[N], ucnt;
int fi[N], ls[N];

void dfs (int now, int fath){
    fa[now][0] = fath;
    dep[now] = dep[fath] + 1;
    eu[++ucnt] = now;
    fi[now] = ucnt;

    for (int i = 1; i < 31; ++i){
        fa[now][i] = fa[fa[now][i-1]][i-1];
    }
    for (int i = head[now]; i; i = e[i].nxt)
        if (e[i].v != fath)
            dfs(e[i].v, now);
    
    eu[++ucnt] = now;
    ls[now] = ucnt;
}

int Lca (int x, int y)

void modify (int x) {
	if (!vis[x]) {
		if (!cnt[a[x]]) tot++;
		cnt[a[x]]++;
	} else {
		if (cnt[a[x]] == 1) tot--;
		cnt[a[x]]--;
	}
	
	vis[x] ^= 1;
}

int main () {
	
	ios::sync_with_stdio (false);
	cin.tie (0); cout.tie (0);
	
	cin >> n >> m;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		b[i] = a[i];
	}
	for (int i = 1; i < n; ++i) {
		int u, v;
		cin >> u >> v;
		addEdge (u, v);
		addEdge (v, u);
	}
	
	sort (b + 1, b + n + 1);
	int ret = unique (b + 1, b + n + 1) - b - 1;
	for (int i = 1; i <= n; ++i) a[i] = lower_bound (b + 1, b + n + 1, a[i]) - b;
	
	len = sqrt (n);
	
	for (int i = 1; i <= m; ++i) {
		int x, y;
		cin >> x >> y;
		
		if (fi[x] > fi[y]) swap (x, y);
		q[i].id = i, q[i].lca = Lca (x, y);
		
		if (q[i].lca == x) {
			q[i].x = fi[x];
			q[i].y = fi[y];
			q[i].lca = 0;
		} else {
			q[i].x = ls[x];
			q[i].y = fi[y];
		}
	}
	
	sort (q + 1, q + m + 1);
	
	int l = 1, r = 0;
	for (int i = 1; i <= m; ++i) {
		while (l > q[i].x) modify (eu[--l]);
		while (r < q[i].y) modify (eu[++r]);
		while (l < q[i].x) modify (eu[l++]);
		while (r > q[i].y) modify (eu[r--]);
		if (q[i].lca) modify (q[i].lca); // 注意特判
		ans[q[i].id] = tot;
		if (q[i].lca) modify (q[i].lca);
	}
	return 0;
}

相關文章