对推导不感兴趣的老大们可以通过搜索”def”直接跳到代码实现部分。不过有闲心还是瞧瞧推导过程的好。我们可以看见好的算法并非无迹可寻,完全依赖某位大牛的灵光闪现,而是通过观察、归纳、试验、迭代改进,逐步雕琢而成。另外,我们时常感叹,要是有时间学习算法就好了。要是有时间仔细读读
TAOCP就好了。其实呢,读TAOCP也不是抢鸡蛋的大事。想读了,就备好纸笔,调出趁手的
编辑器,打开书,翻到自己感兴趣的章节。遇到公式就推一下。遇到算法就
实现 一下。遇到概念就举几个例子印证一下。遇到难解之处就和Google勾兑一下(不要忘了Google Group,真正的技术宝藏)。至于一次读多久,钻多深,施主随喜。再说从俺写的这个系列可以看出,TAOCP不少章节也就需要高中知识,到数列为止。本来是很美好的事。不必正襟危坐。不必如临大敌。不必把自己逼得太紧。你会发现花的时间不多,收获却不小。
其实我们也可以用递推公式定义格雷码:,明确地给出格雷码表里每一位格雷码的表达式:
。这里的g(k)表示在格雷码表里排在第k位的格雷码。其实,因为
以
开头,格雷码的无限序列是所有非负整数的某个排列:
如果我们把每个格雷码看作前面可以填充零的二进制整数,那长度为N的格雷码序列
包含公式(4)中最前面的2
n个整数。只不过每个整数前可能填充0,使之长度为N。再把整数转换成字符串,就生成了相应的格雷码。比如说当N=3时,第二位格雷码是001,相当于在公式(4)里的第二个整数(1)
2前面补上两个0后将其转换为字符串,使得该字符串的长度等于3。
当k = 2
n + r 且
时,我们可以从公式(3)得出g(k) = 2
n + g(2
n – 1 – r )。这个结论的推导也挺容易:既然k > 2
n, 那么我们知道g(k)一定在
里面。既然
表示将
反转,那
中对应第r位的格雷码刚和等于
中第2
n-r-1位的格雷码。最后,在g(r)前添一个1,正好相当于g(r)+(1000…000)
2,共n个0。换成10进制,就得到g(k) = 2
n + g(2
n – 1 – r )了。根据这个公式,我们可以得出一个非常有用的结论:给出整数k和它的二进制形式(…b
2b
1b
0)
2,以及第k位的格雷码g(k)的二进制形式(…a
2a
1a
0)
2,我们可以用数学归纳法证明下面的关系:
这个
表示位运算中的异或运算。也就是说,只有当两个位不一样时他们异或的结果为1。不然就为0。比如说,当k = 3651时,它的二进制形式是(111001000011)
2。那第k位的格雷码g(k)就是 k ^ (k >> 1) = 2402 = (100101100010)
2。证明其实也不难,利用公式g(k) = 2
n + g(2
n – 1 – r )对n做归纳就行。有兴趣的老大们可以自己去做。网上也有答案。
现在我们把
从j开始展开,看看能得到什么:
对等式两边分别求异或,我们得到
中间项都消掉了,因为
等于0, 而0在异或运算中是identity, 也就是说任何一个值与0异或还是等于自身。这个等式看来是无穷项,但因为
迟早会等于0, 所以它其实是有限的,可以计算。
根据公式(5),我们得到很酷的公式:
注意这个
对应编程里的位移运算:k >> 1,所以实现起来也方便。我们立刻有了新的算法,异常简洁:
defgray_g(n)
return [] if n ==0
return (0..((1<<n) -1)).collect{|k|sprintf(“%0#{n}b”, (k ^ (k>>1)))}
end
上述程序无非是说,从0循环到2n, 对每个循环k, g(k) = k ^ (k >> 1)。够简洁吧?唯一的问题是,这个方法还是不够高效。每次循环,我们都要对k做位移。而位移的时间和k的长度成正比。循环2n次是很大的开销,我们自然希望每次循环的计算量越小越好。
同理,公式(6)可以导出很酷的反向公式。于是我们可以求出任意格雷码的位置:
如果有些老大对这种写法不习惯,下面的等价写法也许清楚点:
对应的代码也很直观:
def gray_pos(gray_code)
pos = g = gray_code.to_i(2)
while g > 0
g >>= 1
pos ^= g
end
return pos
end
其实格雷码的原理早已隐含在九连环的解法里。九连环的传统解法对应一种颇为高效的格雷码生成代码。关于九连环的知识,可以到
这里或者
这里去了解。俺就不罗嗦了。从推导算法的角度出发,我们只需要知道玩儿九连环的
两条规则(引自上面链接的文章):
a): “第1号环随时可自由上下”
b): “其他环当且仅当它前面仅有与它相邻的一个环在上面时可自由上下。”
(从TAOCP截的图)
基于这两条规则,我们可以把九连环上每个环的状态用2进制数表示。环在杠上,对应的数字为1,环在杠下,对应的数字为0。比如上图的,从左向右数,对应的数字是(1011000)2。注意第二个环没有套在杠上。这样的话,解环对应于把(111…11)2变成(000…00)2。而套环对应于把(000…00)2变成(111…11)2。因为每次最多变动一个环,解环或套环环的过程相当于生成格雷码全序列。
1872年一个名叫Louis Gros的法国官员证明了如果一个九连环的状态为(an-1…a0)2, 而我们按上面的公式(6)定义一个二进制数k = (bn-1, … , b0)2, 那么我们可以刚好用k步解除该九连环。从这个意义说,Gros老大才是格雷码的真正发明人。
我们用解环来说明格雷码的生成过程。只要九连环的状态不是(000…00)
2或(100…00)
2, 那我们只可能执行规则a)或者b),而且只有其中一步可以让我们靠近答案。规则a)把环取下时,相当于最末一环的状态从1变成0, 而套环相当于把0变成1。也就是说
。参考公式(6),这相当于把把k变成
,因为只有k
0变了。显然我们希望当k的最后一位是1,也就是当k为奇数时按规则a)执行。这样k变成k-1,离我们的目标近了一步。另外一方面,根据规则b),只有形为(…x100…00)
2的九连环可以移动x代表的那个环。换句话说,如果一个长度为n的九连环里某个环所在位置以(10
j-1)
2结尾(这里(10
j-1)
2表示一个二进制数,第一位是1,后面跟了j – 1个0), 1 <= j < n, 则该环可以移动。该环对应a
j+1, 所以移动后该环的状态变成
。根据公式(6),我们知道b
j, b
j-1,… , b
0, 都包含a
j+1项。所以说移动该环后,k变成了
。举个例子哈。上面九连环图里从右数第3个环可以被解下。用数字来表达,就是(1011000)
2可以变成(1001000)
2。具体到右数第三左数第5个环,我们可以说a
4从1变成0。解环前k等于gray_pos(0b101100) = 55=(110111)
2, 解下环后k变成了
= gray_pos(0b100100) = 56 = (111000)
2。同执行规则a)一样,我们希望执行规则b)后k变小:
应该等于k – 1。要达到这个目标,k必须是2
j的倍数,但不能是2
j+1的倍数。这是因为形如(xxx…10
j)
2的数才是2
j的倍数。它和2
j+1-1异或后,才能等于(xxx…01
j )
2,也就是k-1。我们可以把j和k的关系表达为:
这里的函数
叫作ruler function,有兴趣的老大们可以参见
这里的第13页,印张第8页,公式32到45。那节专讲位运算的技巧,可以和IBM某编译器牛人出的
Hacker's Delight以及
这个网页一块儿看。做嵌入式的老大和雅好底层优化的老大们应该可以获益不少。不过这里俺就不跑题了。我们只需要知道
返回k靠右连续0的个数。比如说
。从九连环可以自由移动的那头给环的移动次序编号:0, 1, 2, …, , 那把环的状态从00000…0变成1111…11的移动顺序就是
。那对应的算法也就很直观了:设定长度为n的序列,该算法从(0, 0, 0…, 0)开始,逐次访问每个组合:(a
n-1, …, a
0)
2。每一步只改变一位,相当于移动一个环。对每一步i, 移动对应的环相当于找到
对应的环,然后对它的值求补(异或)。我们也不需要每次通过求异或判断奇偶来决定执行规则a)还是规则b):维护一个奇偶值,parity就行了:
32: def alg_g(n)
33: step = Array.new(n, 0)
34: result = []
35: parity = 0
36:
37: loop do
38: result << step.join
39: parity = 1 - parity
40:
41: if parity == 1
42: j = 0
43: else
44: 0.upto(n-1) do |i|
45: if step[i] == 1
46: j = i+1
47: break
48: end
49: end
50: end
51:
52: return result.reverse if j == n
53:
54: step[j] = 1 - step[j]
55: end
56: end
这个奇偶位很有点意思。当我们要计算求和公式
或者
且计算结果只依赖于二进制字串的奇偶性或者某个子集里的元素个数的时候,这个公式就变得很方便了。而且这个奇偶位也让上面的算法变得高效:没有它的话,决定到底执行规则a)还是规则b)就不容易了。
这个算法最精当的地方在于第54行。我们只需要对一个bit做出改动。不过呢,对每一个生成的格雷码,我们有可能执行第44到48行的内循环。当我们要执行2n次外循环时,这个开销还是大了点。所以我们接下来要介绍除去Gideon Ehrlich在1973年提出的方法,消去内循环,并从中学到新的设计算法解决问题的手段。
分享到:
相关推荐
排列组合问题的算法设计是指如何高效地生成所有可能的排列或组合。今天,我们将讨论基于C语言的排列组合算法实现。 一、全排列算法 全排列算法是指生成所有可能的排列的算法。常见的全排列算法有递归算法、分治...
1. **排列算法实现**: - **回溯法**:回溯法是一种尝试所有可能解的方法,当找到一个解时停止搜索。在排列问题中,可以使用递归的方式,从第一个元素开始,依次尝试后面的元素作为当前位置的值,同时递归地处理...
在实现排列算法时,通常使用回溯法,即从第一个位置开始,尝试所有可能的元素,然后递归地对剩余位置进行操作,直到所有位置都被填满或无法填充为止。在压缩包中的`Permutation.java`文件中,你可以找到这样的实现,...
在编程领域,排列组合是算法设计中的一个重要概念,它涉及到如何有效地生成所有可能的序列或组合作为问题的解决方案。本篇文章将详细讲解如何在Qt C++环境中实现排列组合的算法。 Qt是一个跨平台的C++图形用户界面...
1. **排列算法实现**: - 递归方法:通常使用回溯法,从第一个元素开始,依次将剩余元素与已选元素进行排列,直到所有元素都被使用。 - 迭代方法:可以使用栈来存储当前状态,每次循环时更新未使用的元素,并将其...
排列生成算法是计算机科学中处理有限集合的一种方法,主要用于生成所有可能的排列顺序。这里主要讨论两种算法:字典序法和邻位互换法。 字典序法是一种按照特定顺序(通常是最小字典序)生成排列的算法。在字典序中...
全排列算法是计算机科学中处理数组或集合的一种经典方法,主要应用于组合数学和算法设计领域。在本场景中,我们关注的是"交换算法",它用于生成一个给定数组的所有可能排列。全排列是指从n个不同元素中取出m个元素...
排列生成算法是组合数学中的一个重要分支,在许多实际应用中都有广泛的应用,例如在密码学、计算机科学和信息检索等领域。传统的排列生成算法多种多样,包括但不限于基于回溯技术的字典顺序生成算法、基于相邻元素...
3. **组合数学**:在生成Gray码的过程中,可能用到了组合数学中的排列组合概念。例如,对于n位Gray码,其总数是2^n,可以通过递归或者非递归方法计算出所有可能的序列。 4. **递归算法**:一种可能的生成Gray码的...
【排列组合的算法作业 Java】 在编程领域,排列和组合是经典的算法问题,它们属于组合数学的一部分,常常出现在数据结构与算法课程的作业中。排列指的是从给定的元素集合中选择并按特定顺序排列所有可能的组合,而...
运行此代码,可以观察到EVEN算法生成排列的过程。 在东南大学的组合数学实验中,学生不仅需要理解EVEN算法的原理,还需要通过编写代码来加深对算法的理解,这有助于提升他们的逻辑思维能力和问题解决能力。通过这样...
组合算法在MATLAB中的应用广泛,它涉及到许多数学和计算机科学的基本概念,如排列、组合、穷举搜索、最优化等。本参考资料旨在提供一种深入理解及实践MATLAB中组合算法的方法。 首先,我们要理解什么是组合算法。...
#### 排列算法分类 在组合数学中,常见的全排列算法有以下几种: 1. **递归算法** - **原理**:递归算法的基本思想是从n个元素中选择第一个元素,然后对剩下的n-1个元素进行递归求解,直到只剩下一个元素为止。 -...
本资源包聚焦于几种常见的算法策略,包括穷举法、高精度计算、动态规划、回溯、贪心算法、排列组合以及排序。下面将逐一详细阐述这些算法思想及其应用。 1. **穷举法**:穷举法,也称为全搜索法,是一种通过尝试...
9. **组合算法**:组合算法通常涉及从有限集合中选择元素的问题,如排列组合、子集选择等。这些问题常常出现在组合优化问题中,例如0-1背包问题、组合调度问题等。 以上算法在实际问题中都有广泛的应用,理解和掌握...
2. **定义Reducer类**:Reducer类的作用是接收来自Mapper的输出,并对这些输出进行进一步的组合处理,生成更长的排列组合序列。Reducer需要递归地调用自身来生成所有可能的组合。 3. **配置Job**:配置MapReduce...
在编程领域,排列组合是算法设计中的一个重要概念,它涉及到如何有效地生成所有可能的序列或组合,这在很多问题中都有应用,比如游戏设计、数据分析、密码学等。本示例是以Delphi语言实现的MM排列组合算法,这是一种...
在Golang中实现全排列算法主要是为了解决排列组合问题,特别是在处理字符串或数组时,例如在上述例子中,需要对一组数字进行字典序排序并输出所有可能的排列。全排列算法是找出一个序列中所有可能的排列方式,且每个...
描述提到该算法在Linux环境下用于求解排列熵,对于突变检测有辅助作用,这表明它可能被用在了基因序列分析或类似需要检测序列变化的场景。 排列熵(Permutation Entropy, PE)的计算基于序列的排列结构,其基本思想...