这篇文章接着讲怎样高效地遍历所有的组合。同样,假定全集的大小不大于机器字长,计算模型为 word-RAM,即诸如 +, –, *, /, %, &, |, >>, << 等此类操作皆可以在 O(1) 时间内完成。当然,一般 / 和 % 操作明显要慢上一些,因此我们总是希望能够尽量避免使用两个操作。在上一篇 blog 中的子集遍历比较简单,基本上只用到了 +, – , & 三种操作。而组合遍历相对要复杂得多,一些让人不舒服的操作总是难以避免。下面将要介绍两种完全不同的组合遍历算法,其中一种用到了 / 操作,而另一种则使用了三目运算符。尽管不算十分完美,也应该是足够高效啦。

--------------------------------------------------------------------------------

2 遍历所有组合

2.1 colex & reverse colex

说到各种位运算技巧,早年从 MIT 流传出来的一份技术报告 HAKMEM 可谓是一本黑暗圣经。在 HAKMEM 的第175条中记录着一个非常巧妙而实用的技巧,被称为 Gosper’s hack,它仅仅使用几个非常简单的算术运算和位运算,即可得到与当前所输入的整数含有相同数目的1的下一个整数:

s = x & (-x);

r = s + x;

n = r | (((x ^ r) >> 2) / s);

在上面这段代码中 x 是输入,n 是输出,为大于 x 且与 x 含1个数相同的最小整数。比如若输入 x = 0b0101, 那么将输出 n = 0b0110。使用这一技巧使得我们可以非常容易地生成所有组合,代码如下:(这是一个成员函数,完整的代码可在位运算与组合搜索(一)所附的压缩包中找到):

view sourceprint?1 bool next(unsigned long &x) const

2 {

3     if (x == last_) return false;

4     unsigned long r, s;

5     s = x & (-(long)x);

6     r = s + x;

7     x = r | (((x ^ r) >> 2) / s);

8     return true;

9 }

上面代码中的 last_ 表示的是最后一个组合。这里遍历组合的序为 colex,最小的组合是所有1都在低位,而最大的组合(即 last_) 是当所有1都在高位。比如若全集为 {a, b, c, d, e},我们用以上代码遍历其所有大小为2的子集,顺序将如下表所示:

序号 值 位串 子集
1 0b00011 11000 {a, b}
2 0b00101 10100 {a, c}
3 0b00110 01100 {b, c}
4 0b01001 10010 {a, d}
5 0b01010 01010 {b, d}
6 0b01100 00110 {c, d}
7 0b10001 10001 {a, e}
8 0b10010 01001 {b, e}
9 0b10100 00101 {c, e}
10 0b11000 00011 {d, e}

现在稍微来解释 Gosper’ hack 是怎样工作的:

第一条语句:s = x & (-x), 用于标识出 x 最低位的1(设最低的1右边有 c 个0)。 e.g. 0b10110 –> 0b00010

第二条语句:r = s + x, 将 x 右端的连续一段1清零(红色标识的部分,设这一段有 k 个1),并将前一位设为1。 e.g. 0b10110 –> 0b11000

第三条语句:n = r | (((x ^ r) >> 2) / s), 这里先用 x 异或 r 得到 k + 1 + c 个连续的1。然后右移 2 位,再除于 s (相当于右移 c 位),得到 k – 1 位连续的1,最后添加到 r 的最右边,打完收工。e.g. 0b11000 | 0b00001 = 0b11001

由于该 hack 中的除法实际上只是用来移位的,因此可以想办法绕过去 (如果你实在看不顺眼那个除号的话)。比如可以使用 bsr 指令计算出 c ,然后直接移位即可。但经过我的测试,发现还是直接除法来得比较快。

view sourceprint?1 // Find last bit set

2 static inline unsigned long __fls(unsigned long x)

3 {

4     __asm bsr eax, x;

5 }

现在如果想要反向生成所有的组合那又该如何呢,其实很简单,因为 colex 具有一种某种意义上的对称性:某个组合的前一个组合等于这个组合的补集的下一个组合的补集。如果我们想要得到组合 x 按照 colex 的上一个组合,只需生成 ~x 的下一个组合,再取反即可:

view sourceprint?1 bool prev(unsigned long &x) const

2 {

3     if (x == first_) return false;

4     x = ~x;

5     next(x);

6     x = ~x;

7     return true;

8 }

--------------------------------------------------------------------------------

2.2 cool-lex & reverse cool-lex

cool-lex,顾名思义,就是非常 cool 的 lex。cool-lex 是由 Frank Ruskey 和 Aaron Williams 发明的,如果想要详细的了解 cool-lex 的性质,可以看一下参考文献6。另外在这里还有一段 cool-lex 的音乐,感兴趣的可以试听一下。虽然它不怎么好听,也显然不可能给你带来关于 cool-lex 的任何洞见。下面我只简单介绍一下怎样按照 cool-lex 或者反向 cool-lex 进行组合遍历。

