洛谷题目页面传送门 & AtCoder题目页面传送门
给定一棵带边权的树(T=(V,E),|V|=n,|E|=n-1),每次操作可以选择任意一条链和任意一个整数(x),使这条链上所有边边权都异或上(x)。问最少用多少次操作使得所有边的边权都变为(0)。
(ninleft[2,10^5 ight],forall (x,y,v)in E,vin[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_yoplus v,d_x=d_xoplus v)。对于要异或的某条链(x o y),显然可以拆成(x omathrm{LCA}(x,y),y omathrm{LCA}(x,y)),在(mathrm{LCA}(x,y))处差分异或的(2)次抵消了,所以任意一条链异或依然可以转化为令(d_x=d_xoplus v,d_y=d_yoplus v)。最终的目标将所有边的边权都变成(0)等价于将差分数组清零。于是只需要求出(T)的差分数组(显然不包括根),问题就转化成了:给定(n-1)个数,每次可以将其中(1sim2)个数异或上同一个数,求清零的最小次数。
首先,所有的(0)不用管,当作不存在。显然存在一种方案:将每个数异或上自己,这样(n-1)次操作即可清零。这种方案是每次清空(1)个数的。考虑能不能用(<x)次清空(x)个数来优化方案。不难想到,唯一的优化方式是:若(x)个数的异或和为(0),那么可以用(x-1)次操作将它们清零。
显然,若存在(2)个数相等,那么它们满足异或和为(0)的条件,于是我们贪心地将它们(1)次清零。这样贪心看起来很正确,因为显然用(x-1)次操作清零的集合越多越优,于是集合们的大小要尽可能小。然鹅实际上这样贪心的确是对的。证明:若存在(2)个相同的数,它们没有被(1)次清零,分(4)种情况:
- 它们都异或上自己来清零,操作数为(2)。那么显然将它们(1)次清零更优;
- 它们其中一个异或上自己来清零,另一个包含在一个(x-1)次操作清零的集合里。设这个集合大小为(s),那么操作数为(1+s-1=s)。若将这(2)个数(1)次清零,其他(s-1)个数都异或上自己来清零,操作数仍然是(1+s-1=s),没有变差;
- 它们包含在不同的(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),没有变差;
- 它们包含在同一个(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)次清零需要(sumlimits_{i=1}^{15}leftlfloordfrac{buc_i}2 ight floor)次操作,数(i)会剩下(imod2in{0,1})个。对于剩下的这么少的数,我们就可以状压DP了。设(dp_i)表示剩下(1)个(x)当且仅当(xin i)时清空这(|i|)个数需要的最小操作数。边界为(dp_{varnothing}=0),目标为(sumlimits_{i=1}^{15}leftlfloordfrac{buc_i}2 ight floor+dp_{{xmid buc_xmod2=1}}),状态转移方程为(dp_i=minegin{cases}|i|\|i|-1&igopluslimits_{jin i}j=0\minlimits_{j eqvarnothing,jsubsetneq 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;
}