Prüfer

入门

长度为 \(n - 2\) 的双射,可以构建起数列和无根树的关系。

构建方法 :

  • \(\text index\) 最小的叶子,向 Prüfer 加入其父亲,并删除其本身。

大概是非常容易写 \(n \log n\) 的小根堆即可。

摘自 OI-wiki :

vector<vector<int>> adj;

vector<int> pruefer_code() {
  int n = adj.size();
  set<int> leafs;
  vector<int> degree(n);
  vector<bool> killed(n, false);
  for (int i = 0; i < n; i++) {
    degree[i] = adj[i].size();
    if (degree[i] == 1) leafs.insert(i);
  }

  vector<int> code(n - 2);
  for (int i = 0; i < n - 2; i++) {
    int leaf = *leafs.begin();
    leafs.erase(leafs.begin());
    killed[leaf] = true;
    int v;
    for (int u : adj[leaf])
      if (!killed[u]) v = u;
    code[i] = v;
    if (--degree[v] == 1) leafs.insert(v);
  }
  return code;
}

但是其实可以做到线性构造。

vector<vector<int>> adj;
vector<int> parent;

void dfs(int v) {
  for (int u : adj[v]) {
    if (u != parent[v]) parent[u] = v, dfs(u);
  }
}

vector<int> pruefer_code() {
  int n = adj.size();
  parent.resize(n), parent[n - 1] = -1;
  dfs(n - 1);

  int ptr = -1;
  vector<int> degree(n);
  for (int i = 0; i < n; i++) {
    degree[i] = adj[i].size();
    if (degree[i] == 1 && ptr == -1) ptr = i;
  }

  vector<int> code(n - 2);
  int leaf = ptr;
  for (int i = 0; i < n - 2; i++) {
    int next = parent[leaf];
    code[i] = next;
    if (--degree[next] == 1 && next < ptr) {
      leaf = next;
    } else {
      ptr++;
      while (degree[ptr] != 1) ptr++;
      leaf = ptr;
    }
  }
  return code;
}

大概是考虑暴力删除所有叶子,然后看看父亲行不行。那时间复杂度怎么保证?这个程序是先逐次删除所有叶子节点,那么可以考虑在过程中父亲若 \(\text index\) 较小,那必然会提前删除,否则会留到后面删,故可以保证只会遍历到每一个节点的父亲边到一次,故为 \(O(n)\)

重构树

那么我们可以考虑先求出 \(\text deg_i\) 那么我们可以找度数为 1 意义下的 \(\text index\) 最小,然后连到 Prüfer 上面即可。

这个可以用小根堆维护,但也有线性方法, 方法大致和上面差不多,直接塴了 OI-wiki 了。

vector<pair<int, int>> pruefer_decode(vector<int> const& code) {
  int n = code.size() + 2;
  vector<int> degree(n, 1);
  for (int i : code) degree[i]++;

  int ptr = 0;
  while (degree[ptr] != 1) ptr++;
  int leaf = ptr;

  vector<pair<int, int>> edges;
  for (int v : code) {
    edges.emplace_back(leaf, v);
    if (--degree[v] == 1 && v < ptr) {
      leaf = v;
    } else {
      ptr++;
      while (degree[ptr] != 1) ptr++;
      leaf = ptr;
    }
  }
  edges.emplace_back(leaf, n - 1);
  return edges;
}

trick

完全图 \(K_n\)\(n^{n-2}\) 棵生成树。

考虑到每个节点都有边,那么就可以随便生成树,再考虑 Prüfer 有多少种即可。

度数为 \(d_1 \to d_n\) 的无根树方案有 :

\[\tbinom {n - 2}{d_1 - 1, d_2 - 1, ..., d_n- 1} = \frac{(n - 2)!}{\prod(d_n- 1)!} \]

种。

CF156D Clues 记得交题。

一个 \(n\) 个点 \(m\) 条边的带标号无向图有 \(k\) 个连通块。我们希望添加 \(k-1\) 条边使得整个图连通。求方案数。

考虑连通块缩成点。

\[\tbinom {k - 2}{d_1 - 1, d_2 - 1, ..., d_k - 1} = \frac{(k - 2)!}{\prod(d_i - 1)!} \]

考虑原图联通,不同情况还要乘上:\(\prod size_i ^ {d_i}\)

有点 trash,考虑 \(e_i \to d_i - 1\)

\[\sum_{e_i \geq 0, \sum e_i = k - 2} \tbinom{k - 2}{e_1, ...., e_k}\prod size_i ^ {e_i + 1} \]

考虑多项式定理。

\[(x_1 + x——2 + .... + x_n)^p = \sum_{e_i \geq 0, \sum e_i = p} \tbinom{p}{e_1, ...., e_k}\prod x_i ^ {e_i} \]

然后就是带入有 \((size_1 + size_2 + .... + size_k)^{k - 2}\prod size_i = n^{k - 2} \prod size_i\)

P6086 【模板】Prüfer 序列

P2290 [HNOI2004] 树的计数

P2624 [HNOI2008] 明明的烦恼

考虑到其占有 \(x = \sum d_i - 1\) 这么多位置。那么答案就是 :

\[\tbinom{n - 2}{x} \frac{x!}{\prod{d_i - 1}} (n - node)^{n - 2 - cnt} \]

大概是这样的。用 py 写。

n = (int)(input());
k = 0;
s = 0;
a = [0 for i in range(n + 5)];
for i  in range(1, n + 1) :
	a[i] = (int)(input());
	if a[i] == 0 : print(0); exit();
	if a[i] != -1 : k += 1; s += a[i] - 1;
if s > n - 2 : print(0); exit();
f = [0 for i in range(n + 5)]; 
f[0] = 1;
for i in range(1, n + 1) : f[i] = f[i - 1] * i;
ans = ans = (f[n - 2] // f[s] // f[n - 2 - s]) * f[s]
for i in range(1, n + 1) :
	if a[i] != -1 : ans //= f[a[i] - 1];
print(ans * pow(n - k, n - 2 - s));
posted @ 2023-07-06 07:48  Cust10  阅读(65)  评论(0)    收藏  举报