整数集合用一个位向量来表示。这里是无符号整数的集合,它适合存放范围在0~N-1之内的小整数,N是位向量的位数(这里为32位的unsigned int)。每个整数用集合中的一个位来表示,第i位为1(i=0~N-1),当且仅当整数i属于集合。
1、头文件set.h:整数集合的接口。
/*
* set.h:整数集合,是一个位向量。适合存放范围在0~N-1之内的小整数,N是unsigned int的
* 位数(32位)。每个整数用集合中的一个位来表示。第i位为1(i=0~N-1),当且仅当i属于集合。
*/
#ifndef UINT_SET_H
#define UINT_SET_H
#include <limits.h> /* 用到CHAR_BIT */
/* 类型SET用来代表集合 */
typedef unsigned int SET;
/* SET_BITS: 每个集合的最大位数 */
#define SET_BITS (sizeof(SET)*CHAR_BIT)
/* check(i): 判断i是否能够成为集合的一个元素 */
#define check(i) (((unsigned) (i)) < SET_BITS)
/* emptyset: 返回无任何元素的空集 */
#define emptyset ((SET) 0)
/* singleset(i): 返回只含有一个整数i的集合 */
#define singleset(i) (((SET) 1) << (i))
/* add(s,i): 向集合set中添加一个整数i,将第i位置为1即可 */
#define add(set,i) ((set) | singleset (i))
/* intersect: 返回两个集合的交集 */
#define intersect(set1,set2) ((set1) & (set2))
/* union: 返回两个集合的并集 */
#define union(set1,set2) ((set1) | (set2))
/* setdiff: 返回两个集合的对称差(只属于其中一个集合,而不属于另一个集合的元素) */
#define setdiff(set1,set2) ((set1) ^ (set2))
/* element: 判断i是否在集合set中 */
#define element(i,set) (singleset((i)) & (set))
/* forallelements: 遍历集合s中的每个元素,以便执行一个语句。例如,为了打印
集合S中的每个整数,可以这样写:
int j;
forallelements(j, s)
printf("%d ", j);
*/
#define forallelements(j,s) /
for ((j)=0; (j)<SET_BITS; ++(j)) if (element((j),(s)))
/* first_set_of_n_elements(n): 生成集合{0,1,...,n-1},
这利用了无符号整数的减法性质 */
#define first_set_of_n_elements(n) (SET)((1<<(n))-1)
/* next_set_of_n_elements(s): 对集合s,产生一个大小相等的新集合,它与x必定不相同。
如果你从first_set_of_n_elements(k)的结果{0,1,...,k-1}开始,在前面的结果上不断地
应用next_set_of_n_elements,直到产生的集合包含了整数m。这样你就得到{0,1,...,m-1}
的所有大小为k的子集,其个数为“从m件东西中选出k件东西的所有可能的组合数目”,它应该等于
m!/(k!(m-k)!) */
extern SET next_set_of_n_elements(SET x);
/* printset(s): 打印集合x的所有元素,输出格式为{1, 2, 3, 4} */
extern void printset(SET z);
/* cardinality(s): 返回集合x的基数(即元素个数) */
extern int cardinality(SET x);
/* print_k_of_n(k,n): 打印集合{0,1,...,n-1}的所有大小为k的子集,每一行会打印尽可能
多的子集。同时也会打印这些子集的总个数,它应该等于n!/(k!(n-k)!) */
extern void print_k_of_n(int k, int n);
#endif
解释:
(1)singleset(i):由于要返回只含有一个整数i的集合,因此要将位向量的第i位置1,其余位均为0,只要将1右移i位即可。
(2)first_set_of_n_elements(n):要生成集合{0,1,...,n-1},就要将第0,1,...,n-1位置1,其余位置0。可将1右移n位,这样第n位为1,位向量的值为2**(n-1),这里**为幂。减1后就变成2**(n-1)-1,其第0,1,...,n-1位为1,其余位为0。
2、函数定义文件set.c:整数集合的各个函数实现。
/*
* set.c:整数集合的各个函数实现
*/
#include <stdio.h>
#include "set.h"
/* 返回集合x的基数(即元素个数) */
int cardinality(SET x){
int count = 0;
while (x != emptyset) {
/* -x会对x取反并加1,x & -x获得x的最右边的位1(其余位变成0),
x^(x&-x)会将x最右边的位1置成0,因此就去掉了集合中的最小元素 */
x ^= (x & -x);
++count;
}
return count;
}
/* 对集合x,产生一个大小相等的新集合,它与x必定不相同 */
SET next_set_of_n_elements(SET x){
/* 这里的代码利用了无符号运算的一些技巧,例子如下:
如果 x == 001011001111000, 则
smallest == 000000000001000
ripple == 001011010000000
new_smallest == 000000010000000
ones == 000000000000111
the returned value == 001011010000111
基本思想是找到x中最右边的连续位1组,把这个组最左边的位1向左滑动一位,
把其余的所有1都滑到最右端 */
SET smallest, ripple, new_smallest, ones;
if(x == emptyset)
return x;
smallest = (x & -x); /* 得到只含x最右边的位1的数(其余位为0) */
ripple = x + smallest; /* 会把x最右边的连续位1组清0,并且组的左边一位置1,这样组的
最左边位1就向左滑了一位,这个位成为ripple最右边的位1 */
new_smallest = (ripple & -ripple); /* 得到只含ripple最右边的位1的数 */
/* 将new_smallest的这个位1滑到连续位1组的左边,然后将其后面的所有位都置1(其余位变成0) */
ones = ((new_smallest / smallest) >> 1) - 1;
return (ripple | ones); /* 得到最后结果并返回 */
}
/* 打印集合z的所有元素,输出格式为{1, 2, 3, 4} */
void printset(SET z){
int first = 1; /* 表示集合打印的开始 */
int e;
forallelements(e, z) {
if(first)
printf("{");
else
printf(", ");
printf("%d", e);
first = 0;
}
if (first) printf("{"); /* 上面没有执行,说明集合中无元素,是空集 */
printf("}"); /* 打印集合的闭括号 */
}
#define LINE_WIDTH 54 /* 输出行的行宽 */
/* 打印集合{0,1,...,n-1}的所有大小为k的子集 */
void print_k_of_n(int k, int n){
int count = 0;
/* 集合打印时占的宽度,为k*4+3或k*3+3 */
int printed_set_width = k * ((n > 10) ? 4 : 3) + 3;
/* 每行打印的集合个数 */
int sets_per_line = LINE_WIDTH / printed_set_width;
SET z=first_set_of_n_elements(k); /* 生成集合{0,1,...,k-1} */
/* 生成集合{0,1,...,n-1},然后打印其所有大小为k的子集 */
printf("/nAll the size-%d subsets of ", k);
printset (first_set_of_n_elements(n));
printf(":/n");
do{ /* 打印{0,1,...,n-1}的所有大小为k子集 */
printset(z);
if((++count) % sets_per_line)
printf (" ");
else
printf("/n"); /* 一行已满时,换行 */
z = next_set_of_n_elements(z); /* 产生一个大小为k的新子集 */
}while((z != emptyset) && !element(n, z)); /* 一旦产生的集合为空集或包含了元素n,
说明不再是子集,结束循环 */
if ((count) % sets_per_line) printf ("/n");
printf("The total number of such subsets is %d./n",
count);
}
解释:
(1)cardinality(SET x)用于返回集合中元素的个数,采用的方法每去掉一个最小元素,就让计数器增1。这里-x会对x取反并加1,x & -x获得x的最右边的位1(其余位变成0),x^(x & -x)会将x最右边的位1置成0,其余位不变,因此就去掉了集合中的最小元素。
(2)printset(SET z)用于打印集合中所有元素。它用forallelements(e, z)来遍历集合的每个元素,然后打印它。
(3)print_k_of_n(k,n)用于打印集合{0,1,...,n-1}的所有大小为k的子集。它先设置打印参数,然后用first_set_of_n_elements(k)生成初始的大小为k的子集z={0,1,...,k-1}。打印各个子集时,从初始的子集z开始,不断地应用next_set_of_n_elements(z)生成大小为k的新子集,并用printset打印它。一旦产生的集合为空集或包含了元素n,显然这时就不再是{0,1,...,n-1}的子集了,结束循环。
(4)关键看next_set_of_n_elements(SET x)的实现。它必须保证生成大小为k但与z不相同的子集,同时要保证当生成空集或生成包含了元素n的集合时,{0,1,...,n-1}的所有的大小为k的子集(个数为n!/(k!(n-k)!)都已经产生。采用的算法在注释中已经描述得比较清楚了。看一个例子,对于{0,1,2,3,4}的大小为3的所有子集,从初始子集{0,1,2}开始,由于算法只对最小的连续相邻的数(构成一个位1组)进行移动,因此它能遍历到所有大小为3的子集。对组中最右边的2向左移一位,组中其余的位1移到最右端,得{0,1,3},不停地应用该算法,得{0,2,3},{1,2,3},{0,1,4},....,{2,3,4},再移动就得{0,1,5},生成含有元素5的集合,不再是子集了,因此循环就结束。
3、测试文件testset.c:对整数集合的测试。
/*
* testset.c 对整数集合的测试
*/
#include "set.h"
int main(void){
print_k_of_n(0, 4);
print_k_of_n(1, 4);
print_k_of_n(2, 4); /* 打印集合{0,1,2,3}的所有大小为2的子集 */
print_k_of_n(3, 4);
print_k_of_n(4, 4);
print_k_of_n(3, 5);
print_k_of_n(3, 6);
return 0;
}