演算法學習--分塊和莫隊
一、分塊
分塊的基本思想是,通過對原資料的適當劃分,並在劃分後的每一個塊上預處理部分資訊,從而較一般的暴力演算法取得更優的時間複雜度。
我們將序列按每\(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\)
-
若\(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;
}
-
優化:先咕了……
-
帶修莫隊
-
樹上莫隊