算法学习--分块和莫队
一、分块
分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
我们将序列按每\(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 !(id[x.l]&1)^(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;
}
-
树上莫队
算法流程:
其任意点对a、b之间的路径,具有如下性质,令lca为a、b的最近公共祖先:
-
若lca是a、b之一,则a、b之间的In时刻的区间或者Out时刻区间就是其路径。
-
若lca另有其人,则a、b之间的路径为In[a]、Out[b]之间的区间或者In[b]、Out[a]之间的区间。另外,还需额外特判lca
这样就能将路径查询转化为对应的区间查询。另外需要注意到,在DFS序上应用莫队算法移动指针时,如果是欲添加的节点在当前区间内已经有一个了,这实际上应该是一个删除操作;如果欲删除的节点在当前区间内已经有两个了,这实际上应该是一个添加操作。
- 小tips:人为规定In[x] < In[y],当lca为x或者y的时候,区间为In[x]-In[y],否则为Out[x]-In[y]
例题: SP10707 COT2 - Count on a tree II
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
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 = 1e6+10;
int n,m;
int a[maxn],b[maxn],len;
int f[maxn][30],cnt[maxn];
struct node{int l,r,id,lca;}arr[maxn];
struct edge{int to,nxt;}ed[maxn*2];
int head[maxn],tot;
void add(int u,int to){
ed[++tot].to = to;
ed[tot].nxt = head[u];
head[u] = tot;
}
int id[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 dfn[maxn],pos[maxn],top,low[maxn],dep[maxn];
void dfs(int x){
dfn[x] = ++top,pos[top] = x;
for (int i = 1;i <= 20;i++) f[x][i] = f[f[x][i-1]][i-1];
for (int i = head[x];i;i = ed[i].nxt){
int to = ed[i].to;
if (to == f[x][0]) continue;
f[to][0] = x,dep[to] = dep[x] + 1;
dfs(to);
}
low[x] = ++top,pos[top] = x;
}
int lca(int x,int y){
if (dep[x] > dep[y]) swap(x,y);
for (int i = 19;i >= 0;i--){//20->19
if (dep[f[y][i]] >= dep[x]) y = f[y][i];//> -> >=
}
if (x == y) return x;
for (int i = 19;i >= 0;i--){
if (f[x][i] != f[y][i]) x = f[x][i],y = f[y][i];
}
return f[x][0];
}
int sum,vis[maxn],ans[maxn];
void add(int x){++cnt[a[x]];if(cnt[a[x]] == 1) sum++;}
void del(int x){--cnt[a[x]];if(cnt[a[x]] == 0) sum--;}
void update(int x){
if (vis[x]) del(x);
else add(x);
vis[x]^=1;
}
int main(){
n = read(),m = read();len = sqrt(n*2);
for (int i = 1;i <= n;i++) a[i] = b[i] = read();
for (int i = 1;i <= n*2;i++) id[i] = (i-1)/len+1;
sort(b+1,b+n+1);
int lin = unique(b+1,b+n+1)-b-1;
for (int i = 1;i <= n;i++) a[i] = lower_bound(b+1,b+lin+1,a[i])-b;
for (int i = 1;i <= n-1;i++){
int u = read(),v = read();
add(u,v),add(v,u);
}
dep[1] = 1;dfs(1);
for (int i = 1;i <= m;i++){
int x = read(),y = read();
int tmp = lca(x,y);
if (dfn[x] > dfn[y]) swap(x,y);
if (x == tmp || y == tmp) arr[i].l = dfn[x],arr[i].r = dfn[y],arr[i].id = i;//
else arr[i].l = low[x],arr[i].r = dfn[y],arr[i].id = i,arr[i].lca = tmp;
}
sort(arr+1,arr+m+1,cmp);
for(int i = 1,l = 1,r = 0;i <= m;i++){
while(l > arr[i].l) update(pos[--l]);
while(r < arr[i].r) update(pos[++r]);
while(l < arr[i].l) update(pos[l++]);
while(r > arr[i].r) update(pos[r--]);
ans[arr[i].id] = sum;
if(arr[i].lca) if(!cnt[a[arr[i].lca]]) {ans[arr[i].id] ++;}
}
for(int i = 1;i <= m;i++) printf("%d\n",ans[i]);
return 0;
}
-
带修莫队
-
如何处理修改
其实,我们可以增加一个变量,来记录对于每一个询问操作,在进行询问之前一共进行了多少次修改,然后对于每一次询问,只要像普通莫队的\(l\)指针和\(r\)指针一样新增一个\(t\)指针来表示当前进行了多少次修改,而\(t\)指针的移动也与\(l\)指针和\(r\)指针是类似的。
-
排序函数
现在加上了一个\(t\)变量来表示在每个询问之前进行了几次操作.
-
首先,应该判断\(l\)是否在同一块内,如果不同,就返回左端点所在块小的
-
然后,应该判断\(r\)是否在同一块内,如果不同,就返回右端点所在块小的
-
最后,再比较\(t\)的大小,返回\(t\)小的一个
例题:[国家集训队]数颜色 / 维护队列
和普通莫队版差不多的思想,只是多了个修改
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cmath>
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 = 2e5+10;
int n,m,a[maxn],sum,len;
struct node{
int l,r,tim,pos,val;
}arr1[maxn],arr2[maxn];
int cnt1,cnt2,cnt[maxn*10];
int ans[maxn],id[maxn];
bool cmp(node x,node y){
if (id[x.l] != id[y.l]) return id[x.l] < id[y.l];
if (id[x.r] != id[y.r]) return id[x.r] < id[y.r];
return x.tim < y.tim;
}
void add(int x){cnt[x]++;if (cnt[x] == 1) sum++;}
void del(int x){cnt[x]--;if (cnt[x] == 0) sum--;}
void update(int x, int t){
if (arr1[x].l <= arr2[t].pos&&arr2[t].pos <= arr1[x].r){
del(a[arr2[t].pos]);
add(arr2[t].val);
}
swap(a[arr2[t].pos], arr2[t].val);
}
int main(){
n = read(),m = read(),len = pow(n,2.0/3.0);
for (int i = 1;i <= n;i++) a[i] = read();
for (int i = 1;i <= n;i++) id[i] = (i-1)/len+1;
for (int i = 1;i <= m;i++){
char op[10];scanf ("%s",op);
if (op[0] == 'Q') arr1[++cnt1].l = read(),arr1[cnt1].r = read(),arr1[cnt1].tim = cnt2,arr1[cnt1].pos = cnt1;
else arr2[++cnt2].pos = read(),arr2[cnt2].val = read();
}
sort(arr1+1,arr1+cnt1+1,cmp);
for (int i = 1,l = 1,r = 0,t = 0;i <= cnt1;i++){
while (l > arr1[i].l) add(a[--l]);
while (r < arr1[i].r) add(a[++r]);
while (l < arr1[i].l) del(a[l++]);
while (r > arr1[i].r) del(a[r--]);
while (t < arr1[i].tim) update(i,++t);
while (t > arr1[i].tim) update(i,t--);
ans[arr1[i].pos] = sum;
}
for (int i = 1;i <= cnt1;i++){printf("%d\n",ans[i]);}
return 0;
}