• 算法学习--分块和莫队


    一、分块

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

    我们将序列按每(s=sqrt{n})个元素一块进行分块,并记录每块的区间和(sum_i)

    (egin{matrix}underbrace{a_1+a_2+...+a_s}\{sum_1}end{matrix})(egin{matrix}underbrace{a_{s+1}+a_{s+2}+...+a_{2s}}\{sum_2}end{matrix})(egin{matrix}underbrace{a_{(s-1) imes s+1}+...+a_{n}}\{sum_{bdfrac{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
    ",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(nsqrt{n}))的复杂度内求出所有答案

      实现:离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。

      排序方法:对于区间([l,r]), 以(l)所在块的编号为第一关键字,(r)为第二关键字从小到大排序。

      优化:奇偶分块(并不懂这是什么奇怪的优化)

    例题:小z的袜子

    假设一段区间内有几种颜色的袜子他们的个数分别为(a,b,c...),那么他们对答案的贡献为(frac{frac{a imes (a-1)+b imes (b-1)+c imes (c-1)...}{2}}{frac{(r-l+1) imes (r-l)}{2}})

    当我增加或减少一个颜色的袜子的时候:

    这个颜色的袜子原本有a+1个,此时区间缩小:(frac{(a+1) imes (a)}{2}-frac{a imes (a-1)}{2}=frac{a^2+a-a^2+a}{2}=a)

    这个颜色的袜子原本有a个,此时区间增大:(frac{(a+1) imes (a)}{2}-frac{a imes (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
    ",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
    ",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
    ",ans[i]);}
    	return 0;
    }
    
  • 相关阅读:
    小程序之滚动选择器(时间、普通、日期)
    bzoj 4825: [Hnoi2017]单旋 LCT
    bzoj 4821: [Sdoi2017]相关分析 线段树
    bzoj 4766: 文艺计算姬 矩阵树定理
    bzoj 4031: 小Z的房间 矩阵树定理
    bzoj 4822~4824 CQOI2017题解
    bzoj 4817: [Sdoi2017]树点涂色 LCT+树链剖分+线段树
    bzoj 4816: [Sdoi2017]数字表格
    bzoj 4537: [Hnoi2016]最小公倍数 分块+并查集
    bzoj 4653: [Noi2016]区间
  • 原文地址:https://www.cnblogs.com/little-uu/p/14130663.html
Copyright © 2020-2023  润新知