AtCoder apc001_f - XOR Tree

洛谷题目页面传送门 & AtCoder题目页面传送门

给定一棵带边权的树\(T=(V,E),|V|=n,|E|=n-1\),每次操作可以选择任意一条链和任意一个整数\(x\),使这条链上所有边边权都异或上\(x\)。问最少用多少次操作使得所有边的边权都变为\(0\)

\(n\in\left[2,10^5\right],\forall (x,y,v)\in E,v\in[0,15]\)

考虑到链异或很不好想,不妨用差分把它转化为双点操作。

先介绍一下树上前缀和和树上差分:树上某点处的前缀和是以它为根的子树和。由于前缀和和差分是互逆运算,很容易推出树上某点处的差分是它减去它所有儿子。设差分数组为\(d\),那么对某一条祖先\(x\)到晚辈\(y\)的链上所有点增加\(v\)可以转化为令\(d_y=d_y+v,d_{fa_x}=d_{fa_x}-v\)

本题权值在边上,我们可以用每个点(除了根)代表它连向父亲的那条边。并且本题的操作是异或,异或的逆运算仍然是异或。设差分数组为\(d\),那么对某一条祖先\(x\)到晚辈\(y\)的链上所有边异或\(v\)可以转化为令\(d_y=d_y\oplus v,d_x=d_x\oplus v\)。对于要异或的某条链\(x\to y\),显然可以拆成\(x\to\mathrm{LCA}(x,y),y\to\mathrm{LCA}(x,y)\),在\(\mathrm{LCA}(x,y)\)处差分异或的\(2\)次抵消了,所以任意一条链异或依然可以转化为令\(d_x=d_x\oplus v,d_y=d_y\oplus v\)。最终的目标将所有边的边权都变成\(0\)等价于将差分数组清零。于是只需要求出\(T\)的差分数组(显然不包括根),问题就转化成了:给定\(n-1\)个数,每次可以将其中\(1\sim2\)个数异或上同一个数,求清零的最小次数。

首先,所有的\(0\)不用管,当作不存在。显然存在一种方案:将每个数异或上自己,这样\(n-1\)次操作即可清零。这种方案是每次清空\(1\)个数的。考虑能不能用\(<x\)次清空\(x\)个数来优化方案。不难想到,唯一的优化方式是:若\(x\)个数的异或和为\(0\),那么可以用\(x-1\)次操作将它们清零。

显然,若存在\(2\)个数相等,那么它们满足异或和为\(0\)的条件,于是我们贪心地将它们\(1\)次清零。这样贪心看起来很正确,因为显然用\(x-1\)次操作清零的集合越多越优,于是集合们的大小要尽可能小。然鹅实际上这样贪心的确是对的。证明:若存在\(2\)个相同的数,它们没有被\(1\)次清零,分\(4\)种情况:

  1. 它们都异或上自己来清零,操作数为\(2\)。那么显然将它们\(1\)次清零更优;
  2. 它们其中一个异或上自己来清零,另一个包含在一个\(x-1\)次操作清零的集合里。设这个集合大小为\(s\),那么操作数为\(1+s-1=s\)。若将这\(2\)个数\(1\)次清零,其他\(s-1\)个数都异或上自己来清零,操作数仍然是\(1+s-1=s\),没有变差;
  3. 它们包含在不同的\(x-1\)次操作清零的集合里。设这\(2\)个集合大小分别为\(s_1,s_2\),那么操作数为\(s_1-1+s_2-1=s_1+s_2-2\)。若将这\(2\)个数\(1\)次清零,那么剩下来的\(s_1+s_2-2\)个数异或和显然为\(0\),可以\(s_1+s_2-3\)次清零,操作数仍然是\(1+s_1+s_2-3=s_1+s_2-2\),没有变差;
  4. 它们包含在同一个\(x-1\)次操作清零的集合里。设这个集合大小为\(s\),那么操作数为\(s-1\)。若将这\(2\)个数\(1\)次清零,其他\(s-2\)个数都异或上自己来清零,操作数仍然是\(1+s-2=s-1\),没有变差。

综上,对于任意一个方案,若存在\(2\)个相同的数,它们没有被\(1\)次清零,那么若将它们\(1\)次清零,有办法使得方案不会变差。所以,必存在一种最优方案,其中尽可能将所有相同的\(2\)个数\(1\)次清零。得证。

我们用桶来存储这\(n-1\)个数,设\(buc_i\)表示数\(i\)的个数。那么按照上面贪心的方案,尽可能将所有相同的\(2\)个数\(1\)次清零需要\(\sum\limits_{i=1}^{15}\left\lfloor\dfrac{buc_i}2\right\rfloor\)次操作,数\(i\)会剩下\(i\bmod2\in\{0,1\}\)个。对于剩下的这么少的数,我们就可以状压DP了。设\(dp_i\)表示剩下\(1\)\(x\)当且仅当\(x\in i\)时清空这\(|i|\)个数需要的最小操作数。边界为\(dp_{\varnothing}=0\),目标为\(\sum\limits_{i=1}^{15}\left\lfloor\dfrac{buc_i}2\right\rfloor+dp_{\{x\mid buc_x\bmod2=1\}}\),状态转移方程为\(dp_i=\min\begin{cases}|i|\\|i|-1&\bigoplus\limits_{j\in i}j=0\\\min\limits_{j\neq\varnothing,j\subsetneq i}\{dp_j+dp_{i-j}\}\end{cases}\)(很简单吧)。时间复杂度为一个常数\(3^{15}\)(枚举子集)。还要再加上一些树上操作的\(\mathrm O(n)\)

下面是AC代码:

#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
const int inf=0x3f3f3f3f;
int ppc(int x){return __builtin_popcount(x);}
const int N=100000;
int n;//树的大小 
vector<pair<int,int> > nei[N+1];//邻接表 
int buc[16];//桶 
int dfs(int x=1,int fa=0){//算差分数组&装进桶里,返回x的儿子的异或和 
	int xsm=0;//儿子的异或和 
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i].X,v=nei[x][i].Y;
		if(y==fa)continue;
		xsm^=v;//算儿子的异或和 
		buc[dfs(y,x)^v]++;//将儿子y处的差分装进桶里 
	}
	return xsm;
}
int dp[1<<15];
int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int x,y,z;
		cin>>x>>y>>z;
		x++;y++;
		nei[x].pb(mp(y,z));nei[y].pb(mp(x,z));
	}
	dfs();
	for(int i=1;i<1<<15;i++){//DP 
		dp[i]=ppc(i);//状态转移方程min中第1个式子 
		int xsm=0;
		for(int j=1;j<=15;j++)if(i&1<<j-1)xsm^=j;//集合内的数的异或和 
		if(!xsm)dp[i]=ppc(i)-1;//状态转移方程min中第2个式子 
		for(int j=i-1&i;j;j=j-1&i)dp[i]=min(dp[i],dp[j]+dp[i^j]);//状态转移方程min中第3个式子  
	}
	int ans=0,msk=0;
	for(int i=1;i<=15;i++)ans+=buc[i]>>1,msk|=(buc[i]&1)<<i-1;
	cout<<ans+dp[msk];//目标 
	return 0;
}
posted @ 2020-03-16 11:11  ycx060617  阅读(211)  评论(5)    收藏  举报