UVA 11174 Stand in a Line,UVA 1436 Counting heaps —— (组合数的好题)

  这两个题的模型是有n个人,有若干的关系表示谁是谁的父亲,让他们进行排队,且父亲必须排在儿子前面(不一定相邻)。求排列数。

  我们假设s[i]是i这个节点,他们一家子的总个数(或者换句话说,等于他的子孙数+1(1是他本身)),f[i]是以i为根的节点的排列种数。那么总的种数为n!/(s[1]+s[2]+...+s[n])。关于这个递推式的得出,是根据排列公式的,我们假设一个例子,不妨设4.5是2的子孙,3是1的子孙。那么他们进行排队的话,不妨看成222和11排队就是2的家族和1的家族排队(2和1是平行关系)。那么他们如果真的是全部一样的话,排列数为5!/(2!*3!)。然后再根据他们自身有的排列种数,不妨设为f[2],f[1],那么总的排列数为他们相乘,即:f[2]*f[1]*5!/(2!*3!)。因此,推广开来对于一个节点i(这里看做是2,1的父亲),那么以节点i为根的子节点的排列数为:f[i]=f[c1]*f[c2]*...*d[ck]*(s[i]-1)!/(s[c1]!*s[c2]!*...*s[ck]!),其中这c1到ck代表这他的若干个儿子,如果将每个f都进行同样的递推,一直到这个节点是一个点为止,这样可以消去所有的f(因为一个点的排序种数就是1),这样子的话,可以化成f[i]=(s[i]-1)!/(s[1]+s[2]+...+s[ck]),对所有点,用一个虚拟节点0当做父亲,这样,得到f[0]=(s[0]-1)!/(s[1]+s[2]+...+s[n])。考虑到s[0]=n+1,因此得到上面的公式。

  得到这个公式以后,下面的代码都顺理成章了。第一题用的是求逆元,因为mod的是一个素数,而第二题不是,所以用的是分解质因数。第二题是一个很好的题,涉及到了很多知识点,还包括了非递归的求一棵树的每个节点的包含的子节点的个数的方法(即上面的s,当然,前面的说法不准确,加上1才是上面的s)。

  具体见代码:

 1 #include <stdio.h>
 2 #include <algorithm>
 3 #include <string.h>
 4 #include <iostream>
 5 #include <vector>
 6 using namespace std;
 7 const int mod = 1000000007;
 8 typedef long long ll;
 9 const int N = 40000+5;
10 
11 vector<int> G[N];
12 int n,m,s[N];
13 int in[N];
14 ll fac[N],inv[N];
15 
16 ll qpow(ll x,ll n)
17 {
18     ll ans = 1;
19     while(n)
20     {
21         if(n&1) ans = (ans*x) % mod;
22         n >>= 1;
23         x = (x*x) % mod;
24     }
25     return ans;
26 }
27 
28 void init()
29 {
30     for(int i=1;i<=n;i++) G[i].clear();
31     memset(in,0,sizeof(in));
32     memset(s,0,sizeof(s));
33 }
34 
35 void get()
36 {
37     fac[1]=inv[1]=1;
38     for(int i=2;i<=40000;i++)
39     {
40         fac[i] = (fac[i-1]*i)%mod;
41         inv[i] = qpow(i,mod-2);
42     }
43 }
44 
45 int dfs(int x)
46 {
47     int& ans = s[x];
48     ans = 1;
49     for(int i=0;i<G[x].size();i++)
50     {
51         int v = G[x][i];
52         ans += dfs(v);
53     }
54     return ans;
55 }
56 
57 int main()
58 {
59     get();
60     int T;
61     scanf("%d",&T);
62     while(T--)
63     {
64         init();
65         scanf("%d%d",&n,&m);
66         while(m--)
67         {
68             int u,v;
69             scanf("%d%d",&u,&v);
70             G[v].push_back(u);
71             in[u] ++;
72         }
73         for(int i=1;i<=n;i++)
74         {
75             if(!in[i]) dfs(i);
76         }
77 
78         ll ans = fac[n];
79         for(int i=1;i<=n;i++) ans = (ans*inv[s[i]]) % mod;
80         cout<<ans<<endl;
81     }
82 }
View Code
 1 #include <stdio.h>
 2 #include <algorithm>
 3 #include <string.h>
 4 #include <iostream>
 5 #include <vector>
 6 using namespace std;
 7 const int mod = 1000000007;
 8 typedef long long ll;
 9 const int N = 40000+5;
