1. 程式人生 > 實用技巧 >Luogu P3349 小星星 【DP+容斥原理】

Luogu P3349 小星星 【DP+容斥原理】

題意

\(\;\)
給定一棵\(n\)個點的樹\(T\),和一張\(n\)個點\(m\)條邊的圖\(S\),求有多少種點之間的對應關係的方案使得\(T\)\(S\)的一個子圖(等價於\(T\)中的每條邊在某種點的對應關係下在\(S\)中都存在)

\[n\leq 17 \]

\(\;\)

Solution

暴力

\(\;\)
一種樸素的想法就是暴力的列舉每一種對應方式:
如下是\(n=3\)的情況,箭頭左邊是樹中的每個點,右邊對應的是圖中的每個點
1->1, 2->2, 3->3
1->1, 2->3, 3->2
1->2, 2->3, 3->1
1->2, 2->1, 3->3
1->3, 2->1, 3->2
1->3, 2->2, 3->1
然後我們對每種情況我們再用\(O(n)\)

的時間去判斷樹中的每條邊是否在圖中都存在即可
時間複雜度:\(O(n!\times n)\)
\(\;\)

子集列舉DP

\(\;\)
我們由題中得出\(T\)是一棵樹,而其實如果\(T\)是一張圖,用上面的暴力演算法也是可做的,所以我們要思考如何利用樹的特殊性質來挖掘隱含的細節
我們發現,題中的最大難點並不是處理\(T\)\(S\)的子圖這個限制條件,而是去計算點之間的對應關係,使得方案數不重不漏
所以我們得到了一個思路:在樹\(T\)上進行樹形DP
\(\;\)
首先是設計狀態
第一維:\(i\),當然存的是以\(i\)為根的子樹的資訊
第二維:\(j\),由於子樹與外界之間還有限制條件,即:根節點\(i\)

與其父親在DP中在圖中分別對應的點會有邊的限制條件,所以\(j\)表示的是\(i\)在圖中的對應點
第三維:\(k\),由於樹上的每個點只能唯一對應圖中的點,所以如果只有前兩維狀態,我並不能知道目前這棵子樹中每個點已經對應到了圖中的哪些點,所以就可能導致我們會算重,即:樹上的某幾個點對應到了圖中的同一個點上,所以\(k\)是表示我們目前選的點集,用一個壓縮後的二進位制數來表示
綜上所述:\(f_{i,j,k}\)表示以\(i\)為根的子樹,其中\(i\)對應的是圖中的編號為\(j\)的點,且子樹中已經選了集合為\(k\)的點,滿足這棵子樹是\(T\)中把集合為\(k\)的點在其中的對應點挑出來所對應的圖的子圖
呃,可能有點長,讀者可以仔細地多讀幾遍再消化理解。
那麼對於子圖也就是邊的這個限制條件,我們只需在DP的過程中判斷\(i\)
與它的每個兒子在圖中是否有邊即可。
因此狀態轉移方程也易得出:

\[f_{i,j,k}=\prod_{v\in son_u} \sum_{p\in Edge_{p,j}} \sum_{q\in k} f_{v,p,q} \]

時間複雜度:\(O(n^3\times 3^n)\)
\(\;\)

容斥原理優化

\(\;\)
我們發現,上面的DP限制時間的主要因素是我們枚舉了\(k\),也就是我們目前選的點集。
這樣導致時間暴增。
我們考慮如何去優化這個東西。
而仔細分析即可發現列舉\(k\)是為了防止算重,那我們不妨可以大膽的扔掉\(k\),直接進行DP。
而這樣的時間複雜度只有\(O(n^3)\)
但這樣顯然是錯的。
所以我們考慮如何去減掉重複的情況。
我們發現,因為多個點對應圖中的一個點。那麼圖中一定存在若干個點沒有與樹中的點對應
於是這個東西就可以容斥了:我們在圖中列舉與樹中對應的點有哪些,記為點集\(A\),容斥係數顯然為\((-1)^{n-|A|}\)
則我們就可以通過這種方式,在DP時只需選我們列舉的點集進行狀態轉移即可。
時間複雜度:\(O(n^3 \times 2^n)\)
\(\;\)

Code

#include <bits/stdc++.h>
const int N = 20;
#define LL long long
int n, m, mp[N][N], c[N], len;
LL res, f[N][N];
std::vector<int> G[N];
void Dfs(int u, int fa)
{
	for(int i=1;i<=len;i++) f[u][c[i]] = 1;
	for(int i=0;i<G[u].size();i++)
	{
		int v = G[u][i];
		if(v == fa) continue;
		Dfs(v, u);
		for(int j=1;j<=len;j++)
		{
			LL now = 0;
			for(int k=1;k<=len;k++)
			{
				if(mp[c[j]][c[k]]) now += f[v][c[k]];
			}
			f[u][c[j]] *= now;
		}	
	}
}
int main()
{
	scanf("%d%d", &n, &m);
	for(int i=1;i<=m;i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		mp[u][v] = mp[v][u] = 1;
	}
	for(int i=1;i<n;i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		G[u].push_back(v); G[v].push_back(u);
	}
	for(int S=1;S<(1<<n);S++)
	{
		memset(f, 0, sizeof(f));
		len = 0; int cnt = 0;
		for(int i=0;i<n;i++)
		{
			if(S >> i & 1) c[++len] = i + 1, cnt ++;
		}
		Dfs(1, 0);
		LL t;
		if((n - cnt) & 1) t = -1;
		else t = 1;
		for(int i=1;i<=len;i++) res += t * f[1][c[i]];
	}
	printf("%lld", res);
	return 0;
}