根号数据结构总结——分块

目录

  1. 理论
  2. 基础例题
  3. 大分块

分块

1. 理论

对于长为 \(n\) 的一个数列,把它分为一些块,记块长为 \(b\)

这里是区间加,区间和的板子的写法

区间求和

维护每一个块和,然后再把属于这个区间的大块的和加起来(最多有 \(\dfrac{n}{b}\) 个块),然后再算不属于区间的小块(最多有 \(2(b-1)\) 个)

复杂度 \(\dfrac{n}{b}+b\)

区间修改

维护一个 \(tag\),但是不同于线段树,这里的 \(tag\) 不作下传,记录这个块整体要加的数。

区间修改时,所有的大块更新 \(tag\),所有的小块暴力加。最后查询的时候,结果应该是当前位置的数加上这个数所在的块的 \(tag\)

复杂度仍是 \(\dfrac{n}{b}+b\)

发现正常情况下 \(b=\sqrt n\) 最优,这时区间修改/询问都是 \(\mathcal O(\sqrt n)\),总复杂度 \(O(m\sqrt n)\)

但是这也说明了分块的特点:复杂度高于线段树、树状数组等,应用范围相比后者更广,对常数要求苛刻,有时稍微调块长可以跑的更快。

代码讲解

const int maxn; // n 的范围
const int sqr = sqrt(maxn); // 根号的大小

int n,m,a[maxn]; // a[] 是原数组
int st[sqr],ed[sqr]; // 每个块的下标范围
int B,C,bl[maxn]; // 块长、块数、每个位置所属块的编号
int tag[sqr],sum[sqr]; // 块的加法标记,块的总和

void init(){ // 分块的预处理 
	B = sqrt(n); // 块长取根号
	C = (n - 1) / B + 1; // 这是块编号的取法
	for(int i = 1;i <= C;++i){ // 预处理每个块 
		st[i] = (i - 1) * B + 1; // 前 i-1 个块的后一位
		ed[i] = i == C ? n : i * B; // i 个块长的位置,注意特判 i=C
		for(int j = st[i];j <= ed[i];++j){
			bl[j] = i; // 给这一块赋值编号 
			sum[i] += a[j]; // 计算这块的块和
		}
		tag[i] = 0; // 清空标记 
	}
}

int qry(int l,int r){ // 区间和 
	int res = 0; // 结果 
	if(bl[l] == bl[r]){ // 在同一块,此时暴力 
		for(int i = l;i <= r;++i)
			res += (a[i] + tag[bl[i]]); // 注意要把标记的值算上 
		return res; 
	}
	// 两侧零散块为 l~ed[bl[l], st[bl[r]]~r 
	for(int i = l;i <= ed[bl[l]];++i)
		res += (a[i] + tag[bl[i]]); // 同样方法算两侧零散块 
	for(int i = st[bl[r]];i <= r;++i)
		res += (a[i] + tag[bl[i]]);
	for(int i = bl[l] + 1;i <= bl[r] - 1;++i)
		res += (ed[i] - st[i] + 1) * tag[i] + sum[i]; // 原本的和加上标记的和
	return res; 
}

void upd(int l,int r,int x){ // 区间加 
	if(bl[l] == bl[r]){ // 在同一块,此时暴力 
		for(int i = l;i <= r;++i) a[i] += x; // 暴力加 
		return; 
	}
	for(int i = l;i <= ed[bl[l]];++i) a[i] += x;
	for(int i = st[bl[r]];i <= r;++i) a[i] += x;
	for(int i = bl[l] + 1;i <= bl[r] - 1;++i)
		sum[i] += x; // 这个时候直接暴力加就行了 
}

熟练后可以写的更简略,详见后面

2. 基础例题

数列分块入门 1, 数列分块入门 4

上文已讲。

数列分块入门 2

读题,发现它好像和普通的分块没什么区别,但是看询问:散块可以像上面一样暴力,整块直接没有思路。其实这类问题需要引入一个概念——分块套排序数组

简单来讲,给每块维护一个排序数组,即把每一块排序,于是对于查询的整块部分,可以直接在排序数组里 lower_bound,然后就可以得到块内有多少数小于等于 \(x\)

int n,m,a[maxn];
int st[sqr],ed[sqr];
int B,C,bl[maxn];
int tag[sqr]; // 区间加的标记
int s[maxn]; // 排序数组 

void init(){
	B = sqrt(n);
	C = (n - 1) / B + 1;
	for(int i = 1;i <= C;++i){
		st[i] = (i - 1) * B + 1;
		ed[i] = i == C ? n : i * B;
		for(int j = st[i];j <= ed[i];++j){
			bl[j] = i;
			s[j] = a[j]; // 先处理出开始的 s 数组 
		}
		std::sort(s + st[i],s + ed[i] + 1); // 给 s 数组排序 
	}
}

