bzoj1005 [HNOI2008]明明的烦恼(Prufer数列+高精度(wys算法。。。))

题目链接

分析:
题目所求的就是一个叫:有标号无根树 的数目

我们怎么知道两棵树的形态是否一样呢?
换句话说,我们能不能找到一种表示树的方法,使得不同的树表示出来互不相同呢?

那么我们就需要一个新姿势了:Prufer数列


JMJST大大的blog

Prufer数列是无根树的一种数列
在组合数学中,Prufer数列是由一个对于顶点标过号的树转化来的数列,点数为n的树转化来的Prufer数列长度为n-2

Prufer数列的生成

找出这棵树的当前叶子结点中标号最小的那个,把与ta相连的点的编号加入 prufer p r u f e r 数列,然后删除这个叶子结点
操作到最后剩下两个结点时,这两个结点会被同时删除

举个例子:
这里写图片描述

Prufer数列转化成数

每次找到当前prufer数列中未出现的编号中最小的编号,把ta对应的点和当前prufer数列中开头的点连接,然后删除数列开头的点

Prufer数列的性质

  • 一棵 n n 个结点的有编号无根树的prufer数列长度为n2
    显然,每次删除一个结点就会在数列中加入一个数,直到剩最后两个结点的ta们同时被删除,数列中自然有 n2 n − 2 个数

  • 原树中结点度数为 di d i 的结点编号在prufer数列中出现 di1 d i − 1
    由于一个结点在作为叶子结点删除时,ta的当前度数为1,而每删除一个与ta相邻的叶子结点时,ta的编号会被计入一次,且ta的度数会-1,所以ta最终会被记录 di1 d i − 1 次,然后作为叶子结点被删除

  • 每个prufer数列都和一棵有编号无根树意义对应
    由于在对有标号无根树进行操作时,每一步的操作都是固定的(选取最小标号叶子节点),所以每棵无根树只对应唯一一个prufer数列
    由于prufer数列在还原成树时每步操作也是唯一的,所以每个prufer数列只能还原成唯一的一棵树


对于已确定度数的点( cnt c n t 个),我们可以计算出ta们出现的次数之和: sum=cnti=1(di1) s u m = ∑ i = 1 c n t ( d i − 1 )
那么这 sum s u m 个元素,在长度为 n2 n − 2 的序列中的不同排列数为:
(在 n2 n − 2 中选择 sum s u m 个位置,这些位置上进行排序)

C(n2,sum)sum!cnti=1(di1)! C ( n − 2 , s u m ) ∗ s u m ! ∏ i = 1 c n t ( d i − 1 ) !

剩下的 n2sum n − 2 − s u m 个空位可以随意排列 ncnt n − c n t 个点

C(n2,sum)sum!cnti=1(di1)!(ncnt)n2sum C ( n − 2 , s u m ) ∗ s u m ! ∏ i = 1 c n t ( d i − 1 ) ! ∗ ( n − c n t ) n − 2 − s u m

式子化一下:

(n2)!(n2sum)!cnti=1(di1)!(ncnt)n2sum ( n − 2 ) ! ( n − 2 − s u m ) ! ∗ ∏ i = 1 c n t ( d i − 1 ) ! ∗ ( n − c n t ) n − 2 − s u m

但是这个数实在是太大了,又没有模数
我们只能选择高精度

后面的可以重定义乘法,直接用快速幂
但是前面的除法就有点难受了,怎么破?
我们可以把所有数质因数分解,最后把质因数乘起来即可(高乘低)

无解情况: sum>n2,di=0 s u m > n − 2 , d i = 0

tip

一开始WA了,好像是高精度写错了
后来才发现,因为数太大了,高精度的位数高达1W

注意,只有全局变量不用初始化,局部变量一定要初始化

又T了(时间卡太紧)
memset太费时,去掉
申请结点费事,去掉
三部分都分解质因数费时,只分解 (n2)!(n2sum)!cnti=1(di1)! ( n − 2 ) ! ( n − 2 − s u m ) ! ∗ ∏ i = 1 c n t ( d i − 1 ) !
最后变成这个熊样,A掉了

#include<cstdio>
#include<cstring>
#include<iostream>

using namespace std;

const int N=1005;
int sshu[N],n,d[N],sum=0,cnt=0,tot=0;
struct node{
    int data[100000],len;
};
node ans;

void mult(node &a,int x) {      //高乘低
    for (int i=1;i<=a.len;i++) a.data[i]*=x;
    for (int i=1;i<=a.len;i++) {
        a.data[i+1]+=a.data[i]/10;
        a.data[i]%=10;
    }
    while (a.data[ans.len+1]) {
        a.len++;
        a.data[a.len+1]+=a.data[a.len]/10;
        a.data[a.len]%=10;
    }
}

void cl(int x,int z) {
    if (x<=1) return;
    for (int i=2;i*i<=x;i++) 
        while (x%i==0) x/=i,sshu[i]+=z;
    if (x!=1) sshu[x]+=z;
}

int main()
{
    scanf("%d",&n); 
    if (n==1) {
        int x;
        scanf("%d",&x);
        if (x==0||x==-1) printf("1\n");
        else printf("0\n");
        return 0;
    }
    for (int i=1;i<=n;i++) {
        int x;
        scanf("%d",&x);
        if (!x) {
            printf("0\n");
            return 0;
        } 
        else if (x!=-1) sum+=x-1,d[++tot]=x-1;
        else cnt++;                            //未知度数的结点数 
    }
    if (sum>n-2) {
        printf("0\n");
        return 0;
    }

    for (int i=n-2;i>n-2-sum;i--) cl(i,1);
    for (int i=1;i<=tot;i++) 
        for (int j=2;j<=d[i];j++) cl(j,-1);

    ans.data[1]=1; ans.len=1;
    for (int i=2;i<=n;i++)
        while (sshu[i]) mult(ans,i),sshu[i]--;
    for (int i=1;i<=n-sum-2;i++) mult(ans,cnt);
    for (int i=ans.len;i>=1;i--) printf("%d",ans.data[i]);
    return 0;
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值