这篇文章接着讲怎样高效地遍历所有的组合。同样,假定全集的大小不大于机器字长,计算模型为 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. python--复习之路的目录
  2. pix2pix, pix2pixHD, vid2vid
  3. (一).NET SubSonic2.0 的配置
  4. 浅谈代码的执行效率(1):算法是关键
  5. 解决php写入mysql乱码问题汇总
  6. 一些没啥用的大数据组件以及理由(持续更新中)+2020的Gartner曲线
  7. ios 图像翻转_在iOS 14中使用计算机视觉的图像差异
  8. linux 中将文件设置密码,linux – 如何使用公钥在openssl中加密大文件
  9. 世界首富比尔·盖茨的母亲有多厉害?
  10. [NHibernate]一对多关系(级联删除,级联添加)
  11. 卡饭里的云计算机,微云可以在电脑用吗
  12. 华为盒子 原生android,手把手教你刷机把华为悦盒刷机为安卓网络机顶
  13. fedora linux搜狗输入法,Linux_Fedora20 32位系统中安装搜狗拼音输入法图文教程,搜狗输入法在windows系统中是非 - phpStudy...
  14. 线程的先进先出,后进先出,以及优先级队列
  15. LeetCode-781-森林中的兔子
  16. 什么相片可以两张弄成一张_美图秀秀怎么把两张图片合成一张?美图秀秀两张图片融合方法汇总_图形图像_软件教程_脚本之家...
  17. Postgresql 使用 Pl/python实现邮件监控
  18. 用Python编程语言来实现阿姆斯特朗数的检查
  19. 高校房产管理系统应具备哪些基本功能?
  20. Oracle计算时间差

热门文章

  1. SpringCloud 入门教程(八): 断路器指标数据监控Hystrix Dashboard 和 Turbine
  2. Elasticsearch内存
  3. 面试题,你如何进行产品改版的?
  4. drools动态配置规则_基于 Flink 和 Drools 的实时日志处理
  5. 黑马程序员python笔记_三年Python程序员平时学习笔记总结,对于学习Python非常有帮助...
  6. python 键盘输入_跟我一起学python | 探究07
  7. 【程序设计】流程图的规范和绘制
  8. 【系统设计】发现类的方法
  9. 高手追小萝莉的故事(洛谷P1184题题解,Java语言描述)
  10. 通过“FBI树”复习二叉树算法(洛谷P1087题题解,Java语言描述)