int qry(int l,int r,int x){
	int res = 0;
	if(bl[l] == bl[r]){ // 散块直接暴力 
		for(int i = l;i <= r;++i)
			res += (a[i] + tag[bl[i]] <= x); // 暴力统计 
		return res;
	}
	for(int i = l;i <= ed[bl[l]];++i)
		res += (a[i] + tag[bl[i]] <= x); // 暴力统计 
	for(int i = st[bl[r]];i <= r;++i)
		res += (a[i] + tag[bl[i]] <= x); // 暴力统计
	for(int i = bl[l] + 1;i <= bl[r] - 1;++i){
		// 整块需要借助排序数组使用 lower_bound 来进行统计
		// lower_bound(bg,ed,x)->第一个大于等于 x 的位置 
		// upper_bound(bg,ed,x)->第一个大于 x 的位置,二者有区别
		res += std::lower_bound(s + st[i],s + ed[i] + 1,x - tag[i]) - s - st[i];
		// 注意 x-tag[i]
	}
}

但是它的复杂度是 \(O(m\sqrt n\log n)\),这道题不会卡你,但是事实上这个问题也有 \(O(m\sqrt n)\) 解法,只是此解法难得多,不适宜入门。

数列分块入门 3

有了上一道题的思想,这个题就很简单了。

给每块维护排序数组,然后对于散块暴力取 \(\max\),整块还是统计,散块仍然是借助 lower_bound

实现和上题类似,我写的很丑很长(甚至是用 set 写的),就不放了。

P5356 [Ynoi2017] 由乃打扑克

想不到吧 这就 Ynoi 了 其实这题蛮水的。

首先,这里区间 \(k\) 小的求法是这样的:在值域上二分,然后查询当前二分的 mid 在区间的排名(分块套排序数组,就是分块入门 2),如果小于 \(k\) 答案就会更大,否则答案应该更小。剩下的修改其实也是板子。

这题不卡常,如果你要优化,就写两个函数求区间最大/最小值,再在这之中二分,减小二分次数即可达到目的。

#include<stdio.h>
#include<math.h>
#include<algorithm>

#ifdef ONLINE_JUDGE
static char buf[1000000],*p1=buf,*p2=buf;
#define getchar() p1==p2&&(p2=(p1=buf)+fread(buf,1,1000000,stdin),p1==p2)?EOF:*p1++
#endif

inline int read(){
    register int x = 0,f = 1;
    register char c = getchar();
    for(;c < '0' || c > '9';c = getchar())
    	if(c == 45) f = -1;
    for(;c >= '0' && c <= '9';c = getchar())
        x = x * 10 + (c ^ '0');
    return x * f;
}

inline int max2(register int a,register int b){ return a>b?a:b; }
inline int min2(register int a,register int b){ return a<b?a:b; }

const int maxn = 1e5 + 5;
const int sqr = sqrt(maxn) + 5;
const int inf = 2e9 + 5;

int n,m,b,c,a[maxn],s[maxn],bl[maxn],st[sqr],ed[sqr],tag[sqr];

void init(){
	b = sqrt(n); c = (n - 1) / b + 1;
	for(int i = 1;i <= n;++i){
		int sq = (i - 1) / b + 1;
		if(!st[sq]) st[sq] = i;
		ed[sq] = i;
		bl[i] = sq;
	}
	for(int i = 1;i <= c;++i)
		std::sort(s + st[i],s + ed[i] + 1);
}

void blk0upd(int l,int r,int x){
	for(int i = st[bl[l]];i <= ed[bl[r]];++i) s[i] = a[i];
	for(int i = l;i <= r;++i) a[i] += x,s[i] += x;
	std::sort(s + st[bl[l]],s + ed[bl[r]] + 1);
}

void upd(int l,int r,int x){
	if(bl[l] == bl[r]) return blk0upd(l,r,x),void();
	for(int i = bl[l] + 1;i <= bl[r] - 1;++i) tag[i] += x;
	blk0upd(l,ed[bl[l]],x); blk0upd(st[bl[r]],r,x);	
}

int max(int l,int r){
	int res = -inf;
	if(bl[l] == bl[r]){
		for(int i = l;i <= r;++i) res = max2(res,a[i] + tag[bl[i]]);
		return res;
	}
	for(int i = bl[l] + 1;i <= bl[r] - 1;++i) res = max2(res,s[ed[i]] + tag[i]);
	for(int i = l;i <= ed[bl[l]];++i) res = max2(res,a[i] + tag[bl[i]]);
	for(int i = st[bl[r]];i <= r;++i) res = max2(res,a[i] + tag[bl[i]]);
	return res;
}

