使用排序化简组合生成算法

  组合问题在日常生活中随处可见,先来看一个摘于《离散数学》(第六版,Richard Johnsonbaugh著)的例子:

  摇滚乐队“Unhinged Universe”录制了n段视频节目,时间长度分别为t1 ,t2 ,t3 ,…,tn 秒。一盘磁带可以容纳C秒的视频。这是Unhinged Universe发行的第一盘磁带,乐队希望这盘磁带能尽可能多的收入他们的视频节目。问题转化为从{1,2,3,...,n}中选出一个子集{i1 ,i2 ,i3 ,…,ik } 使在k j=1 tij 不超过C的情况下尽可能大。最直接的算法是穷举{1,2,3,...,n}的所有子集,选出使上式在不超过C的情况下最大的子集。该算法需要生成n元素集合的所有组合。

  生活中有很多像Unhinged Universe这样的例子,我们可能并不总是在整数集合上生成组合,为了解决这些问题,我们需要生成n元素集合X的所有组合,至于X中的元素并不总是整数,它们可以是任何东西。

  问题看来是那么熟悉,也许你立刻就能给出一个生成X上所有组合的思路:遍历X中的所有元素,每遇到一个元素视为一步,每步都作出两种选择,将该元素加入到正在生成的组合中或不将其加入这个组合。这样,生成所有组合的过程就是作出n步选择的过程。由乘法原理得,n步选择会让我们得到2n 种可能的情况,这正好是X子集的总数。这个想法着实不错,那就开始实现吧,每作出一次选择问题的规模就会减一,这也许会让你想的递归,下边给出递归算法的伪代码:
 

  递归函数总是带来一些让我们无法接受的额外开销,一般需要提供一个非递归的版本,这可以使用辅助栈和辅助队列来实现,这里只提供一种使用辅助栈实现的思路。定义两个栈用以存放结果,同一时间只有一个栈是有效的。开始时向一个栈中压入一个空集合,并置该栈有效。然后遍历X中的元素,每遇到一个元素Xi就依次弹出当前有效栈中的所有元素,每弹出一个元素,就把它压入当前无效栈,再把Xi加入到刚弹出的栈元素所代表的X的一个子集中,并把新子集压入当前无效栈,然后反置栈的有效和无效标志。这样重复n次,当前有效栈中的元素就是X上所有的组合。分析算法1及它的非递归版本,他们的时间复杂度都是 θ(2n ) ,符合组合问题的固有复杂度。虽然递归算法在构建栈和反解栈时开销很大,但我们的非递归版本可以很好的解决问题。Unhinged Universe乐队一定会很满意。

  故事还没有结束,后来Unhinged Universe乐队决定,要在这盘磁带中收入r个视频节目。现在问题变为了求解n元素集合X的所有r组合,我们可以简单的剔除算法1结果中非r元素子集来得到新问题的解,删除操作需要进行 2n -C(n,r) 次。问题似乎有得到了解决。

  但这次Unhinged Universe乐队可能会抱怨了。他们也许明白我们现在得到的候选解较之以前少了 2n -C(n,r) 个,但却耗掉的和原来一样甚至更长的时间。也许他们会问,生成 2n 个候选解的时间复杂度为 θ(2n ) ,那为什么生成C(n,r)个候选解的时间复杂度不可以是θ(C(n,r))?

  迫于压力,你开始修改算法1,以使该算法能尽早发现并结束对不可能产生最终解情况的计算,算法可能像是这样:

  算法执行过程中,当出现n<r的情况时,表示后续过程中需要从少于r个元素中选出r个元素,这说明当前中间解不可能产生最终解,应立刻停止对该中间解的进一步求解。较之算法1,该算法确实减少了不必要的计算,但是在发现某一个中间解不可能产生最终解前,他总会不可避免的进行一些多余的计算,尤其是当|n-r|较大时。问题一筹莫展,也许你会问,真的存在θ(C(n,r))的算法吗。此时,问题化简的思想就是救星。

  让我们放弃以前的思路,从长计议,排序可以使问题得到化简。

  首先让我们给出一些假设。假设R是集合X上的一个偏序关系,则R可以定义集合X上元素的序。换言之,我们可以使用R对X中的元素排序。此处规定:若x∈X,y∈X,x≠y且xRy,则x排在y的前边,记作x<y。另外,我们将X上的r组合 {x1 ,x2 ,…,xr } 记为字符串 S1 S2 …Sr ,其中 S1 <S2 <…<Sr ,且 {x1 ,x2 ,…,xr }={S1 ,S2 ,…,Sr } 。并且假设集合Y是由所有的字符串S组成。接着,我们再定义一个对X上字符串 S1 S2 …Sr 排序的偏序关系Q,在关系Q中,当需要对X中的元素排序时,使用关系R。

  有了上边这些假设,我们的新思路就很清晰了。因为集合Y是有限的且存在关系Q,可得Y是良序的,即可以在Y中找到一个最小的串a。又由关系Q可以确定的求出Y中任何一个非最大串b的后续c。这样,从a开始重复进行C(n,r)次对c的求解,就可以得到Y中的所有元素,即X上的所有r组合。

  也许上边的描述让你迷惑,让X={1,2,...,n}是个不错的想法,这个整数集合上的特化版本会让你更好的理解思路。这里之所以把X定义在整数集合上,主要原因是将问题域定义在整数集合更具有代表性。因为X可以是任何东西的集合,所以其上关系R的确定多样且通常并不容易。此时我们可以简单的使用集合X中各元素的下标来代替真正的元素(虽然数学意义上的集合是无序的,也就不存在下标;但当在计算机中存储集合时,却总是按某一顺序进行的,计算机中存储的集合是一个序列,此处我们假设这个序列以下标1开始),求出这些下标的组合后,把下标替换为相应的元素即可。现在我们只要使用整数集合上的二元关系“小于等于”来定义R,就可以解决所有的问题。在给出算法前我们还差点什么呢?对,我们需要定义关系Q。这里使用字典序来定义Q,即把字典中的有序字母集替换为有序整数集{1,2,...,n}(该整数集的序由其上的二元关系“小于等于”关系来定义),这样我们就把单词在字典中的顺序推广为定义在整数集{1,2,...,n}上的字典序,其定义如下:

  定义1  设a= S1 S2 …Sp 和b= T1 T2 …Tq 为定义在{1,2,...,n}上的字符串。称在字典序中a在b之前,当且仅当(1)或(2)成立,记为a<b。
  1)p<q且 Si =Ti ,i=1,2,...,p。
  2)i为使 Si Ti 成立的最小的i,且 Si <Ti

  所有的准备工作都已完成,下面让我们来看怎样确定串b的后续c。假设b= S1 S2 …Sr ,c= T1 T2 …Tr ,在串b中从右向左找到第一个非最大值的元素 Sm Sr 的最大值为n, Sr-1 的最大值为n-1,依此类推),则:

  Ti =Si , i=1,2,…,m-1

  Tn =Sm +1

  Tm+1 Tm+2 …Tr =(Sm +2)(Sm +3)…

 

  一切都已明了,终于迎来了我们的算法:

 

  我们的新算法好高效的解决了问题,同时也证明了生成n元素集合上的r组合的时间复杂度为O(C(n,r))。对比前边的两种方法,算法2像一个莽夫,它盲目的尝试将一个元素加入一个中间集合或者不加入其中,直到发现这个中间集合不可能得到任何最终解时,才停止对该集合的进一步计算;而算法三不存在这样的尝试,像一位智者,它永远都那么明确的生成下一个解,而且它知道下一个解一定是一个要求的r组合。
  整个过程中我们所做的就是把组合问题化简为排序问题,而我们得到的是无与伦比的效率。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值