这两天要把这本比较简单的组合数学看完, 然后收获什么的就写这里了。
【重复组合】
这个以前应该学过。。n个物品可重复选择地选取m个。 可以想象成有m个没有区别的小球然后插n - 1个挡板, 每个挡板隔开的就是一种颜色了, 所以数量应该是小球和挡板排列起来的(n + m - 1)! 然后因为挡板间和同种颜色的小球间是没有区别的, 所以要再除以(n - 1)! 和 (m)!, 最后的结果应该就是 C(n, n + m - 1) 。
【Cayley 定理】
n个有标号(1 ~ n)的顶点的树的数目是n ^ (n - 2)。
我觉得这个定理相当的不显然。。所以说应该背下来。不过百度了一下cayley定理好像不是这个东西啊。。
这里证明的方法是每次删掉最小的那个点并且把与它相邻的那个点放入一个序列中, 最后没有在序列中出现过的点显然就是树在初始时候的叶节点。
恢复树T:
序列I 1,2,…n
序列II b1,b2,…,b[n-2]
在I中找出第一个不出现在II中数,显然是a1,连接边(a1, b1),在I中消去a1,在II中消
去b1.如此步骤重复n-2次,序列I中两个数,构成最后一条边.
以下是来自Matirx67的blog.
ayley公式是说,一个完全图K_n有n^(n-2)棵生成树,换句话说n个节点的带标号的无根树有n^(n-2)个。Cayley公式的一个非常简单的证明,证明依赖于Prüfer编码,它是对带标号无根树的一种编码方式。
给定一棵带标号的无根树,找出编号最小的叶子节点,写下与它相邻的节点的编号,然后删掉这个叶子节点。反复执行这个操作直到只剩两个节点为止。由于节点数n>2的树总存在叶子节点,因此一棵n个节点的无根树唯一地对应了一个长度为n-2的数列,数列中的每个数都在1到n的范围内。下面我们只需要说明,任何一个长为n-2、取值范围在1到n之间的数列都唯一地对应了一棵n个节点的无根树,这样我们的带标号无根树就和Prüfer编码之间形成一一对应的关系,Cayley公式便不证自明了。
看到这,我建议自己划一划,结果就出来了(这句话是我的建议,非Matrix67原文)。
注意到,如果一个节点A不是叶子节点,那么它至少有两条边;但在上述过程结束后,整个图只剩下一条边,因此节点A的至少一个相邻节点被去掉过,节点A的编号将会在这棵树对应的Prüfer编码中出现。反过来,在Prüfer编码中出现过的数字显然不可能是这棵树(初始时)的叶子。于是我们看到,没有在Prüfer编码中出现过的数字恰好就是这棵树(初始时)的叶子节点。找出没有出现过的数字中最小的那一个(比如④),它就是与Prüfer编码中第一个数所标识的节点(比如③)相邻的叶子。接下来,我们递归地考虑后面n-3位编码(别忘了编码总长是n-2):找出除④以外不在后n-3位编码中的最小的数(左图的例子中是⑦),将它连接到整个编码的第2个数所对应的节点上(例子中还是③)。再接下来,找出除④和⑦以外后n-4位编码中最小的不被包含的数,做同样的处理……依次把③⑧②⑤⑥与编码中第3、4、5、6、7位所表示的节点相连。最后,我们还有①和⑨没处理过,直接把它们俩连接起来就行了。由于没处理过的节点数总比剩下的编码长度大2,因此我们总能找到一个最小的没在剩余编码中出现的数,算法总能进行下去。这样,任何一个Prüfer编码都唯一地对应了一棵无根树,有多少个n-2位的Prüfer编码就有多少个带标号的无根树。
一个有趣的推广是,n个节点的度依次为D1, D2, …, Dn的无根树共有(n-2)! / [ (D1-1)!(D2-1)!..(Dn-1)! ]个,因为此时Prüfer编码中的数字i恰好出现Di-1次。
【排列的生成算法】~~~序数法
0 ~ (n! - 1)之间的任何整数m都可以唯一地表示为 a(n - 1) * (n - 1)! + a(n - 2) * (n - 2)! + ······· + a2 * 2! + a1 * 1!
于是我们就可以得到 n! 个 (n - 1)位的序列
下面考虑把这个序列转化为一个排列。
从a1 到a(n - 1) 每个数中 ai 的取值是 <= i 。
我们可以把ai 看作是数列 p 中 i + 1 这个数的右侧比 i + 1 小的个数 也就是说 an 就确定是数字n从右到左的位置(- 1)了。
时间复杂度 n ^ 2 * (n!)。。好大。。但是这种东西一般就不管时间复杂度什么的了。
附代码(如果我把它这个算法理解正确了的话。。因为我也不知道它之间为什么非要算出来一个整数而不是直接写dfs 去。。网上也没见到这种算法可能是写我看的这本书的人自己yy出来的。。。)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#define MAXN 30
using namespace std;
int n, a[MAXN], p[MAXN];
long long jie = 1;
void work(long long x){
int k = 2;
while(x){
a[k - 1] = x % k;
x /= k; k ++;
}
}
int main()
{
scanf("%d", &n);
for(int i = 2; i <= n; i ++)jie *= i;
for(int i = 0; i < jie; i ++){
memset(a, 0, sizeof(a));
work(i);
memset(p, 0, sizeof(p));
int q = n;
while(q){
int k = 0;
while(a[q - 1] >= 0){
if(!p[k])a[q - 1] --;
k ++;
}
p[k - 1] = q; q --;
}
for(int j = n - 1; j >= 0; j --)printf("%d", p[j]);cout<<endl;
}
system("pause");
return 0;
}
~~~字典树法
这种方法比较直观应该是最显然的方法了吧, 就是把排列按照字典序输出(刚才好像也是按照字典序输出的、、、), 显然每次要把能改变的(就是p[j] > p[j - 1]的)最靠后的那个改变, 而改变的时候要保证它从前面数最小, 所以就要找出在它的后面所有比它大的数中最小的那个与其交换(考虑到它后面的序列显然是单调减的, 所以也可以说是找到比它大的数中序号最大的那个), 交换之后 j 及其之后的序列自然还是单调减的, 然而为了让它最小, 我们要让这个序列单调增, 所以只要把p[j] ~ p[n]全部倒过来就好了!
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#define MAXN 50
using namespace std;
int main()
{
int n, p[MAXN], jie = 1;scanf("%d", &n);
for(int i = 1; i <= n; i ++)p[i] = i, jie *= i;
while(jie --){
for(int i = 1; i <= n; i ++)printf("%d", p[i]);cout<<endl;
int ii = n; while(p[ii] < p[ii - 1])ii --;
int kk = n; while(p[kk] < p[ii - 1])kk --;
swap(p[ii - 1], p[kk]);
for(int i = 0; i < (n - ii + 1) / 2; i ++)swap(p[ii + i], p[n - i]);
}
system("pause");
return 0;
}
这种方法虽然想起来很朴素但是写起来真的好优美啊! 特别是它的两个while那里, 美哭了。;。。
~~~邻位互换法
定义一个乱起八糟的活动状态, 可是不就是在n - 1的排序上的每一个位置加上一个数吗,,觉得颇画蛇添足
n=1: 1
n=2: 12, 21.
n=3: 123,132,312;321,231,213.
n=4: 1234, 1243,1423,4123
4132,1432,1342,1324
3124,3142,3412,4312
4321,3421,3241,3214
2314,2341,2431,4231
4213, 2413,2143,2134
~~~轮转法
不是很主流, 不过还是值得学习的
(a)先生成以N1N2¼Nn-2打头的所有排列
N1N2¼Nn-2 Nn-1Nn, N1N2¼Nn-2Nn Nn-1
(b)再生成以N1N2¼Nn-3打头的排列.
在(a)中生成的两个排列均在其内,并对它们中的每一排列,使N1N2¼Nn-3不动, 对其后继元素从左向右按顺时针方向轮转两次,每轮转一次便生成一个新排列,由此共生成四个新排列,连同(a)中的两个排列共六个排列:
N1N2¼Nn-3 Nn-2Nn-1Nn
N1N2¼Nn-3 Nn-1NnNn-2
N1N2¼Nn-3 NnNn-2Nn-1
N1N2¼Nn-3 Nn-2Nn Nn-1
N1N2¼Nn-3 NnNn-1Nn-2
N1N2¼Nn-3 Nn-1Nn-2 Nn
(c)生成以N1N2¼Nn-4打头的所有排列:
在(b)中生成的排列均在其内,并对其中每一排列,保持N1N2¼Nn-4不动, 使其后继元素从左向右按顺时针方向轮转n-(n-3)=3次,每轮转一次便生成一个新排列,共生成18个新排列,连同(b)中的6个排列共24个排列.[省略]
(d) 按照上述方法, 依次分别生成以
N1N2¼N5, N1N2¼N4, ¼, N1N2, N1
打头的所有排列为止.
(2)生成以N2打头的所有排列
(a) 先将基准排列N1N2¼Nn-1Nn从左向
右依顺时针方向轮转一次,生成排列
N2¼Nn-1Nn N1
(b)然后按(1)的方法和步骤生成以N2打
头的所有排列.
(3)生成以N3打头的所有排列
(a)先将基准排列基准排列N1N2¼Nn-1Nn从左向右依顺时针方向轮转两次, 生成以下排列N3¼Nn-1Nn N1 N2
(b)然后按(1)的方法和步骤生成以N3打头的所有排列.
(4)依次类推,按同样方法依次生成以N4,N5,¼,Nn-1,Nn打头的所有排列.
【组合的生成】