學習筆記:樹與圖上的計數問題

Lu_xZ發表於2024-05-23

Prüfer 序列

\(n\) 個點的有標號無根樹可以與一個長度為 \(n - 2\) 的 Prüfer 序列對應。

從樹到 Prüfer 序列

  1. \(f\) 為空序列。
  2. 如果當前樹上多於兩個節點,假設當前標號最小的葉子為 \(x\),與 \(x\) 相連的節點標號為 \(y\),那麼把 \(x\) 從樹上刪除,把 \(y\) 加入 \(f\) 末尾。
  3. 重複 2. 直到樹上只有兩個節點。

性質

  1. 在構造完 Prufer 序列後原樹中會剩下兩個節點,其中一個一定是編號最大的點 \(n\)
  2. 每個節點在序列中出現的次數是其度數減 \(1\),因此沒有出現的就是葉節點。

實現

可以 \(O(n)\) 實現這個轉換。

由於最後一定會留下 \(n\),不妨欽定 \(n\) 為根(把 \(n\) 拎起來)。

定義:

  • \(f_i\)\(i\) 的父節點。
  • \(d_i\)\(i\) 的兒子數量。
  • \(p_i\) 為 prufer 序列的第 \(i\) 項。

流程:

定義 \(i\) 表示當前編號最小的葉子的指標,\(cur\) 為當前序列填到哪一項。

  • \(p_{cur} = f_i\)
  • \(d_{p_{cur}}\)\(1\),表示刪去 \(i\) 這個節點。
  • 如果 \(d_{p_{cur}} = 0\)\(p_{cur} < i\),那麼 \(p_{cur}\) 比當前最小值還要小,直接令 \(p_{cur + 1} = f_{p_{cur}}\)
void Tree_to_Prufer() {
	for(int i = 1; i < n; ++ i) {
		cin >> f[i];
		++ d[f[i]];
	}
	for(int cur = 0, i = 1; cur < n - 2; ++ i) {
		while(d[i]) ++ i;
		p[++ cur] = f[i];
		while(cur < n - 2 && -- d[p[cur]] == 0 && p[cur] < i) {
			p[cur + 1] = f[p[cur]];
			++ cur;
		}
	}
	for(int i = 1; i <= n - 2; ++ i) cout << p[i] << ' ';
}

從 Prüfer 序列到樹

  1. 找到當前不在 \(f\) 中且還未使用的最小元素 \(x\)
  2. \(x\)\(f\) 的第一個元素連邊。
  3. 刪除 \(f\) 的第一個元素,如果 \(f\) 非空,重複上述過程。
  4. 最後還剩兩個點未使用,將他們連邊。

程式碼實現類似。

void Prufer_to_Tree() {
	for(int i = 1; i <= n - 2; ++ i) {
		cin >> p[i];
		++ d[p[i]];		// 以 $n$ 為根,兒子數量
	}
	p[n - 1] = n;
	for(int cur = 1, i = 1; cur <= n - 1; ++ cur, ++ i) {
		while(d[i]) ++ i;
		f[i] = p[cur];
		while(cur < n - 1 && -- d[p[cur]] == 0 && p[cur] < i) {
			f[p[cur]] = p[cur + 1];
			++ cur;
		}
	} 
	for(int i = 1; i <= n - 1; ++ i) cout << f[i] << ' ';
}

Caylay 定理

\(n\) 個點的有標號無根樹有 \(n^{n -2}\) 種。

例題

P6086 【模板】Prufer 序列

submission

P2290 [HNOI2004] 樹的計數

題意:給定每個點的度數 \(d_i\),求不同的有標號無根樹個數。

相當於 \(d_i - 1\)\(i\) 排進長為 \(n - 1\) 的序列的方案數。

\[\dfrac{(n - 1)!}{(d_1 - 1)!(d_2 - 1)!\cdots(d_n - 1)!} \]

由於保證了答案小於 \(10^{17}\),可以不用高精,全程用一個大於 \(10^{17}\) 的質數取模(如 \(4179340454199820289\))。

特判無解,根據 prufer 序列的性質,一定有 \(\sum d_i - 1 = n - 2\)

submission

相關文章