int min(int l,int r){
	int res = inf;
	if(bl[l] == bl[r]){
		for(int i = l;i <= r;++i) res = min2(res,a[i] + tag[bl[i]]);
		return res;
	}
	for(int i = bl[l] + 1;i <= bl[r] - 1;++i) res = min2(res,s[st[i]] + tag[i]);
	for(int i = l;i <= ed[bl[l]];++i) res = min2(res,a[i] + tag[bl[i]]);
	for(int i = st[bl[r]];i <= r;++i) res = min2(res,a[i] + tag[bl[i]]);
	return res; 
}

int rnk(int l,int r,int x){
	int res = 0;
	if(bl[l] == bl[r]){
		for(int i = l;i <= r;++i) res += ((a[i] + tag[bl[i]]) <= x);
		return res;
	}
	for(int i = bl[l] + 1;i <= bl[r] - 1;++i){
		if(s[ed[i]] + tag[i] <= x) res += ed[i] - st[i] + 1;
		else if(s[st[i]] + tag[i] > x) continue;
		else res += std::upper_bound(s + st[i],s + ed[i] + 1,x - tag[i]) - (s + st[i]);
	}
	for(int i = l;i <= ed[bl[l]];++i) res += ((a[i] + tag[bl[i]]) <= x);
	for(int i = st[bl[r]];i <= r;++i) res += ((a[i] + tag[bl[i]]) <= x);
	return res;
}

int qry(int l,int r,int k){
	if(r - l + 1 < k || k < 1) return -1;
	int lf = min(l,r),rg = max(l,r),ans = -1;
	while(lf <= rg){
		int mid = (lf + rg) >> 1;
		if(rnk(l,r,mid) < k) lf = mid + 1;
		else rg = mid - 1,ans = mid;
	}
	return ans;
}

int main(){
	n = read(),m = read();
	for(int i = 1;i <= n;++i) a[i] = s[i] = read();
	init();
	while(m--){
		int op = read(),l = read(),r = read(),k = read();
		if(op == 2) upd(l,r,k);
		else printf("%d\n",qry(l,r,k));
	}
	return 0;
}

3. 大分块

第二分块

查询

首先,借鉴 CF1620E 的思想,使用并查集以及一些数组来处理这里的查询操作。

  • 并查集存下标,把值相同的下标 \(\tt merge\) 在一起,在一个 \(\tt dsu\) 的祖先上记录这个 \(\tt dsu\) 的值是多少。

  • \(pos_i\) 代表值为 \(i\) 的所有下标在并查集内的祖先。

  • \(num_i\) 只在 \(i\) 为一个值中并查集内的祖先。

并查集维护一个 \(size\) 就是这题询问的答案。

修改

分开处理,保证复杂度。设当前操作的 \(x\)\(x\)

  1. \(x\) 大于区间最大值的 \(\dfrac 12\) 时,把所有大于 \(x\) 的数减去 \(x\)
  2. \(x\) 小于等于区间最大值的 \(\dfrac 12\) 时,把所有小于 \(x\) 的数加上 \(x\),并打 \(lazy\) 让整个区间的数减去 \(x\)

注意以上这两个操作修改一个数时是在并查集里修改它。

\(lazy\) 在询问时还要算上。

空间优化

首先这里的空间是 \(\mathcal O(n\sqrt n)\) 的(这是因为对于每一个块都要开一个并查集),\(\tt 64MB\) 下会 \(\tt MLE\)

发现只要把并查集那一坨的东西优化掉即可 \(\tt AC\)

询问离线下来,把每一个块单独处理,把 \(m\) 遍操作全部过一遍,假如操作的 \(l,r\) 不属于这个块就跳过,否则暴力修改。

这样就不需要对于每一个块开一个并查集了,空间降至 \(\mathcal O(n)\)

// 第二分块 
const int maxn = 1e6 + 5;
const int maxm = 5e5 + 5;
const int maxv = 1e5 + 5;

int n,m,a[maxn],ans[maxm];

int pos[maxn],num[maxn],siz[maxn],fa[maxn];

il int fnd(rg int i){ return i == fa[i] ? i : fa[i] = fnd(fa[i]); }

il void chg(rg int i,rg int x){
	if(!pos[x]){
		pos[x] = i;
		num[i] = x;
		siz[x] = 1;
		fa[i] = i;
	} else {
		fa[i] = pos[x];
		++siz[x];
	}
}

il void updt(rg int x,rg int y){
	if(!pos[y]){
		pos[y] = pos[x];
		num[pos[y]] = y;
		siz[y] += siz[x];
		pos[x] = siz[x] = 0;
	} else {
		fa[pos[x]] = pos[y];
		siz[y] += siz[x];
		pos[x] = siz[x] = 0;
	}
}

#define val(i) num[fnd(i)]

const int blk = sqrt(maxn) + 5;
const int inf = 2e9; 

