peiwenjun's blog 没有知识的荒原

P3349 [ZJOI2016]小星星 题解

题目描述

给定一棵 \(n\) 个点的树和一张 \(n\) 个点, \(m\) 条边的图。

求有多少种编号的双射,使得树上每条边的两端点映射到图上后,这条边仍存在。

数据范围

  • \(1\le n\le 17,1\le m\le\frac{n(n-1)}2\) 。

时间限制 \(\texttt{1s}\) ,空间限制 \(\texttt{512MB}\) 。

分析

针对全排列的优化,配上这个数据范围,考虑树形 \(\texttt{dp}\) + 状压 \(\texttt{dp}\) 。

\(dp_{i,j,s}\) 表示考虑以 \(i\) 为根的子树, \(i\) 的映射编号为 \(j\) , \(i\) 子树映射到的点的编号集合为 \(s\) 的方案数(\(|s|=sz_i\))。

转移需要合并两棵子树,即枚举子集,时间复杂度 \(\mathcal O(n^23^n)\) 。


如果不想让 \(3^n\) 出现时间复杂度中,那么不能枚举子集,进而记录第三维状态就没有任何意义。

直接令 \(dp_{i,j}\) 为以 \(i\) 为根的子树,将 \(i\) 映到 \(j\) 的方案,然后做上述转移。

显然这样会算重,我们需要减掉存在某个点出现多次,即存在某个点不出现的情况。

钦定某个点不出现,继续跑上述转移。。。

容易发现这就是容斥原理的雏形。

用 \(2^n\) 的代价钦定不出现的点集,然后跑上述转移,时间复杂度 \(\mathcal O(n^32^n)\) ,不过远远跑不满。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int m,n,u,v;
ll res,dp[17][17];
bool b[17][17];
vector<int> g[17];
void dfs(int u,int fa)
{
    for(auto v:g[u])
    {
        if(v==fa) continue;
        dfs(v,u);
        for(int j=0;j<n;j++)
        {
            if(!dp[u][j]) continue;
            ll sum=0;
            for(int k=0;k<n;k++) sum+=b[j][k]?dp[v][k]:0;
            dp[u][j]*=sum;
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    while(m--) scanf("%d%d",&u,&v),u--,v--,b[u][v]=b[v][u]=1;
    for(int i=1;i<n;i++) scanf("%d%d",&u,&v),g[--u].push_back(--v),g[v].push_back(u);
    for(int s=0;s<1<<n;s++)
    {
        for(int i=0;i<n;i++) for(int j=0;j<n;j++) dp[i][j]=!(s>>j&1);
        dfs(0,-1);
        res+=(__builtin_parity(s)?-1:1)*accumulate(dp[0],dp[0]+n,0ll);
    }
    printf("%lld\n",res);
    return 0;
}

posted on 2022-05-30 20:06  peiwenjun  阅读(13)  评论(0)    收藏  举报

导航