People who play with bits should expect to get bitten.

-- Jurg Nievergelt

I failed math twice, never fully grasping probability theory.

I mean, first off, who cares if you pick a black ball or a white ball out of the bag?

And second if you’re bent over about the color, don’t leave it to chance.

Look in the damn bag and pick the color you want.

-- Stephanie Plum

这篇文章接着讲怎样高效地遍历所有的组合。同样,假定全集的大小不大于机器字长,计算模型为 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。使用这一技巧使得我们可以非常容易地生成所有组合,代码如下:(这是一个成员函数,完整的代码可在位运算与组合搜索(一)所附的压缩包中找到):

bool next(unsigned long &x) const
{if (x == last_) return false;unsigned long r, s;s = x & (-(long)x);r = s + x;x = r | (((x ^ r) >> 2) / s);return true;
}

上面代码中的 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 ,然后直接移位即可。但经过我的测试,发现还是直接除法来得比较快。

// Find last bit set
static inline unsigned long __fls(unsigned long x)
{__asm bsr eax, x;
}

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

bool prev(unsigned long &x) const
{if (x == first_) return false;x = ~x;next(x);x = ~x;return true;
}

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++版:

bool next(unsigned long &x) const
{if (x == last_) return false;unsigned long r, s;r = x & (x + 1);s = r ^ (r - 1);r = ((s + 1) & x) ? s : 0;x = x + (x & s) - r;return true;
}

上面代码中的 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 作了介绍。)

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

bool prev(unsigned long &x) const
{if (x == first_) return false;unsigned long r, s, v;v = x | 1;r = v & (v + 1);s = r ^ (r - 1);v = s & x;r = (v & 1) ? s - (s >> 1) : 0;x = x & (~s) | r | (v >> 1);return true;
}

上面的代码中,基本上都是非常基础的位运算技巧,如果你对此并不熟悉,不妨看一下参考文献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 指令绕过去。我并没有比较哪种更快一些。

完。(做人要厚道,转载请注明出处:http://www.cnblogs.com/atyuwen/)


3 参考文献

  1. Henry S. W., Hacker’s Delight.
  2. Jörg A., Matters Computational.
  3. Donald E. K., The Art of Computer Programming: Bitwise Tricks and Techniques. Volume 4, Pre-Fascicle 1A.
  4. Donald E. K., The Art of Computer Programming: Generating all Combinations and Partitions. Volume 4, Fascicle 3.
  5. Beeler M., Gosper R. W., and Schroeppel R., HAKMEM
  6. Frank R., Aaron W., The Coolest Way To Generate Combinations.

P.S. 对于我这种从小就怕写作文的人来说,写篇稍正式一点的技术文章实在是太辛苦了,因此关于位运算与组合搜索就先写到这里,虽然有很多想说的还没有谈到。以后有心情了再来讨论怎样高效实现(一)中提到的映射操作及其逆操作。

转载于:https://www.cnblogs.com/atyuwen/archive/2010/08/05/bit_comb_2.html

位运算与组合搜索(二)相关推荐

  1. 【飞秋】位运算与组合搜索(二)

    这篇文章接着讲怎样高效地遍历所有的组合.同样,假定全集的大小不大于机器字长,计算模型为 word-RAM,即诸如 +, –, *, /, %, &, |, >>, << ...

  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. 位运算之二进制中1的个数

    本篇文章主要详解位运算的相关问题,以及解析python语言在求解该问题上的不方便. 涉及知识点: 1.原码,补码,反码的知识 2.与,或,非,异或,左移,右移,负数右移的相关知识 3.python数据 ...

最新文章

  1. 改变ie浏览器的收藏夹位置
  2. 关于python中excel写入案例
  3. 最不像地球的45个地方,你都见过几个?
  4. 对pthread_create未定义的引用
  5. 04 16 团队竞技(第二场) 赛后总结
  6. 神经网络 异或_深度学习入门笔记(2)线性神经网络
  7. 上下文路径request.getContextPath();与${pageContext.request.contextPath}
  8. 关于抽象和多态的总结
  9. 人工智能-SVM 支持向量机
  10. Array Implementation of min-Heaps 最小堆数组实现
  11. 关于灵魂,意识,自我和死亡
  12. 全新型App开放框架—Clouda
  13. VSCode如何打开Interpreter
  14. 英语听力自动断句程序
  15. 【Electron】 NSIS 打包 Electron 生成exe安装包
  16. SpringMVC入门
  17. 常用开源监控系统分析推荐(必备知识)|附优质监控书籍资源
  18. sm总线控制器找不到驱动程序_【KHGEARS钧兴谐波 | 新品】埃斯顿发布总线伺服驱动系统 ProNet Summa...
  19. 有道难题2010有道谜题标准答案
  20. windows上在python玩耍深度学习资源合集

热门文章

  1. Windows核心编程 第2 5章 未处理异常和C ++异常(上)
  2. UVA11021麻球繁衍
  3. 【Android 逆向】ELF 文件格式 ( ELF 文件头 | ELF 文件头标志 | ELF 文件位数 | ELF 文件大小端格式 )
  4. 【Android 逆向】Android 进程简介 ( Android 应用启动流程 )
  5. 【Android 插件化】VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )
  6. 【Android 逆向】获取安装在手机中的应用的 APK 包 ( 进入 adb shell | 获取 root 权限 | 进入 /data/app/ 目录 | 拷贝 base.apk 到外置存储 )
  7. 【RecyclerView】 十三、RecyclerView 数据更新 ( 移动数据 | 数据改变 )
  8. 【Android 内存优化】自定义组件长图组件 ( 长图滚动区域解码 | 手势识别 GestureDetector | 滑动计算类 Scroller | 代码示例 )
  9. 【Android RTMP】RTMP 数据格式 ( FLV 视频格式分析 | 文件头 Header 分析 | 标签 Tag 分析 | 视频标签 Tag 数据分析 )
  10. 【Android NDK 开发】JNI 线程 ( JNI 线程创建 | 线程执行函数 | 非 JNI 方法获取 JNIEnv 与 Java 对象 | 线程获取 JNIEnv | 全局变量设置 )