您的位置:首页 > 其它

探索并优化组合算法的中的01移位转换法(非递归)

2017-05-24 23:17 447 查看
//今天来讲讲如何把逐步优化串行(单机处理)的组合算法--01移位法
//以下所有方法只针对数组下标处理,与数组具体有什么元素没有任何关
系,所有方法均返回相同的组合数结果集(不输出)
//可能有些同学不知道什么是01移位法,以下作简单的说明:
首先我们建立一个长度的5的数组来映射它的位置,并用0或1表示该位
置上的元素是否被选到,比如[1,1,1,0,0]这个序列表示[0,1,2],
[0,1,1,1,0]则表示[1,2,3];
//下一个序列,总有当前的序列产生,它的产生规律如下:初始化第
从左向右找到第一对'10',交换他们的值,即'10'=>'01',然后将它们
前面的'1'全部移动到序列的开头,就产生了一个新的序列,新序列依照
前面的变化规律产生下一个新的序列,如5选3,变化规律如下
/**
*1
4000
1 1 0 0 //1,2,3
*    1 1 0 1 0 //1,2,4
*    1 0 1 1 0 //1,3,4
*    0 1 1 1 0 //2,3,4
*   1 1 0 0 1 //1,2,5
*   1 0 1 0 1 //1,3,5
*     0 1 1 0 1 //2,3,5
*    1 0 0 1 1 //1,4,5
*   0 1 0 1 1 //2,4,5
*     0 0 1 1 1 //3,4,5
*
* 然后我们来实现第一个版本的01移位法
* @param  array  $count [数组长度]
* @param  int    $selectMany  [选择多少个]
* @return [mixed]              [成功则返回形
* 如[1,1,1,0,0]的组合数结果集(二维数组)]
*/
public  function combinationsByTransform1(int $count,int$selectMany){
if($selectMany<2 || $count<$selectMany) return false;
$combination = [];  //each combination
for ($i=0; $i < $count; ++$i) {
$combination[] = 0;
}
//init the first combination 初始化第一个序列
for ($i=0; $i < $selectMany ; ++$i) {
$combination[$i] = 1;
}
$transform = [$combination]; //store combination 保存第一个序列
$end = $count-1; //如果i移动到序列末端,未找到'10'则已经产生了所有序列
for ($i=0,$one=-1;$i<$end; ++$i) {
if($combination[$i]) ++$one; //统计直到当前位置,'1'的个数
//找到第一对'10'并交换
if($combination[$i]==1 && $combination[$i+1]==0){
$combination[$i] =0;
$combination[$i+1] = 1;
//以下两个for是把第一对'10'前的'1'全部移动到序列开头
for ($k=0; $k < $one; ++$k) {
$combination[$k] = 1;
}
for ($k=$one; $k < $i; ++$k) {
$combination[$k] = 0;
}
//保存序列
$transform[] = $combination;
//查找下一个序列
$i=-1;
$one = -1;
}
}
//返回组合数形如[1,1,1,0,1]结果集
return $transform;

}
/**combinationsByTransform1 返回的形式如[1,1,1,0,1]
要没被选择到的位置,而且版本1还浪费空间,故而时间上也会很有影响
所以我们将版本1改了一下,让其返回形如[0,1,2,3]组合数,这正是
我们需要的结果
* [combinationsByTransform2 01转换法的改进版,输出组合
* 数形如[0,1,2,3]的结果集]
* @param  int    $count      [同上]
* @param  int    $selectMany [同上]
* @return [mixed]             [返回组合数结果集]
*/
public  function combinationsByTransform2(int $count,int$selectMany){
if($selectMany<2 || $count<$selectMany) return false;
$combination = [];  //each combination
for ($i=0; $i < $count; ++$i) {
$combination[] = 0;
}
//init the first combination
for ($i=0,$transform=[]; $i < $selectMany ; ++$i) {
$combination[$i] = 1;
$transform [] = $i;
}
$combinations = [$transform];
$end = $count-1;
for ($i=0,$one=-1;$i<$end; ++$i) {
if($combination[$i]) ++$one;
if($combination[$i]==1 && $combination[$i+1]==0){
$combination[$i] =0;
$combination[$i+1] = 1;
for ($k=0; $k < $one; ++$k) {
$combination[$k] = 1;
}
for ($k=$one; $k < $i; ++$k) {
$combination[$k] = 0;
}
//与版本1不一样的地方,这里将序列转换成了形如[1,2,3]的组合数
for ($k=0,$transform = []; $k < $count; ++$k) {
if($combination[$k]) $transform[] = $k;
}
$combinations[] = $transform;
$i=-1;
$one = -1;
}
}
return $combinations;
}

