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

Gosper‘s Hack 例如 二进制数 110110,我们需要找到它从左往右的最后一个 01,然后把这个 01 变成 10,再把它右边的 1 全部集中到最右边(这里右边的 1 显然都是连续的,否则与最后一个 01 矛盾),即 110110→111001。上面的代码有很多无效枚举,即大小不等于 cols 的集合,每次也要枚举进来。Gosper’s Hack 算法,让每次枚举的二进制数 1 的个数恰好等于 cols。Gosper’s Hack 是生成 n 元集合中所有包含 k 个元素的子集的算法。 阅读详情

这篇文章接着讲怎样高效地遍历所有的组合。同样,假定全集的大小不大于机器字长,计算模型为 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小时专业转载。

Matters Computational 比较经典的算法的书。书比较厚,内容比较丰富。 立即下载

相关推荐

计算问题:思想,算法,源代码Matters Computational: Ideas, Algorithms, Source Code

本书为计算人员提供了算法和思想,无论是工作的程序员还是对计算方法感兴趣的人。

位运算与组合搜索(二)

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 w...

weixin_34023863的博客 341

Matters Computational(pdf)

This is a draft of a book about selected algorithms. The audience in mind are programmers who are interested in the treated algorithms and actually want to create and understand working and reasonably optimized code.

计算机程序设计艺术 介绍

《计算机程序设计艺术》(The Art of Computer Programming),簡稱TAOCP,是高德纳(Donald Ervin Knuth)编著的关于计算机程序设计的七卷本著作。作者並因此获得美国计算机协会1974年图灵奖。[1]目录[隐藏]1 概述2 章節3 第4A卷,列舉與

Xiao_Qiang_的专栏 2575

MS建模mmt

一、流程:1.定义好每个原子力场类型:Edit Sets(分组) 2.加H(一般是仅与Al-O八面体中的Al相连的O原子)、给H分组 3.“make P1”、校核化学式(正确与否) 4.扩成超晶胞(Na-mmt:4*1*1,Ca-mmt:4*2*1,illite:2*1*1,由化学式而定) 5.置换阳离子: mmt:模型中每 32 个 Si 4+有 1 个被 Al 3+ 取...

weixin_30721899的博客 2174

weblogic等保测评操作过程

weblogic网络安全等级测评保护操作

qq_48257021的博客 2761

算法与数据结构_算法与数据结构必备书单!还能免费下载

程序员书库(ID:OpenSourceTop) 编译书单来自:https://www.improgrammer.net/free-algorithm-books-for-download/算法可以说是用于解决问题的公式,它是基于执行一系列指定的操作,或者我们可以说是一步一步地解决问题的方法。在计算机科学中,算法通常是指解决一个反复出现的问题的小程序,数据结构是一种数据组织、管理和存储格式...

weixin_39519769的博客 392

OpenPose 论文理解

OpenPose: Real-time multi-person keypoint detection library for body, face, hands, and foot estimation paper: Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields link: OpenPose paper...

JustForYou的博客 2万+

Gosper‘s Hack 算法

从小到大枚举,每次将最靠近右边的 01 转换为 10, 然后将右边的剩余的 1 全部放到最右边即可。:组合枚举,生成n个元素所有k元子集,即生成 C(n, k),:以二进制的形式枚举所有可能的组合。

weixin_54069688的博客 1210

Dify DSL文件分享 Dify讨论群 大模型token资源等(持续收集更新)

基于LLMOps平台-Dify的一站式学习平台。包含不限于:Dify案例、DSL文件分享、模型接入、Dify交流讨论等各类资源分享。

同名公众号:dify实验室 3717

大厂面经:大疆嵌入式面试题及参考答案(4万字长文:持续更新)

目录 Linux 系统调用的过程,中间发生了什么? 表格总结 Linux 中断流程,谈谈你对中断上下文的理解 中断流程 中断上下文理解 Linuxschedule()函数的原理和调用的时机 schedule()函数原理 调用时机 页表实现机制,分页的缺点? 页表机制 分页的缺点 介绍操作系统的多级反馈调度策略,时间片轮转,在项目中如何指定优先级来调度进程完成快速响应(nice命令) 多级反馈队列调度 时间片轮转 使用nice命令 聊内存分配,进程内存分配,段页式存

大模型大数据攻城狮的专栏 4172

位运算与组合搜索(一)

文章转自:http://www.cnblogs.com/atyuwen/archive/2010/07/19/bit_combinatorics.html

BiuBiuBiu 612