cool-lex 的生成算法是基于后缀旋转的(如果是针对位串表示则是前缀旋转,但下面我们都是针对二进制整数表示,也就是低位在右边):找到最短的以010或者110开始的后缀(如果不存在则选定全部位),然后向左旋转1位。比如组合0b01101,  首先找出最短的以010或者110开始的后缀(用红色表示):0b01101,然后将这个后缀向左旋转1位(即循环左移1位)即得到下一个组合:0b01011。

如何借助于位运算高效的完成后缀旋转呢,Donald 在 TAoCP 中7.2.1.3节习题55的答案中给出了一个 MMIX 实现。下面的代码是我写的一个C++版:

view sourceprint?01 bool next(unsigned long &x) const

02 {

03     if (x == last_) return false;

04

05     unsigned long r, s;

06     r = x & (x + 1);

07     s = r ^ (r - 1);

08     r = ((s + 1) & x) ? s : 0;

09     x = x + (x & s) - r;

10     return true;

11 }

上面代码中的 last_ 当然也是指最后一个组合。cool-lex 中的第一个组合也是所有1在低位,即类似于这样:0b0…01…1。最后一个组合是1个1在最高位,而其余的1在低位,即形如 0b10…01…1。这段代码到底是怎么起作用的?你猜!我就不分析了,不过我等下会详细解释生成 reverse cool-lex 的代码。下表是 cool-lex 序的一个例子(同样,全集为 {a, b, c, d, e},子集大小为 2):

序号 值 位串 子集
1 0b00011 11000 {a, b}
2 0b00110 01100 {b, c}
3 0b00101 10100 {a, c}
4 0b01010 01010 {b, d}
5 0b01100 00110 {c, d}
6 0b01001 10010 {a, d}
7 0b10010 01001 {b, e}
8 0b10100 00101 {c, e}
9 0b11000 00011 {d, e}
10 0b10001 10001 {a, e}

现在来讲怎样反向遍历 cool-lex。reverse cool-lex 被提到的不多,网上以及各种文献上也并没有生成 reverse cool-lex 的代码,因此我只好自己写了一个。想要得到高效的 cool-lex 反向遍历代码,首先需要一个简单的生成规则。这个规则其实根据正向 cool-lex 的规则可以很容易地yy出来:找到最短的以100或者101开始的后缀(如果不存在则选定全部位),然后向右旋转1位。(后来我向 Frank 请教了一下,他说这个规则的确是正确的,另外还告诉我 Aaron 的另一篇文章 “loopless generation of multiset permutations by prefix shifts” 对 reverse cool-lex 作了介绍。)

规则有了,还剩下最后一个问题,那就是怎样借助于位运算高效的实现这个规则。下面是我的实现:

view sourceprint?01 bool prev(unsigned long &x) const

02 {

03     if (x == first_) return false;

04     unsigned long r, s, v;

05     v = x | 1;

06     r = v & (v + 1);

07     s = r ^ (r - 1);

08     v = s & x;

09     r = (v & 1) ? s - (s >> 1) : 0;

10     x = x & (~s) | r | (v >> 1);

11     return true;

12 }

上面的代码中,基本上都是非常基础的位运算技巧,如果你对此并不熟悉,不妨看一下参考文献1或3。首先,我们需要找到最短的以 100 或者 101 开始的后缀,这将通过下面四条语句来完成:

第一条语句:v = x | 1,将最低位置1。e.g. 0b01010 –> 0b01011

第二条语句:r = v & (v + 1),清除右边连续的1。 e.g. 0b01011 –> 0b01000

第三条语句:s = r ^ (r – 1),标记最低位的1以及其后的0。e.g. 0b01000 –> 0b01111

第四条语句:v = s & x,得到后缀。e.g. 0b01111 & 0b01010 –> 0b01010

至此,满足条件的后缀已经找出来了,下一步的工作就是将它右旋一位:

第五条语句:r = (v & 1) ? s - (s >> 1) : 0, 得到旋转后的后缀的最高位。 e.g. 0b01111 - 0b00111 –> 0b01000

第六条语句:x = x & (~s) | r | (v >> 1),将后缀右移一位,与最高位相或,再与其余不相干的位合并,即得到最终结果。 e.g. 0b00101

在第五条语句用到了三目运算符,这里其实也可以借助 bsr 指令绕过去。我并没有比较哪种更快一些。

