0x08算法设计与分析复习(二):算法设计策略-回溯法2

参考书籍:算法设计与分析——C++语言描述(第二版)

算法设计策略-回溯法

子集和数

问题描述

已知n个不同的正数wi(0in1)的集合,求该集合的所有满足条件的子集,使得每个子集中的正数之和等于另一个给定的正数M

回溯法求解

对于子集和数问题问题,可采用两种不的解结构形式:可变长度元组和固定长度元组。

  • 采用可变长度元组表示解

    当采用可变长度元组表示解时,代表一个可行解的元组长度可以不同,成为一个k-元组(x0,x1,,xk1),0kn。元组的每个分量的取值可以是元素值,也可以是选入子集的正数的下标。通常的做法是列出入选子集的元素下标,如果采用这种形式的解,其显式约束可描述为:xi{j|j0j<n}xi<xi+1(0i<n1)。加入条件xi<xi+1可以避免产生重复子集现象。隐式约束如下:k1i=0wxi=M

  • 采用固定长度元组表示解

    固定长度n-元组(x0,x1,,xn1),xi{0,1},0i<nxi=0表示wi未选入子集;xi=1表示wi入选子集。采用这种形式的解,其显示约束可描述为:xi{0,1}(0i<n1)。问题的隐式约束是选入子集的正数之和等于M,即n1i=0wixi=M

一般称从n个元素的集合中找出满足某些性质的子集的状态空间树为子集树。子集树有2n个解状态,遍历子集树的时间为Ω(2n)

最后讨论子集和数的约束函数Bk。假定采用固定长度元组的结结构,已经知道,若约束函数Bk(x0,x1,,xk)=true,则算法需要进入考察(x0,x1,,xk)子树的过程,在本问题中,可以容易想到如下的约束函数:

Bk(x0,x1,,xk)=true,i=0k1wixi+i=kn1wiM

  • 因为如果上式得不到满足,那么意味着当剩余的正数全部加入子集后,子集中的正数之和仍小于M,自然表明部分向量(x0,x1,,xk)不可能导致一个答案状态。

如果假定n个正数wi,0in1已经按非减次序排列,那么还可以在上式约束条件中添加约束条件:

i=0k1wixi+wkM

  • 因为如果子集中加入下一个待选的正数wk(它是剩余正数中最小的),使整个子集之和已大于M,这显然意味着从剩余正数中选取任意一个正数加入到现有子集后都不可能使子集和数等于M

根据上面两式,可以得到约束函数:

Bk(x0,x1,,xk)为true,当且仅当:

i=0k1wixi+i=kn1wiMi=0k1wixi+wkM

子集和数算法

函数定义:

void SumOfSub(float s, int k, float r, int *x, float m, float *w)

  • 前置条件:wiwi+1(0i<n1),s+rm&&s+wkm(s=k1i=0wixi,r=n1i=kwi)
  • 后置条件:在以(x0,x1,,xk)为根的子树上搜索答案状态

函数SumOfSub的初始条件是s=0,r=n1i=0wimw0m

//子集和数的回溯算法
void SumOfSub(float s, int k, float r, int *x, float m, float *w)
{
    x[k]=1;
    if(s+w[k]==m){
        for(int j = 0;j<k;j++){
            //得到一个可行解
            cout << x[j]<<" ";
        }
        cout << endl;
    } else if(s+w[k]+w[k+1]<=m){
            //搜索左子树
            SumOfSub(s+w[k],k+1,r-w[k],x,m,w);
    }
    if((s+r-w[k]>=m)&&(s+w[k+1]<=m)){
        x[k]=0;
        //搜索右子树
        SumOfSub(s,k+1,r-w[k],x,m,w);
    }
}

void SumOfSub(int *x,int x, float m,float *w)
{
    float r=0;
    for(int i = 0;i<n;i++){
        r=r+w[i];
        //计算r=\sum_{i=k}^{n-1}w[i];
    }
    if(r>=m && w[0]<=m)
        SumOfSub(0,0,r,x,m,w);
}

图的着色

问题描述

已知无向图G=(V,E)和m中不同颜色,如果只允许使用这m中颜色对图G的结点着色,每个结点着一种颜色,问是否存在一种着色方案,使得图中任意相邻的两个结点都有不同的颜色。这就是m-着色判定问题(m-colorability decision problem)。对给定的无向图G,求对图G的结点着色所需的最少颜色数m,使得图中任意两个相邻结点有不同的颜色。这被称为m-着色最优化问题(m-colorability optimization problem),整数m称为图G的着色数(chromatic number)

回溯法求解

设无向图G=(V,E)采用如下定义的邻接矩阵定义:

