把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【BZOJ3197】[SDOI2013] Assassin(树的同构+树形DP+费用流)

点此看题面

大致题意: 给定两棵完全相同的树,两棵树各有些点被打了标记。你可以在保证树的形态不变的前提下,给第二棵树上的点重新编号,然后求出至少修改第二棵树上多少个点的标记才能使得两棵树标记完全一样。

前言

神仙题。看来对网络流套路了解得还不够,根本想不到这种问题居然还能套上一个费用流。

简单浏览过题解后根据自己的想法写了一发,算算复杂度好像不太对劲,结果调完样例交上去不仅一发入魂而且跑得还挺快?

果然二分图上的网络流复杂度是玄学。

树的重心

考虑这种题我们应该是需要进行树形\(DP\)的,而树形\(DP\)一个非常重要的条件便是根节点。

根节点究竟应该选取什么节点,这应该并不难(毕竟我这种对树的同构了解并不多的蒟蒻都知道),肯定是树的重心。

一棵树只有一个重心或是两个重心。

  • 若只有一个重心,那么这个重心就是根。
  • 若有两个重心,则这两个重心必然相邻。因此我们删去原先连接两个重心的边,然后新建一个点向它们连边,显然这个点就成为了新的、唯一的重心,也就是我们所需要的根。

然后就可以树形\(DP\)了。

树形\(DP\)

我们设\(f_{i,j}\)表示令第一棵树中以\(i\)为根的子树与第二棵树中以\(j\)为根的子树匹配所需要的最小代价,则最终答案就应该是\(f_{rt,rt}\)

考虑如何转移。

显然\(f_{i,j}\)有值,一个必要条件就是\(i\)\(j\)的子树同构(树同构显然可以用哈希判断),为了节省复杂度还可以发现\(i\)\(j\)的深度必然相同(不然贡献不可能转移到根节点上)。

既然同构,那么二者子节点数目必然相同,且\(i\)\(j\)的子树匹配就需要二者子节点一一对应匹配。同时\(i\)\(j\)位置上的标记也需要修改成一样。

于是我们枚举\(i\)的子节点\(x\)\(j\)的子节点\(y\),它们能够匹配同样需要满足同构,而将它们匹配的代价就是\(f_{x,y}\)

那么怎么求出一一对应匹配的最小代价呢?这就是典型的网络流。

费用流

我们把\(i\)的子节点视作一边,\(j\)的子节点视作另一边,则这就是一张二分图。

\(i\)的某些子节点和\(j\)的某些子节点有边相连,我们令这些边流量为\(1\),费用为二者匹配的代价。

然后我们建立超级源/超级汇,从超级源向\(i\)的子节点连流量为\(1\);费用为\(0\)的边,从\(j\)的子节点向超级汇连流量为\(1\),费用为\(0\)的边。

然后跑最小费用最大流,就可以求出最小代价了。

具体实现详见代码。

