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)。
具体见代码:
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <iostream>
#include <vector>
using namespace std;
const int mod = ;
typedef long long ll;
const int N = +; vector<int> G[N];
int n,m,s[N];
int in[N];
ll fac[N],inv[N]; ll qpow(ll x,ll n)
{
ll ans = ;
while(n)
{
if(n&) ans = (ans*x) % mod;
n >>= ;
x = (x*x) % mod;
}
return ans;
} void init()
{
for(int i=;i<=n;i++) G[i].clear();
memset(in,,sizeof(in));
memset(s,,sizeof(s));
} void get()
{
fac[]=inv[]=;
for(int i=;i<=;i++)
{
fac[i] = (fac[i-]*i)%mod;
inv[i] = qpow(i,mod-);
}
} int dfs(int x)
{
int& ans = s[x];
ans = ;
for(int i=;i<G[x].size();i++)
{
int v = G[x][i];
ans += dfs(v);
}
return ans;
} int main()
{
get();
int T;
scanf("%d",&T);
while(T--)
{
init();
scanf("%d%d",&n,&m);
while(m--)
{
int u,v;
scanf("%d%d",&u,&v);
G[v].push_back(u);
in[u] ++;
}
for(int i=;i<=n;i++)
{
if(!in[i]) dfs(i);
} ll ans = fac[n];
for(int i=;i<=n;i++) ans = (ans*inv[s[i]]) % mod;
cout<<ans<<endl;
}
}
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <iostream>
#include <vector>
using namespace std;
const int mod = ;
typedef long long ll;
const int N = +; vector<int> G[N];
int n,m,s[N];
bool vis[N];
ll fac[N],inv[N]; ll qpow(ll x,ll n)
{
ll ans = ;
while(n)
{
if(n&) ans = (ans*x) % mod;
n >>= ;
x = (x*x) % mod;
}
return ans;
} void init()
{
for(int i=;i<=n;i++) G[i].clear();
memset(vis,false,sizeof(vis));
memset(s,,sizeof(s));
} void get()
{
fac[]=inv[]=;
for(int i=;i<=;i++)
{
fac[i] = (fac[i-]*i)%mod;
inv[i] = qpow(i,mod-);
}
} int dfs(int x)
{
int& ans = s[x];
if(ans != ) return ans;
ans = ;
vis[x] = true;
for(int i=;i<G[x].size();i++)
{
int v = G[x][i];
ans += dfs(v);
}
return ans;
} int main()
{
get();
int T;
scanf("%d",&T);
while(T--)
{
init();
scanf("%d%d",&n,&m);
while(m--)
{
int u,v;
scanf("%d%d",&u,&v);
G[v].push_back(u);
}
for(int i=;i<=n;i++)
{
if(!vis[i]) dfs(i);
} ll ans = fac[n];
for(int i=;i<=n;i++) ans = (ans*inv[s[i]]) % mod;
cout<<ans<<endl;
}
}
上面两题都是递归求s的方法,第一种是根据入度来求,第二种是根据记忆化搜索来求。
第二题的代码如下,第二题的代码注释很详细,值得好好研究。代码如下:
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
const int N = +; int n,mod;
bool isprime[N];
int prime[N],tot=,fa[N],cnt[N],vis[N]; void getPrimeTable()
{
memset(isprime,true,sizeof(isprime));
for(int i=;i<N;i++)
{
if(isprime[i])
{
prime[++tot] = i;
for(ll j=(ll)i*i;j<(ll)N;j+=i)
{
isprime[j]=false;
}
}
}
} ll qpow(ll x,ll n)
{
ll ans = ;
while(n)
{
if(n&) ans = (ans*x) % mod;
n >>= ;
x = (x*x) % mod;
}
return ans;
} void getNode() // 这里用bfs的方法从子节点到根节点倒序统计出每个节点所包含的子节点的数目
{
memset(cnt,,sizeof(cnt)); // 这里cnt表示的是每个节点所包含的节点的数目
queue<int> Q;
for(int i=;i<=n;i++)
{
if(!vis[i]) Q.push(i); // 如果该节点出度为0,则是叶子节点,加入队列
}
while(!Q.empty())
{
int x = Q.front();Q.pop();
cnt[x] += ; // 该叶子节点本身也算在内
cnt[fa[x]] += cnt[x]; // 父亲节点数增加该节点的节点数
vis[fa[x]] --; // 父亲节点的出度减1
if(!vis[fa[x]]) Q.push(fa[x]);
}
} void init()
{
memset(vis,,sizeof(vis));
for(int i=;i<=n;i++)
{
scanf("%d",fa+i);
vis[fa[i]] ++; // 这里vis表示的是该点的出度
}
getNode(); memset(vis,,sizeof(vis)); // 这里vis重新用于记载某个分母中某个因子存在的个数
for(int i=;i<=n;i++) // 因为s[i]的数目最大不会超过n
vis[cnt[i]] ++;
} void cal(int u,int dt) // 用于分解质因数的计算
{
for(int i=;i<=tot;i++)
{
int k = prime[i];
while(u%k == )
{
cnt[k] += dt;
u /= k;
}
if(isprime[u]) // 如果u已经是质数了,就可以返回了
{
cnt[u] += dt;
return;
}
}
} ll solve()
{
memset(cnt,,sizeof(cnt)); // 这里cnt重新用于记录分数上下约分以后每个质因数的个数
for(int i=;i<=n;i++) cal(i,); // 对n的阶乘的计算
for(int i=;i<=n;i++)
{
if(vis[i]) cal(i,-vis[i]); // 对分母的计算
} ll ans = ;
for(int i=;i<=tot;i++)
{
int k = prime[i];
if(cnt[k]) ans = (ans*qpow(k,(ll)cnt[k])) % mod;
}
return ans;
} int main()
{
getPrimeTable();
int T;
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&mod);
init();
cout<<solve()<<endl;
}
}
另外值得注意的是,线性筛素数的方法。
UVA 11174 Stand in a Line,UVA 1436 Counting heaps —— (组合数的好题)的更多相关文章
- uva 11174 Stand in a Line
// uva 11174 Stand in a Line // // 题目大意: // // 村子有n个村民,有多少种方法,使村民排成一条线 // 使得没有人站在他父亲的前面. // // 解题思路: ...
- UVA 11174 Stand in a Line 树上计数
UVA 11174 考虑每个人(t)的所有子女,在全排列中,t可以和他的任意子女交换位置构成新的排列,所以全排列n!/所有人的子女数连乘 即是答案 当然由于有MOD 要求逆. #include & ...
- uva 11174 Stand in a Line (排列组合)
UVa Online Judge 训练指南的题目. 题意是,给出n个人,以及一些关系,要求对这n个人构成一个排列,其中父亲必须排在儿子的前面.问一共有多少种方式. 做法是,对于每一个父节点,将它的儿子 ...
- UVA 11174 Stand in a Line (组合+除法的求模)
题意:村子里有n个人,给出父亲和儿子的关系,有多少种方式可以把他们排成一列,使得没人会排在他父亲的前面 思路:设f[i]表示以i为根的子树有f[i]种排法,节点i的各个子树的根节点,即它的儿子为c1, ...
- UVA 11174 Stand in a Line 树dp+算
主题链接:点击打开链接 题意:白书的P103. 加个虚根就能够了...然后就是一个多重集排列. import java.io.PrintWriter; import java.util.ArrayLi ...
- 【递推】【推导】【乘法逆元】UVA - 11174 - Stand in a Line
http://blog.csdn.net/u011915301/article/details/43883039 依旧是<训练指南>上的一道例题.书上讲的比较抽象,下面就把解法具体一下.因 ...
- uva 1436 - Counting heaps(算)
题目链接:uva 1436 - Counting heaps 题目大意:给出一个树的形状,如今为这棵树标号,保证根节点的标号值比子节点的标号值大,问有多少种标号树. 解题思路:和村名排队的思路是一仅仅 ...
- 数学:UVAoj 11174 Stand in a Line
Problem J Stand in a Line Input: Standard Input Output: Standard Output All the people in the bytela ...
- CDQ分治入门 + 例题 Arnooks's Defensive Line [Uva live 5871]
CDQ分治入门 简介 CDQ分治是一种特别的分治方法,它由CDQ(陈丹琦)神犇于09国家集训队作业中首次提出,因此得名.CDQ分治属于分治的一种.它一般只能处理非强制在线的问题,除此之外这个算法作为某 ...
随机推荐
- 怎样通过CSS选择器获取元素节点或元素节点集合
使用 document.querySelector() 和 document.querySelectorAll(), 将 CSS选择器 作为参数传入即可. // 标签选择器 document.quer ...
- javascript常用内置对象——Array对象
Array对象: 创建 Array 对象的语法: new Array(); new Array(元素个数); new Array(element0, element1, ..., elementn); ...
- sql server 数据库中明明有值但是查询怎么都查不到值
产生原因是因为编码问题 数据库是英文版 但是数据库中数据又是中文的 所以查询中文时需要加上N select * from customer where Username=N'张三'
- javabean转成json字符首字母大写
今天写接口的时候有个需求将接口返回的json字符串首字母大写:{"SN":"","Result":""}格式, 只需要在 ...
- asp.net 代码片段的
片段标签 说明 <% ...
- easyUi 的form和validate组件
以下代码不能运行,只是我在学习过程中记录的笔记,但代码可以用!!! 可以按照需要截取. <%@ page language="java" contentType=" ...
- LLVM的RTTI特性
本文思路来源于http://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html,叙述有不同,望谅解,希望能从其他方面帮助大家了解C++语言的底层实现. 背景 在LLV ...
- 2.XML语言
XML语言 常见应用: XML技术除用于 /*保存有关系的数据*/之外,它还经常作软件配置文件,以描述程序模块之间的关系. 在一个系统软件中,为提高系统的灵活性,它所启动的模块通常由其配置文件决定 例 ...
- BootStrap【三、组件】
特有标签属性 role 用于浏览器识别 aria-label 用于浏览器识别 tabIndex 用于浏览器识别 data- 自定义数据属性 图标 直接引用官方图标库中的class 官方图标库 Demo ...
- 前端基础(三):JavaScript
JavaScript概述 JavaScript的历史 1992年Nombas开发出C-minus-minus(C--)的嵌入式脚本语言(最初绑定在CEnvi软件中),后将其改名ScriptEase(客 ...