a[i][j]={10(i,j)E

下面采用n-元组(x0,x1,,xn1)表示图G的m-着色判定问题的解。每个xi{1,2,3,,m},0i<n,表示结点的颜色,这是显式约束xi=0表示没有可用的颜色。因此解空间的大小为mn。其隐式约束可描述为:如果边(i,j)E,则xixj

约束函数的设计:约束函数从隐式约束产生,对所有i和j(0i,j<k,ij),若a[i][j]=1,则xixj

图着色算法

//图的m-着色算法
template<class T>
void MGraph<T>::NextValue(int k,int m,int *x)
{
    //本函数在[1,m]中为x[k]确定一个值最小,且不与其邻接点冲突的颜色
    //x[k]=0表示没有可用颜色,颜色从1开始编号
    do{
        x[k]=(x[k]+1)%(m+1);//尝试下一种颜色
        if(!x[k])
            return;//没有可用颜色
        for(int j=0;j<k;j++){
            if(a[k][j] && x[k]==x[j]){
                //若(i,j)是图的边,且相邻结点k和j颜色相同
                break;//发生冲突,选择下一种颜色
            }
        }
        if(j==k)
            return;//成功选择一种颜色返回
    }while(1);
}

template<class T>
void MGraph<T>::mColoring(int k,int m,int *x)
{
    do{
        NextValue(k,m,x);//为x[k]分配颜色
        if(!x[k])
            break;//x[k]=0表示没有适当的颜色
        if(k==n-1){
            //得到图G的一种m-着色方案
            for(int i=0;i<n;i++){
                cout << x[i] <<" ";
            }
            cout << endl;
        } else {
            //已经对前k个结点分配了颜色,尝试其余结点
            mColoring(k+1,m,x);
        }
    }while(1);
}

template<class T>
void MGraph<T>::mColoring(int m,int *x)
{
    mColoring(0,m,x);
}

函数mColoring对一个给定的无向图G和m,列出图中结点的所有可能的m-着色方案。算法以深度优先方式生成状态空间树中的结点,寻找所有答案结点,即m-着色方案。搜索中使用约束函数剪去不可能包含答案结点的分枝。

时间分析

算法的计算时间上界可以由状态空间树的结点树n1i=0mi来确定。每个结点的处理时间即NextValue的执行时间为O(mn),因此总时间为:

i=1nmin=n(mn+1m)/(m1)=O(nmn)

哈密顿环

问题描述

已知图G=(V,E)是一个n个结点的连通图。连通图G的一个哈密顿环(Hamiltonian cycle)是图G的一个回路,它经过图中的每一个结点,且只经过一次。一个哈密顿环是从某个结点v0V开始,沿着图G的n条边环行的一条路径(v0,v1,,vn1,vn),其中,(vi,vi+1)E,(0i<n),它访问图中每个结点且只访问一次,最后返回开始结点,即除v0=vn外,路径上其余各点各不相同。并不是每个连通图都存在哈密顿环。

哈密顿环算法

对于n个结点的图G=(V,E)的哈密顿环问题,可采用n-元组表示问题的解(x0,x1,,xn1)。每个xi{0,1,,n1},0i<n,代表路径上一个结点的编号,这就是显式约束。因此解空间的大小为nn其隐式约束可描述为xixj(0i,j<n,ij),且(xi,xi+1)E,xi,xi+1V(i=0,1,,n2),又(xn1,x0)E

//哈密顿环算法
//,函数NextValue的功能是约束函数。
template<class T>
void MGraph<T>::NextValue(int k,int *x)
{
    //函数NextValue的功能是约束函数
    do{
        x[k]=(x[k]+1)%n;//下一个结点编号
        if(!x[k])
            return;
        if(a[x[k-1]][x[k]]){
            //检查(x[k-1],x[k])是否是图中一条边
            for(int j=0;j<k;j++){
                //检查与前k个结点是否相同
                if(x[j]==x[k])
                    break;
            }
            if(j==k){
                //x[k]是当前可取的结点编号
                if((k<n-1)||((k==n-1)&&a[x[n-1]][x[0]]))
                    return;
            }
        }
    }while(1);
}

template<class T>
void ExtMGraph<T>::Hamiltonian(int k,int *x)
{
    //递归函数Hamiltonian实际计算哈密顿环
    do{
        NextValue(k,x);//产生x[k]的下一个值
        if(!x[k])
            return;//x[k]=0表示x[k]已经没有可取的值
        if(k==n-1){
            //输出一个哈密顿环
            for(int i=0;i<n;i++){
                cout << x[i]<<" ";
            }
            cout << "0" << endl;
        } else {
            //深度优先进入下一层
            Hamiltonian(k+1,x);
        }
    }while(1);
}

template<class T>
void ExtMGraph<T>::Hamiltonian(int *x)
{
    Hamiltonian(1,x);//x[0]=0为约定的起始结点
}
posted @ 2018-01-09 11:07  main_c  阅读(327)  评论(0编辑  收藏  举报