int blc,b,st,ed,mx,tag;

il void build(rg int i){
	st = (i - 1) * blc + 1;
	ed = min2(i * blc,n);
	mx = -inf; tag = 0;
	for(rg int i = st;i <= ed;++i)
		mx = max2(mx,a[i]), chg(i,a[i]);
}

il void blk0upd(rg int l,rg int r,rg int x){
	l = max2(l,st); r = min2(r,ed);
	for(rg int i = st;i <= ed;++i){
		a[i] = val(i);
		siz[a[i]] = 0; pos[a[i]] = 0;
		a[i] -= tag;
	}
	for(rg int i = st;i <= ed;++i) num[i] = 0;
	for(rg int i = l;i <= r;++i)
		if(a[i] > x) a[i] -= x;
	build(b); 
}

il void blkupd(rg int x){
	if(x > (mx - tag) / 2){
		for(rg int i = x + tag + 1;i <= mx;++i)
			if(pos[i]) updt(i,i - x);
		mx = min2(mx,x + tag);
	} else {
		for(rg int i = tag;i <= x + tag;++i)
			if(pos[i]) updt(i,i + x);
		tag += x;
	}
}

il int qry0(rg int l,rg int r,int x){
	rg int cc = 0;
	l = max2(l,st); r = min2(r,ed);
	for(int i = l;i <= r;++i)
		if(val(i) == x + tag) ++cc;
	return cc;
}

struct qry{ int op,l,r,x; } q[maxm];

int main(){
	scanf("%d%d",&n,&m);
	for(rg int i = 1;i <= n;++i) scanf("%d",a + i);
	for(rg int i = 1;i <= m;++i) 
		scanf("%d%d%d%d",&q[i].op,&q[i].l,&q[i].r,&q[i].x);
	blc = sqrt(n);
	for(b = 1;b <= blc;++b){
		memset(pos,0,sizeof pos);
		memset(siz,0,sizeof siz);
		build(b);
		for(rg int i = 1;i <= m;++i){
			if(st > q[i].r || ed < q[i].l) continue;
			rg bool use0 = !(q[i].l <= st && ed <= q[i].r);
			if(q[i].op == 1){
				if(use0) blk0upd(q[i].l,q[i].r,q[i].x);
				else blkupd(q[i].x);
			} else {
				if(q[i].x + tag > 1e5 + 2) continue;
				if(use0) ans[i] += qry0(q[i].l,q[i].r,q[i].x);
				else ans[i] += siz[q[i].x + tag];
			}
		}
	}
	for(int i = 1;i <= m;++i) if(q[i].op == 2) printf("%d\n",ans[i]);
	return 0;
}

最初分块

查询

分两次块。一次是对数组分,一次是对值域分。

预处理两个数组。

  • \(cnt1_{i,j}\) 代表前 \(i\) 个块中出现了多少个值域块 \(j\) 中的数.
  • \(cnt2_{i,j}\) 代表前 \(i\) 个块中出现了多少个 \(j\)

因为上面的两个数组都是前 \(i\) 个块的,类似前缀和,所以作差可以得到多个块的答案。

那么询问就可以变成:

  • 对于 \(l,r\) 在一个块的情况直接 nth_element
  • 对于不在一个块的情况:
    1. 开两个桶,分别维护
      • 属于某个值域块的数的个数
      • 某个值的个数
    2. 对于散块暴力计算出现次数(加进桶里)。
    3. \(k\) 不断减小。具体为
      • 从小往大枚举值域块。假如一个块在桶的出现次数小于 \(k\),就让 \(k\) 减去这个出现次数。(到减不动时进入下一个操作)
      • 从小往大枚举(第一个减不动的块的)值。假如一个值在桶的出现次数小于 \(k\),就让 \(k\) 减去这个出现次数。
      • 最后当 \(k\le i\) 的出现次数 的时候,答案就是 \(i\)

复杂度为 \(\mathcal O\left(\sqrt{10^5}\right)\)

修改

散块暴力,以下是整块如何处理。

这个东西是很像 \(\tt CF1620E\) 的。可以借鉴一下思想。那个题是

  • 用下标维护并查集(\(fa\) 数组),同一个 \(\tt dsu\) 内都是值相同的下标。
  • \(\tt dsu\) 的祖先维护这个 \(\tt dsu\) 的值(\(num\) 数组)
  • 对于每个值维护这个值在并查集内的 \(\tt dsu\) 的祖先的下标。(\(pos\) 数组)

于是一次修改操作就等价于并查集的合并。

好的我明确告诉你并查集过不去

