1. 程式人生 > 其它 >[ZJOI2016]小星星

[ZJOI2016]小星星

題面

[ZJOI2016]小星星

題解

就是對於節點數相同的一棵樹和一張圖,對於這顆樹的點集的子集向這幅圖的點集的子集建立一個對映,且滿足在樹上的每一條邊的兩個端點對映到圖中時也有邊連線,問這樣的對映方式有多少種。
我們設 \(f[x][j]\) 表示以 \(x\) 為根的子樹,當 \(x\) 對映成 \(j\) 時的方案數。
所以我們可以先直接列舉點集的子集。
然後在 dfs 這棵樹的時候,對於邊 \((x,y)\) ,即某種對映 $x -> i,y -> j $ 我們直接判斷在圖中是存邊 \((i,j)\) 即可。
對於子節點 \(y\) 的所有對映的方案之間顯然是累加關係。
而子節點與父節點的方案之間顯然是乘法關係。
所以轉移關係便很清晰了。
總的時間複雜度是 \(O(n ^ 3 2 ^ n)\)

,理論可以過,不過需要卡常,不過顯然我沒有卡過。
以下程式碼需要吸氧,但是吸了之後快了 \(4\) 秒就很離譜。

程式碼

#include<cstdio>
#include<vector>

using namespace std;

#define re register

typedef long long LL;

int n, m, q[25], tot; bool g[20][20];

vector < int > to[20]; LL ans = 0, f[20][20];

inline void add(int u, int v) { to[u].push_back(v); to[v].push_back(u); }

void dfs(re int x, re int fa) {
	for(re unsigned int i = 0; i < to[x].size(); i++)
		if(to[x][i] != fa) dfs(to[x][i], x);
	for(re int i = 1; i <= tot; i++) {
		f[x][q[i]] = 1;
		for(re unsigned int p = 0; p < to[x].size(); p++) {
			if(to[x][p] == fa) continue; re LL sum = 0;
			for(re int j = 1; j <= tot; j++)
				if(g[q[i]][q[j]]) sum += f[to[x][p]][q[j]];
			f[x][q[i]] *= sum;
		}
	}
}

inline void solve() {
	re int lim = 1 << n;
	for(re int i = 0; i < lim; i++) {
		tot = 0;
		for(re int j = i, k = 1; j; j >>= 1, k++)
			if(j & 1) q[++tot] = k;
		dfs(1, 0);
		for(re int j = 1; j <= tot; j++)
			ans += (((n - tot) & 1) ? -1 : 1) * f[1][q[j]];
	}
}

int main() {
	scanf("%d%d", &n, &m);
	for(re int i = 1, u, v; i <= m; i++) scanf("%d%d", &u, &v), g[u][v] = g[v][u] = 1;
	for(re int i = 1, u, v; i <  n; i++) scanf("%d%d", &u, &v), add(u, v);
	solve(); printf("%lld\n", ans);
	return 0;
}