1. 程式人生 > 其它 >【ZJOI2016】小星星 容斥+樹形DP

【ZJOI2016】小星星 容斥+樹形DP

題意

UOJ #185.
給出一個\(n(n \le 17)\) 的樹和 \(n\) 個點的圖,求有多少種樹點和圖點的對應方案是“好的”
“好的”方案,如果現在樹上兩點間存在邊,那麼要求圖上對應的這兩點之間也有邊。

題解

考慮用 \(dp\) 去計算方案數,如果需要確定一個順序來dp,顯然是從葉子向上樹形dp。
不妨設 1 為整棵樹的根,現在用 \(f_{i, j}\) 表示樹上一顆以 \(i\) 為根的子樹全部確定對應點,\(i\) 樹點對應 \(j\) 圖點。

\[f_{u, i} = \prod_v (\sum_j f_{v, j}) \]

但這樣計算有一個問題,會有多個樹點選擇了同一個圖點,會算重。
這時注意到 \(n\)

非常小,因此可以容斥。
狀態為\(1\) 的圖點可以選擇,其他不能。然後每個狀態計算一下,最後統計就好。

總的來說就是先容斥再每個狀態都樹形DP。

程式碼

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
typedef long long ll;
const int N = 18;
int n, m, cnt, id[N];
int e, to[N << 1], nxt[N << 1], hd[N];
ll f[N][N];
bool ed[N][N];
void add(int u, int v){
	to[++e] = v; nxt[e] = hd[u]; hd[u] = e;
}
void dfs(int u, int fa){
	for(int i = hd[u]; i; i = nxt[i]){
		int v = to[i]; if(v == fa) continue;
		dfs(v, u);
	}
	for(int i = 1; i <= cnt; i++){
		f[u][i] = 1;
		for(int j = hd[u]; j; j = nxt[j]){
			int v = to[j]; if(v == fa) continue;
			ll sum = 0;
			for(int k = 1; k <= cnt; k++)
				if(ed[id[i]][id[k]]) sum += f[v][k];
			f[u][i] *= sum;
		}
	}//樹形DP
	return;
}
int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1, u, v; i <= m; i++)
		scanf("%d%d", &u, &v), ed[u][v] = ed[v][u] = 1;
	for(int i = 1, u, v; i < n; i++)
		scanf("%d%d", &u, &v), add(u, v), add(v, u);
	ll ans = 0;
	for(int i = 0; i < (1 << n); i++){
		cnt = 0;
		for(int j = 1; j <= n; j++)
			if(i & (1 << (j - 1)))
				id[++cnt] = j;
		dfs(1, 0);
		ll sum = 0;
		for(int j = 1; j <= cnt; j++)
			sum += f[1][j];
		ans += (((cnt ^ n) & 1) ? -1 : 1) * sum;
	}//容斥
	printf("%lld\n", ans);
	return 0;
}