10 
11 vector<int> G[N];
12 int n,m,s[N];
13 bool vis[N];
14 ll fac[N],inv[N];
15 
16 ll qpow(ll x,ll n)
17 {
18     ll ans = 1;
19     while(n)
20     {
21         if(n&1) ans = (ans*x) % mod;
22         n >>= 1;
23         x = (x*x) % mod;
24     }
25     return ans;
26 }
27 
28 void init()
29 {
30     for(int i=1;i<=n;i++) G[i].clear();
31     memset(vis,false,sizeof(vis));
32     memset(s,0,sizeof(s));
33 }
34 
35 void get()
36 {
37     fac[1]=inv[1]=1;
38     for(int i=2;i<=40000;i++)
39     {
40         fac[i] = (fac[i-1]*i)%mod;
41         inv[i] = qpow(i,mod-2);
42     }
43 }
44 
45 int dfs(int x)
46 {
47     int& ans = s[x];
48     if(ans != 0) return ans;
49     ans = 1;
50     vis[x] = true;
51     for(int i=0;i<G[x].size();i++)
52     {
53         int v = G[x][i];
54         ans += dfs(v);
55     }
56     return ans;
57 }
58 
59 int main()
60 {
61     get();
62     int T;
63     scanf("%d",&T);
64     while(T--)
65     {
66         init();
67         scanf("%d%d",&n,&m);
68         while(m--)
69         {
70             int u,v;
71             scanf("%d%d",&u,&v);
72             G[v].push_back(u);
73         }
74         for(int i=1;i<=n;i++)
75         {
76             if(!vis[i]) dfs(i);
77         }
78 
79         ll ans = fac[n];
80         for(int i=1;i<=n;i++) ans = (ans*inv[s[i]]) % mod;
81         cout<<ans<<endl;
82     }
83 }
View Code

上面两题都是递归求s的方法,第一种是根据入度来求,第二种是根据记忆化搜索来求。

  第二题的代码如下,第二题的代码注释很详细,值得好好研究。代码如下:

  1 #include <stdio.h>
  2 #include <algorithm>
  3 #include <string.h>
  4 #include <iostream>
  5 #include <vector>
  6 #include <queue>
  7 using namespace std;
  8 typedef long long ll;
  9 const int N = 500000+5;
 10 
 11 int n,mod;
 12 bool isprime[N];
 13 int prime[N],tot=0,fa[N],cnt[N],vis[N];
 14 
 15 void getPrimeTable()
 16 {
 17     memset(isprime,true,sizeof(isprime));
 18     for(int i=2;i<N;i++)
 19     {
 20         if(isprime[i])
 21         {
 22             prime[++tot] = i;
 23             for(ll j=(ll)i*i;j<(ll)N;j+=i)
 24             {
 25                 isprime[j]=false;
 26             }
 27         }
 28     }
 29 }
 30 
 31 ll qpow(ll x,ll n)
 32 {
 33     ll ans = 1;
 34     while(n)
 35     {
 36         if(n&1) ans = (ans*x) % mod;
 37         n >>= 1;
 38         x = (x*x) % mod;
 39     }
 40     return ans;
 41 }
 42 
 43 void getNode() // 这里用bfs的方法从子节点到根节点倒序统计出每个节点所包含的子节点的数目
 44 {
 45     memset(cnt,0,sizeof(cnt)); // 这里cnt表示的是每个节点所包含的节点的数目
 46     queue<int> Q;
 47     for(int i=1;i<=n;i++)
 48     {
 49         if(!vis[i]) Q.push(i); // 如果该节点出度为0,则是叶子节点,加入队列
 50     }
 51     while(!Q.empty())
 52     {
 53         int x = Q.front();Q.pop();
 54         cnt[x] += 1; // 该叶子节点本身也算在内
 55         cnt[fa[x]] += cnt[x]; // 父亲节点数增加该节点的节点数
 56         vis[fa[x]] --; // 父亲节点的出度减1
 57         if(!vis[fa[x]]) Q.push(fa[x]);
 58     }
 59 }
 60 
 61 void init()
 62 {
 63     memset(vis,0,sizeof(vis));
 64     for(int i=2;i<=n;i++)
 65     {
 66         scanf("%d",fa+i);
 67         vis[fa[i]] ++; // 这里vis表示的是该点的出度
 68     }
 69     getNode();
 70 
 71     memset(vis,0,sizeof(vis)); // 这里vis重新用于记载某个分母中某个因子存在的个数
 72     for(int i=1;i<=n;i++) // 因为s[i]的数目最大不会超过n
 73         vis[cnt[i]] ++;
 74 }
 75 
 76 void cal(int u,int dt) // 用于分解质因数的计算
 77 {
 78     for(int i=1;i<=tot;i++)
 79     {
 80         int k = prime[i];
 81         while(u%k == 0)
 82         {
 83             cnt[k] += dt;
 84             u /= k;
 85         }
 86         if(isprime[u]) // 如果u已经是质数了,就可以返回了
 87         {
 88             cnt[u] += dt;
 89             return;
 90         }
 91     }
 92 }
 93 
 94 ll solve()
 95 {
 96     memset(cnt,0,sizeof(cnt)); // 这里cnt重新用于记录分数上下约分以后每个质因数的个数
 97     for(int i=2;i<=n;i++) cal(i,1); // 对n的阶乘的计算
 98     for(int i=1;i<=n;i++)
 99     {
100         if(vis[i]) cal(i,-vis[i]); // 对分母的计算
101     }
102 
103     ll ans = 1;
104     for(int i=1;i<=tot;i++)
105     {
106         int k = prime[i];
107         if(cnt[k]) ans = (ans*qpow(k,(ll)cnt[k])) % mod;
108     }
109     return ans;
110 }
111 
112 int main()
113 {
114     getPrimeTable();
115     int T;
116     scanf("%d",&T);
117     while(T--)
118     {
119         scanf("%d%d",&n,&mod);
120         init();
121         cout<<solve()<<endl;
122     }
123 }
View Code

另外值得注意的是,线性筛素数的方法。

原文地址:https://www.cnblogs.com/zzyDS/p/5664778.html