[省选联考 2020 A 卷] 树

220 篇文章 2 订阅
98 篇文章 0 订阅

题目

传送门 to luogu

思路

既然是二进制操作,就应该考虑拆位。现在我们只考虑二进制下的第 k ( k ≥ 0 ) k(k\ge 0) k(k0) 位,也就是代表 2 k 2^k 2k 的一位。

紧接着,我们让题目变的平凡,假设所有的 v x = 0 v_x=0 vx=0

现在我们回头去看题目中的定义式

⨁ c ∈ t r e e ( x ) [ v c + d ( c , x ) ] \bigoplus_{c\in tree(x)}\big[v_c+d(c,x)\big] ctree(x)[vc+d(c,x)]

根据假设, v c = 0 v_c=0 vc=0 。既然我们只考虑第 k k k 位,我们就可以进行取模。对于第 k k k 位,我们只关心

⨁ c ∈ t r e e ( x ) [ d ( c , x )   m o d   2 k + 1 ] \bigoplus_{c\in tree(x)}\big[d(c,x)\bmod 2^{k+1}\big] ctree(x)[d(c,x)mod2k+1]

正确性是显而易见的,因为 2 k + 1 2^{k+1} 2k+1 的倍数不需要使用第 k k k 或更低的二进制位来表示。 d d d 是距离,我们可以重新定义 d d d 为深度。而深度作差再取模,也可以 先取模再作差,变成

⨁ c ∈ t r e e ( x ) [ d ( c )   m o d   2 k + 1 − d ( x )   m o d   2 k + 1 ] \bigoplus_{c\in tree(x)}\Big[d(c)\bmod 2^{k+1}-d(x)\bmod 2^{k+1}\Big] ctree(x)[d(c)mod2k+1d(x)mod2k+1]

因为懒,不想把差值再打一个取模。

你惊奇地发现,我们只在乎深度取模后的结果。不难想到这样一个思路, f ( k , i ) f(k,i) f(k,i) 表示 深度   m o d     2 k + 1 = i \bmod\;2^{k+1}=i mod2k+1=i 的点有多少个。当然啦,因为是异或,也可以只存其奇偶性。

而这个差值需要是多少,才能在异或中改变第 k k k 位(即第 k k k 位为 1 1 1)呢?应当是 [ 2 k , 2 k + 1 ) [2^k,2^{k+1}) [2k,2k+1) 内。所以我们只需求

∑ i = 2 k 2 k + 1 − 1 f ( k , i + d ( x ) ) \sum_{i=2^k}^{2^{k+1}-1}f(k,i+d(x)) i=2k2k+11f(k,i+d(x))

因为懒,我又没有把二者的和打上取模。

事实上,求这个值的奇偶性就够了,为奇则第 k k k 位为 1 1 1 。不过这对解题没啥帮助。顶多让代码更好实现。

考虑这个 f f f 数组的空间占用。应该是 1 + 2 + 4 + 8 + ⋯ + n = O ( n ) 1+2+4+8+\cdots+n=\mathcal O(n) 1+2+4+8++n=O(n) 的。单次修改也是 O ( log ⁡ n ) \mathcal O(\log n) O(logn) 的——对于每个 k k k 都要改。查询?好像是 O ( n ) \mathcal O(n) O(n) 的呢!

我们教练在考试时随便整了一个算法,用 B I T \tt BIT BIT 来维护 f f f ,以求高效区间求和。单次修改变为 O ( log ⁡ 2 n ) \mathcal O(\log^2 n) O(log2n) 的,单次查询变成 O ( log ⁡ 2 n ) \mathcal O(\log^2 n) O(log2n) 的。总复杂度 O ( n log ⁡ 2 n ) \mathcal O(n\log^2 n) O(nlog2n)

看一看题解,找到了更优的做法,重拾树形 d p \tt dp dp ,利用子树的信息更新父亲。

为啥这样想?因为二者的深度差是 1 1 1 呢。你的儿子已经求出

∑ i = 2 k 2 k + 1 − 1 f ( k , i + d ( y ) ) \sum_{i=2^k}^{2^{k+1}-1}f(k,i+d(y)) i=2k2k+11f(k,i+d(y))

i + d ( y ) = i + d ( x ) + 1 i+d(y)=i+d(x)+1 i+d(y)=i+d(x)+1 ,于是上式等价于(注意 i i i 的取值范围的变化):

∑ i = 2 k + 1 2 k + 1 f ( k , i + d ( x ) ) \sum_{i=2^k+1}^{2^{k+1}}f(k,i+d(x)) i=2k+12k+1f(k,i+d(x))

