Prufer 序列

可以用来做一些树上计数问题,相当于一个转化工具。主要用在生成树的问题上。

定义

考虑一棵无根树 \(\mathrm T\),定义度数为 \(1\) 的节点为叶子节点,令叶子节点集合为 \(S\)。每次找到 \(S\) 中编号最小的元素,并把 它连接的那个节点 加入到序列末端,并把这个元素删去,直到只剩两个元素为止。

那么现在显然可以使用堆来维护 \(S\),可以做到 \(O(n \log n)\) 构造。

但事实上,有更优秀的 \(O(n)\) 做法。

考虑从 \(1\)\(n\) 扫描,假设现在遍历到点 \(u\)且满足 \(1, 2, ...., u - 1\) 都不是叶子节点 ,有两种情况:

  • \(u\) 不是叶子节点:直接跳过即可。

  • \(u\) 是叶子节点:设它连接的那个节点为 \(fa_u\),把 \(fa_u\) 加入序列中,把 \(u\) 删去。然后考虑更新 \(S\),不难发现它只影响了 \(fa_u\)。假如 \(fa_u < u\)\(fa_u \in S\),那么重复这个过程,并且再考虑 \(fa_{fa_u}\),直到不满足上面的条件。

不难发现,这个过程中,每个点至多被访问 \(2\) 次,且 \(u\) 单调递增,因此时间复杂度为 \(O(n)\)

qwq
for(int i = 1; i <= n - 1; i++){
	if(tot == n - 2) continue;
	if(deg[i] == 1){
		ans[++tot] = fa[i]; deg[fa[i]]--; int p = fa[i];
		while(deg[p] == 1 && p < i && tot < n - 2) ans[++tot] = fa[p], deg[fa[p]]--, p = fa[p]; 
	}
}

性质

1. \(\mathrm {Prufer}\) 序列的长度为 \(n - 2\),点 \(i\)\(\rm Prufer\) 序列中出现 \(deg_i - 1\) 次(\(deg_i\) 为点 \(i\) 的度数)。

证明非常显然。

应用: 当我们在用 \(\rm Prufer\) 序列还原树的时候,可以还原出每个节点的度数,从而知道叶子节点集合,于是就知道了每个点在 \(\rm Prufer\) 序列中出现时,另一个节点是哪一个叶子,实现与上面那个相似。

2. 每个 \(\rm Prufer\) 序列与一个有标号无根树唯一对应,即 \(\rm Prufer\) 序列与有标号无根树 构成双射

证明:从构造过程中可以发现,一个 \(\rm Prufer\) 序列唯一对应一棵树,而每棵树都有唯一的合法的 \(\rm Prufer\) 序列。

3.\(n\) 个点组成的有标号无根树的个数一共有 \(n^{n - 2}\) 个。

证明: 由于有标号无根树与 \(\rm Prufer\) 序列构成双射,因此可以直接对 \(\rm Prufer\) 序列进行计数。由于每个 \(\rm Prufer\) 序列都是合法的,所以前面 \(n - 2\) 位置随便放都可以。

例题:

CF156D Clues

首先把每个联通块看成一个大点,这样由 \(\rm Prufer\) 序列的性质,树的形态为 \(n^{k - 2}\),然后不难发现,假如我们随便钦定一个点为根,可以把每条边都归到儿子处,这样每个属于每个联通块的边都只有一条了,方案树为 \(\prod s_i\)\(s_i\) 为联通块大小。

CF917D Stranger Trees

首先无脑二项式繁衍,转化为求钦定的数量。假设现在钦定了 \(k\) 条边,那么就会有 \(n - k\) 个联通块,这下就转化为了求 \(n - k\) 个联通块连成一颗树的方案树了。同上一道题,方案数为 \(n^{n - k - 2} \prod s_i\)。容易直接树形 \(\rm DP\) 做到 \(O(n^3)\)。但是不妨考虑 \(\prod s_i\) 原来的意义,其实就是在每个联通块中选一个点的方案数,直接 \(f_{u, j, 0/1}\) 设点 \(u\) 子树内,选了 \(j\) 个联通块,且 \(u\) 的联通块中是否已经选了点,树上背包合并即可。

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
using namespace std;

const int N = 500 + 10;
const ll mod = 1e9 + 7;

void ADD(ll& x, ll y){x += y; (x >= mod) ? x -= mod : 0;}

int n, siz[N];
ll f[N][N][2], jc[N], jcinv[N], lstf[N][N][2]; 
vector<int> vec[N];

ll qpow(ll x, int y){
    ll ret = 1; if(y < 0) x = qpow(x, mod - 2), y = -y;
    for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
    return ret;
}
ll C(int x, int y){
    return jc[x] * jcinv[x - y] % mod * jcinv[y] % mod;
} 

void dfs(int u, int fa){
    f[u][1][0] = f[u][1][1] = 1; siz[u] = 1;
    for(auto v : vec[u]){
        if(v == fa) continue;
        dfs(v, u);
        for(int i = 1; i <= siz[u]; i++) lstf[u][i][0] = f[u][i][0], lstf[u][i][1] = f[u][i][1], f[u][i][1] = f[u][i][0] = 0;
        for(int i = 1; i <= siz[u]; i++){
            for(int j = 1; j <= siz[v]; j++){
                ADD(f[u][i + j][0], lstf[u][i][0] * f[v][j][1] % mod);
                ADD(f[u][i + j][1], lstf[u][i][1] * f[v][j][1] % mod);
                ADD(f[u][i + j - 1][0], lstf[u][i][0] * f[v][j][0] % mod);
                ADD(f[u][i + j - 1][1], (lstf[u][i][1] * f[v][j][0] % mod + lstf[u][i][0] * f[v][j][1] % mod) % mod);
            }
        } siz[u] += siz[v];
    }
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin >> n;
    for(int i = 1; i < n; i++){
        int x, y; cin >> x >> y;
        vec[x].pb(y); vec[y].pb(x);
    }
    dfs(1, 0); jc[0] = jcinv[0] = 1;
    for(int i = 1; i <= n; i++) jc[i] = jc[i - 1] * i % mod;
    jcinv[n] = qpow(jc[n], mod - 2);
    for(int i = n - 1; i > 0; i--) jcinv[i] = jcinv[i + 1] * (i + 1) % mod;
    for(int i = 0; i < n; i++){
        ll ans = 0;
        for(int j = i; j <= n; j++) ADD(ans, (((j - i) & 1) ? mod - 1 : 1ll) * C(j, i) % mod * f[1][n - j][1] % mod * qpow(n, n - j - 2) % mod);
        cout << ans << " ";
    }

    return 0;
}**
posted @ 2025-01-20 10:04  Little_corn  阅读(61)  评论(0)    收藏  举报