对于这个题,我们维护一些类似的。

  • \(pos_{i,j}\) 代表块 \(i\) 内值 \(j\) 所对应的 \(\tt dsu\) 的祖先的下标。
  • \(num_{i,j}\) 代表块 \(i\) 内祖先下标 \(j\) 所对应的值
  • \(fa_i\) 记录 \(i\) 的祖先。(这可不是并查集,只是为了让辅助让我们使数组还原到原来的值用的用的)

这样,num[block][fa[i]] 就是这个位置原来的值。

好,那么修改就可以变得和 \(\tt CF1620E\) 很像了

对于整块间,按如下方式修改:

  • 假如有 \(x\)\(y\),那么执行如下代码:
pos[i][y] = pos[i][x]; // 将y的位置设为x的位置
num[i][pos[i][y]] = y; // 把原来x的位置的对应的值改成y
pos[i][x] = 0;	       // 将x的位置清空(没有x了) 
  • 假如有 \(x\)\(y\),我们直接重构这个块。(重构指重新建 \(pos,num,fa\) 三个数组)

那么在修改过程中如何维护 \(cnt\) 数组呢?我们写两个函数:

  • cnt_set(i,x,y),表示将第 \(i\) 个值域块及其后的值域块的 \(cnt\) 做一次差分(得到做前缀和前的答案)
  • cnt_reset(i,x,y),表示将第 \(i\) 个值域块及其后的值域块的 \(cnt\) 做一次前缀和(得到 cnt_set 前的答案)

那么,在修改操作就变成这样:

  • 先对 blk[l]~blk[n] 做一次 cnt_set
  • 散块暴力
    1. 让包含散块的整块内的每一个数通过 \(num,fa\) 数组回到原来的值。
    2. 暴力遍历散块,修改原数组及 \(cnt\) 数组
    3. 散块重构
  • 遍历 \(l\sim r\) 间每个整块,使用上文所提及的修改方式修改。

复杂度

最玄学的地方就是修改,要重构整块那里。

整个数组最多只会有 \(n+m\) 种数值,这样一次重构会让块内权值减少 \(1\),所以势能分析,复杂度总共是 \(\mathcal O(\sqrt n(n+m))\)


代码

垃圾代码跑的很慢,可能有时过不了。

// 最初分块
const int maxn = 1e5 + 5;
const int sqr = sqrt(maxn) + 5;
const int V = 1e5;

int n,m,a[maxn],b1,b2,c1,c2;
int st1[sqr],ed1[sqr],bl1[maxn];
int st2[sqr],ed2[sqr],bl2[maxn];
int cnt1[sqr][sqr],cnt2[sqr][maxn];
int pos[sqr][maxn],num[sqr][maxn],fa[maxn];
int buc1[sqr],buc2[maxn],cur;

void build(int k){
	int anc = 0;
	rep(i,1,b1) pos[k][num[k][i]] = 0;
	rep(i,st1[k],ed1[k]){
		if(pos[k][a[i]]) continue;
		pos[k][a[i]] = ++anc;
		num[k][anc] = a[i];
	}
	rep(i,st1[k],ed1[k]) fa[i] = pos[k][a[i]];
}

void init(){
	b1 = sqrt(n) + 5,c1 = (n - 1) / b1 + 1;
	b2 = sqrt(V) + 5,c2 = (V - 1) / b2 + 1;
	rep(i,1,n){
		int sq = (i - 1) / b1 + 1;
		if(!st1[sq]) st1[sq] = i;
		ed1[sq] = i; bl1[i] = sq;
	}
	rep(i,1,V){
		int sq = (i - 1) / b2 + 1;
		if(!st2[sq]) st2[sq] = i;
		ed2[sq] = i; bl2[i] = sq;
	}
	rep(i,1,c1){
		rep(j,1,c2) cnt1[i][j] = cnt1[i - 1][j];
		rep(j,1,V)  cnt2[i][j] = cnt2[i - 1][j];
		rep(j,st1[i],ed1[i]){
			++cnt1[i][bl2[a[j]]];
			++cnt2[i][a[j]];
		}
	}
	rep(i,1,c1) build(i);
}

void cnt_set(int k,int x,int y){
	Rep(i,c2,k){
		cnt1[i][bl2[x]] -= cnt1[i - 1][bl2[x]];
		cnt2[i][x] -= cnt2[i - 1][x];
		cnt1[i][bl2[y]] -= cnt1[i - 1][bl2[y]];
		cnt2[i][y] -= cnt2[i - 1][y];
	}
}

void cnt_reset(int k,int x,int y){
	rep(i,k,c2){
		cnt1[i][bl2[x]] += cnt1[i - 1][bl2[x]]; 
		cnt2[i][x] += cnt2[i - 1][x];
		cnt1[i][bl2[y]] += cnt1[i - 1][bl2[y]]; 
		cnt2[i][y] += cnt2[i - 1][y];
	}
}

//

