青岛集训是你,$DP$题单是你,今天比赛还是你。
大概这就是缘分吧。你不要过来啊!
定义一个点的点权为与它相连的边的异或和。
结论是点权全为$0$等价于边权全为$0$。证明的话,使用逆反证法,因为举不出反例所以是对的。
对一条路径异或$x$,路径端点其中一条边被异或,点权异或$x$,而中间的点因为有两条边被异或,抵消了。这个操作就转成了任取两个点使它们异或上同一个值。
首先有一个显然的贪心:每次取两个相同的点权异或它们自己,一次操作能消掉两个数。
之后每种点权最多剩下$1$个。而且点权最大不超过$15$,这玩意可以状压。
设$f(S)$为状态为$S$($0/1$表示每种点权的数量)全部消成$0$的最小次数。
每次操作我们可以枚举两个数量为$1$的数$x,y$异或上$x$,$x,y$就没了,产生了一个$x\ xor\ y$(记为$z$)。
讨论一下:
- $z\notin S$,直接把$z$加进去,$f(S)=f(S\ xor\ (1<<x)\ xor\ (1<<y)\ xor\ (1<<z))+1$
- $z\in S$,这时会出现两个$z$,根据刚才的贪心再来一步消掉两个$z$是最优的,$f(S)=f(S\ xor\ (1<<x)\ xor\ (1<<y)\ xor\ (1<<z))+2$
这个$DP$转移方向很鬼畜,不是很好递推,直接记搜就行。
复杂度$O(n+w^22^w)$
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#define maxn 100005
#define inf 0x3f3f3f3f
using namespace std;
inline int read(){
int x=0,y=0;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')y=1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return y?-x:x;
}
int a[maxn],tax[16],f[1<<16];
int dfs(int s){
if(~f[s])return f[s];
int x;
f[s]=inf;
for(register int i=1;i<=15;++i)
for(register int j=1;j<=15;++j){
if(i==j||!(s>>i-1&1)||!(s>>j-1&1))continue;
x=i^j;
if(s>>x-1&1)f[s]=min(f[s],dfs(s^(1<<i-1)^(1<<j-1)^(1<<x-1))+2);
else f[s]=min(f[s],dfs(s^(1<<i-1)^(1<<j-1)^(1<<x-1))+1);
}
return f[s];
}
int main(){
int n=read(),x,y,z,state=0,ans=0;
for(register int i=1;i<n;++i)x=read()+1,y=read()+1,z=read(),a[x]^=z,a[y]^=z;
for(register int i=1;i<=n;++i)++tax[a[i]];
for(register int i=1;i<=15;++i){
ans+=tax[i]>>1;
if(tax[i]&1)state|=1<<i-1;
}
memset(f,-1,sizeof f),f[0]=0;
printf("%d\n",ans+dfs(state));
}