趣题:用位运算生成下一个含有k个1的二进制数

    在所有8-bit的整数中,含有k个数字“1”的二进制数一共有C(8,k)个。给出其中的一个二进制数,你如何利用位运算快速找到下一个恰有k个“1”的数?例如,如果给你二进制数01011100,那么下一个(含4个“1”的)数就是01100011。在继续阅读下去之前,建议你仔细思考一下。你或许会想看看我很早以前写的一篇介绍位运算的文章。这是一道很好的题目,很多位运算技巧在这里都有体现。

    在草稿纸上随便举几个例子,规律很容易看出来。由于“1”的个数是固定的,为了让这个二进制数更大,我们必须把第一个出现在“1”左边的“0”改成“1”;同时,为了让这个二进制数尽可能小,我们必须把它右边那些“1”重新排到最低位去。
    更具体地说,下一个二进制数可以通过以下步骤得到:找到右起第一个单个的或连续的数字“1”,把它们全改成“0”,同时把它们左边的那个“0”改为“1”。此时,“1”的个数可能减少了,我们只需把还差的“1”摆在最右边就行了。举个例子,01011100的右起第一个“1”在第三位,把它和左边紧挨着的“1”一并变为“0”,并把再左边那个“0”变为“1”,于是我们得到01100000。我们还差两个“1”,把这两个“1”补在最低位得到01100011即可。现在我们的任务是,想出一个用位运算来实现这些步骤的办法。
    我们已经熟知,用x & -x可以提取最右边的那个“1”。当意识到可以利用加法来消除连续的“1”时,我们很快得到了第一步操作的位运算实现:把x & -x加到x上,利用二进制加法的进位把“..01111..”变成“..10000..”。现在,我们需要计算出刚才的操作中一共“跳过”了多少个“1”,换句话说现在的x的右起第一个“1”和原来的x的右起第一个“1”差了多少位。关键就在这里!我们可以用除法来完成这一步,例如100000除以100就相当于把被除数右移2位,得到的结果即可以表示两个数中的“1”差了多少位。在最低位产生指定数量的“1”需要用到另一个技巧:减1操作可以把右边连续的“0”都变成“1”,即把…10000变成…01111。我们得到了该问题的第一个算法:

b = x & -x;
t = x + b;
c = t & -t;
m = (c/b >> 1) - 1;
r = t | m; //最终结果

    我们对上述算法做一个简单的说明:

操作              | 样例     |  说明
——————+———-+—————————-
x                 | 01011100 |  原数
b = x & -x        | 00000100 |  提取x的右起第一个“1”
t = x + b         | 01100000 |  把x的右起第一个位于某个“1”左边的“0”变成“1”,并把它右边的那些“1”都变为“0”
c = t & -t        | 00100000 |  提取t的右起第一个“1”
c / b             | 00001000 |  右移c中的那个“1”,其结果中最低位连续的“0”的个数正好是c和b中的“1”相差的距离
m = (c/b >> 1) – 1| 00000011 |  在最低位产生数字“1”,其个数比上述的“距离”少1
r = t | m         | 01100011 |  最终结果

    除去赋值,我们一共用了9个运算符。有可能用更少的运算么?

Read more…

思考的乐趣:UyHiP趣题之用最少的块移动实现逆序操作

    Michael BrandUsing your Head is Permitted是我最喜欢的谜题挑战网站,很多题目相当精彩。我已经多次翻译了那上面的谜题(点击这里看看)。不过,以往我都是静候月底释出答案,从来没有想过参与挑战。这个月的题相当诱人,只看题目描述的最后一句话我就知道这绝对是我喜欢的类型,于是我有了向本月题目发起挑战的冲动。印象中我真的是很久很久没有像这样疯狂地思考一个问题了。然后呢,我非常得意地告诉大家,哈哈~~~经过昨天一整夜的思考,我终于把它解决了!!于是赶忙写下我的解答过程,再三检查后发到了Michael Brand的邮箱。今天起床时收到了Michael Brand热情洋溢的回信,List of solvers也写上了我的名字,我很是兴奋。自我感觉这是一道非常好的题目,题目简洁有趣而有挑战性,解答本身并不难但也不太好想,很适合大家花一整天的时间仔细琢磨;因此这里也推荐给大家来挑战一下。解答不要发到下面的评论里,也不用发给我,直接发过去好了。期待过几天在List of solvers里看到大家的名字。

    在这里简单翻译一下题目。对数列的一次“块移动”是指把一段数取出来插入到数列中的另一个地方(说穿了就是一次选择剪切粘贴的操作)。例如,数列1,4,5,6,2,3,7可以通过一次块移动完成排序(把456挪到3后面)。那么,想要让一个1到n的逆序排列n, n-1, …, 3, 2, 1变为顺序排列,最少需要多少次块移动?给出你的算法,并证明这个移动数目不能再少了。

Update: 为防止进一步讨论,我把评论禁了…

经典证明:Prüfer编码与Cayley公式

  
    Cayley公式是说,一个完全图K_n有n^(n-2)棵生成树,换句话说n个节点的带标号的无根树有n^(n-2)个。今天我学到了Cayley公式的一个非常简单的证明,证明依赖于Prüfer编码,它是对带标号无根树的一种编码方式。
    给定一棵带标号的无根树,找出编号最小的叶子节点,写下与它相邻的节点的编号,然后删掉这个叶子节点。反复执行这个操作直到只剩两个节点为止。由于节点数n>2的树总存在叶子节点,因此一棵n个节点的无根树唯一地对应了一个长度为n-2的数列,数列中的每个数都在1到n的范围内。下面我们只需要说明,任何一个长为n-2、取值范围在1到n之间的数列都唯一地对应了一棵n个节点的无根树,这样我们的带标号无根树就和Prüfer编码之间形成一一对应的关系,Cayley公式便不证自明了。
Read more…