代码

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 700
#define INF (int)1e9
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
#define Gmax(x,y) (x<(y)&&(x=(y)))
using namespace std;
int n,ee,a[N+5],b[N+5],lnk[N+5];struct edge {int to,nxt;}e[2*N+5];
class MinCostMaxFlow//最小费用最大流
{
	private:
		#define s (2*n+1)
		#define t (2*n+2)
		#define AddE(x,y,f,c) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].F=f,e[ee].C=c)
		#define E(x) ((((x)-1)^1)+1)
		int ti,p[2*N+5],P,S[2*N+5],ee,lnk[2*N+5],F[2*N+5],C[2*N+5],lst[2*N+5],IQ[2*N+5];
		struct Edge {int to,nxt,F,C;}e[N*N+2*N+5];queue<int> q;
		I bool SPFA()
		{
			RI i;for(i=1;i<=P;++i) F[S[i]]=C[S[i]]=INF;C[s]=0,q.push(s);//只初始化出现过的点
			RI k;W(!q.empty()) for(i=lnk[k=q.front()],q.pop(),IQ[k]=0;i;i=e[i].nxt)
			{
				if(!e[i].F||C[k]+e[i].C>=C[e[i].to]) continue;
				F[e[i].to]=min(F[k],e[i].F),C[e[i].to]=C[k]+e[lst[e[i].to]=i].C,
				!IQ[e[i].to]&&(q.push(e[i].to),IQ[e[i].to]=1);
			}return C[t]^INF;
		}
	public:
		I void Clear() {++ti,ee=P=0;}//清空
		I void Add(CI x,CI y,CI f,CI c)
		{
			p[x]^ti&&(p[S[++P]=x]=ti,lnk[x]=0),p[y]^ti&&(p[S[++P]=y]=ti,lnk[y]=0),//对于新出现的点清空
			AddE(x,y,f,c),AddE(y,x,0,-c);//连边
		}
		I int Flow()//最小费用最大流
		{
			if(!P) return 0;RI x,res=0;W(SPFA())//若没有边直接退掉
			{
				res+=F[t]*C[t],x=t;
				W(x^s) e[lst[x]].F-=F[t],e[E(lst[x])].F+=F[t],x=e[E(lst[x])].to;
			}return res;
		}
}F;
namespace TreeDP
{
	int rt,rtt,Sz[N+5],Mx[N+5],q[N+5],dep[N+5],f[N+5][N+5];vector<int> g[N+5],v[N+5];
	struct Hash//哈希
	{
		#define ull unsigned long long
		#define RU Reg ull
		#define CU Con ull&
		ull x,y;I Hash() {x=y=0;}I Hash(CU a):x(a),y(a){}I Hash(CU a,CU b):x(a),y(b){}
		I Hash operator + (Con Hash& o) Con {return Hash(x+o.x,y+o.y);}
		I Hash operator - (Con Hash& o) Con {return Hash(x-o.x,y-o.y);}
		I Hash operator * (Con Hash& o) Con {return Hash(x*o.x,y*o.y);}
		I bool operator < (Con Hash& o) Con {return x^o.x?x<o.x:y<o.y;}
		I bool operator == (Con Hash& o) Con {return x==o.x&&y==o.y;}
	}h[N+5],seed(20050521,302627441);
	I bool cmp(CI x,CI y) {return h[x]<h[y];}//根据哈希值排序
	I void GetRt(CI x=1,CI lst=0)//找重心
	{
		Sz[x]=1,Mx[x]=0;for(RI i=lnk[x];i;i=e[i].nxt)
			e[i].to^lst&&(GetRt(e[i].to,x),Sz[x]+=Sz[e[i].to],Gmax(Mx[x],Sz[e[i].to]));
		Gmax(Mx[x],n-Sz[x]),Mx[x]^Mx[rt]?Mx[x]<Mx[rt]&&(rt=x,rtt=0):(rtt=x);
	}
	I void dfs(CI x,CI lst=0)//遍历树初始化
	{
		RI i;for(v[dep[x]].push_back(x),Sz[x]=1,i=lnk[x];i;i=e[i].nxt)//扔入x所在深度的vector
			e[i].to^lst&&(dep[e[i].to]=dep[x]+1,dfs(e[i].to,x),Sz[x]+=Sz[e[i].to]);//递归子树
		RI cnt=0;for(h[x]=Hash(Sz[x]),i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&(q[++cnt]=e[i].to);//存下子节点
		for(sort(q+1,q+cnt+1,cmp),i=1;i<=cnt;++i) h[x]=h[x]*seed+h[q[i]],g[x].push_back(q[i]);//哈希存储当前点信息,把子节点扔入vector
	}
	I void Cut(CI x,CI y)//断掉两个重心间的边,直接暴力就好了
	{
		if(e[lnk[x]].to==y) lnk[x]=e[lnk[x]].nxt;
		else for(RI i=lnk[x];;i=e[i].nxt) if(e[e[i].nxt].to==y) {e[i].nxt=e[e[i].nxt].nxt;break;}
		if(e[lnk[y]].to==x) lnk[y]=e[lnk[y]].nxt;
		else for(RI i=lnk[y];;i=e[i].nxt) if(e[e[i].nxt].to==x) {e[i].nxt=e[e[i].nxt].nxt;break;}
	}
	I void Init() {Mx[0]=INF,GetRt(),rtt&&(++n,Cut(rt,rtt),add(n,rt),add(n,rtt),rt=n),dfs(rt);}
	I void DP()//树形DP
	{
		RI d,i,j,p,q,sz,tz,x,y;for(d=n;~d;--d)//枚举深度
		{
			for(i=0,sz=v[d].size();i^sz;++i) for(j=0;j^sz;++j) if(h[v[d][i]]==h[v[d][j]])//若两个子树同构,说明能完全匹配
			{
				for(F.Clear(),p=0,tz=g[v[d][i]].size();p^tz;++p) for(q=0;q^tz;++q)//枚举子节点
					h[x=g[v[d][i]][p]]==h[y=g[v[d][j]][q]]&&(F.Add(x,n+y,1,f[x][y]),0);//能匹配就连边
				for(p=0;p^tz;++p) F.Add(s,g[v[d][i]][p],1,0),F.Add(n+g[v[d][j]][p],t,1,0);//和超级源/汇连边
				f[v[d][i]][v[d][j]]=(a[v[d][i]]^b[v[d][j]])+F.Flow();//求出最小代价
			}
		}printf("%d\n",f[rt][rt]);//输出答案
	}
}using namespace TreeDP;
int main()
{
	RI i,x,y;for(scanf("%d",&n),i=1;i^n;++i) scanf("%d%d",&x,&y),add(x,y),add(y,x);
	for(i=1;i<=n;++i) scanf("%d",a+i);for(i=1;i<=n;++i) scanf("%d",b+i);return Init(),DP(),0;
}
posted @ 2020-05-26 15:03  TheLostWeak  阅读(183)  评论(0编辑  收藏  举报