學習筆記:Prufer 編碼
阿新 • • 發佈:2020-09-07
Prufer 編碼可以將無根樹與序列之間進行轉化。
一個 \(n\) 個點、區分編號的無向圖 和 Prufer 序列一定是一一對應的,下面會給出對映方式。
藉此可以證明 Cayley 定理: \(n\) 個點的無根、區分編號生成樹個數為 \(n ^ {n-2}\)
無根樹轉序列
設一棵 \(n\) 個節點的無根樹,寫出轉化為 Prufer 序列的步驟:
- 找到編號最小的葉節點 \(x\),把 \(x\) 相鄰的點加入序列,然後,刪掉 \(x\)
- 當點數 \(= 2\) 時,終止(若不終止這次輸出的一定是 \(n\) 所以是確定資訊 # 無區分資訊),否則繼續迭代。
所以 Prufer 序列長度為 \(n - 2\)
依此我們可以看出一個性質
考慮 \(x\) 在 Prufer 序列中出現的次數 + 1: \(cnt_x + 1\) 即 \(x\) 的度數
顯然可以 \(O(n \log n)\) 用堆做。
也有線性 \(O(n)\) 的做法:
-
從小到大列舉哪個選擇的點 \(j\)
-
每次刪除,至多會多出來一個待選葉子點 \(x\)。
-
若沒有多出來葉子點 / \(x > j\),那麼繼續增大 \(j\)
-
否則,遞迴繼續刪除 \(x\) 即可。
-
序列轉無根樹
步驟:
考慮 \(x\) 在序列中出現的次數 + 1: \(cnt_x + 1\) 即 \(x\) 的度數,所有 \(cnt_x = 1\)
- 按順序考慮序列每一項 \(x\)
- 從備選集合中找到編號最小的 \(y\),連邊 \((x, y)\)。
- 減少 \(cnt_x \gets cnt_x - 1\),若減到 \(1\),把 \(x\) 加入備選集合。
複雜度也可以做到 \(O(n)\),和上述方法類似。
想法
後來突然有一個疑問:為什麼轉化後的 Prufer 是唯一的呢?
後來百思不得其解後發現自己鑽牛角尖了,如上兩步,我們給出了:
- Prufer 序列轉為唯一確定形態無根樹的方法
- 無根樹轉化為唯一確定 Prufer 序列的方法
那麼無根樹和 Prufer 序列就是一一對應的了。
模板題
注意,這裡規定了 \(n\) 為根,那麼顯然更方便了一點,一定是從葉子往上刪:
- \(cnt_x\) 為 \(x\) 的兒子數
- \(cnt_x = 0\) 加入備選集合
就可以了。
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 100005;
int n, m, f[N], p[N], d[N];
void inline fToP() {
for (int i = 1; i < n; i++) d[f[i]]++;
for (int i = 1, j = 1; i <= n - 2; j++) {
while (d[j]) j++;
p[i++] = f[j];
while (i <= n - 2 && --d[p[i - 1]] == 0 && p[i - 1] < j) p[i++] = f[p[i - 1]];
}
}
void inline pToF() {
for (int i = 1; i <= n - 2; i++) d[p[i]]++;
p[n - 1] = n;
for (int i = 1, j = 1; i < n; i++, j++) {
while (d[j]) j++;
f[j] = p[i];
while (i < n - 1 && --d[p[i]] == 0 && p[i] < j) f[p[i]] = p[i + 1], ++i;
}
}
int main() {
scanf("%d%d", &n, &m);
if (m == 1) {
for (int i = 1; i < n; i++) scanf("%d", f + i);
fToP();
for (int i = 1; i <= n - 2; i++) printf("%d ", p[i]);
} else {
for (int i = 1; i <= n - 2; i++) scanf("%d", p + i);
pToF();
for (int i = 1; i < n; i++) printf("%d ", f[i]);
}
return 0;
}