1. 程式人生 > 實用技巧 >演算法學習--分塊和莫隊

演算法學習--分塊和莫隊

一、分塊

分塊的基本思想是,通過對原資料的適當劃分,並在劃分後的每一個塊上預處理部分資訊,從而較一般的暴力演算法取得更優的時間複雜度。

我們將序列按每\(s=\sqrt{n}\)個元素一塊進行分塊,並記錄每塊的區間和\(sum_i\)

\(\begin{matrix}\underbrace{a_1+a_2+...+a_s}\\{sum_1}\end{matrix}\)\(\begin{matrix}\underbrace{a_{s+1}+a_{s+2}+...+a_{2s}}\\{sum_2}\end{matrix}\)\(\begin{matrix}\underbrace{a_{(s-1)\times s+1}+...+a_{n}}\\{sum_{b\dfrac{n}{s}}}\end{matrix}\)

最後一個塊可能是不完整的(因為\(s\)很可能不是\(s\)的倍數),但是這對於我們的討論來說並沒有太大影響。

演算法流程:

首先看查詢:

  • \(l\)\(r\)在同一個塊內,直接暴力求和即可,因為塊長為\(s\),因此最壞複雜度為\(O(s)\)

  • \(l\)\(r\)不在同一個塊內,則答案由三部分組成:以\(l\)開頭的不完整塊,中間幾個完整塊,以\(r\)結尾的不完整塊。對於不完整的塊,仍然採用上面暴力計算的方法,對於完整塊,則直接利用已經求出的\(sum_i\)求和即可。這種情況下,最壞複雜度為\(O(\frac{n}{s}+s)\)

修改操作:

  • \(l\)\(r\)

    在同一個塊內,直接暴力修改即可,因為塊長為\(s\),因此最壞複雜度為\(O(s)\)

  • \(l\)\(r\)不在同一個塊內,則需要修改三部分:以\(l\)開頭的不完整塊,中間幾個完整塊,以\(r\)結尾的不完整塊。對於不完整的塊,仍然是暴力修改每個元素的值(別忘了更新區間和\(sum_i\)),對於完整塊,則直接修改\(sum_i\)即可。這種情況下,最壞複雜度和仍然為\(O(\frac{n}{s}+s)\)

code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#define int long long
using namespace std;
int read(){
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 5e4+10;
int n,len;
int a[maxn],b[maxn],sum[maxn],id[maxn];
void add(int l,int r,int x){
	int lid = id[l],rid = id[r];
	if (lid == rid){
		for (int i = l;i <= r;i++) a[i] += x,sum[lid] += x;
		return;
	}
	for (int i = l;id[i] == lid;i++) a[i] += x,sum[lid] += x;
	for (int i = lid+1;i < rid;i++) b[i] += x,sum[i] += len*x;
	for (int i = r;id[i] == rid;i--) a[i] += x,sum[rid] += x; 
}
int query(int l,int r,int x){
	int lid = id[l],rid = id[r];
	int ans = 0;
	if (lid == rid){
		for (int i = l;i <= r;i++) (ans += a[i]+b[lid]) %= x; 
		return ans;
	}
	for (int i = l;id[i] == lid;i++) (ans += a[i]+b[lid]) %= x;
	for (int i = lid+1;i < rid;i++) (ans += sum[i]) %= x;
	for (int i = r;id[i] == rid;i--) (ans += a[i]+b[rid]) %= x;
	return ans;
}
signed main(){
	n = read();len = sqrt(n);
	for (int i = 1;i <= n;i++){
		a[i] = read();
		id[i] = (i-1)/len+1;
		sum[id[i]] += a[i];
	}
	for (int i = 1;i <= n;i++){
		int op,l,r,x;
		op = read(),l = read(),r = read(),x = read();
		if (op == 0) add(l,r,x);
		else printf("%lld\n",query(l,r,x+1));
	} 
	return 0;
} 

二、莫隊

  • 普通莫隊

    形式:假設\(n=m\),那麼對於序列上的區間詢問問題,如果能從\([l,r]\)的答案\(O(1)\)擴充套件到區間\([l-1,r],[l+1,r],[l,r-1],[l,r+1]\)(即與\([l,r]\)相鄰區間的答案),那麼可以再\(O(n\sqrt{n})\)的複雜度內求出所有答案

    實現:離線後排序,順序處理每個詢問,暴力從上一個區間的答案轉移到下一個區間答案(一步一步移動即可)。

    排序方法:對於區間\([l,r]\), 以\(l\)所在塊的編號為第一關鍵字,\(r\)為第二關鍵字從小到大排序。

例題:小z的襪子

假設一段區間內有幾種顏色的襪子他們的個數分別為\(a,b,c...\),那麼他們對答案的貢獻為\(\frac{\frac{a\times (a-1)+b\times (b-1)+c\times (c-1)...}{2}}{\frac{(r-l+1)\times (r-l)}{2}}\)

當我增加或減少一個顏色的襪子的時候:

這個顏色的襪子原本有a+1個,此時區間縮小:\(\frac{(a+1)\times (a)}{2}-\frac{a\times (a-1)}{2}=\frac{a^2+a-a^2+a}{2}=a\)

這個顏色的襪子原本有a個,此時區間增大:\(\frac{(a+1)\times (a)}{2}-\frac{a\times (a-1)}{2}=\frac{a^2+a-a^2+a}{2}=a\)

莫隊求解,程式碼如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#define int long long
using namespace std;
int read(){
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 5e4+10;
int n,m,c[maxn],len;
int ans1[maxn],ans2[maxn],id[maxn];
int sum,cnt[maxn];
struct node{
	int l,r,id;
}a[maxn];
bool cmp(node x,node y){
	if (id[x.l] == id[y.l]) return x.r < y.r;
	return id[x.l] < id[y.l];
}
int gcd(int a,int b){
	if (a%b == 0) return b;
	return gcd(b,a%b);
}
void add(int x){
  	sum += cnt[x],cnt[x]++;
}
void del(int x){
  	cnt[x]--,sum -= cnt[x];
}
signed main(){
	n = read(),m = read();len = sqrt(n);
	for (int i = 1;i <= n;i++) c[i] = read();
	for (int i = 1;i <= n;i++) id[i] = (i-1)/len+1;
	for (int i = 1;i <= m;i++) a[i].l = read(),a[i].r = read(),a[i].id = i;
	sort(a+1,a+m+1,cmp);
	for (int i = 1,l = 1,r = 0;i <= m;i++) {
	    if (a[i].l == a[i].r) {
	      	ans1[a[i].id] = 0, ans2[a[i].id] = 1;
	      	continue;
	    }
	    while (l > a[i].l) add(c[--l]);
	    while (r < a[i].r) add(c[++r]);
	    while (l < a[i].l) del(c[l++]);
	    while (r > a[i].r) del(c[r--]);
	    ans1[a[i].id] = sum;
	    ans2[a[i].id] = (r-l+1)*(r-l)/2;
  	}
	for (int i = 1;i <= m;i++){
	    if (ans1[i] != 0) {
	      	int tmp = gcd(ans1[i], ans2[i]);
	      	ans1[i] /= tmp,ans2[i] /= tmp;
	    } 
		else ans2[i] = 1;
		printf("%lld/%lld\n",ans1[i],ans2[i]);
	}
	return 0;
}
  • 優化:先咕了……

  • 帶修莫隊

  • 樹上莫隊