分塊莫隊學習筆記

_zqh發表於2024-11-18

優雅的暴力。

引入

link

這道題顯然可以用線段樹、樹狀陣列做,但如果我偏不用這些資料結構呢?

我們知道,暴力修改和查詢最壞是 \(\mathcal{O}(n)\) 的,這樣肯定會掛掉。

那該怎麼辦呢?

正題

分塊

考慮將序列分成若干塊,我們設每塊長為 \(B\)

對於每次查詢 \(\left [ l, r \right ]\),我們涉及到修改的塊是 \(\left [ b_l, b_r \right ]\)\(b_i\) 代表 \(i\) 屬於哪個塊)。

其中 \(\left [ b_l + 1, b_r - 1 \right ]\) 是整塊都被修改了。

不妨設定一個懶標記,把每塊的整塊操作都加到這裡面。

這樣修改的複雜度是 \(\mathcal{O}(\frac{n}{B})\) 的。

那剩下的我們就可以暴力操作,複雜度是 \(\mathcal{O}(B)\) 的。

查詢同理。

此時修改查詢的複雜度就變成了 \(\mathcal{O}(B + \frac{n}{B})\) 了。

使得該數最小的顯然是 \(B = \sqrt{n}\),所以該演算法的時間複雜度是 \(\mathcal{O}(m\sqrt{n})\)

分塊主要解決區修區查類問題,只要滿足以下條件即可:

  • 可以打懶標記(結合律)。
  • 時間複雜度允許。

優勢:可解決問題範圍廣。

劣勢:時間複雜度高。

時間複雜度:\(\mathcal{O}(m\sqrt{n})\)

空間複雜度:\(\mathcal{O}(n)\)

莫隊

普通莫隊

莫隊是一種離線演算法,需要滿足以下條件:

  • 在知道 \(\left [ l, r \right ]\) 的答案的情況下,可以 \(\mathcal{O}(1)\) 求出 \(\left [ l, r + 1 \right ]\)\(\left [ l, r - 1 \right ]\)\(\left [ l + 1, r \right ]\)\(\left [ l - 1, r \right ]\) 的答案。
  • 允許離線。
  • 只有詢問沒有修改。

首先將所有的詢問離線下來,記為 \(\left [ ql_1, qr_1 \right ],\left [ ql_2, qr_2 \right ],\dots,\left [ ql_m, qr_m \right ]\)

將詢問排序(這正是莫隊演算法的精髓),從上一個詢問的答案一個個改到當前詢問,得到答案。

實現:

for (int i = 1; i <= m; i++) {
	while (l < q[i].l) del(l++);
	while (r > q[i].r) del(r--);
	while (l > q[i].l) add(--l);
	while (r < q[i].r) add(++r);
	ans[q[i].id] = res;
}

但是仔細分析發現時間複雜度仍然可以被卡成 \(nm\),一點都不優秀,甚至會更慢。

考慮最佳化

我們想要最佳化複雜度的根本是讓 \(l\)\(r\) 指標移動的距離儘量少。

對詢問範圍進行分塊,塊長為 \(B\)

以詢問左端點的塊編號為第一關鍵字,右端點為第二關鍵字排序。

  • 如果當前詢問與上一次處於同一塊,則 \(l\) 最多移動 \(B\)
  • 不同塊的詢問,\(l\) 最多移動 \(2B\)

則:

  • \(l\) 移動的複雜度是 \(m\times B = mB\)
  • \(r\) 的複雜度是 \(\frac{n}{B} \times n = \frac{n^2}{B}\)

則複雜度是 \(\mathcal{O}(mB + \frac{n^2}{B})\)

使得該式最小的 \(B\) 的值是 \(\frac{n}{\sqrt m}\),則此時的時間複雜度就是 \(\mathcal{O}(n\sqrt{m} + m\log m)\)

\(m \log m\) 是排序的複雜度。

總結一下。

普通莫隊解決的問題滿足以下條件:

  • 在知道 \(\left [ l, r \right ]\) 的答案的情況下,可以 \(\mathcal{O}(1)\) 求出 \(\left [ l, r + 1 \right ]\)\(\left [ l, r - 1 \right ]\)\(\left [ l + 1, r \right ]\)\(\left [ l - 1, r \right ]\) 的答案。
  • 允許離線。
  • 只有詢問沒有修改。

