[luogu8353] [SDOI2022 d2t2] 无处存储 - 卡空间 - 树分块 - 虚树 - 随机算法

10 篇文章 0 订阅
4 篇文章 0 订阅

传送门:P8353 [SDOI/SXOI2022] 无处存储

免责声明:我不是本题的出题人。

出题人是小N和小 Ω \Omega Ω ,跟我小Z有什么关系?
题目大意:给定一棵树,点有点权,支持链加、链求和。强制在线。
(先别急着说“为什么省选放模板题”,接着往下看。)
数据范围: n ≤ 7 × 1 0 6 , q ≤ 5 × 1 0 4 n \leq 7\times10^6,q\leq 5\times 10^4 n7×106q5×104
时空限制: 5 s ,   64 M B 5s,\ 64MB 5s, 64MB
——是的你没看错,卡空间题!更准确地说,跟一些比较屑的、卡一些实现不优秀正解的空间常数的相比,这题是一个纯纯的需要你设计算法来卡空间的题!
——对此,我的评价是:

也难怪出题人对自己题目的评价是:

“出题人爬!”
“为什么要喷自己?”
“如果我主动喷自己的话,就没有选手来喷我了吧(大概吧”

我们冷静分析一下这个数据范围和空间限制意味着什么:
一个大小为 n = 7 × 1 0 6 n=7\times 10^6 n=7×106 的int数组需要大约 26.7 M B 26.7MB 26.7MB 的空间,也就是说最多只能开 2 2 2 个。
显然需要一个数组存所有的点权,另外需要一个数组存每个点在树上的父亲。
然后就没了,你没法开下任何 O ( n ) O(n) O(n) 级别的数据结构,甚至没法直接dfs整棵树,因为栈空间也要算在内。
——那还玩个啥?只能暴力了?
冷静分析:其实 64 M B 64MB 64MB 的限制没有想象中那么死,大概还留下接近 10 M B 10MB 10MB 来给我们开一些 o ( n ) o(n) o(n) 的小数组,比如 O ( n ) O(\sqrt n) O(n ) 甚至 O ( n / log ⁡ n ) O(n/\log n) O(n/logn) 的数组还是能开的,包括 n / 32 n/32 n/32 的bitset(每个不到 1 M B 1MB 1MB)也可以开好几个。
如果你还在想 log ⁡ \log log 做法,可以就此打住了。据我所知,空间最小的 log ⁡ \log log 做法应该是树剖+树状数组,即使考虑到人工栈dfs+数组复用, 4 n 4n 4n 的空间是避免不了的。由此可以大胆猜测这一定是个根号题,而看到这个数据范围不难猜应该是 O ( q n ) O(q\sqrt n) O(qn ) 一类的算法。
当然你也许会想到一些奇技淫巧的卡空间技巧,比如:考虑到fa数组只有低 24 24 24 位是有用的,或许可以强行用 3 3 3 个int存储 4 4 4 个数。但遗憾的是,即使你将此方法运用到极致,也刚好无法开下第 3 3 3 O ( n ) O(n) O(n) 的数组(需要至少 65 M B 65MB 65MB )。不过放心,以下的 2 2 2 个数组解法完全无需依赖这一卡空间方法。
我们姑且先弱化问题。序列上怎么做?这还用说?树状数组只需要2n的空间。
众所周知的一种分块方法是:每个单点维护(不含标记的)权值,每个块内维护这个块的和,以及这个块的整体加标记。修改时整块打标记,零散暴力;查询时整块直接查,零散暴力。时间复杂度 O ( q n ) O(q\sqrt n) O(qn ) ,空间 n + O ( n ) n + O(\sqrt n) n+O(n ) (只需要开一个 O ( n ) O(n) O(n) 的数组)。
放到树上呢?当然是树分块了!这个名词听起来就相当哈人,这里推荐一种相对好写的树分块写法:
在树上随机撒 O ( n ) O(\sqrt n) O(n ) 个关键点,并建一棵虚树,把涉及到的lca也纳入关键点的范畴。
这样的结构可以把整棵树划分成 O ( n ) O(\sqrt n) O(n ) 条链(从每个关键点开始向上,到虚树上它的父亲(不含)),以及一些零碎的部分。由于随机撒点的性质,树上任意一个点向上跳到一个关键点,期望只会跳 O(\sqrt n) 步。接下来我们称这关键点“代表”或者“控制”这条树链。
建虚树的作用是减少讨论,同时也使得每个关键点代表的树链不相交。这样一来,如果一个点子树内有关键点且它本身不是关键点,那么它就在某条树链上,而且它的所有子树中一定只有一个有关键点,找到最浅的关键点就是控制它的关键点。
然后在每条树链上重复刚刚在序列上做的事:每个关键点维护它控制的这一段树链的和、整体加标记。修改和查询分为两端和lca处的零散部分和整段的链。
实际实现起来可能还有一些细节,比如:需要用bitset来维护每个点在树链上还是在零散部分(当然你也可以用fa数组闲置的高位来维护以省去bitset),以便于查询:从一个询问的端点出发,跳多久能离开零散部分进入某条树链。同时需要支持对于一个在某条链上的点,查询它究竟在哪条链上(也就是它被哪个关键点控制),这可以通过记录每条链的最后一个点(即最靠近虚树上父亲的点)来实现。
处理一个询问的详细细节展开讲大概是这样的:
1、判断两个端点的lca是否在零散的部分。可以通过查询两个点跳到树链上的第一个点是否相同来实现。如果是的话直接暴力。
2、将两个端点暴力向上跳直到来到树链上。
3、查询树链上的一个点被哪个关键点控制。方法为向上跳直到遇到一个关键点,记录遇到关键点的前一个点,以此为关键字可以唯一确定这条树链和控制它的关键点。(我一开始写的时候以为这里需要map一类的数据结构,后来发现反正是 O ( q n ) O(q\sqrt n) O(qn ) 的,直接暴力枚举每条树链判断即可。)
4、接下来要处理两种可能的情况:lca是/不是关键点。区分依据为现在的这两个点是否为祖先关系,但这不容易直接查询,因此采用如下方法:
5、称一个点的“链深度”为对应关键点在虚树上的深度,先跳链深度较深的端点,暴力跳到上面的关键点之后跳整块(即沿虚树向上跳),直到两个点的链深度一致。
6、如果两个点在同一条链上,那么lca也在这条链上,暴力处理。
7、否则,lca一定是某个关键点,然后可以放心大胆沿着虚树跳了:先将原先较浅的点暴力跳到上面的关键点,再将两点的链深度统一,最后两边一起沿着虚树向上跳直到相遇即可。
最后总的时间复杂度 O ( q n ) O(q \sqrt n) O(qn ) ,空间为 2 n + O ( n ) 2n + O(\sqrt n) 2n+O(n ) ,实际实现起来 O ( n ) O(\sqrt n) O(n ) 的数组大概需要开 5 ∼ 10 5\sim 10 510 个,当然在此基础上多几个 n / 32 n/32 n/32 的bitset是可以接受的。除去libc库等之后空间约为 55 ∼ 58 M B 55\sim 58MB 5558MB

后记:其实将卡空间作为一个独立且严肃的考点放在正式的oi比赛里是极其大胆而冒险的,我虽不是出题人但也算完整参与了整个出题过程,其实已经做好被全网点吊起来喷的准备了。从事实反馈来看,虽然喷的人确实很多,但也“没有那么多”,还算是心里小小地安慰了一下。(或许还幻想过以此作为“时代的开端”,正式把空间优化算法引入oi?不过如果大家一时半会接受不了就先算了哈哈哈。)

#include<bits/stdc++.h>
char buf[100000],*buff = buf + 100000;
#define gc ((buff == buf + 100000 ? (fread(buf,1,100000,stdin),buff = buf) : 0),*(buff++))
#define pc putchar
#define li long long
#define uli unsigned li
#define ui unsigned int
using namespace std;
inline ui read(){
	ui x = 0;
	int c = gc;
	while(c < '0' || c > '9') c = gc;
	while(c >= '0' && c <= '9') x = x * 10 + c - '0',c = gc;
	return x;
}
inline void print(ui x){
	if(x >= 10) print(x / 10);
	pc(x % 10 + '0');
}
li s1 = 19491001,s2 = 23333333,s3 = 998244853,srd;
inline li rd(){
	return srd = (srd * s1 + s2 + rand()) % s3;
}
int n,m,k;
int f[7000010];
ui a[7000010];
ui oa,ob,oc,a0; 
struct gjd{
	int id,f,dpt,sz,lst,fsts,nxt;
	ui s,c;
}g[10010];
bitset<7000010> t1,t2;
inline void addnd(int x){
	if(a[x] & 2) return;
	g[++k].id = x;a[x] |= 2;
}
inline void pre(int x,int &ax,int &bx){
	for(ax = x;!t1.test(ax);ax = f[ax]);
	if(t2.test(ax)){
		for(bx = 1;bx <= k;++bx) if(g[bx].id == ax) break;
	}
	else{
		int tmp = 0;
		for(bx = ax;!t2.test(bx);bx = f[bx]) tmp = bx;
		for(bx = 1;bx <= k;++bx) if(g[bx].lst == tmp) break;
	}
}
ui cx(int x,int y){
	ui as = 0;
	int ax,ay,bx,by;
	pre(x,ax,bx);pre(y,ay,by);
	if(g[bx].dpt > g[by].dpt){//确保by比bx深 
		swap(x,y);swap(ax,ay);swap(bx,by);
	}
	if(ax == ay){//在走到主分支上之前就gg了 
		int d1 = 0,d2 = 0,tp;
		for(tp = x;tp != ax;tp = f[tp]) ++d1;
		for(tp = y;tp != ay;tp = f[tp]) ++d2;
		while(d1 > d2) as += a[x],x = f[x],--d1;
		while(d2 > d1) as += a[y],y = f[y],--d2;
		while(x != y){
			as += a[x];x = f[x];
			as += a[y];y = f[y];
		}
		as += a[x];if(x == ax) as += g[bx].c;//特判刚好在主分支上交汇
		return as; 
	}
	
	//两个点先分别走到主分支上
	while(x != ax) as += a[x],x = f[x];
	while(y != ay) as += a[y],y = f[y];
	ax = g[g[bx].f].id;ay = g[g[by].f].id;//上面的关键点 
	if(bx == by){//下面的点相同,那么lca在这条树枝上 
		int d1 = 0,d2 = 0,tp;
		for(tp = x;tp != ax;tp = f[tp]) ++d1;
		for(tp = y;tp != ay;tp = f[tp]) ++d2;
		while(d1 > d2) as += a[x] + g[bx].c,x = f[x],--d1;
		while(d2 > d1) as += a[y] + g[by].c,y = f[y],--d2;
		return as + a[x] + g[bx].c;
	}
	
	//先把y往上面的关键点走,总是安全的
	while(y != ay) as += a[y] + g[by].c,y = f[y];
	by = g[by].f;
	while(g[bx].dpt < g[by].dpt) as += g[by].s,by = g[by].f;
	if(bx == by){//这说明只要把y走到x就行了 
		y = g[by].id;
		while(y != x) as += a[y] + g[by].c,y = f[y];
		return as + a[y] + g[by].c;
	} 
	
	//此时lca一定是某个关键点
	//先把x走到上面的关键点,再把by向上走到与x平齐(可能不走或走一步) 
	while(x != ax) as += a[x] + g[bx].c,x = f[x];
	ax = g[bx].f;
	while(g[by].dpt > g[ax].dpt) as += g[by].s,by = g[by].f;
	ay = by;
	//再跳到同一个点
	while(ax != ay){
		as += g[ax].s,ax = g[ax].f;
		as += g[ay].s,ay = g[ay].f;
	} 
	return as + a[g[ax].id] + g[ax].c;//别忘了加上lca 
}

void xg(int x,int y,ui as){
	int ax,ay,bx,by;
	pre(x,ax,bx);pre(y,ay,by);
	if(g[bx].dpt > g[by].dpt){//确保by比bx深 
		swap(x,y);swap(ax,ay);swap(bx,by);
	}
	if(ax == ay){//在走到主分支上之前就gg了 
		int d1 = 0,d2 = 0,tp;
		for(tp = x;tp != ax;tp = f[tp]) ++d1;
		for(tp = y;tp != ay;tp = f[tp]) ++d2;
		while(d1 > d2) a[x] += as,x = f[x],--d1;
		while(d2 > d1) a[y] += as,y = f[y],--d2;
		while(x != y){
			a[x] += as;x = f[x];
			a[y] += as;y = f[y];
		}
		a[x] += as;if(x == ax) g[bx].s += as;//特判刚好在主分支上交汇
		return; 
	}
	
	//两个点先分别走到主分支上
	while(x != ax) a[x] += as,x = f[x];
	while(y != ay) a[y] += as,y = f[y];
	ax = g[g[bx].f].id;ay = g[g[by].f].id;//上面的关键点 
	if(bx == by){//下面的点相同,那么lca在这条树枝上 
		int d1 = 0,d2 = 0,tp;
		for(tp = x;tp != ax;tp = f[tp]) ++d1;
		for(tp = y;tp != ay;tp = f[tp]) ++d2;
		while(d1 > d2) a[x] += as,g[bx].s += as,x = f[x],--d1;
		while(d2 > d1) a[y] += as,g[by].s += as,y = f[y],--d2;
		a[x] += as;g[bx].s += as;
		return;
	}
	
	//先把y往上面的关键点走,总是安全的
	while(y != ay) a[y] += as,g[by].s += as,y = f[y];
	by = g[by].f;
	while(g[bx].dpt < g[by].dpt) g[by].s += as * g[by].sz,g[by].c += as,by = g[by].f;
	if(bx == by){//这说明只要把y走到x就行了 
		y = g[by].id;
		while(y != x) a[y] += as,g[by].s += as,y = f[y];
		a[x] += as,g[bx].s += as;
		return;
	} 
	
	//此时lca一定是某个关键点
	//先把x走到上面的关键点,再把by向上走到与x平齐(可能不走或走一步) 
	while(x != ax) a[x] += as,g[bx].s += as,x = f[x];ax = g[bx].f;
	while(g[by].dpt > g[ax].dpt) g[by].s += as * g[by].sz,g[by].c += as,by = g[by].f;
	ay = by;
	//再跳到同一个点
	while(ax != ay){
		g[ax].s += as * g[ax].sz,g[ax].c += as,ax = g[ax].f;
		g[ay].s += as * g[ay].sz,g[ay].c += as,ay = g[ay].f;
	} 
	a[g[ax].id] += as,g[ax].s += as;//别忘了加上lca 
}

int main(){
	srand(time(0));rd();
	int i,op,u,v;
	ui w;
	read();n = read();m = read();
	oa = read();ob = read();oc = read();a0 = read();
	for(i = 2;i <= n;++i) f[i] = read();
	addnd(1);for(i = sqrtl(n);i;--i) addnd(rd() % n + 1);
	//求虚树
	a[1] |= 1;
	for(i = 2;i <= k;++i){ 
		u = g[i].id;
		while(!(a[u] & 1)){
			a[u] |= 1;u = f[u];
		}
		addnd(u);
	}
	//计算每个关键点的父亲,以及跳到父亲的前一个点 
	memset(a,0,sizeof(a));
	for(i = 1;i <= k;++i){
		u = g[i].id;t1.set(u);t2.set(u);a[u] = i;
	}
	for(i = 2;i <= k;++i){
		u = g[i].id;v = u;
		for(u = f[u];!a[u];u = f[u]) v = u,t1.set(u);
		g[i].f = a[u];g[i].nxt = g[a[u]].fsts;g[a[u]].nxt = i;
		g[i].lst = v;
	}
	
	//计算a数组 
	memset(a,0,sizeof(a));
	a[0] = a0;
	for(i = 1;i <= n;++i) a[i] = oa * a[i - 1] * a[i - 1] + ob * a[i - 1] + oc;
	a[0] = 0;
	
	//每个关键点的控制区域之和预处理 
	g[1].s = a[1];g[1].sz = 1;
	for(i = 2;i <= k;++i){
		u = g[i].id;v = g[g[i].f].id;
		g[i].s = a[u];g[i].sz = 1;
		for(u = f[u];u != v;u = f[u]) g[i].s += a[u],++g[i].sz;
	}
	
	//求每个关键点的深度
	g[1].dpt = 1;
	for(i = 2;i <= k;++i){
		int o = 0;
		for(u = i;!g[u].dpt;u = g[u].f) ++o;
		for(v = i;v != u;v = g[v].f) g[v].dpt = g[u].dpt + (o--);
	} 
	//计算答案 
	ui lstas = 0; 
	for(i = 1;i <= m;++i){
		op = read(),u = read() ^ lstas;v = read() ^ lstas;
		if(op == 0){
			w = read() ^ lstas;
			xg(u,v,w);
		} 
		else{
			print(lstas = cx(u,v));pc('\n');lstas &= (1 << 20) - 1;
		}
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值