void blk0upd(int l,int r,int x,int y){
	rep(i,st1[bl1[l]],ed1[bl1[l]]) a[i] = num[bl1[l]][fa[i]];
	rep(i,l,r){
		if(a[i] != x) continue; a[i] = y;
		--cnt1[bl1[l]][bl2[x]]; ++cnt1[bl1[l]][bl2[y]];
		--cnt2[bl1[l]][x]; ++cnt2[bl1[l]][y];
	}
	build(bl1[l]);
} 

void update(int l,int r,int x,int y){
	if(x == y) return;
	if(!(cnt2[bl1[r]][x] - cnt2[bl1[l] - 1][x])) return;
	cnt_set(bl1[l],x,y);
	if(bl1[l] == bl1[r]){
		blk0upd(l,r,x,y);
		cnt_reset(bl1[l],x,y);
		return;
	}
	blk0upd(l,ed1[bl1[l]],x,y);
	blk0upd(st1[bl1[r]],r,x,y);
	rep(i,bl1[l] + 1,bl1[r] - 1){
		if(!cnt2[i][x]) continue;
		if(!cnt2[i][y]){
			cnt1[i][bl2[y]] += cnt2[i][x];
			cnt1[i][bl2[x]] -= cnt2[i][x];
			cnt2[i][y] = cnt2[i][x];
			cnt2[i][x] = 0;
			pos[i][y] = pos[i][x];
			num[i][pos[i][y]] = y;
			pos[i][x] = 0;
		} else blk0upd(st1[i],ed1[i],x,y);
	}
	cnt_reset(bl1[l],x,y);
}

int query(int l,int r,int k){
	if(bl1[l] == bl1[r]){
		rep(i,st1[bl1[l]],ed1[bl1[l]]) a[i] = num[bl1[l]][fa[i]];
		rep(i,l,r) buc2[i] = a[i];
		std::nth_element(buc2 + l,buc2 + l + k - 1,buc2 + r + 1);
		int ret = buc2[l + k - 1];
		rep(i,l,r) buc2[i] = 0;
		return ret;
	}
	rep(i,st1[bl1[l]],ed1[bl1[l]]) a[i] = num[bl1[l]][fa[i]];
	rep(i,st1[bl1[r]],ed1[bl1[r]]) a[i] = num[bl1[r]][fa[i]];
	rep(i,l,ed1[bl1[l]]) ++buc1[bl2[a[i]]],++buc2[a[i]];
	rep(i,st1[bl1[r]],r) ++buc1[bl2[a[i]]],++buc2[a[i]];
	rep(i,1,c2){
		cur = buc1[i] + cnt1[bl1[r] - 1][i] - cnt1[bl1[l]][i];
		if(k > cur){ k -= cur; continue; }
		rep(j,st2[i],ed2[i]){
			cur = buc2[j] + cnt2[bl1[r] - 1][j] - cnt2[bl1[l]][j];
			if(k > cur){ k -= cur; continue; }
			rep(i,l,ed1[bl1[l]]) buc1[bl2[a[i]]] = buc2[a[i]] = 0;
			rep(i,st1[bl1[r]],r) buc1[bl2[a[i]]] = buc2[a[i]] = 0;
			return j;
		}
	}
	return -1;
}

int main(){
	n = read(),m = read();
	rep(i,1,n) a[i] = read();
	init();
	while(m--){
		int op = read(),l = read(),r = read(),x = read();
		if(op == 1) update(l,r,x,read());
		else printf("%d\n",query(l,r,x));
	}
	return 0;
}

第四分块

第四分块序列分块做法的讲解。
当然我讲的是这题的 序列分块加强版


我们先来回顾一下第四分块的序列分块做法。

这个修改和 最初分块 一样,所以我们还是考虑从值入手维护一个类似于并查集的东西(并查集会让复杂度变高,所以我们和最初分块一样通过改良写法规避掉并查集)。

你想过直接维护一个块任意两个元素的最近距离,但是这时的空间是 \(O(\sqrt n\times n^2)\) 的,所以要块内离散化,最后会维护以下几个数组。

以下的数组都是对于一个块而言的(即对于每种数组我们都要开 \(\sqrt n\) 个来存每个块)

  • \(id\) 数组代表 实际值 \(\to\) 离散化值.
  • \(val_i\) 代表 离散化值 \(\to\) 实际值
  • \(pl,pr\) 代表 一个离散化值在块内第一次/最后一次出现位置
  • \(dis\) 代表 两个离散化值的距离

修改对每个块分类讨论:

  • \(x\)\(y\):改 \(val_{id_x}\)\(y\)
  • \(x\)\(y\):这时块内的颜色会 \(-1\),而块内颜色最多减根号次就会到达 \(1\),所以对于每个块这种修改的总次数是根号的,因此用 \(O(\sqrt n)\) 的时间重构这个块。
  • 没有 \(x\) 就跳过

