1. 程式人生 > 實用技巧 >[洛谷P4336] SHOI2016 黑暗前的幻想鄉

[洛谷P4336] SHOI2016 黑暗前的幻想鄉

問題背景

四年一度的幻想鄉大選開始了,最近幻想鄉最大的問題是很多來歷不明的妖怪湧入了幻想鄉,擾亂了幻想鄉昔日的秩序。但是幻想鄉的建制派妖怪(人類)博麗靈夢和八雲紫等人整日高談所有妖怪平等,幻想鄉多元化等等,對於幻想鄉目前面臨的種種大問題卻給不出合理的解決方案。

風見幽香是幻想鄉里少有的意識到了問題嚴重性的大妖怪。她這次勇敢地站了出來參加幻想鄉大選,提出包括在幻想鄉邊境建牆(並讓人類出錢),大力開展基礎設施建設挽回失業率等一系列方案,成為了大選年出人意料的黑馬並順利地當上了幻想鄉的大統領。

問題描述

幽香上臺以後,第一項措施就是要修建幻想鄉的公路。幻想鄉一共有 n 個城市,之前原來沒有任何路。幽香向選民承諾要減稅,所以她打算只修 n-1 條公路將這些城市連線起來。但是幻想鄉有正好 n-1 個建築公司,每個建築公司都想在修路地過程中獲得一些好處。雖然這些建築公司在選舉前沒有給幽香錢,幽香還是打算和他們搞好關係,因為她還指望他們幫她建牆。所以她打算讓每個建築公司都負責一條路來修。

每個建築公司都告訴了幽香自己有能力負責修建的路是哪些城市之間的。所以幽香打算 n - 1 條能夠連線幻想鄉所有城市的邊,然後每條邊都交給一個能夠負責該邊的建築公司修建,並且每個建築公司都恰好修建一條邊。

幽香現在想要知道一共有多少種可能的方案呢?兩個方案不同當且僅當它們要麼修的邊的集合不同,要麼邊的分配方式不同。

輸入格式

第一行包含一個整數 n,表示城市個數。

第 2 到第 n 行,第 (i + 1) 行表示 第 i 個建築公司可以修建的路的列表:以一個非負整數 mi 開頭,表示其可以修建條路的條數;接下來有 mi 對整數 u, v,每對數表示一條邊的兩個端點。其中不會出現重複的邊,也不會出現自環。

輸出格式

輸出一行一個整數,表示所有可能的方案數對 \(10^9+7\) 取模的結果。

樣例輸入

4
2 3 2 4 2
5 2 1 3 1 3 2 4 1 4 3
4 2 1 3 2 4 1 4 2

樣例輸出

17

資料範圍

對於\(100\%\) 的測試點,\(2 \leq n \le 17\)\(0 \leq m_i \leq \frac{n(n - 1)}{2}\)\(1 \leq u, v \leq n\)

解析

所有建築公司能夠修建的道路構成了一張圖,而最後修建的道路一定構成了原圖的一個生成樹。\(n\) 的範圍如此的小,我們不妨考慮容斥。列舉哪些建築公司沒有可修建的道路,並將列舉的建築公司的邊集從原圖中刪去。那麼剩下的圖中若還能構成生成樹,說明這棵生成樹一定是不合法的。我們可以對剩下的圖也統計一次生成樹個數。結合容斥原理,設 \(f_S\)

表示至少集合 \(S\) 中的公司沒有修路時的方案數:

\[Ans=\sum_{S\in \{1...n\}}(-1)^{|S|}f_{S} \]

生成樹的方案數可以用矩陣樹定理計算。

程式碼

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#define int long long
#define N 20
using namespace std;
const int mod=1000000007;
struct edge{
	int u,v;
}e[N][N*N];
int n,i,j,m[N],ans;
int g[N][N],d[N][N];
bool vis[N];
int read()
{
	char c=getchar();
	int w=0;
	while(c<'0'||c>'9') c=getchar();
	while(c<='9'&&c>='0'){
		w=w*10+c-'0';
		c=getchar();
	}
	return w;
}
int poww(int a,int b)
{
	int ans=1,base=a;
	while(b){
		if(b&1) ans=ans*base%mod;
		base=base*base%mod;
		b>>=1;
	}
	return ans;
}
int gcd(int a,int b)
{
	if(b==0) return a;
	return gcd(b,a%b);
}
int solve()
{
	int ans=1,dv=1;
	for (int i=1, j=1; i<n && j<n; ++i, ++j)
	{
		int id=i;
		for (int k=i; k<n; ++k)
			if (d[k][j]!=0) { id=k; break; }
		if (d[id][j]==0) { --i; continue; }
		if (id!=i)
		{
			ans*=-1;
			for (int k=j; k<n; ++k) swap(d[id][k], d[i][k]);
		}
		for (int k=i+1; k<n; ++k)
			if (d[k][j]!=0)
			{
				int tmp1=gcd(abs(d[i][j]), abs(d[k][j]));
				int tmp2=d[i][j]/tmp1;
				tmp1=d[k][j]/tmp1;
				dv=dv*tmp2%mod;
				for (int p=j; p<n; ++p)
					d[k][p]=(d[k][p]*tmp2-d[i][p]*tmp1+mod)%mod;
			}
	}
	for(int i=1;i<n;i++) ans=(ans*d[i][i]%mod+mod)%mod;
	return (ans*poww(dv,mod-2)%mod+mod)%mod;
}
void dfs(int x)
{
	if(x==n){
		memset(g,0,sizeof(g));
		memset(d,0,sizeof(d));
		int cnt=0;
		for(int i=1;i<n;i++){
			if(!vis[i]){
				for(int j=1;j<=m[i];j++){
					int u=e[i][j].u,v=e[i][j].v;
					g[u][v]++;g[v][u]++;
					d[u][u]++;d[v][v]++;
				}
			}
			else cnt++;
		}
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++) d[i][j]-=g[i][j];
		}
		if(cnt%2==0) ans=(ans+solve())%mod;
		else ans=(ans-solve()+mod)%mod;
		return;
	}
	dfs(x+1);
	vis[x]=1;dfs(x+1);vis[x]=0;
}
signed main()
{
	n=read();
	for(i=1;i<n;i++){
		m[i]=read();
		for(j=1;j<=m[i];j++) e[i][j].u=read(),e[i][j].v=read();
	}
	dfs(1);
	printf("%lld\n",ans);
	return 0;
}