组合对象,算法课刚学的时候我也挺小看他的。之后在写算法,特别是蛮力法,的时候才发现他的重要性。
先说说他的用处。在算法问题中常遇到一些组合问题,如:
给你一个集合S = {2, 7, 36, 40, 53, 59, 62, 69, 77, 80, 87, 89, 95, 98, 100, 102, 103, 106, 112, 115},要你从S中找出他的子集T,使得T中所有元素的和为220。
当然,这个问题不光可以用蛮力来解决。但如果用蛮力法的话,我们有没有最简便的方法,使得算法效率最高呢。这就要用到组合对象了。
组合对象X = {xi},我的理解,它是一个与集合S = {si}有相同元素个数的集合,并且X与S中的元素一一对应,X中的每个元素都只有两个值,a和b,且:xi = a 时,si被选中;xi = b 时,si没被选中。(大部分情况下我们默认a = 1,b = 0)
下图为组合对象的一个例子,其中T是全集S={1,2,3}的一个子集合,他是根据组合对象的取值决定的。
可以看到,如果我们能有个方法能够迭代的依次生成出所有的组合对象,那么对于全集S的不同子集合的选取将会方便很多。而且现在已经有了不少牛人的研究成果,用他们的算法绝对比我们自己做要省事,关键是效率很高。
以下介绍Lexicographic ordering和Gray codes,具体事例都以S为全集,T为子集,且|S| = |T| = 3。元素索引从左向右递增,首位索引为0。
Lexicographic ordering
{0,0,1}
{0,1,0}
{0,1,1}
{1,0,0}
{1,0,1}
{1,1,0}
{1,1,1}
{0,0,0}
算法思想:指针每次从右侧开始向左扫描: //以{0,0,1}为例
(1)每遇到1,则将其改为0,并继续往左扫描下一位,即指针指向下一位; //组合对象改为{0,0,0},指针指向红色元素
(2)若指针索引为-1,即遇到{1,1,1}这种情况,则算法结束; //若以{1,1,1}为例,此时组合对象为{0,0,0},算法结束
(3)否则,将指针所指位改为1,并结束扫描; //接(1)中例子:此时指针所指元素改为1,即{0,1,0},此为下一个组合对象
此时得到下一种组合对象
以下为代码,供参考:
public boolean[] next() { if(!hasNext) return null; //组合对象生成完毕 int index = n - 1; while(index >= 0 && set[index]){ set[index] = false; index--; } if(index == -1){ hasNext = false; }else{ set[index] = true; } return set; }
Gray codes
{0,0,0}
{0,0,1}
{0,1,1}
{0,1,0}
{1,1,0}
{1,1,1}
{1,0,1}
{1,0,0}
算法思想:如上图所示,组合对象的产生是遵循固定规律的,即沿着图中所绘图形的边框呈“之”字形变化:
(1)当沿着直线向上或向下时,仅将最右边一位做取反操作,即得下一个组合对象;
(2)当沿着直线向右时(向左可以类推):
a.当在(1)中取反操作是0置1时,此时只要将右边第二位置反,即得下一个组合对象;
b.否则,从右向左扫描找到第一个1的索引 i ,若 i = 0,即 i 为最左边的索引,则不再有下一个组合对象,算法终止,否则将第 i - 1位置反,即得下一个组合对象;
代码如下,仅供参考:
//数组索引从0到n public boolean[] next(){ if(begin){ //如果是第一次调用begin=true,则返回{0,0,0,...,0} begin = false; return set; } if(!hasNext) return null; //如果没有下一个组合对象 返回空 if(turnFlag){ //为向下或向上移动 if(!set[n]){ set[n] = !set[n]; //0变1 index = n - 1; }else{ set[n] = !set[n]; //1变0 index = n - 1; while(!set[index]) index--; //如果当前位为0继续向左移 if(index == 0){ hasNext = false; //此时为100...的情况 说明已经到最后了 没有next了 }else{ index--; //此时为...100...情况 还有next } } turnFlag = false; }else{ //为向右移动 set[index] = !set[index]; turnFlag = true; } return set; }