完。(做人要厚道,转载请注明出处

--------------------------------------------------------------------------------

3 参考文献

Henry S. W., Hacker’s Delight.
Jörg A., Matters Computational.
Donald E. K., The Art of Computer Programming: Bitwise Tricks and Techniques. Volume 4, Pre-Fascicle 1A.
Donald E. K., The Art of Computer Programming: Generating all Combinations and Partitions. Volume 4, Fascicle 3.
Beeler M., Gosper R. W., and Schroeppel R., HAKMEM
Frank R., Aaron W., The Coolest Way To Generate Combinations.
P.S. 对于我这种从小就怕写作文的人来说,写篇稍正式一点的技术文章实在是太辛苦了,因此关于位运算与组合搜索就先写到这里,虽然有很多想说的还没有谈到。以后有心情了再来讨论怎样高效实现(一)中提到的映射操作及其逆操作。

关注技术文章飞秋:http://www.freeeim.com/,24小时专业转载。

【飞秋】位运算与组合搜索(二)相关推荐

  1. 位运算与组合搜索(二)

    People who play with bits should expect to get bitten. -- Jurg Nievergelt I failed math twice, never ...

  2. 给力!高效!易懂!位运算求组合

    本篇摘要 本篇介绍一个非常给力的求组合的算法!上一篇"c_c++刁钻问题各个击破之位运算及其实例(2)"介绍了6个比较复杂的位操作,但是没有给出任何应用实例,本篇就之前谈到的位操作 ...

  3. solr java score_java-Apache Solr:按位运算来过滤搜索结果

    我需要过滤与cms中访问权限相对应的solr搜索结果(基于位掩码的drupal 7自定义访问控制机制). 我在tomcat6(在Debian系统上)上使用Solr 3.6.1(/var/lib/tom ...

  4. 技巧专题1(二分、三分、位运算)

    二分 二分答案一般有以下的一些特征: A. 候选答案在区间[min,max]上按照某种属性有序,一般枚举复杂度较高. B. 容易判断某个点是否为可行 最大值最小. 判断一个东西是否在一个有序集合中出现 ...

  5. 按位运算操作符底层实现原理

    本篇文章给大家讲解编程软件中的运算符底层实现原理! 1.按位与运算& 按位与运算的底层运算过程如下 十进制: 3&5=1 二进制 0011&0101 = 0001 按位与运算就 ...

  6. python(Django之组合搜索、JSONP、XSS过滤 )

    一.组合搜索 二.jsonp 三.xss过滤 一.组合搜索 首先,我们在做一个门户网站的时候,前端肯定是要进行搜索的,但是如果搜索的类型比较多的话,怎么做才能一目了然的,这样就引出了组合搜索的这个案例 ...

  7. leetcode刷题笔记——剑指offer(二)[回溯、排序、位运算、数学、字符串]

    这里写目录标题 搜索与回溯 剑指 Offer 12. 矩阵中的路径 剑指 Offer 13. 机器人的运动范围 剑指 Offer 34. 二叉树中和为某一值的路径 剑指 Offer 36. 二叉搜索树 ...

  8. 【LeetCode笔记】剑指 Offer 65. 不用加减乘除做加法(Java、位运算、二刷)

    文章目录 题目描述 思路 & 代码 二刷 题目描述 讲道理,感觉算有点难度的题目了= =,还是需要时不时看看. 思路 & 代码 正负数情况可以不考虑(补码) 核心:加法 = 进位和 + ...

  9. 【飞秋】一起学Windows Phone7开发(十三.二 按钮控件)

    在Silverlight中有多种按钮控件,这些控件在Windows phone7中也都得到了很好的支持. 一.Button: 这个控件只是一个基础控件,通过blend可以创建出多种效果的按钮来. &l ...

最新文章

  1. POJ 3258:River Hopscotch (最大化最小值)
  2. Object.create()方法
  3. python程序设计基础教程答案山东联盟答案_Python程序设计基础 (山东联盟) 期末...
  4. Elasticsearch 摄取节点(Ingest Node)常用的数据处理器(Processor)
  5. Android开发之Android Studio第三方库的六种依赖讲解说明
  6. Spring系列(五):@Lazy懒加载注解用法介绍
  7. python3 gui tk代码_【基础】学习笔记30-python3 tkinter GUI编程-实操12
  8. RESTful API实现APP订餐实例
  9. 有啥区别?谷歌Chrome浏览器图标8年来首次更新
  10. The log scan number (620023:3702:1) passed to log scan in database 'xxxx' is not valid
  11. docker login Error response from daemon: Get http://ip:port/v2/: net/http: request canceled
  12. 计算机设备管理器命令,打开设备管理器的命令,教你一分钟学会最简单
  13. 基于matlab的微分例题,基于MATLAB的rlc电路模型仿真例题.doc
  14. 【历史上的今天】12 月 8 日:D 语言发布;“复制粘贴”的发明者逝世;人人网成立
  15. 【HTML——盛开花朵】(效果+代码)
  16. vue :to设置路由导航的用法
  17. 基于STM32的煤矿井下探测系统
  18. php-2612硒鼓加墨_HP2612A硒鼓加粉图解
  19. Linux下的常见指令以及权限理解(下)
  20. gdbt java_GBDT算法详解与代码实现

热门文章

  1. html5简介、选项输入框、表单元素分组、input新增属性及属性值
  2. 如何召开一次无效的会议?
  3. 互联网晚报 | 11月11日 星期四 | 腾讯第三季度营收1424亿元;华为捐赠百万价值职业技能券;货拉拉成立司机权益保障委员会...
  4. 2021社区居家养老现状与未来趋势报告
  5. 了不起的女子力:美妆消费蓝海与趋势赛道
  6. java中演示类_java中抽象类与接口的图文演示
  7. pyinstaller安装失败_用 Pyinstaller 打包文件为应用程序
  8. 【操作系统】Semaphore处理生产者-消费者问题
  9. 【C语言】第七章 模块化与函数 题解
  10. 【Python】Matplotlib绘制百变椭圆