優勢:再沒有更快的思維做法之前,她幾乎是跑得最快並且思維含量最低的。

劣勢:只支援離線。

時間複雜度: \(\mathcal{O}(n\sqrt{m} + m\log m)\)

空間複雜度: \(\mathcal{O}(n)\)

例題 1:小 B 的詢問

非常板子的一道,維護一下 \(c\) 陣列即可。

#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define FRE(x) freopen(x ".in", "r", stdin), freopen(x ".out", "w", stdout)
#define ALL(x) x.begin(), x.end()
using namespace std;

int _test_ = 1;

const int N = 50008;

int n, m, k, block_size, res, cnt[N], a[N], ans[N];
struct node {
	int l, r, id;
} q[N];

bool operator<(node x, node y) {
	int xl = (x.l - 1) / block_size + 1, xr = (x.r - 1) / block_size + 1;
	int yl = (y.l - 1) / block_size + 1, yr = (y.r - 1) / block_size + 1;
	return (xl != yl) ? (xl < yl) : (x.r < y.r);
}

void add(int x) {
	res += cnt[a[x]] * 2 + 1;
	cnt[a[x]]++;
}

void del(int x) {
	res -= cnt[a[x]] * 2 - 1;
	cnt[a[x]]--;
}

void init() {}

void clear() {}

void solve() {
	cin >> n >> m >> k;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	block_size = n / sqrt(m); // 塊長
	for (int i = 1; i <= m; i++) {
		cin >> q[i].l >> q[i].r;
		q[i].id = i;
	}
	sort(q + 1, q + m + 1);
	int l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		while (l < q[i].l) del(l++);
		while (r > q[i].r) del(r--);
		while (l > q[i].l) add(--l);
		while (r < q[i].r) add(++r);
		ans[q[i].id] = res;
	}
	for (int i = 1; i <= m; i++) {
		cout << ans[i] << "\n";
	}
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
//	cin >> _test_;
	init();
	while (_test_--) {
		clear();
		solve();
	}
	return 0;
}

不過此題塊長就是 \(1\) 都能在 \(700\) 毫秒以內過,資料太水。

例題 2:小 Z 的襪子

也是非常板子的一道,維護一下 \(c\) 陣列,並將上一題中的答案分別記分子分母即可。

請注意分子為 \(0\) 的情況。

#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define FRE(x) freopen(x ".in", "r", stdin), freopen(x ".out", "w", stdout)
#define ALL(x) x.begin(), x.end()
using namespace std;

int _test_ = 1;

const int N = 500008;

int n, m, k, block_size, len;
pii res;
int cnt[N], a[N];
pii ans[N];
struct node {
	int l, r, id;
} q[N];

bool operator<(node x, node y) {
	int xl = (x.l - 1) / block_size + 1, xr = (x.r - 1) / block_size + 1;
	int yl = (y.l - 1) / block_size + 1, yr = (y.r - 1) / block_size + 1;
	return (xl != yl) ? (xl < yl) : (x.r < y.r);
}

void add(int x) {
	res.first += cnt[a[x]];
	res.second += len;
	len++;
	cnt[a[x]]++;
}

void del(int x) {
	len--;
	cnt[a[x]]--;
	res.first -= cnt[a[x]];
	res.second -= len;
}

void init() {}

void clear() {}

void solve() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	block_size = n / sqrt(m);
	for (int i = 1; i <= m; i++) {
		cin >> q[i].l >> q[i].r;
		q[i].id = i;
	}
	sort(q + 1, q + m + 1);
	int l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		if (q[i].l == q[i].r) ans[q[i].id] = {0, 1};
		while (l < q[i].l) del(l++);
		while (r > q[i].r) del(r--);
		while (l > q[i].l) add(--l);
		while (r < q[i].r) add(++r);
		if (res.first == 0) {
			ans[q[i].id] = {0, 1};
			continue;
		}
		int g = __gcd(res.first, res.second);
		ans[q[i].id] = {res.first / g, res.second / g};
	}
	for (int i = 1; i <= m; i++) {
		cout << ans[i].first << "/" << ans[i].second << "\n";
	}
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
//	cin >> _test_;
	init();
	while (_test_--) {
		clear();
		solve();
	}
	return 0;
}

事實證明,還是 \(B = \frac{n}{\sqrt{m}}\) 跑得最快。

(咕咕咕。。。)

相關文章