[BZOJ4455][UOJ185][Zjoi2016]小星星(树形DP+容斥)

树图编号计数
本文介绍了一种在给定树和图的情况下,如何通过动态规划和容斥原理来计算所有合法节点编号方案的数量的方法。该问题要求每个节点的编号构成1到n的排列,并且树的每条边对应的节点编号在图中也必须存在边。文章详细解释了状态定义、转移方程及优化技巧。

将问题抽象化:
一个nn个节点的树,和一个n个节点的图,要求给树上的每个节点编号,使得编号是一个11n的排列,并且要满足树上任意一条边(u,v)(u,v),图中一定要有边(xu,xv)(xu,xv)xuxu表示点uu的编号),求方案数。
暴力的做法是定义状态f[i][j][S]表示节点ii编号为jii的子树内的编号集合为S的方案数。
但是这样的瓶颈在于枚举子集,复杂度是O(n3×3n)O(n3×3n)的,显然TLE。
Q:为什么要记录SS这一维?
A:要求中有「编号是一个1nn的排列」。
尝试把「编号是一个1nn的排列」这一条件去掉,就不用记录S了。
这样只需要定义f[i][j]f[i][j]为在ii的子树内,点i的编号为jj的方案数。
而这时候会出现重复编号,怎么办呢?
容斥!
2n枚举{1,2,...,n}{1,2,...,n}的一个子集SS,强制规定树上每个点的编号必须是S的子集,然后每次O(n3)O(n3)一次DP,总方案数为:
(|S|=n(|S|=n的方案数)(|S|=n1)−(|S|=n−1的方案数)+(|S|=n2)+(|S|=n−2的方案数)...)−...
复杂度降到O(n3×2n)O(n3×2n),在UOJ上需要进行一定的常数优化。
代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
    int res = 0; bool bo = 0; char c;
    while (((c = getchar()) < '0' || c > '9') && c != '-');
    if (c == '-') bo = 1; else res = c - 48;
    while ((c = getchar()) >= '0' && c <= '9')
        res = (res << 3) + (res << 1) + (c - 48);
    return bo ? ~res + 1 : res;
}
typedef long long ll;
const int N = 20, M = 40;
int n, m, ecnt, nxt[M], adj[N], go[M], tot, whi[N];
bool g[N][N], vis[N]; ll f[N][N], ans;
inline void add_edge(const int &u, const int &v) {
    nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
    nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u;
}
inline void dfs(const int &u, const int &fu) {
    int i, j; for (int e = adj[u], v; e; e = nxt[e]) {
        if ((v = go[e]) == fu) continue; dfs(v, u);
    }
    for (i = 1; i <= tot; i++) {
        int x = whi[i]; f[u][x] = 1;
        for (int e = adj[u], v; e; e = nxt[e]) {
            if ((v = go[e]) == fu) continue; ll sum = 0;
            for (j = 1; j <= tot; j++) {
                int y = whi[j]; if (!g[x][y]) continue; sum += f[v][y];
            }
            f[u][x] *= sum;
        }
    }
}
inline void solve() {
    int i; tot = 0; for (i = 1; i <= n; i++) if (vis[i]) whi[++tot] = i;
    dfs(1, 0); for (i = 1; i <= tot; i++)
        if (n - tot & 1) ans -= f[1][whi[i]]; else ans += f[1][whi[i]];
}
inline void Dfs(const int &dep) {
    if (dep == n + 1) return solve();
    vis[dep] = 0; Dfs(dep + 1);
    vis[dep] = 1; Dfs(dep + 1);
}
int main() {
    int i, x, y; n = read(); m = read();
    for (i = 1; i <= m; i++) x = read(), y = read(), g[x][y] = g[y][x] = 1;
    for (i = 1; i < n; i++) x = read(), y = read(), add_edge(x, y);
    Dfs(1); cout << ans << endl; return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值