//版本2虽然返回了我们需要的结果,并且大大降低了空间的使用,但是
转换过程又遍历了一次,也就是说又花了一次遍历的时间
如果序列长度很大,就相当影响性能了,所以需要优化
/**版本3:在版本2的基础上,优化转换过程,使用索引映射和反索引映射,
可去掉版本2中用于转换的遍历过程
* [combinationsByTransform3 based on combinationsByTransform2
*  ,deleting the transform process by using an index(report all
location is the No.x '1') ]
* @param  int    $count      [description]
* @param  int    $selectMany [description]
* @return [type]             [description]
*/
public  function combinationsByTransform3(int $count,int$selectMany){
if($selectMany<2 || $count<$selectMany) return false;
$combination = [];  //each combination
for ($i=0; $i < $count; ++$i) {
$combination[] = 0;
}
//init the first combination
for ($i=0,$index=[]; $i < $selectMany ; ++$i) {
$combination[$i] = 1;
$index[] = $i;  //初始态时每个'1'所在的位置,即第几个再原数组的第几个位置上
}
$reverse = $index;  //初始态时原数组中某个'1'是第几个'1',
//与$index互相映射$reverse[$j] = $i,则$index[$i] = $j,
//$j表示原数组的$j下标,$i表示第$i个1;
$combinations = [$index];
$end = $count-1;
for ($i=0,$one=-1;$i<$end; ++$i) {
if($combination[$i]) ++$one;
if($combination[$i]==1 && $combination[$i+1]==0){
$combination[$i] =0;
$combination[$i+1] = 1;
$index[$reverse[$i]] = $i+1; //更新第$i个1的位置信息
$reverse[$i+1] = $reverse[$i]; //更新原数组第$i+1的位置上的对应的第$i个1的位置
unset($reverse[$i]);
//以下两个for将第一对10前的1全部移动到序列开头,并更新相应的$index和$reverse
for ($k=$one; $k < $i; ++$k) {
$combination[$k] = 0;
if(isset($reverse[$k])) unset($reverse[$k]);
}
for ($k=0; $k < $one; ++$k) {
$combination[$k] = 1;
$reverse[$k] = $k;
$index[$k] = $k;
}
$combinations[] = $index; //保存该组合数 $index表示[1,2,3]这样的一维数组
$i=-1;
$one = -1;
}
}
return $combinations;
}

