dp
题解
首先第一问就是奇度点个数/2
直接$dp$不好$dp$,考虑关于奇度点来设立
边权下放到节点
$f[x][0/1]$结构体定义,$f[x][0].c1$表示$x$与父亲连边不反转时奇度点个数,$f[x][0].c2$表示$x$与父亲连边翻转是要的最小操作数$f[x][1]$表示翻转
转移就是分情况讨论,我们首先要将子树合并完,最后再上传到当前节点
w1表示子树有一条边指向当前节点代价(奇度点个数为奇)
w2表示子树没有边指向当前节点代价(奇度点个数为偶)
分别转移
$w1=min(w2+f[y][1],w1+f[y][0])$
解释一下保证奇度点个数为奇可以是当前已考虑子树内两两合,并翻转当前这条边;或者已考虑子树内伸出一条边当前边不伸
$w2=min(w1+f[y][1],w2+f[y][0])$
和上面类似
那么对于当前来说
$f[x][0]$由两部分转移,可以是没操作$w2$可以当前点和子树一奇度点结合$c1=w1.c1+1,c2=w1.c2$(路径长度不+1是因为在下面统计过了)
$f[x][1]$由两部分转移,可以是让子树伸的边继续延伸$c1=w1.c1,c2=w1.c2+1$可以是当前边翻转然后当前点成为奇点$c1=w2.c1+1,c2=w2.c2+1$
代码
#include<bits/stdc++.h> using namespace std; #define ll long long #define inf 1e9+7 #define A 1111111 ll head[A],nxt[A],ver[A],edg[A]; ll tot,n; void add(ll x,ll y,ll opt){ nxt[++tot]=head[x],head[x]=tot,ver[tot]=y,edg[tot]=opt; } struct node{ ll c1,c2; node(){} node (const ll &a,const ll &b){ c1=a,c2=b; } friend bool operator < (const node &a,const node &b){ return (a.c1==b.c1)?(a.c2<b.c2):(a.c1<b.c1); } friend node operator +(const node &a,const node &b){ node c;c.c1=0,c.c2=0; c.c1=a.c1+b.c1,c.c2=a.c2+b.c2; return c; } }f[A][2]; void dfs(ll x,ll pre,ll opt){ node w1(inf,inf),w2(0,0),n1,n2; for(ll i=head[x];i;i=nxt[i]){ ll y=ver[i]; if(y==pre) continue ; dfs(y,x,edg[i]); n1=min(w1+f[y][0],w2+f[y][1]); n2=min(w2+f[y][0],w1+f[y][1]); w1=n1,w2=n2; } // printf("w1.c1=%lld w1.c2=%lld w2.c1=%lld w2.c2=%lld ",w1.c1,w1.c2,w2.c1,w2.c2); if(opt==2){ f[x][1]=min(node(w1.c1,w1.c2+1),node(w2.c1+1,w2.c2+1)); f[x][0]=min(node(w1.c1+1,w1.c2),node(w2.c1,w2.c2)); } else if(opt==1){//must f[x][1]=min(node(w1.c1,w1.c2+1),node(w2.c1+1,w2.c2+1)); f[x][0]=node(inf,inf); } else if(opt==0){//must`not f[x][0]=min(node(w1.c1+1,w1.c2),node(w2.c1,w2.c2)); f[x][1]=node(inf,inf); } } int main(){ scanf("%lld",&n); for(ll i=1;i<n;i++){ ll x,y,opt,case1,case2; scanf("%lld%lld%lld%lld",&x,&y,&case1,&case2); if(case2==2){ add(x,y,2);add(y,x,2); } else if(case1==case2){ add(x,y,0);add(y,x,0); } else if(case1!=case2){ add(x,y,1);add(y,x,1); } } dfs(1,0,0); printf("%lld %lld ",f[1][0].c1>>1,f[1][0].c2); }