1. 程式人生 > 實用技巧 >「分塊」學習筆記

「分塊」學習筆記

「分塊」

演算法思想

當我們對於一個很大陣列 \((1e5)\) 進行區間修改和區間查詢時,我們會想到線段樹的 \(nlog_n\) 的優秀效率。

分塊——優雅的暴力!!!

我們將區間分成每個大小為 \(S\) 的小塊,這樣我們的複雜度就會從 \(n\) 降到 \(\frac n S\) 的效率。

基本思路

初始化

我們先將陣列分成長度為 \(S\) 小塊,用原下標除以 \(S\) 向上取整,就是他分塊後的小塊標號。

void Init() { //分塊初始化
	S = sqrt(n);
	for (register int i = 1; i <= n; i ++) {
		bel[i] = (i - 1) / S + 1; //向上取整
		size[bel[i]] ++; //統計每個塊的大小,為什麼不直接L[i] - R[i],因為可能最後一個塊的大小不是S
		sum[bel[i]] += a[i]; //統計每個塊的權值總和
		if (!L[bel[i]]) L[bel[i]] = i; //每個塊的左端點
		R[bel[i]] = i; //每個塊的右端點
	}
}

區間修改

若我們要將 \(l\)\(r\) 的區間內都加上權值 \(w\)

若這個區間在一個小塊裡面

直接 \(S\) 的效率一個一個修改即可。

若這個區間橫跨了多個小塊

如下圖

我們先用 \(S\) 效率將左右兩端無法組成一個塊的單點都加上。

然後對於中間的塊,一個一個塊都打上標記,跟線段樹上的標記差不多,最後區間查詢時加上即可。

void Modify(int l, int r, int w) { //區間修改
	if (bel[l] == bel[r]) { //如果這個區間在同一個塊裡
		sum[bel[l]] += (r - l + 1) * w; //塊的值加上
		for (register int i = l; i <= r; i ++) { //每個單點的值也加上
			a[i] += w;
		}
	}else {
		Modify (l, R[bel[l]], w); //最左邊無法組成一個塊的
		Modify (L[bel[r]], r, w); //最右邊無法組成一個塊的
		for (register int i = bel[l] + 1; i < bel[r]; i ++) { //把中間的塊加上,並打上花火 &!@*^$&*^*&^@
			tag[i] += w;
			sum[i] += size[i] * w;
		}
	}
}

區間查詢

跟區間修改的思想類似

若這個區間在一個小塊裡面

將這個塊裡標記值加上,在加上單點的值即可。

若這個區間橫跨了多個小塊

將無法組成左右無法組成一個小塊的部分用上面的思路加和。

剩下的一塊一塊直接加上 \(sum[i]\) 即可。

inline int Query(int l, int r) { //區間查詢
	int ans = 0;
	if (bel[l] == bel[r]) {
		ans += (r - l + 1) * tag[bel[l]]; //把之前整塊修改的值加上
		for (register int i = l; i <= r; i ++) { // 把之前單點修改後的值加上
			ans += a[i];
		}
	}else {
		ans += Query(l, R[bel[l]]); //最左邊無法組成塊的值加上
		ans += Query(L[bel[r]], r); //最右邊無法組成塊的值加上
		for (register int i = bel[l] + 1; i < bel[r]; i ++) {
			ans += sum[i]; //這樣我們的sum陣列就可以直接查了
		}
	}
	return ans;
}

時間效率

自我感覺比線段樹好寫,而且在某些題上會比線段樹跑得快。

例如這個板子題

分塊


線段樹


練習題

打一波廣告

程式碼

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int maxn = 1e5 + 50, INF = 0x3f3f3f3f;

inline int read(){
	int x = 0, w = 1;
	char ch;
	for (; ch < '0' || ch > '9'; ch = getchar()) if (ch == '-') w = -1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
	return x * w;
}

int n, m, S;
int a[maxn];
int bel[maxn], size[maxn], sum[maxn], L[maxn], R[maxn];
int tag[maxn];

void Init() { //分塊初始化
	S = sqrt(n);
	for (register int i = 1; i <= n; i ++) {
		bel[i] = (i - 1) / S + 1; //向上取整
		size[bel[i]] ++; //統計每個塊的大小,為什麼不直接L[i] - R[i],因為可能最後一個塊的大小不是S
		sum[bel[i]] += a[i]; //統計每個塊的權值總和
		if (!L[bel[i]]) L[bel[i]] = i; //每個塊的左端點
		R[bel[i]] = i; //每個塊的右端點
	}
}

void Modify(int l, int r, int w) { //區間修改
	if (bel[l] == bel[r]) { //如果這個區間在同一個塊裡
		sum[bel[l]] += (r - l + 1) * w; //塊的值加上
		for (register int i = l; i <= r; i ++) { //每個單點的值也加上
			a[i] += w;
		}
	}else {
		Modify (l, R[bel[l]], w); //最左邊無法組成一個塊的
		Modify (L[bel[r]], r, w); //最右邊無法組成一個塊的
		for (register int i = bel[l] + 1; i < bel[r]; i ++) { //把中間的塊加上,並打上花火 &!@*^$&*^*&^@
			tag[i] += w;
			sum[i] += size[i] * w;
		}
	}
}

inline int Query(int l, int r) { //區間查詢
	int ans = 0;
	if (bel[l] == bel[r]) {
		ans += (r - l + 1) * tag[bel[l]]; //把之前整塊修改的值加上
		for (register int i = l; i <= r; i ++) { // 把之前單點修改後的值加上
			ans += a[i];
		}
	}else {
		ans += Query(l, R[bel[l]]); //最左邊無法組成塊的值加上
		ans += Query(L[bel[r]], r); //最右邊無法組成塊的值加上
		for (register int i = bel[l] + 1; i < bel[r]; i ++) {
			ans += sum[i]; //這樣我們的sum陣列就可以直接查了
		}
	}
	return ans;
}

int main(){
	n = read(), m = read();
	for (register int i = 1; i <= n; i ++) {
		a[i] = read();
	}
	Init();
	while (m --) {
		int opt = read(), l = read(), r = read();
		if (opt == 1) {
			int w = read();
			Modify(l, r, w);
		}else {
			printf("%d\n", Query(l, r));
		}
	}
	return 0;
}