/**虽然版本3比版本2少作了以此转换过程的遍历,性能也提高了不少,
但是它在每次产生新序列的过程中都遍历了长度为$count的序列,如果,
这个长度非常非常长的花,将严重影响产生新序列的效率
我们来重新理解01移位法的规则:"从前向后找到第一对'10',交换它们",
这句话如果换种理解就是,从前向后遍历所有的1,一旦遍历到的1,如果它
的后一位是0的话,则交换它们,这样我们每产生一个新的序列,最多需要
遍历$selectMany次,如果$count>>$selectMany ,这一步优化的影
响尤为客观以下是实现这一优化的版本4
* [combinationsByTransform4 based on combinationsByTransform3 ,
* change the max searching lenght of find first '10' from $count
*  to $selectMany]
* @param  int    $count      [description]
* @param  int    $selectMany [description]
* @return [mixed]             [description]
*/
public  function combinationsByTransform4(int $count,int$selectMany){
if($selectMany<2 || $count<$selectMany) return false;
$combination = [];  //each combination
for ($i=0; $i < $count; ++$i) {
$combination[] = 0;
}
//init the first combination
for ($i=0,$index=[]; $i < $selectMany ; ++$i) {
$combination[$i] = 1;
$index[] = $i;
}
$reverse = $index;
$combinations = [$index];
//与版本3不一样的地方只有这里改成了对所有的1遍历,省去了对0的遍历
for ($i=0,$one=-1;$i<$selectMany; ++$i) {
$j = $index[$i]; //$j表示每个1在原序列中的位置
++$one; //计算1的遍历次数,表示到目前为止已经出现这么多个1
//以下同版本3
if($combination[$j]==1 && $j+1<$count &&$combination[$j+1]==0){
$combination[$j] =0;
$combination[$j+1] = 1;
$index[$reverse[$j]] = $j+1;
$reverse[$j+1] = $reverse[$j];
unset($reverse[$j]);
for ($k=$one; $k < $j; ++$k) {
$combination[$k] = 0;
if(isset($reverse[$k])) unset($reverse[$k]);
}
for ($k=0; $k < $one; ++$k) {
$combination[$k] = 1;
$reverse[$k] = $k;
$index[$k] = $k;
}
$combinations[] = $index;
$i=-1;
$one = -1;
}
}
return $combinations;
}
/**乍看一下,版本4似乎已经逆天了,其每产生一个新的组合数最多只需要O(m)
的时间复杂度,m表示选多少个元素,但是它还不能得瑟得太早,版本5表示我要和
版本4谈人生在实现版本5之前,我们再来看01移位法的规则:"从前向后找到第一
对'10',交换它们",我们把这句话先多读几遍,有木有明白它的本质?
木有的话,也没关系,我来解释给你听:它想说的是,定位到从前向后数的第一对'10'
,然后交换它们。没错,就是定位,精确的定位,无须任何的遍历!一针见血!
直接找到第一对'10'。
既然要做到定位,那我们总得知道第一对'10'的位置在哪吧,也就是说,我们需要
一个栈去记录它们的位置信息,
以下的版本5使得每一个组合数的生成只需要O(1)的时间复杂度。
* [combinationsByTransform5 based on combinationsByTransform4 ,
* plus accurate locating method which can quickly get the index
*  of '10' without traversal]
* @param  int    $count      [description]
* @param  int    $selectMany [description]
* @return [type]             [description]
*/
public  function combinationsByTransform5(int $count,int$selectMany){
if($selectMany<2 || $count<$selectMany) return false;
$combination = [];  //each combination
for ($i=0; $i < $count; ++$i) {
$combination[] = 0;
}
//init the first combination
for ($i=0,$index=[]; $i < $selectMany ; ++$i) {
$combination[$i] = 1;
$index[] = $i;
}
$stackLenght = 0; //栈的初始长度
$stack = [$selectMany-1]; //第一对'10'中'1'的的位置
$reverse = $index;
$combinations = [$index];
while ($stackLenght>=0) {
//出栈:该序列从前往后数的第一对'10'中'1'的位置
$curIndex = $stack[$stackLenght];
unset($stack[$stackLenght]);
--$stackLenght; //栈的长度减1
$combination[$curIndex] = 0;
$combination[$curIndex+1] = 1;
if($curIndex+2<$count && $combination[$curIndex+2]==0)
//进栈:下一对'10'中'1'的位置
$stack[++$stackLenght] = $curIndex+1;
$index[$reverse[$curIndex]] = $curIndex+1;
$reverse[$curIndex+1] = $reverse[$curIndex];
$one = $reverse[$curIndex];
unset($reverse[$curIndex]);
//以下两个for是把当前位置前面的1移动到开头
for ($k=$one; $k < $curIndex; ++$k) {
$combination[$k] = 0;
if(isset($reverse[$k])) unset($reverse[$k]);
}
for ($k=0; $k < $one; ++$k) {
$combination[$k] = 1;
$reverse[$k] = $k;
$index[$k] = $k;
}
if($combination[$one]==0 && $one-1>=0)
//进栈:下一对'10'中'1'的位置
$stack[++$stackLenght] = $one-1;
$combinations[] = $index;
}
return $combinations;
}
/**写完了版本5,测试时发现,版本5在n比较大的情况下,
依然效率低下,原因在于移位过程中,将1转化为0时,遍历
不少不需要转换的位置,为此,我在版本5的基础上做了一个
优化的版本6,这个版本的优化的特点在于:把第一对10前所
有的1移到序列开头的同时,并把原来的位置上的置设为0,删
除对应的反索引
* [combinationsByTransform6 based on combinationsByTransform5]
* @param  int    $count      [description]
* @param  int    $selectMany [description]
* @return [mixed]             [description]
*/
public  function combinationsByTransform6(int $count,int$selectMany){
if($selectMany<2 || $count<$selectMany) return false;
$combination = [];  //each combination
for ($i=0; $i < $count; ++$i) {
$combination[] = 0;
}
//init the first combination
for ($i=0,$index=[]; $i < $selectMany ; ++$i) {
$combination[$i] = 1;
$index[] = $i;
}
$stackLenght = 0; //栈的初始长度
$stack = [$selectMany-1]; //第一对'10'中'1'的的位置
$reverse = $index;
$combinations = [$index];
while ($stackLenght>=0) {
//出栈:该序列从前往后数的第一对'10'中'1'的位置
$curIndex = $stack[$stackLenght];
unset($stack[$stackLenght]);
--$stackLenght; //栈的长度减1
$combination[$curIndex] = 0;
$combination[$curIndex+1] = 1;
if($curIndex+2<$count && $combination[$curIndex+2]==0)
//进栈:下一对'10'中'1'的位置
$stack[++$stackLenght] = $curIndex+1;
$index[$reverse[$curIndex]] = $curIndex+1;
$reverse[$curIndex+1] = $reverse[$curIndex];
$one = $reverse[$curIndex];
unset($reverse[$curIndex]);
//以下是把当前位置前面的1移动到开头
for ($k=0; $k < $one; ++$k) {
$combination[$index[$k]] = 0;
if(isset($reverse[$index[$k]])) unset($reverse[$index[$k]]);
$combination[$k] = 1;
$reverse[$k] = $k;
$index[$k] = $k;
}
if($combination[$one]==0 && $one-1>=0)
//进栈:下一对'10'中'1'的位置
$stack[++$stackLenght] = $one-1;
$combinations[] = $index;
}
return $combinations;
}
//后记:虽然我把01移位法优化到了每产生一个组合数只需要
//O(m)的时间复杂度,实际上上面每个版本的性能并非线性
//增强,它与n、m的大小关系密切,比如4有时比5性能要高,5有
//时性能比6要高,但是无论哪个版本它都比不上另一种算法,无论
//是时间复杂度还是空间复杂度,都比不上,更好的组合算法请查看
//分治位偏移法(每产生一个组合数只需要O(1)的时间复杂度)
//以及另一种同样高效的动态建模法


分治位偏移量法

动态建模法
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息