查询也很好办:

  • 对于在一个块的答案,直接查表。
  • 不在一个块的答案,使用 \(pl,pr\) 辅助统计。

这个写法是非常简单的,大概 \(\texttt{1.9k}\)(就是跑得慢)。


考虑这个题有哪些变化:散块查询和散块修改

散块查询是很简单的,直接暴力遍历计算答案即可。散块修改就有点恶心人。

大体我们要干的事是更新 \(dis\) 以及更新 \(pl,pr\),更新 \(pl,pr\) 的过程是平凡的取 \(\min\)\(\max\),不说了。

更新 \(dis\) 的过程可以理解为有 \(\sqrt n\) 个位置 \([l,r]\),又有 \(k\) 个位置 \(b_{1\cdots k}\)\(k\le\sqrt n\)),然后你要计算出对于每个 \(i\in[l,r]\),距离 \(i\) 最近的 \(b_j\) 的值。

这个很寻常,你直接从左到右再从右到左扫两次,计算最近的 \(b_j\) 值就好了。

散块修改需要分类讨论,这里先记 \([l,r]\) 为散块,\([L,R]\)完全包含散块的那个整块

  1. \([l,r]\)\(x\):跳过。
  2. \([L,R]\)\(y\)\([L,R]\) 中除 \([l,r]\) 外无 \(x\):改离散化值。
  3. \([L,R]\)\(y\)\([L,R]\) 中除 \([l,r]\) 外无 \(x\)
  4. \([L,R]\)\(y\)\([L,R]\) 中除 \([l,r]\) 外有 \(x\)
  5. \([L,R]\)\(y\)\([L,R]\) 中除 \([l,r]\) 外有 \(x\)
  • 对于 3 情况,你需要删除掉 \(x\) 的离散化值。
  • 对于 4 情况,你需要给 \(y\) 添加新离散化值。
  • 对于 4 情况,你需要初始化 \(y\)\(dis\) 答案(全部设为 \(\infty\)
  • 对于 3,4,5 情况,你需要更新 \(y\)\(dis\) 答案(上述方法)
  • 对于 4,5 情况,你需要更新 \(x\)\(dis\) 答案(上述方法)

关于实现,提几个注意事项。

  • 可以把 \(dis\) 数组设为位置从小到大,即对于 \(dis_{x,y}\) 只有当 \(x\le y\) 才有用(优化常数)。
  • 可以把一些功能函数化(比如更新 \(dis\),散块修改之类的)(写方便使用)。
  • \(\tt WA\) 可以考虑调大/小块长,然后看看对的点有没有增多/变少,判断出是块内部分还是块间部分出问题。
  • 这题不卡常(对非并查集写法而言),只要你没写错基本不会 \(\tt TLE\)
  • 调块长没多大用(对我而言),\(\sqrt n\) 是跑的最快的,目前跑到 \(\tt\color{black}c\color{red}yffff\) 后的第二优解。

const int N = 1e5 + 5,SQ = 330;
const int inf = 1e9; const short Inf = 3e4;

int n,m,a[N],b[N];
int B,C,st[SQ],ed[SQ],bl[N];
int pl[SQ][SQ],pr[SQ][SQ],val[SQ][SQ];

short id[SQ][N],s[SQ][SQ][SQ],stk[SQ][SQ],tp[SQ],use[SQ];

#define S(i,j,k) s[i][min(j,k)][max(j,k)]

void init(){
	B = sqrt(n); C = (n - 1) / B + 1;
	rep(i,1,C){
		st[i] = ed[i - 1] + 1;
		ed[i] = i == C ? n : i * B;
		int sz = ed[i] - st[i] + 1;
		rep(j,st[i],ed[i]){
			if(!id[i][a[j]]) val[i][id[i][a[j]] = ++use[i]] = a[j];
			b[j] = id[i][a[j]]; bl[j] = i;
		}
		Rep(j,sz,use[i] + 1) stk[i][++tp[i]] = j;
		Rep(j,ed[i],st[i]) pl[i][b[j]] = j;
		rep(j,st[i],ed[i]) pr[i][b[j]] = j;
		rep(j,1,sz) rep(k,1,sz) s[i][j][k] = Inf;
		rep(j,st[i],ed[i]) rep(k,j + 1,ed[i]) chkmin(S(i,b[j],b[k]),k - j);
	}
}

void build(int l,int r,int x,int y){
	int i = bl[l],X = id[i][x],Y = id[i][y];
	chkmin(pl[i][Y],pl[i][X]); chkmax(pr[i][Y],pr[i][X]);
	pl[i][X] = pr[i][X] = 0; stk[i][++tp[i]] = X; id[i][x] = 0;
	rep(j,l,r) (a[j] = val[i][b[j]]) == x && (a[j] = y,b[j] = Y);
	rep(j,1,B) chkmin(S(i,j,int(Y)),S(i,j,X)),S(i,j,X) = Inf;
}

int pos[N],t;

void clrs(int k,int p){ rep(j,st[k],ed[k]) S(k,b[j],p) = Inf; }

void chgs(int i,int v){
	rep(k,st[i],pos[1]) chkmin(S(i,b[k],v),pos[1] - k);
	rep(p,1,t - 1) rep(k,pos[p],pos[p + 1])
		chkmin(S(i,b[k],v),min(k - pos[p],pos[p + 1] - k));
	rep(k,pos[t],ed[i]) chkmin(S(i,b[k],v),k - pos[t]);
}

void sol(int l,int r,int x,int y,int ty){
	int i = bl[l],X = id[i][x],Y = id[i][y];
	if(ty == 0) val[i][id[i][y] = Y = stk[i][tp[i]--]] = y;
	if(ty == 2) val[i][X] = pl[i][X] = pr[i][X] = id[i][x] = 0,stk[i][++tp[i]] = X;
	t = 0; rep(j,l,r) if(a[j] == x) a[j] = y,b[j] = Y,pos[++t] = j;
	if(ty == 0) clrs(i,Y); chgs(i,Y);
	if(ty != 2){
		t = 0; rep(j,st[i],ed[i]) if(a[j] == x) pos[++t] = j;
		clrs(i,X); chgs(i,X);
	}
	rep(j,st[i],ed[i]) a[j] == x && (pr[i][X] = j),a[j] == y && (pr[i][Y] = j);
	Rep(j,ed[i],st[i]) a[j] == x && (pl[i][X] = j),a[j] == y && (pl[i][Y] = j);
}

bool chk(int l,int r,int x){ rep(j,l,r) if(a[j] == x) return 1; return 0; }

void upd(int l,int r,int x,int y){
	int i = bl[l],L = st[i],R = ed[i],X = id[i][x],Y = id[i][y];
	if(!X) return;
	rep(j,L,R) a[j] = val[i][b[j]];
	if(!chk(l,r,x)) return;
	if(!chk(L,l - 1,x) && !chk(r + 1,R,x)){
		if(Y) sol(l,r,x,y,2);
		else val[i][X] = y,id[i][y] = X,id[i][x] = 0;
	} else sol(l,r,x,y,Y ? 1 : 0);
}

void wjy(int l,int r,int x,int y){
	if(x == y) return;
	if(bl[l] == bl[r]) return upd(l,r,x,y);
	upd(l,ed[bl[l]],x,y); upd(st[bl[r]],r,x,y);
	rep(i,bl[l] + 1,bl[r] - 1) if(id[i][x]){
		if(id[i][y]) build(st[i],ed[i],x,y);
		else val[i][id[i][x]] = y,id[i][y] = id[i][x],id[i][x] = 0;
	}
}

int xsy(int l,int r,int x,int y){
	int res = inf,lx = -inf,ly = -inf;
	#define rt return res == inf ? -1 : res
	if(x == y){
		#define _(l,r) rep(i,l,r) (a[i] = val[bl[i]][b[i]]) == x && (res = 0)
		if(bl[l] == bl[r]){ _(l,r); rt; }
		_(l,ed[bl[l]]); _(st[bl[r]],r);
		rep(i,bl[l] + 1,bl[r] - 1) id[i][x] && (res = 0);
		rt;
		#undef _
	}
	#define _(l,r) rep(i,l,r)\
		a[i] = val[bl[i]][b[i]],\
		a[i] == x && (chkmin(res,i - ly),lx = i),\
		a[i] == y && (chkmin(res,i - lx),ly = i)
	if(bl[l] == bl[r]) _(l,r); else {
		_(l,ed[bl[l]]);
		rep(i,bl[l] + 1,bl[r] - 1){
			int X = id[i][x],Y = id[i][y];
			X && chkmin(res,pl[i][X] - ly);
			Y && chkmin(res,pl[i][Y] - lx);
			X && Y && S(i,X,Y) != Inf && chkmin(res,S(i,X,Y));
			X && (lx = pr[i][X]); Y && (ly = pr[i][Y]);
		}
		_(st[bl[r]],r);
	}
	rt;
}

int main(){
	read(n,m);
	rep(i,1,n) read(a[i]);
	init();
	int op,l,r,x,y;
	while(m--){
		read(op,l,r,x,y);
		if(op == 1) wjy(l,r,x,y);
		else print(xsy(l,r,x,y),'\n');
	}
	return 0;
}

\(\texttt{3.7k}\)(去快读等一些东西),可能是写过最长的分块。

posted @ 2022-08-08 11:59  One_Zzz  阅读(340)  评论(0)    收藏  举报