ZJOI2016 小星星

题目链接

感觉是第一次入手容斥原理 (?)

小Y是一个心灵手巧的女孩子,她喜欢手工制作一些小饰品。她有\(n\)颗小星星,用\(m\)条彩色的细线串了起来,每条细线连着两颗小星星。有一天她发现,她的饰品被破坏了,很多细线都被拆掉了。这个饰品只剩下了\(n−1\)条细线,但通过这些细线,这颗小星星还是被串在一起,也就是这些小星星通过这些细线形成了树。小Y找到了这个饰品的设计图纸,她想知道现在饰品中的小星星对应着原来图纸上的哪些小星星。如果现在饰品中两颗小星星有细线相连,那么要求对应的小星星原来的图纸上也有细线相连。小Y想知道有多少种可能的对应方式。只有你告诉了她正确的答案,她才会把小饰品做为礼物送给你呢。

抽象化的题意:用\(1\sim n\)的排列给树上节点编号,有树边\((u,v)\)的两个端点编号\(p-u,p-v\)在原图上也要有一条边。

最暴力的方法是枚举全排列,然后去验证合不合法。

可以把全排列变成枚举子集,树形\(dp\)暴力合并。

状态设置为\(dp[i][j][s]\),数值表示编号的方案数。

  1. 第二维是为了满足转移的时候\(u,v\)直接有边。
  2. 第三维是子树中用了的点的编号,目的是保证最后选出的是一个排列

可以用容斥原理去掉第三维。

钦定可以用的点,然后计算方案数,容斥原理得出全集的答案。

具体来说\(dp[i][j]\)表示\(i\)点编号为\(j\)时子树的编号方案数。

转移方程\(dp[u][i]=\prod_{v\in son-u}\sum dp[v][j]*[\text{some limit}]\)

比如这里条件限制是\(i,j\)可用并且\((i,j)\)这条边存在。

没有删点时,可能把有一些点没用的方案数算进去。

比如一个点没用,所以枚举缺一个点的情况,减去它的方案数。

但是你会发现缺两个点的情况又被减去两次,所以又要加回来,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int N = 18;
ll dp[N][N];
int n, m;
vector<int> e[N];
bool mp[N][N], b[N];

void dfs(int u, int fa) {
for (int i = 1; i <= n; i++) dp[u][i] = 1;
for (int v : e[u]) {
if (v == fa) continue;
dfs(v, u);
for (int i = 1; i <= n; i++) {
ll sum = 0;
for (int j = 1; j <= n; j++) {
sum += dp[v][j] * (mp[i][j] & b[i] & b[j]);
}
dp[u][i] *= sum;
}
}
}

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);
e[u].push-back(v);
e[v].push-back(u);
}
ll res = 0;
for (int i = 1; i <= (1 << n) - 1; i++) {
int size = 0;
memset(b, 0, sizeof b);
for (int j = 1; j <= n; j++) b[j] = ((i >> (j - 1)) & 1), size += !b[j];
dfs(1, 0);
for (int i = 1; i <= n; i++) res = (size & 1) ? res - dp[1][i] : res + dp[1][i];
}
printf("%lld\n", res);
return 0;
}