C语言名题精选百则——排列,组合与集合
本篇博文,D.S.Qiu将对《C语言名题精选百则——排列,组合和集合》进行整理推出,不光只是书上的名题,还会依据互联网的资源进行不断补充,加强。等全书各个章节都整理完,会做一个总汇。如果你有建议、批评或补充,请你不吝提出(email:gd.s.qiu@gmail.com,或者直接在本文末评论)。你的支持和鼓励(一个人整理真的很累,几度想放弃),我将渐行渐远!
《排列,组合和集合》主要是介绍了关于集合的子集以及子集字典排序,Gray码,排列以及字典排列,集合的分割,整数的分割等8个组合数学基本的问题,介绍比较繁复(废话比较多),只要看了就能理解(点击查看更多数组和字符串问题),相对简单,但是要是尽善进美还有待不断的挖掘。下一篇是《查找》(其实关于有序数组的一些问题),更多关注:http://dsqiu.iteye.com。
问题3.1列出所有子集(DIRECT.C )
编写一个程序,列出{1,2,3,…,n}这个集合的所有子集,包括空集合。
【说明】
列出一个集合的所有子集有很多做法,题目中并没有要求依某个特定的次序来排列, 因此是不难做出来的。
因为集合中一共有n个元素,所以总共就会有2^n子集;例如{1,2,3}有如下子集:
{}
{1} {2} {3}
{1,2} {1,3} {2,3}
{1,2,3}
【解答】
看到2^n,就想起了二进制数,可以使用二进制的第 i 位表示是否包含集合的第 i 个元素,如数字6的二进制形式是110,表示取集合的第2,3两个元素组成的子集。这样0~2^n -1的数字就可以表示全部子集,每一个数字代表一个子集,实现应该不难。
【问题实现】
#include <stdio.h> #include <stdlib.h> #define MAXSIZE 20 #define LOOP 1 void main(void) { char digit[MAXSIZE]; int i, j; int n; char line[100]; printf("\nDirect Generation of All Subsets of a Set"); printf("\n========================================="); printf("\n\nNumber of Elements in the Given Set --> "); gets(line); n = atoi(line); /* ---You'd better check to see if n is too large--- */ for (i = 0; i < n; i++) /* clear all digits to 0 */ digit[i] = '0'; printf("\n{}"); /* outpout empty set {} */ while (LOOP) { for (i = 0; i < n && digit[i] == '1'; digit[i] = '0', i++) ; /* find first 0 position */ if (i == n) /* if none, all pos. are 1 */ break; /* thus all elem. are in set*/ else digit[i] = '1';/* now add one to this pos */ for (i = 0; i < n && digit[i] == '0'; i++) ; /* find first 1 position */ printf("\n{%d", i+1); /* show its numner and */ for (j = i + 1; j < n; j++) /* others */ if (digit[j] == '1') printf(",%d", j + 1); printf("}"); } }
问题3.2列出所有子集——字典顺序(LEXICAL.C )
编写一个程序,用字典顺序(Lexical Order)把一个集合的所有子集找出来。
【说明】
如果不知道何谓字典顺序,在此作一个简单的说明。假设给定的集合有n个元素,
{1,2,3,4}与{1,2,4}是两集合,它们前面的两个元素相同,但第三个不同,因此包含小的元 素的集合就排在前面。请回想一下,这与字符串的比较有什么不一样呢?完全相同,惟一 的差异,就是在集合中的元素要从小到大排好。
下面左边是n=3,右边是n=4的结果,如表3-1所示。
【解答】
事实上,这是一个十分简单的程序,除了空集合之外,最“小”的一个集合就是{1}再下一个就是包含1,而且再加上1的下一个元素(1+1=2)的集合,即{1,2};下一个元素 自然就是含有1与2,并且还有2的下一个元素(2+1=3)的集合{1,2,3}了。就这样一直到包含了所有元素为止,亦即{1,2,3,····,n}。下一个集合是谁?绝不是{1,2,3,…,n-1},.因为它 比{1,2,3,…,n}小,事实上应该是{1,2,3,…,n-2,n}。为什么呢?在{1,2,3,…,n-1,n}与 {1,2,3,…,n-2,n}之间,前n-2个元素完全相同,但是n-1<n,这不就证实了以上说法了吗?
由于以上原因,因此可以用一个数组set[]来存放集合的元素,一开始时set[0]=1,表 示{1};另外,用一个变量position,指出目前最右边的位置何在,在开始时自然就是1。 接下来,就陆续地产生下一个集合了。注意,目前集合中最右边的元素是set[position],如 果它的值比n小,那就表示还可以加进一个元素,就像是从{1,2,3}加上一个元素变成{1,2,3, 4}一样(n>4)。这倒是容易做,下一个元素在set[position+1],因此在那存入set[position+1] 这个值就行了;同时position也向右移一位。如果目前最右边元素set [position]已经是n, 因而不能再增加元素了。例如,当n=4时,如果有{1,3,4},那自然不能像前面所说的加入一个5。这时看最右边元素的位置,亦即position,是不是在第一位(如果n=6,而现在的集合是{6}),如果不在第一位,那就可以往回移一位,并且把那个位置的值加上1。例如, 如果现在有{1,3,4},而n=4;最右边(4)的位置不是在第一位,因而退回一位,等于是{1,3}; 但这是不对的,因为{1,3}比{1,3,4} “小”,要做得比{1,3,4}大,把3加上1而变成{1,4}就 行了。如果最右边(4)的位置是在第一位,那么程序就完成了。
【问题实现】
#include <stdio.h> #include <stdlib.h> #define MAXSIZE 20 #define LOOP 1 void main(void) { int set[MAXSIZE]; int n, i; int position; char line[100]; printf("\nAll Possible Subsets Generation by Lexical Order"); printf("\n================================================"); printf("\n\nNumber of Elements in the Set --> "); gets(line); n = atoi(line); printf("\n{}"); /* the empty set */ position = 0; /* start from the 1st pos. */ set[position] = 1; /* it gets a '1' */ while (LOOP) { /* loop until done... */ printf("\n{%d", set[0]); /* print one result */ for (i = 1; i <= position; i++) printf(",%d", set[i]); printf("}"); if (set[position] < n) { /* this pos. can be inc*/ set[position+1] = set[position] + 1; /* YES*/ position++; /* inc. next pos. */ } else if (position != 0) /* NO, the 1st pos? */ set[--position]++; /* backup and increase */ else /* NO, the 1st pos and can */ break; /* not be inc. JOB DONE! */ } }
【习题】
(1)有n个元素的集合的子集个数有2^n个,为什么前面所有的程序都不用一个for从 1数到2^n,而用break离开循环呢?(提示:想一想当n=50或100时会有什么后果)
(2)这个程序稍加改动就可以求出n个元素集合中元素个数不超过m(m<n)的所有子 集,请把它写出来。
(3)这个程序也可以改成求出n个元素的集合中元素个数恰好是m(m<n)的所有子 集,请把它写出来;请验查一下是否恰好有C(n,m)个?
注意:在编写(2)与(3)两题时,切不可以把所有子集都求出来,看看元素的个数, 如果在所要的范围,就提出该集合。这样的写法是不能接受的,虽然正确;应该在编写本 程序的概念上动动脑筋,只产生所要的集合,而不产生任何多余的部分才是正途。
(4)请写一个程序,把II个元素的集合的子集,用字典顺序的反顺序列出来(注意, 不能保存各个子集),然后用反顺序列出来,因为当n很大时内存就不够用了;试一试了解 上面所讲的观点,直接把程序列出来。
问题 3.3 产生 Gray 码(GRAYCODE.C )
编写一个程序,用Gray码(Gray Code)的顺序列出一个集合的所有子集。
【说明】
这个问题其实是在看有没有办法把Gray (人名)码用程序编写出来,有了Gray码, 找出对应的集合是件简单的事,问题3.2己经讲过了。
什么是Gray码? nbit的Gray码是一连串共有2^n个元素的数列,每一个元素都有nbit, 而且任何相邻的两个元素之间只有1bit的值不同。例如,3个bit的Gray码:
000 001 011 010 110 111 101 100
是一组Gray码,任何相邻两个元素都只有1bit值不同。但是,Gray码却并不是惟一的, 把它循环排列或是用反过来的顺序写,也会得到一组Gray码;比如说,如果把最后3个元 素放到最前面去,就会得到:
111 101 100 000 001 011 010 110
也是一组Gray码。
Gray码是一个很直观的几何意义,不妨把nbit看成是n度空间中一个点的坐标,因此 有2^n个坐标点,正好是n维空间中的一个正立方体的2^n个角落。如图3-1a所示,当n=2 时就是平方,是个正方形,如图3-1b所示,时就是个正立方体了。
如果能够从原点出发把每个顶点都走一次再回到原点,且每一个顶点都不重复,那么 沿途经过的点的坐标,就是一个Gray码。在图3-1a中,依箭头顺序,会得到00,10,11,01;对图3-1b而言,则是000,001,011,010,110,111,101,100。当然,用不同的走法,就会有不同的Gray码。例如在图3-1b中,000,100,101,011,111,110,010就是一个很好的例子。
看看下面n=4的Gray码: