FZOJ 4108 世界树的考验

这题的转换非常的巧妙。

众所周知,树上一条路径点权异或有非常好的性质,可惜这道题是边权啊,这就需要一个转化:

  • 设每个点的点权为与它相连的所有边的边权的异或和。那么使得每个点的点权为\(0\)就是题目要求达到的状态。
    证明:考虑度数为\(1\)的点,显然与他们相连的边为\(0\),然后把他们删掉,又会有很多度数为\(1\)的点(类似于拓扑排序)……最后可以推出所有边都变成\(0\)

那么,每次操作把一条路径的每一条边异或同一个数\(x\),相当于把路径两端的点权都异或\(x\),其他点权都不变(大家可以自己考虑考虑)。

现在问题变成了,给你若干个数(数值 \(\leq\) 15),每次给两个数异或上同一个数,问至少多少次可以把所有数变成\(0\)

显然相同的数直接异或掉就可以了,所以可以看成若干个不同的数。设\(f[S]\)表示当状态为\(S\)时需要的最少操作数(\(i\)属于集合\(S\)当且仅当 \((1<<i\&S)!=0\) ),其实就是一个状态压缩,然后发现状态总数非常少(只有\(2^{16}\)种),所以可以直接一发记忆化搜索解决。

代码:

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N=100009,INF=1<<30;
int siz[N],point[N],n,S,f[N*10];

void init()
{
	scanf("%d",&n);
	int x,y,z;
	for (int i=1;i<n;i++)
	{
		scanf("%d %d %d",&x,&y,&z);
		point[x]^=z,point[y]^=z;
	}
	for (int i=0;i<=n;i++)
		siz[point[i]]++;
}

int dfs(int S)
{
	if(S==0)
		f[S]=0;
	if(f[S]!=INF)
		return f[S];
	for (int i=0;i<=15;i++)
		if(S&1<<i)
			for (int j=0;j<=15;j++)
				if(i!=j&&(S&1<<j))
				{
					int p=S^(1<<i)^(1<<j)^(1<<(i^j));
					if(S&(1<<(i^j)))
						f[S]=min(f[S],dfs(p)+2);
					else
						f[S]=min(f[S],dfs(p)+1);
				}
	return f[S];
}

void work()
{
	int ans=0;
	for (int i=1;i<=15;i++)
		ans+=siz[i]/2,siz[i]&1?(S^=(1<<i)):0;
	for (int i=0;i<1<<16;i++)
		f[i]=INF;
	printf("%d\n",ans+dfs(S));
}

int main()
{
	init();
	work();
	return 0;
}
posted @ 2020-04-15 15:49  With_penguin  阅读(138)  评论(0编辑  收藏  举报