常用算法原理

1章 回溯法

1.1 一般原理

可用回溯法求解的问题P,通常要能表达为:对于已知的由n元组(x1x2xn)组成的一个状态空间E={(x1x2xn)xiSi ,i=12n},给定关于n元组中的一个分量的一个约束集D,要求E中满足D的全部约束条件的所有n元组。其中Si是分量xi的定义域,且 |Si有限,i=12n。我们称E中满足D的全部约束条件的任一n元组为问题P的一个解。

解问题P的最朴素的方法就是枚举法,即对E中的所有n元组逐一地检测其是否满足D的全部约束,若满足,则为问题P的一个解。但显然,其计算量是相当大的。

我们发现,对于许多问题,所给定的约束集D具有完备性,即i元组(x1x2xi)满足D中仅涉及到x1x2xi的所有约束意味着j(j<=i)元组(x1x2xj)一定也满足D中仅涉及到x1x2xj的所有约束,i=12n。换句话说,只要存在0≤jn-1,使得(x1x2xj)违反D中仅涉及到x1x2xj的约束之一,则以(x1x2xj)为前缀的任何n元组(x1x2xjxj+1xn)一定也违反D中仅涉及到x1x2xi的一个约束,nij。因此,对于约束集D具有完备性的问题P,一旦检测断定某个j元组(x1x2xj)违反D中仅涉及x1x2xj的一个约束,就可以肯定,以(x1x2xj)为前缀的任何n元组(x1x2xjxj+1xn)都不会是问题P的解,因而就不必去搜索它们、检测它们。回溯法正是针对这类问题,利用这类问题的上述性质而提出来的比枚举法效率更高的算法。

回溯法首先将问题Pn元组的状态空间E表示成一棵高为n的带权有序树T,把在E中求问题P的所有解转化为在T中搜索问题P的所有解。树T类似于检索树,它可以这样构造:

Si中的元素可排成xi(1) xi(2) xi(mi-1|Si| =mii=12n。从根开始,让T的第I层的每一个结点都有mi个儿子。这mi个儿子到它们的双亲的边,按从左到右的次序,分别带权xi+1(1) xi+1(2) xi+1(mii=012n-1。照这种构造方式,E中的一个n元组(x1x2xn)对应于T中的一个叶子结点,T的根到这个叶子结点的路径上依次的n条边的权分别为x1x2xn,反之亦然。另外,对于任意的0≤in-1En元组(x1x2xn)的一个前缀I元组(x1x2xi)对应于T中的一个非叶子结点,T的根到这个非叶子结点的路径上依次的I条边的权分别为x1x2xi,反之亦然。特别,E中的任意一个n元组的空前缀(),对应于T的根。

因而,在E中寻找问题P的一个解等价于在T中搜索一个叶子结点,要求从T的根到该叶子结点的路径上依次的n条边相应带的n个权x1x2xn满足约束集D的全部约束。在T中搜索所要求的叶子结点,很自然的一种方式是从根出发,按深度优先的策略逐步深入,即依次搜索满足约束条件的前缀1元组(x1i)、前缀2元组(x1x2)、,前缀I元组(x1x2xi),,直到i=n为止。

在回溯法中,上述引入的树被称为问题P的状态空间树;树T上任意一个结点被称为问题P的状态结点;树T上的任意一个叶子结点被称为问题P的一个解状态结点;树T上满足约束集D的全部约束的任意一个叶子结点被称为问题P的一个回答状态结点,它对应于问题P的一个解。

用回溯法解题的一般步骤:

1)针对所给问题,定义问题的解空间;

2)确定易于搜索的解空间结构;

3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

1.2 n元全排列

1.2.1 问题  

如果p1p2pn中的元素取自12n,且互不相同,则称之为12n的一个全排列。全排列问题就是要求输出所有的全排列。其搜索树为

 

1-1全排列搜索树

1.2.2 算法设计

setp1 k=1p1=p2=…pn=1

setp2 pk,从pk当前的值往前寻求pk的值,要求pkp1pk-1不相同,且不超过n;如果找不到,到step4

step3 如果k=n,打印全排列,pk=pk+1,回到step2

step4 回溯,重新设置当前分量pk=1,然后回到上一分量k=k-1

step5 如果k<1,终止算法;

step6 回到step2

1-2 全排列回溯法流程图

1.2.3 C++代码

由于C++代码中的数组索引是从0开始的,所以,下面代码中k的值就是从0开始,求出的是01n-1的全排列,在打印部分每个分量加1就得到了12n的全排列。

void Backtracking(int n){

int *p=new int[n];

memset(p,0,n*sizeof(int));

    int k = 0;

    while (k >= 0) {

        //找一个合适的值给当前状态

        while (p[k] < n){

            int i;

            for (i = 0; i < k; i++)

                if (p[i] == p[k])

                    break;

            if (i < k)   p[k]++;

            else       break;

        }

        //找到了

        if (p[k]<n) {

            if (k < n-1) k++;//如果没有找到全部解就前进

            else//找到了一个解

            {

                int i;

                for(i=0;i<=k;i++)

                    cout<<p[i]+1<<" ";

                cout<<endl;

                p[k]++;//探索下一个解

            }

        }

        else//没找到需要回溯

        {

            p[k] = 0;//清除当前处理的信息

            k--;//回溯

            if(k>=0)

                p[k]++;//探索下一个解

        }

    }

}

注:这个算法很容易就修改成输出n个元素中选出mm<n)个元素进行排列,只需要将判断k<n-1变成k<m-1,余皆不变。

1.3 正整数的拆分

1.3.1 问题

对于给定的正整数n,将其表述成若干个正整数的和,不计先后次序。例如

5=1+1+1+1+1

 =1+1+1+2

 =1+1+3

     =1+2+2

 =1+4

 =2+3

 =5

1.3.2 算法设计

假定我们将n拆分成n=p1+p2+…+pn,并要求p1p2≤…≤plpl+1=…=pn=0,如5=1+2+2+0+0。基本思想:开始时k=1pk=1,从pk往前,因为要求pk+1pk,所以把余额部分分成等于pk的若干份,赋给后面的pj,不够pk的余额就直接给最后一个分量。一旦得到一个解,就进行回溯,也就是令最后一个分量为0,次后的分量增加1个,然后继续。算法步骤如下:

step1 p1=p2=…=pn=1k=n;

step2 打印p1p2pknum=pkpk=0

step3 如果 k<=1 终止;

step4  pk-1=pk-1+1num=num-1k=k-1

step5  t=num/pk取整,pk+1=pk+2=…=pk+t-1=pkpk+t=pk+num mod pk k=k+t,回到step2

                      

1-3 整数拆算法分流程图

1.3.3 C++代码

void SplitInteger(int n){

int*p=new int[n];//分量

int rem,k=n-1,i;//剩余、回溯变量、循环变量

for(i=0;i<n;i++)p[i]=1;

while(1){

cout<<n<<"="<<p[0];

for(i=1;i<=k;i++)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值