青少年C/C++软件编程(四级)等级考试真题及答案解析目录

3. 带通配符的字符串匹配。1. 怪盗基德的滑翔翼。3.最长公共子上升序列。4.宠物小精灵之收服。

长春高老师编程 3036

利用位运算解决组合问题

据,问从这n个数据中取出3个组成三角形,有多少种取法? 经典的组合问题 下面上代码 while(kk){ kk = kk & (kk - 1); counted++; }//统计1的个数 例如:1011&(1011-1)=1010 1010&(1010-1)=1010&1001=1000 ....

weixin_45725925的博客 423

智能车图像处理0-变量释义

这篇文章主要讲述智能车图像处理中,各篇主要变量的含义。如果在看具体分析代码时感到困惑,可以在本篇文章中查询,辅助理解。

taiyuezyh的博客 3691

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Wireshark 搜索组合速查表

捕捉过滤器用于在数据包捕获阶段过滤流量。它决定了哪些数据包会被捕获并传递给 Wireshark 进行进一步分析。捕捉过滤器的设置是在 Wireshark 开始捕获数据包之前进行的,这样可以避免捕获不必要的流量,从而节省存储空间和提高效率。筛选过滤器用于捕获之后对已捕获的数据进行筛选和分析。与捕捉过滤器不同,筛选过滤器是实时应用于捕获的数据包的,它并不会影响数据包的捕获,而是通过指定的条件来查看或隐藏符合某些标准的数据包。搜索条件符号用于帮助用户更精确地定义筛选规则,以便定位特定的数据包或分析流量。

rufeike的博客 1752

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

组合,全排列以及子集超详细讲解

应该是特别特别宇宙无敌详细的有关排列组合子集的相关解答了,此篇文章汇集了DFS,位运算的相关解法,相信阅读本篇文章的同学们一定会有大大滴收获的!

qq_60556896的博客 2841

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

poj 2436 位运算+组合枚举

题意:一个农场有N只奶牛,现在发现出现了D种疾病,已知每头奶牛都得了哪几种疾病。现在要选出尽可能多的奶牛,使得这些奶牛所带的疾病种类不大于K种,问最多能选几只。 思路:疾病不多于15种,很显然暗示位运算。每一位表示一种疾病。先求出组合数C(D,K)表示的疾病,对于每种搜索N只奶牛,计数即可。一开始先把疾病数量大于K的奶牛直接删去,然后对于重复的奶牛hash一下。 #include #incl

dearmango 515

位运算总结

1、   位运算讲解系列文章 by matrix67 (百度文库 http://wenku.baidu.com/view/b6d21117866fb84ae45c8dad.html) 2、基本规律总结 by 泳裤王子 并, | ,并1 , 变1; 并0, 不变 与

tclh123 - 悲剧的泳裤王子 513

搜索之组合问题

本文章主要记录搜索中组合的问题

Shea_Qiu的博客 772

LeetCode 1286. 字母组合迭代器(回溯/位运算)

文章目录1. 题目2. 解题2.1 回溯2.2 位运算 1. 题目 请你设计一个迭代器类,包括以下内容: 一个构造函数,输入参数包括:一个 有序且字符唯一 的字符串 characters(该字符串只包含小写英文字母)和一个数字 combinationLength 。 函数 next() ,按 字典序 返回长度为 combinationLength 的下一个字母组合。 函数 hasNext() ,...

Michael是个半路程序员 2395

洛谷 P2036 [COCI 2008/2009 #2] PERKET深度优先搜索/位运算

洛谷 P2036 [COCI 2008/200以上两种方法分别是使用深度优先搜索(DFS)和位运算来解决问题的。深度优先搜索(DFS)方法的思路是通过递归遍历所有可能的选择组合。对于每一种食材,都有两种选择:选取或不选取。通过递归调用DFS函数来尝试所有可能的选择组合,并计算总的酸度和总的苦度的绝对差,最终取得最小的绝对差。这种方法的特点是直观易理解,但搜索的时间复杂度为指数级别,对于大规模的输入可能较慢。位运算方法利用了二进制表示来表示各种配料被选取的情况。通过遍历所有可能的选取组合,来求解最小的绝对差。

zkdjen的博客 1399

【Python位运算】——左移操作(<<)右移操作>>

python 位运算【左移操作和右移操作】

python_AI_fans的博客 2万+
上一篇: 【飞秋】使用C++语言创建Silverlight中的VisualState
下一篇: 【飞秋】做需求分析一点心得
普通网友
博客等级 码龄22年 1万+粉丝 3156原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值