1. 程式人生 > 實用技巧 >【CF611H】New Year and Forgotten Tree(網路流)

【CF611H】New Year and Forgotten Tree(網路流)

點此看題面

  • 有一棵\(n\)個點的樹,編號為\(1\sim n\)
  • 給出每條邊兩個點的編號十進位制下的位數。
  • 求構造一棵合法的樹,或判斷無解。
  • \(n\le2\times10^5\)

核心點

考慮對於每一種位數相同的點,我們都選取一個點作為該位數的核心點

可以證明,任何一種合法方案必然能夠轉化成沒有兩個非核心點相連的情況。

這是我們做這道題的基礎。

既然沒有兩個非核心點相連,那麼就只有核心點與核心點、核心點與非核心點兩種情況了。

核心點與核心點

顯然核心點與核心點之間的連邊構成的必然是一棵樹。

因此我們直接暴搜樹的形態。

正常的方法應該是利用\(prufer\)序列直接\(O(m^{m-2})\)

列舉。

不正常的方式(我的做法)也可以是強制\(1\)號點為根,\(O((m-1)^{m-1})\)列舉其餘每個點的父節點,然後暴力判一下這是不是一棵樹即可。

這個暴搜本來就有很多寫法,畢竟\(m=6\)隨便瞎跑都能跑過去,主要看個人喜好。

核心點與非核心點

這類邊才是本道題的關鍵。

對於同種位數的點之間的連邊,就每次取一個非關鍵點連向關鍵點。

對於不同位數,顯然每一種位數的一個非關鍵點只會連一次邊,因此我們很容易算出該種位數的關鍵點需要恰好連多少條邊。

每一條邊有兩種選擇,每一種位數又有一個容量限制,容易想到網路流,且有解的條件就是能流滿。

具體建圖,從超級源向代表的點連邊,從代表的點向代表這條邊連的兩種位數

的兩點連邊,從代表位數的點向超級匯連邊。

注意到連向兩種位數相同的邊可以合在一起,所以種數是\(C_m^2\)的。

發現網路流總點數很少,隨便跑。

程式碼

#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 200000
#define mp make_pair
#define pb push_back
using namespace std;
int n,m,tot,tn[10],ps[10],p[10][10],t[10][10],X[N+5],Y[N+5];
vector<pair<int,int> > res;
namespace NetFlow//網路流
{
	#define PS 30
	#define ES 100
	#define add(x,y,f) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].F=f)
	int ee,lnk[PS+5],cur[PS+5],d[PS+5],q[PS+5];struct edge {int F,to,nxt;}e[2*ES+5];
	I void Add(CI x,CI y,CI f) {add(x,y,f),add(y,x,0);}
	I bool BFS()
	{
		RI i,k,H=1,T=1;for(i=1;i<=tot;++i) d[i]=0;d[q[1]=1]=1;W(H<=T&&!d[2])
			for(i=lnk[k=q[H++]];i;i=e[i].nxt) e[i].F&&!d[e[i].to]&&(d[q[++T]=e[i].to]=d[k]+1);
		return d[2]&&memcpy(cur,lnk,sizeof(lnk)),d[2];
	}
	I int DFS(CI x=1,RI f=1e9)
	{
		if(!f||x==2) return f;RI i,t,res=0;for(i=cur[x];i;i=e[i].nxt)
		{
			if(d[e[i].to]^(d[x]+1)||!(t=DFS(e[i].to,min(f,e[i].F)))) continue;
			if(e[i].F-=t,e[((i-1)^1)+1].F+=t,res+=t,!(f-=t)) break;
		}return cur[x]=i,res;
	}
	I int MaxFlow() {RI f=0;W(BFS()) f+=DFS();return f;}//最大流
	int M,Mx[10];I void Build()//建圖
	{
		RI i,j;for(ee=0,i=1;i<=tot;++i) lnk[i]=0;for(i=1;i<=m;++i) Mx[i]=0;//清空
		for(i=1;i<=m;++i) for(j=i+1;j<=m;++j) t[i][j]&&(Mx[i]+=t[i][j],Mx[j]+=t[i][j],
			Add(1,p[i][j]+m+2,t[i][j]),Add(p[i][j]+m+2,i+2,t[i][j]),Add(p[i][j]+m+2,j+2,t[i][j]),0);//連邊
		for(M=0,i=1;i<=m;++i) M+=(Mx[i]-=((i^m)?tn[i+1]:n+1)-ps[i]-1),Add(i+2,2,Mx[i]);//連邊,M統計總流量
	}
	I void Work()//把流法轉化成答案
	{
		RI i,j,k,w;for(i=1;i<=m;++i) for(j=i+1;j<=m;++j)
			if(t[i][j]) for(k=lnk[p[i][j]+m+2];k;k=e[k].nxt)
			{
				if(e[k].to==i+2) for(w=1;w<=t[i][j]-e[k].F;++w) res.pb(mp(tn[i],++ps[j]));//從i的核心點連出邊
				if(e[k].to==j+2) for(w=1;w<=t[i][j]-e[k].F;++w) res.pb(mp(++ps[i],tn[j]));//從j的核心點連出邊
			}
	}
	I bool Check()//檢驗
	{
		Build();for(RI i=1;i<=m;++i) if(Mx[i]<0) return 0;
		return MaxFlow()==M?(Work(),1):0;//是否能流滿
	}
}
namespace DFS//暴搜樹的形態
{
	int fa[10],vis[10];I bool IsTree()//檢驗是否為樹
	{
		memset(vis,0,sizeof(vis));for(RI i=2,x;i<=m;++i)
			{x=i;W(x^1&&vis[x]^i) vis[x]=i,x=fa[x];if(x^1) return 0;}return 1;//如果能跳父親跳到根
	}
	I bool dfs(CI x)//搜尋
	{
		if(x>m) return IsTree()&&NetFlow::Check();
		for(RI i=1;i<=m;++i) if(x^i&&t[x][i])
		{
			fa[x]=i,--t[x][i],--t[i][x],res.pb(mp(tn[x],tn[i]));
			if(dfs(x+1)) return 1;++t[x][i],++t[i][x],res.pop_back();
		}return 0;
	}
}
int main()
{
	RI i,j,k=0;char s1[10],s2[10];for(scanf("%d",&n),i=1;i^n;++i)//讀入
		cin>>s1>>s2,++t[X[i]=strlen(s1)][Y[i]=strlen(s2)],X[i]^Y[i]&&++t[Y[i]][X[i]];
	for(k=m=1;k<=n;++m) k*=10;for(--m,k=0,i=1;i<=m;++i) for(j=i+1;j<=m;++j) p[i][j]=++k;//給每種邊標號
	for(i=1;i<=m;++i) for(ps[i]=tn[i]=(i^1)?tn[i-1]*10:1,j=1;j<=t[i][i];++j) res.pb(mp(tn[i],++ps[i]));//對於同種位數,向關鍵點連邊
	if(tot=p[m-1][m]+m+2,!DFS::dfs(2)) return puts("-1"),0;//無解輸出-1
	for(i=0;i<n-1;++i) printf("%d %d\n",res[i].first,res[i].second);return 0;//輸出合法方案
}