@description@

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

输入格式
第一行包含两个正整数 n,m,表示原来的饰品中小星星的个数和细线的条数。
接下来 m 行,每行包含两个正整数 u,v,表示原来的饰品中小星星 u 和 v 通过细线连了起来。
这里的小星星从 1 开始标号。保证 u ≠ v,且每对小星星之间最多只有一条细线相连。 接下来 n-1 行,每行包含两个正整数 u,v,表示现在的饰品中小星星 u 和 v 通过细线连了起来。保证这些小星星通过细线可以串在一起。

输出格式
输出共一行,包含一个整数表示可能的对应方式的数量。

如果不存在可行的对应方式则输出 0。

样例输入
4 3
1 2
1 3
1 4
4 1
4 2
4 3
样例输出
6

数据范围与提示
对于所有的数据,n <= 17, m <= n*(n-1)/2。

@solution@

普通的做法人人都会:定义 dp[i][j][s] 表示以 i 为根的子树,i 对应 j,这棵子树内已经用了集合 s 的点。
然后一波枚举子集转移。然后就炸了。
我没试过 FWT 行不行,不过好像比下面的算法多个 n 的复杂度,所以估计过不了。

考虑抽象问题模型:我们要找的其实是一个满足 “树边的两个端点对应过去也存在边” 这一限制的置换。
问题在于,置换必须要满足每个元素恰好被对应一次,所以我们才要枚举子集啊之类。
但其实只有一条边相邻两个点才会产生限制。因此我们直接统计置换是很吃亏的。

我们可以把置换看成 n 个点都要被对应,通过容斥转成只有 m 个点可以被对应。
形式化来说,假如我们可被对应的点的集合为 S。
我们的原问题相当于 S 中每一个点都要被对应,通过容斥转为 T 中的点可以被对应,其中 \(T\subset S\)

那么就回到我们一开始的树形 dp,只是少了 s 这一维,变成 dp[i][j] 表示以 i 为根的子树,i 对应 j 的方案数。

外层容斥 \(O(2^n)\),树形 dp 的复杂度为 \(O(n^3)\),所以总时间复杂度为 \(O(2^n*n^3)\)

@accepted code@

#include<cstdio>
#include<vector>
using namespace std;
typedef long long ll;
vector<int>G[20];
void addedge(int u, int v) {
    G[u].push_back(v);
    G[v].push_back(u);
}
bool tag[20];
int A[20][20], n, m;
ll dp[20][20];
void dfs(int x, int f) {
    for(int i=0;i<G[x].size();i++)
        if( G[x][i] != f ) dfs(G[x][i], x);
    for(int i=0;i<n;i++)
        if( tag[i] ) {
            dp[x][i] = 1;
            for(int j=0;j<G[x].size();j++) {
                int p = G[x][j];
                if( p == f ) continue;
                ll del = 0;
                for(int k=0;k<n;k++)
                    if( A[i][k] ) del += dp[p][k];
                dp[x][i] *= del;
            }
        }
        else dp[x][i] = 0;
}
int f[1<<20];
int main() {
    scanf("%d%d", &n, &m);
    for(int i=1;i<=m;i++) {
        int u, v; scanf("%d%d", &u, &v), u--, v--;
        A[u][v] = A[v][u] = true;
    }
    for(int i=1;i<n;i++) {
        int u, v; scanf("%d%d", &u, &v), u--, v--;
        addedge(u, v);
    }
    int t = (1<<n); f[0] = 1;
    for(int i=1;i<t;i++)
        f[i] = (i&1) ? -f[i>>1] : f[i>>1];
    ll ans = 0;
    for(int s=0;s<t;s++) {
        for(int i=0;i<n;i++)
            tag[i] = !((s>>i) & 1);
        dfs(0, -1);
        for(int i=0;i<n;i++)
            if( tag[i] ) ans = ans + f[s]*dp[0][i];
    }
    printf("%lld\n", ans);
}

@details@

ZJOI 的题都是神仙题 * 2。

代码倒是非常简洁。

01-25 23:22