一个N-tuple是一个包含N个元素的序列,一般写作
。比如说
就是一个6-tuple。注意tuple和集合的区别。Tuple里可以包含重复的元素。而且Tuple的元素是有序的,就跟数组的元素一样。
那么给定一个N-tuple,
,我们怎么生成全部有序列呢?由简入繁总是学习的不二法门。所以我们从简单的二进制N元有序列(binary n-tuple)开始:二进制N元有序列的每个元素都是一个bit,非0即1。比如说一个二进制3-tuple,生成的全部有序列为
(0, 0, 0)
(0, 0, 1)
(0, 1, 0)
(0, 1, 1)
(1, 0, 0)
(1, 0, 1)
(1, 1, 1)
一共8个。二进制N元有序列虽然简单,却已经有了广泛的用途。比如说知道怎么生成二进制N元有序列,我们也就知道怎么生成幂集:一个N元集合
,我们规定
属于于一个子集,当且仅当对应的二进制N元有序列的元素
。用上面的3-tuple作例子。给出一个集合
,则
下面还会讨论更多的应用。而且从讨论二进制N元有序列中得出的结论为我们以后探讨怎么生成更复杂的组合样式奠定基础。
生成全部二进制N元有序列的方法再简单不过:我们从二进制数
开始,逐次加一,直到得到
为止。利用进位加法,我们刚好遍历了所有的N-tuples。简单,但是美妙。代码用Ruby实现的。因为Ruby的代码和伪代码差别不大,会不会Ruby的老大都可以看懂。再说算法就是算法。无所谓那门语言。学算法时非找到《XXX语言算法》,纯属无聊。
外层循环负责一个一个地加1,而内层循环负责进位。如果所有位都是1了,则跳出所有循环,因为加无可加。
def binary_all_tuples(tuple_length)
tuple = Array.new(tuple_length, 0)
loop do
puts tuple.inspect
j = tuple_length - 1
while tuple[j] == 1 do
tuple[j] = 0
j -= 1
end
return if j == -1
tuple[j] += 1
end
end
我们知道循环一共进行了 次。所以我们不用每次判断j的值。不过这是细节,无关主旨。
def binary_all_tuples_2(tuple_length)
tuple = Array.new(tuple_length, 0)
1.upto(1<<tuple_length) do |i|
puts tuple.inspect
j = tuple_length - 1
while tuple[j] == 1 do
tuple[j] = 0
j -= 1
end
tuple[j] += 1
end
end
有了这个算法,要生成一个集合的所有子集也就容易了:
def each_tuple(tuple_length=0, &proc)
tuple = Array.new(tuple_length, 0)
loop do
yield tuple
j = tuple_length - 1
while tuple[j] == 1 do
tuple[j] = 0
j -= 1
end
return if j == -1
tuple[j] += 1
end
end
require ’set’
def powerset(set = [])
each_tuple(set.size) do |tuple|
puts “tuple: #{tuple.inspect}”
subset = []
tuple.each_index do |index|
subset << set[index] if tuple[index] == 1
end
puts subset.inspect
end
end
第一个函数each_tuple()和最前面的binary_all_tuples()基本一样。唯一的区别是each_tuple()没有打印出得到的每一个tuple,而是yield每一个得到的tuple,让附着的过程&proc来处理。第二个函数powerset()只需要找出每个tuple里为一的元素对应的坐标index,然后把对应的set[index]收集起来。
这么简单的算法也有精彩的引申。比如说,我们可以把二进制扩展到10进制。十进制的N-tuple:
里,
。所以我们只需要把
逐步递加到
。我们还能进一步总结,处理多进制序列。也就是说,n元序列里的任意元素
的进制是
,用公式表达为:
对不同的
,
不一定相同。本质上,生成全序列的方法并没有改变。我们现在只需要对混合进制的数执行累加。这个混合进制的数可以写为:
累加的算法仍然直观:对满足条件(1)的混合进制序列,我们不断在公式(2)里的数上累加1。进位规则与以前统一进制的进位规则没有本质区别。在第
位时,加到
再进位就行了。
def all_tuples(radix_list = [])
tuple_length = radix_list.size
tuple = Array.new(tuple_length, 0)
loop do
puts tuple.inspect
j = tuple_length - 1
while tuple[j] == radix_list[j] -1 do
tuple[j] = 0
j -= 1
end
return if j == -1
tuple[j] += 1
end
end
比较诡异的是,高老太爷建议当内嵌的循环数目小时,不如直接手写成N层嵌套循环,反而简单。其实也有道理。手写嵌套循环,无非敲的字多了点,但省去了构思和排错的时间,代码也比较直观。当初我们几个朋友一起做操作系统的作业。当我还在那里构思怎么把复杂的循环写得简单通用的时候,同组的一个快枪手早已调试完毕。他就是先手写嵌套循环和判断,事后再来重构。
上述的算法按字典顺序(或者算术顺序)生成所有的序列。有时候,我们需要按其它顺序生成序列。最有名的就是所谓的格雷码了,也就是生成的序列中,任何相邻的两个序列只相差一位。比如说,2进制三元序列的格雷码生成顺序是:
(0, 0, 0)
(0, 0, 1)
(0, 1, 1)
(0, 1, 0)
(1, 1, 0)
(1, 1, 1)
(1, 0, 1)
(1, 0, 0)
很明显,我们可以对用一序列生成不同的格雷码。下面是另外一组格雷码:
(0, 0, 0)
(1, 0, 0)
(1, 0, 1)
(0, 0, 1)
(0, 1, 1)
(0, 1, 0)
(1, 1, 0)
(1, 1, 1)
格雷码应用广泛。从模拟信号到遗传算法到离散数学到九连环的解法,通通有它的影子。有兴趣的可以去google或者百度一下。讨论到后面我们可以看到九连环的解法和格雷码的关系,以及从解法中推导出高效的算法。
有很多等价的格雷码定义。我们从简单的开始,看一个比较直观的:
公式(3)里的
表示空字串,
表示把序列
里每一个字串前加一个0,组成新的序列,而
则表示把序列
翻转,然后把里面每一字串前置一个1,形成新的序列。看看前N = 3的情况:
n = 0 ,我们得到空序列
n = 1,
,所以我们得到唯一的格雷码:0, 1
n = 2,
= 0(0, 1), 1(1, 0) = 00, 01, 11, 10
n = 3,
= 0(00, 01, 11), 1(11, 01, 00) = 000, 001, 011, 111, 101, 000 直观上也好理解,所有序列都是从无到有搭建起来的。所以
和
里所有字串都是符合格雷码定义――相邻字串相差一位。而
的最后一个字串刚好等于
的第一个字串,所以分别加上0和1后,
的最后一个字串和
刚好相差一位。有了直观的概念,归纳法证明近乎琐碎(本来公式就是用来表达我们的直观思想的)。前面的例子可算初始条件,假设
是格雷码,下面的步骤不用写出来了吧?
因为公式(3)是递归定义,我们能直接把它转换成代码:
def gray_rec(n)
return [] if n == 0
return [“0″,“1″] if n == 1
partial = gray_rec(n-1)
return partial.collect{ |e| “0″+e} + partial.reverse.collect{|e|“1″+e}
end
加上 return [“0”, “1”] if n == 1纯粹是为了省下后面对空集的判断。用惯LISP系列的老大们多半不能没有经过tail-recursion优化的代码,所以我们小小改动一下。Ruby其实没有tail-recursion优化,所以下面的代码应该没有什么实质性的改进。
def gray_rec_helper(count, max, partial_result)
return [] if max == 0
return partial_result if count == max
return gray_rec_helper(count+1,
max,
partial_result.collect{ |e| “0″+e}+ partial_result.reverse.collect{ |e| “1″+e})
end
def tail_gray(n)
return gray_rec_helper(1, n, [“0″,“1″])
end
这样的生成方式并不经济。我们后面会通过玩儿九连环推导出更快的算法。再逐步改进,得到不需要内循环的高速算法。
查看本文来源