算法学习--分块和莫队

一、分块

分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。

我们将序列按每\(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的最近公共祖先:

  1. 若lca是a、b之一,则a、b之间的In时刻的区间或者Out时刻区间就是其路径。

  2. 若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\)变量来表示在每个询问之前进行了几次操作.

  1. 首先,应该判断\(l\)是否在同一块内,如果不同,就返回左端点所在块小的

  2. 然后,应该判断\(r\)是否在同一块内,如果不同,就返回右端点所在块小的

  3. 最后,再比较\(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;
}
posted @ 2020-12-13 22:06  小又又yyyy  阅读(167)  评论(0编辑  收藏  举报