• Prufer 序列


    Prufer 序列

    定义与建立

    Prufer 序列可以将一个带标号 \(n\) 个结点的树用 \([1,n]\) 中的 \(n-2\) 个整数表示。一个无向带标号生成树与数列之间的双射。

    对于一棵树,每次我们选择它编号最小叶子结点,删除它并记录下与它相连的节点的编号,那么最终记录下的 \(n-2\) 个数就组成了这棵树的 Prufer 序列。

    显然用这个东西维护树的结构感觉非常不好,这个东西主要用来数数用的。

    对树建立 Prufer 序列

    按照定义直接建立,那么每次从堆中取出最小的数,删去它并记录。

    这样可以得到一个 \(\mathcal{O(n\log n)}\) 的做法,但是显然这不够优美,我们需要一个 \(\mathcal{O(n)}\) 的解法。

    我们发现剩余的叶子结点数量是递减的,每次删除要么少一个,要么少一个又多一个。

    从小到大枚举编号 \(p\),如果是叶子结点,将和它们相邻的点放入 Prufer 序列中。考虑这个叶子结点带来的新叶子结点:

    • 如果和它相邻的点编号 \(p'>p\) 或者 \(p'\) 没有变成叶子结点,那么不用管,它会在之后被枚举到。
    • 但是如果 \(p'<p\),它在时候不会被枚举到了。但是发现删除之前当前最小的叶子结点是 \(p\),所以删后 \(p'\) 必然是最小的叶子,所以可以直接继续删除 \(p'\)
    for(int i=1,x;i<n;i++) x=rd(),add(i,x),add(x,i),ind[i]++,ind[x]++;
    // [1,n-1] 的父亲序列 
    for(int i=1;i<=n;i++) if(ind[i]==1) exist[i]=true;
    for(int i=1,p;i<=n;i++)
    {
    	 if(!exist[i]) continue;
    	 p=i;
    	 while(p<=i)
    	 {
    	 	 used[p]=true;
    	 	 for(int j=hea[p];j;j=nex[j])
    	 	 	 if(!used[ver[j]]) { p=ver[j]; break; }
    	 	 if(ind[p]==1) break;
    	 	 ind[p]--,pru[++cnt]=p;
    	 	 if(ind[p]==1) exist[p]=true;
    	 	 if(ind[p]!=1 || p>i) break;
    	 }
    	 if(ind[p]==1) exist[p]=true;
    }
    assert(cnt==n-2);
    for(int i=1;i<=cnt;i++) ret^=1ll*i*pru[i];
    printf("%lld\n",ret);
    

    用 Prufer 还原无根树

    发现 Prufer 序列中的每个数的出现次数就是原树中每个点的度数 \(-1\),可以每次连一个叶子结点重构。

    方法类似,一个暴力的做法是每次选出最小的当前的叶子结点,将它与 Prufer 序列最前面的元素相连,这样复杂度是 \(\mathcal{O(n\log n)}\) 的。

    发现叶子结点编号递增,设删去的叶子结点为 \(p\),和它相邻的是 \(p'\)。当加完 \(p\)\(p'\) 的度数变为 \(1\) 并且 \(p'<p\),那么它下一次一定被选择,直接继续连边即可。

    for(int i=1;i<=n-2;i++) pru[i]=rd(),ind[pru[i]]++;
    for(int i=1;i<=n;i++) if(!ind[i]) exist[i]=true;
    int pos=1;
    for(int i=1,p;i<=n;i++)
    {
    	 if(!exist[i]) continue;
    	 p=i;
    	 while(p<=i)
    	 {
    	 	 if(pos==n-1) { fa[p]=n; break; }
    	 	 fa[p]=pru[pos],p=pru[pos],ind[p]--,pos++;
    	 	 if(pos>=n) break;
    	 	 if(ind[p] || p>i) break;
    	 }
    	 if(!ind[p]) exist[p]=true;
    }
    for(int i=1;i<n;i++) ret^=1ll*i*fa[i];
    printf("%lld\n",ret);
    

    主要性质

    • Prufer 序列与无根树一一对应(好像比较显然)

    • 度数为 \(d_i\) 的点在 Prufer 序列中出现 \(d_i-1\) 次(上面还用到了这个结论)

    • 【根据度数求方案】对于给定每个点度数为 \(d_i\) 的无根树,方案数为:

      \[\dfrac{(n-2)!}{\prod_{i=1}^{n}(d_i-1)!} \]

      证明:Prufer 序列长度为 \(n-2\),每个数出现次数为 \(d_i-1\),根据组合意义直接计算全排列即可。

    • 【根据连通块数量与大小求方案】一个 \(n\) 个点 \(m\) 条边的带标号无向图有 \(k\) 个连通块,每个连通块大小为 \(s_i\),需要增加 \(k-1\) 条边使得整个图联通,方案数为:(但是当 \(k=1\) 时需要特判)

      \[n^{k-2}\cdot\prod_{i=1}^{k}s_i \]

      证明见 OI-wiki

  • 相关阅读:
    LuaJIT 之 FFI
    rtmp时间戳问题导致的丢帧,帧率显示错误
    c# 调用 c++的 dll 中关于 char*传入传出参数
    “Microsoft”中不存在类型或命名空间名称“Office”(是否缺少程序集引用?)
    udp,select超时和recvfrom收不到数据原因
    rtmpdump应用在window中
    Win7下安装openssl
    debug模式不报错,release模式报错
    qt 断点无效
    http协议 c++ 接收
  • 原文地址:https://www.cnblogs.com/EricQian/p/15996222.html
Copyright © 2020-2023  润新知