于是我们找差值部分,无非是 i = 2 k i=2^k i=2k i = 2 k + 1 i=2^{k+1} i=2k+1 两个端点,一个加一个减就行。当然,还得把自己加上。单次查询复杂度降级为 O ( log ⁡ n ) \mathcal O(\log n) O(logn) 。修改复杂度不变。

亲爱的读者,你有没有忘记我们规定了 v = 0 v=0 v=0 呢?对于 v ≠ 0 v\ne 0 v=0 怎么处理?

x x x 的子树处理完之后,假装 v x + d ( x ) v_x+d(x) vx+d(x) 处有一个 v = 0 v=0 v=0 。因为 x x x 的子树已搞定,对于接下来的计算,与 x x x 处有一个 v x v_x vx 是等价的。

复杂度 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 。如果 f f f 使用定长数组,就是 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 的空间复杂度。

我的口胡听上去完美,实际上有缺陷,那就是 f f f 使用后不清空,会对兄弟子树产生影响。用 d s u    o n    t r e e \tt dsu\; on\; tree dsuontree 解决,就要平添一个 log ⁡ n \log n logn 。不可接受!

考虑到这道题的特殊性,用 log ⁡ n \log n logn 个元素就能唯一确定当前点的取值。你会兴奋地想到,只清空这 log ⁡ n \log n logn 的值就行了!这个想法完全正确。按照字面意思实现就会失败,你清空,你的子孙也把这几个值清零怎么办?正确的方法是 记录变化量,这相当于重设了原点、实现了清空。

在本题中,可以直接先异或原有的值,再异或现有的值。这些都是为了代码好写而已。你非要把代码写的又臭又长也没人拦你。

代码

小坑点:最大异或值可能达到 v + d = 2 n v+d=2n v+d=2n 级别,所以 log ⁡ n \log n logn 要再加一。

#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long int_;
inline int readint() {
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0' or c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c and c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
void writeint(int_ x){
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) writeint(x/10);
	putchar((x%10)^48);
}
# define MB template < typename T >
MB void getMax(T &a,const T &b){ if(a < b) a = b; }
MB void getMin(T &a,const T &b){ if(b < a) a = b; }

const int MaxN = 525011;
struct Edge{
	int to, nxt;
	Edge(int T=0,int N=0){
		to = T, nxt = N;
	}
} edge[MaxN<<1];
int head[MaxN], cntEdge;
void addEdge(int a,int b){
	edge[cntEdge] = Edge(b,head[a]);
	head[a] = cntEdge ++;
}

const int LogN = 21; // 注意!最大可达 v+d=2n
int cnt[LogN][2<<LogN], v[MaxN];
long long ans; // 递归顺便统计答案
/** @param d 当前点的深度 */
int dfs(int x,int d){
	int now = v[x]; // 当前点的答案
	/* 记录变化量,一定要先于递归 */
	/* (这一段与下面的那一段是相同的) */
	for(int i=0; i<LogN; ++i)
		now ^= (cnt[i][(d+(1<<i))%(2<<i)]
			^cnt[i][d%(2<<i)])<<i;
	/* 递归,并且利用好它们的信息 */
	for(int i=head[x]; ~i; i=edge[i].nxt)
		now ^= dfs(edge[i].to,d+1);
	for(int i=0; i<LogN; ++i){
		/* 加上 d+2^{i} 的情况 */
		if(cnt[i][(d+(1<<i))%(2<<i)])
			now ^= 1<<i;
		/* 减去 d+2^{i+1}=d 的情况 */
		if(cnt[i][d%(2<<i)])
			now ^= 1<<i;
		/**
		 * 为啥加和减长得一样?
		 * 因为我们只求奇偶性。
		 **/
	}
	/* 假装 d+v[x] 深度的地方有一个 v=0 */
	for(int i=0; i<LogN; ++i) // %(2<<i)
		cnt[i][(d+v[x])%(2<<i)] ^= 1;
	ans += now; return now;
}

int main(){
	int n = readint();
	for(int i=1; i<=n; ++i){
		v[i] = readint();
		head[i] = -1;
	}
	for(int i=2; i<=n; ++i)
		addEdge(readint(),i);
	dfs(1,0); printf("%lld\n",ans);
	return 0;
}

后记

本来都过去很久了,突然又想起这道题,是因为机房里的一个妹儿 z x y \tt zxy zxy 做了这道题

我作为一个男生,肯定不能服输!虽然还是被她吊着打。

话说洛咕题解里也有很近似的做法,但是都称其为树上差分……?果然我图论一点都不会,我只会代数。在考场上我连代数都不会,我只会痴呆。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值