【算法设计与分析】期中复习

一、概论

什么是算法?

算法是求解问题的一系列步骤,用来将输入数据转换成输出结果

算法设计应该满足以下目标(对使用算法的人来讲)

  1. 正确性:算法能正确地执行,能完成任务
  2. 可使用性:算法要能被方便地使用
  3. 可读性:算法应具有较好的可读性,易于理解
  4. 健壮性:要有异常处理,对不合理的数据进行检查,从而避免异常中断或死机
  5. 高效率与低存储量需求:低时间复杂度,低空间复杂度。二者往往不可得兼

算法的五个特征(对设计算法的人来讲)

  1. 有限性:算法的的执行步骤是有限的,每个步骤都在有限时间内完成,即算法可在有限时间内被只执行完成
  2. 确定性:算法中每一条指令都有确切的含义,没有二义性(例:取出数组中差不多大的数,取出数组中最大的数)
  3. 可行性:算法的每个步骤都是可实现的,例如:整数乘法操作可用加法运算实现,乘2操作可用移位运算实现(表达算法中的操作可以被实现,对计算机而言,要有对应的指令,对人而言,要有步骤,纸,笔)
  4. 输入性:0个或多个
  5. 输出性:1个或多个

【例1.2】有下面两段描述,它们违反了算法的哪些特征?

答:第一段代码是一个死循环,违反了算法的有限性。第二段出现了零除错误,违法了算法的可行性。

算法的描述

以设计求1+2+…+n的值的算法为例说明C/C++语言描述算法的一般形式

在设计算法时,如果某个形参需要将执行结果回传给实参,需要将该形参设计为引用型参数。

算法与数据结构

  • 算法+数据结构=程序(Niklaus Emil Wirth, 1934-)
  • 联系:数据结构是算法设计的基础。算法的操作对象是数据结构,在设计算法时,通常要构建适合这种算法的数据结构。数据结构设计主要是选择数据的存储方式,如确定求解问题中的数据采用数组存储还是采用链表存储等。算法设计就是在选定的存储结构上设计一个满足要求的好算法。
  • 区别:数据结构关注的是数据的逻辑结构、存储结构以及基本操作,而算法更多的是关注如何在数据结构的基础上解决实际问题。算法是编程思想,数据结构则是这些思想的逻辑基础。

算法分析

算法分析是分析算法占用计算机资源的情况
算法分析的两个主要方面是分析算法的时间复杂度空间复杂度

非递归算法时间复杂度分析

  1. 符号:
    1. 上界:O(n)(主要用这个)
    2. 下界:Ω(n)
    3. 同阶:
  2. 计算
    1. 大吞小,常系数删掉
    2. O(3n+2)=O(3n)=O(n)
    3. O(6n2+3n+2)=O(6n2+3n)=O(6n2)=O(n2)
    4. O(1):常数时间复杂度,即算法执行时间不会随着问题规模的增大而增大
    5. 【例1.3】分析以下算法的时间复杂度
      void fun(int n)
      {
          int s=0,i,j,k;
          for (i=0;i<=n;i++)
              for (j=0;j<=i;j++)
                  for (k=0;k<j;k++)
                      s++;
      }

        

      补充:对该问题而言,有几层for循环就是n的几次方,如果再加一层for循环for(l=0;l<k;i++),则时间复杂度为O(n4)
  3. 算法的三种情况
    1. 最好:算法基本语句执行的最小次数
    2. 最坏:算法基本语句执行的最大次数
    3. 平均:各种特定输入下的基本语句执行次数的带权平均值


递归算法时间复杂度分析

递归算法是采用一种分而治之的方法,把一个“大问题”分解为若干个相似的“小问题”来求解。

对递归算法时间复杂度的分析,关键是根据递归过程建立递推关系式,然后求解这个递推关系式,得到一个表示算法执行时间的表达式,最后用渐进符号来表示这个表达式即得到算法的时间复杂度。

非递归算法空间复杂度分析

临时空间随着算法的执行能增大多快

若所需临时空间相对于输入数据量来说是常数,则称此算法为原地工作就地工作算法。若所需临时空间依赖于特定的输入,则通常按最坏情况来考虑。

非递归算法中如果有循环,则要注意在循环过程中是否申请了新的空间(malloc,new),非递归算法的时间复杂度不一定是O(1)

递归算法空间复杂度分析

算法设计工具-STL

随用随查,此处省略

二、递归

什么是递归?

函数调用自身,称之为直接递归。若过程函数p调用过程函数q,而q又调用p,称之为间接递归

任何间接递归都可以等价地转换为直接递归。

如果一个递归过程或递归函数中递归调用语句是最后一条执行语句,则称这种递归调用为尾递归

【例2.1】设计求n!(n为正整数)的递归算法

用递归能解决什么问题?

一般来说,能够用递归解决的问题应该满足以下3个条件:

  1. 需要解决的问题可以转化为一个或多个子问题来求解,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同
  2. 递归调用的次数必须是有限的。
  3. 必须有结束递归的条件来终止递归。

何时使用递归?

  1. 定义是递归的:n!,Fibonacci数列
  2. 数据结构是递归的:单链表,二叉树
    • 单链表
      typedef struct LNode
      {
          ElemType data;
          struct LNode *next;
      }LinkList;
      /*
      结构体LNode的定义中用到了它自身,即指针域next是一种指向自
      身类型的指针,所以它是一种递归数据结构。
      */
  3.  问题的求解方法是递归的:汉诺塔(Hanoi)

递归模型

递归算法的执行过程

要会画递归树递归工作栈

n!问题的求解过程

斐波那契数列的求解过程

函数形式的递归调用过程

递归与数学归纳法

递归算法的设计

递归数据结构的定义

单链表

二叉树

递归算法设计

单链表递归算法设计

二叉树递归算法设计

基于归纳思想的递归算法设计

基于归纳思想的递归算法设计通常不像基于递归数据结构的递归算法设计那样直观,需要通过对求解问题的深入分析提炼出求解过程中相似性而不是数据结构的相似性,这就增加了算法设计的难度。

递归算法实例-简单选择排序

简单选择排序采用简单比较方式在无序区中选择最小元素并放到开头处。

 

递归算法实例-冒泡排序

冒泡排序采用交换方式将无序区中的最小元素放到开头处。

递归算法实例-n皇后问题

递归算法转化为非递归算法

  1. 直接用循环结构的算法替代递归算法
  2. 用栈模拟系统的运行过程,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。
  3. 第1种是直接转化法,不需要使用栈。第2种是间接转化法,需要使用

三、分治

什么是分治?

将规模为n的问题分解为k个小规模的子问题,这些子问题与原来的问题互相独立,且与原问题的形式相同,通过递归解决这些子问题,最后将子问题的解合并,得到原问题的解,这种算法设计策略叫分治法。

如何分解问题?

人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。换句话说,将一个问题分成大小相等的k个子问题的处理方法是行之有效的。

  • k=1时称为减治法
  • k=2时,称为二分法,如图所示,这种使子问题规模大致相等的做法是出自一种平衡子问题的思想,它几乎总是比子问题规模不等的做法要好。

分治实例-快速排序

以一个数作为基准(一般是第一个),将数组分为两部分,左边小,右边大

/*
快速排序
*/
#include <stdio.h>
void disp(int a[], int n) //输出a中的所有元素
{
    int i;
    for (i = 0; i < n; i ++)
        printf(" %d ", a[i]);
    printf("\n");
}
int Partition(int a[], int s, int t)//划分算法
{
    int i = s, j = t;
    int tmp = a[s];//用序列的第1个记录作为基准
    while (i != j)//从序列两端交替向中间扫描,直到i=j为止
    {
        while (j > i && a[j] >= tmp)
            j--;//从右向左扫描,找第1个关键字小于tmp的a[j]
        a[i] = a[j];//将a[j]前移到a[i]的位置
        while (i < j && a[i] <= tmp)
            i++;//从左向右扫描,找第1个关键字大于tmp的a[i]
        a[j] = a[i];//将a[i]后移到a[j]的位置
    }
    a[i] = tmp;
    return i;
}
void QuickSort( int a[], int s, int t)//对a[s..t]元素序列进行递增排序
{
    if (s < t)//序列内至少存在两个元素的情况
    {
        int i = Partition(a, s, t) ;
        QuickSort( a, s, i - 1); //对左子序列递归排序
        QuickSort(a, i + 1, t);//对右子序列递归排序

    }
}
int main()
{
    int n = 10;
    int a[] = {2, 5, 1, 7, 10, 6, 9, 4, 3, 8} ;
    printf("排序前:");
    disp(a, n);
    QuickSort(a, 0, n - 1);
    printf("排序后:");
    disp(a, n);
    return 0;
}

分治实例-归并排序(自底向上)

【算法分析】对于上述二路归并排序算法,当有n个元素时需要⌈log2n⌉趟归并,每一趟归并,其元素比较次数不超过n-1,元素移动次数都是n,因此二路归并排序的时间复杂度为O(nlog2n)。

/*
归并排序
 */
#include <stdio.h>
#include <malloc.h>
void disp( int a[], int n)//输出a中的所有元素
{
    int i;
    for (i = 0; i < n; i++)
        printf( " %d ", a[i]);
    printf( "\n" );
}
void Merge( int a[], int low, int mid, int high)
//将a[low..mid]和a[mid+1..high]两个相邻的有序子序列归并为一个有序子序列aLlow..high]
{
    int * tmpa;
    int i = low, j = mid + 1, k = 0;//k是 tmpa的下标,i、j分别为两个子表的下标
    tmpa = (int*)malloc((high - low + 1) * sizeof(int));
    while (i <= mid && j <= high)//在第1个子表和第2个子表均未扫描完时循环
        if (a[i] <= a[j])//将第1个子表中的元素放入tmpa中
        {
            tmpa[k] = a[i];
            i++;
            k++;
        }
        else//将第2个子表中的元素放入tmpa 中
        {
            tmpa[k] = a[j];
            j++;
            k++;
        }
    while (i <= mid)//将第1个子表余下的部分复制到tmpa
    {
        tmpa[k] = a[i];
        i++;
        k++;
    }
    while (j <= high)//将第2个子表余下的部分复制到 tmpa
    {
        tmpa[k] = a[j];
        j++;
        k++;
    }
    for (k = 0, i = low; i <= high; k++, i++) //将tmpa复制回a中
        a[i] = tmpa[k];
    free(tmpa);//释放临时空间
}
void MergePass( int a[], int length, int n)//一趟二路归并排序
{
    int i;
    for (i = 0; i + 2 * length - 1 < n; i = i + 2 * length) //归并length长的两个相邻子表
        Merge(a, i, i + length - 1, i + 2 * length - 1);
    if (i + length - 1 < n) //余下两个子表,后者的长度小于length
        Merge(a, i, i + length - 1, n - 1); //归并这两个子表
}
void MergeSort(int a[], int n)//二路归并算法
{
    int length;
    for (length = 1 ; length < n; length = 2 * length)
        MergePass(a, length, n) ;
}
int main()
{
    int n = 10;
    int a[] = {2, 5, 1, 7, 10, 6, 9, 4, 3, 8};
    printf("排序前:");
    disp(a, n);
    MergeSort(a, n);
    printf("排序后: ");
    disp(a, n);
    return 0;
}

分治实例-归并排序(自顶向下)

void MergeSort(int a[], int low, int high) //二路归并算法(自顶向下)
{
    int mid;
    if (low < high) //子序列有两个或两个以上元素
    {
        mid = (low + high) / 2 ; //取中间位置
        MergeSort( a, low, mid) ; //对a[low..mid]子序列排序
        MergeSort( a, mid + 1, high); //对a[mid+1 ..high]子序列排序
        Merge( a, low, mid, high);  //将两个子序列合并,见前面的算法
    }
}

分治实例-折半查找

折半查找又称二分查找,它是一种效率较高的查找方法。但是折半查找要求查找序列中的元素是有序的,为了简单,假设是递增有序的。

/*
折半查找-递归写法
 */
#include <stdio.h>
int BinSearch(int a[], int low, int high, int k)  //折半查找算法
{
    int mid;
    if (low <= high)//当前区间存在元素时
    {
        mid = (low + high) / 2 ;//求查找区间的中间位置
        if (a[mid] == k)//找到后返回其物理下标mid
            return mid;
        if (a[mid] > k)//当a[mid]> k时在a[low..mid一1]中递归查找
            return BinSearch(a, low, mid - 1, k) ;
        else//当a[mid]<k时在a[mid+1..high]中递归查找
            return BinSearch( a, mid + 1, high, k) ;
    }
    else return -1;    //当前查找区间没有元素时返回一1
}
int main()
{
    int n = 10, i;
    int k = 6;//要找的数
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} ;
    i = BinSearch(a, 0, n - 1, k);
    if (i >= 0)printf("a[%d]=%d\n", i, k);
    else printf("未找到%d元素\n", k);
    return 0;
}
int BinSearch1 (int a[], int n, int k) //非递归折半查找算法
{
    int low = 0, high = n - 1, mid;
    while (low <= high) //当前区间存在元素时循环
    {
        mid = (low + high) / 2; //求查找区间的中间位置
        if (a[mid] == k) //找到后返回其物理下标mid
            return mid;
        if (a[mid] > k) //继续在a[low..mid-1]中查找
            high = mid - 1;
        else// a[mid]< k
            low = mid + 1; //继续在a[mid+1..high]中查找
    }
    return -1;//当前查找区间没有元素时返回一1
}

四、蛮力

枚举出所有可能的情况,进行判断即可

依赖计算机的强大运算能力

常见情况

  1. 搜索所有解空间
  2. 搜索所有路径
  3. 直接计算
  4. 模拟和仿真:对一个系统的运行过程进行仿真

【例4.1】编写一个程序,输出2~1000 的所有完全数。

所谓完全数是指这样的数,该数的各因子(除该数本身以外)之和正好等于该数本身。例如:


for循环枚举

dfs枚举(要会画递归搜索树(分支,回溯,剪枝))

求幂集(2n枚举,数组中两种状态:0不选,1选)

求幂集延伸-枚举0/1背包

/*
dfs求解 0/1 背包问题
input 1:
10
6 4 8 2 7 16 4 8 3 5
9 6 13 15 25 14 9 15 8 9
34
output 1:

input 2:
10
16 5 8 2 8 16 4 8 7 4
9 6 12 15 25 14 9 15 12 9
34
output 2:
------------------------------------
解:
------------------------------------
最大总价值:85
------------------------------------
物品编号  3  4  5  7  8 10
物品重量  8  2  8  4  8  4
物品价值 12 15 25  9 15  9
------------------------------------
*/
#include <iostream>
using namespace std;
const int N = 1005;
int n;      //几个物品
int w[N];   //物品重量
int v[N];   //物品体积
int ans[N]; //最优解
//函数相关变量
int OP[N]; //解空间(0:不选 1:选)
int W;     //重量上限
int RW;    //所有物品重量和
int maxv;  //最大总价值
/*
i:当前物品下标
tw:当前背包中物品总重量
tv:当前背包中物品总价值
rw:当前剩余所有物品重量和
op:解空间
*/
void dfs(int i, int tw, int tv, int rw, int op[])
{
    //初始调用时 rw 为所有物品重量和
    int j;
    if (i > n) //找到一个叶子结点
    {
        if (tw == W && tv > maxv) //找到一个满足条件的更优解,保存
        {
            maxv = tv;
            for (j = 1; j <= n; j++) //复制最优解
                ans[j] = op[j];
        }
    }
    else //尚未找完所有物品
    {
        if (tw + w[i] <= W) //左孩子结点剪枝
        {
            op[i] = 1; //选取第 i 个物品
            dfs(i + 1, tw + w[i], tv + v[i], rw - w[i], op);
        }
        if (tw + rw - w[i] >= W)
        {
            op[i] = 0; //不选取第 i 个物品,回溯
            dfs(i + 1, tw, tv, rw - w[i], op);
        }
    }
}
void print_ans()
{
    printf("------------------------------------\n");
    printf("解:\n");
    printf("------------------------------------\n");
    printf("最大总价值:%d\n", maxv);
    printf("------------------------------------\n");
    printf("%6s", "物品编号");
    for (int i = 1; i <= n; i++)
    {
        if (ans[i] == 1)
            printf("%3d", i);
    }
    printf("\n");
    printf("%6s", "物品重量");
    for (int i = 1; i <= n; i++)
    {
        if (ans[i] == 1)
            printf("%3d", w[i]);
    }
    printf("\n");
    printf("%6s", "物品价值");
    for (int i = 1; i <= n; i++)
    {
        if (ans[i] == 1)
            printf("%3d", v[i]);
    }
    printf("\n");
    printf("------------------------------------\n");
}
int main()
{
    //读数
    cout << "输入物品个数:";
    cin >> n;
    cout << "输入物品重量:";
    for (int i = 1; i <= n; i++)
    {
        cin >> w[i];
        RW += w[i];
    }
    cout << "输入物品价值:";
    for (int i = 1; i <= n; i++)
    {
        cin >> v[i];
    }
    cout << "输入重量上限:";
    cin >> W;
    //题解
    dfs(1, 0, 0, RW, OP);
    //输出答案
    print_ans();
    return 0;
}

dfs枚举-全排列(每个位置有n个分支)

dfs枚举-组合数

关键:从大到小枚举,以确保枚举出的情况不会重复

五、回溯(dfs找所有解)

回溯法能解决什么问题

具体问题具体分析

  1. 找可行解
  2. 找最优解

回溯法求解步骤

  1. 应用回溯法求解问题时,首先应该明确问题的解空间。解空间中满足约束条件的决策序列称为可行解
  2. 一般来说,解任何问题都有一个目标,在约束条件下使目标达到最优的可行解称为该问题的最优解

子集树与排列树

问题的解空间树虚拟的,并不需要在算法运行时构造一棵真正的树结构,然后再在该解空间树中搜索问题的解,而是只存储从根结点到当前结点的路径。

回溯法名词解释

活结点:活结点是指自身已生成但其孩子结点没有全部生成的结点
扩展结点:扩展结点是指正在产生孩子结点的结点
死结点:死结点是指由根结点到该结点构成的部分解不满足
约束条件,或者其子结点已经搜索完毕

回溯法过程/解题步骤

在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点(开始结点)出发搜索解空间树。

  • 首先根结点成为活结点,同时也成为当前的扩展结点。
  • 在当前的扩展结点处,搜索向纵深方向移至一个新结点。这个新结点就成为新的活结点,并成为当前扩展结点。
  • 如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。
  • 此时应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。

回溯法以这种方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点为止。

回溯法解题步骤

回溯法算法框架

掌握递归法即可,知道有非递归法就行

子集树(求幂集):O(2n)枚举

排列数(求全排列):O(n*n!)枚举

//1. 非递归回溯框架
int x[n]; //x存放解向量,全局变量,解空间
void backtrack(int n) //非递归框架
{
    int i = 1; //根结点层次为1
    while (i >= 1) //尚未回溯到头
    {
        if (ExistSubNode(t)) //当前结点存在子结点
        {
            for (j = 下界; j <= 上界; j++) //对于子集树,j=0到1循环
            {
                x[i]取一个可能的值;
                if (constraint(i) && bound(i))//x[i]满足约束条件或界限函数
                {
                    if (x是一个可行解)
                        输出x;
                    else i++; //进入下一层次
                }
            }
        }
        else i--; //回溯:不存在子结点,返回上一层
    }
}
//2. 递归的算法框架
//(1)解空间为子集树
int x[n]; //x存放解向量,全局变量
void backtrack(int i) //求解子集树的递归框架
{
    if (i > n) //搜索到叶子结点,输出一个可行解
        输出结果;
    else
    {
        for (j = 下界; j <= 上界; j++) //用j枚举i所有可能的路径
        {
            x[i] = j; //产生一个可能的解分量
            … //其他操作
            if (constraint(i) && bound(i))
                backtrack(i + 1); //满足约束条件和限界函数,继续下一层
        }
    }
}
//(2)解空间为排列树
int x[n]; //x存放解向量,并初始化
void backtrack(int i) //求解排列树的递归框架
{
    if (i > n) //搜索到叶子结点,输出一个可行解
        输出结果;
    else
    {
        for (j = i; j <= n; j++) //用j枚举i所有可能的路径
        {
            … //第i层的结点选择x[j]的操作
            swap(x[i], x[j]); //为保证排列中每个元素不同,通过交换来实现
            if (constraint(i) && bound(i))
                backtrack(i + 1); //满足约束条件和限界函数,进入下一层
            swap(x[i], x[j]); //恢复状态
            … //第i层的结点选择x[j]的恢复操作
        }
    }
}

回溯法实例-求全排列问题

回溯法实例-0/1背包问题(左右剪枝)

void dfs(int i, int tw, int tv, int op[]) //求解0/1背包问题
{
    if (i > n) //找到一个叶子结点
    {
        maxv = tv; //存放更优解
        for (int j = 1; j <= n; j++)
            x[j] = op[j];
    }
    else //尚未找完所有物品
    {
        if (tw + A[i].w <= W) //左孩子结点剪枝
        {
            op[i] = 1; //选取序号为i的物品
            dfs(i + 1, tw + A[i].w, tv + A[i].v, op);
        }
        if (bound(i, tw, tv) > maxv) //右孩子结点剪枝
        {
            op[i] = 0; //不选取序号为i的物品,回溯
            dfs(i + 1, tw, tv, op);
        }
    }
}

回溯法实例-求解装载问题

回溯法实例-求解复杂装载问题

回溯法实例-n皇后问题(非递归)

void Queens(int n) //求解n皇后问题
{
    int i = 1; //i表示当前行,也表示放置第i个皇后
    q[i] = 0; //q[i]是当前列,每个新考虑的皇后初始位置置为0列
    while (i >= 1) //尚未回溯到头,循环
    {
        q[i]++; //原位置后移动一列
        while (q[i] <= n && !place(i)) //试探一个位置(i,q[i])
            q[i]++;
        if (q[i] <= n) //为第i个皇后找到了一个合适位置(i,q[i])
        {
            if (i == n) //若放置了所有皇后,输出一个解
                dispasolution(n);
            else //皇后没有放置完
            {
                i++; //转向下一行,即开始下一个新皇后的放置
                q[i] = 0; //每个新考虑的皇后初始位置置为0列
            }
        }
        else i--; //若第i个皇后找不到合适的位置,则回溯到上一个皇后
    }
}

回溯法实例-图的m着色问题

bool Same(int i) //判断顶点i是否与相邻顶点存在相同的着色
{
    for (int j = 1; j <= n; j++)
        if (a[i][j] == 1 && x[i] == x[j])
            return false;
    return true;
}
void dfs(int i) //求解图的m着色问题
{
    if (i > n) //达到叶子结点
        count++; //着色方案数增1
    else
    {
        for (int j = 1; j <= m; j++) //试探每一种着色
        {
            x[i] = j; //试探着色j
            if (Same(i)) //可以着色j,进入下一个顶点着色
                dfs(i + 1);
            x[i] = 0; //回溯
        }
    }
}

回溯法实例-求解任务分配问题

void dfs(int i) //为第i个人员分配任务
{
    if (i > n) //到达叶子结点
    {
        if (cost < mincost) //比较求最优解
        {
            mincost = cost;
            for (int j = 1; j <= n; j++)
                bestx[j] = x[j];
        }
    }
    else
    {
        for (int j = 1; j <= n; j++) //为人员i试探任务j:1到n
            if (!worker[j]) //若任务j还没有分配
            {
                worker[j] = true;
                x[i] = j; //任务j分配给人员i
                cost += c[i][j];
                dfs(i + 1); //为人员i+1分配任务
                worker[j] = false; //回退
                x[j] = 0;
                cost -= c[i][j];
            }
    }
}

回溯法实例-求解活动安排问题

void dfs(int i) //搜索活动问题最优解
{
    if (i > n) //到达叶子结点,产生一种调度方案
    {
        if (sum > maxsum)
        {
            maxsum = sum;
            for (int k = 1; k <= n; k++)
                bestx[k] = x[k];
        }
    }
    else
    {
        for (int j = i; j <= n; j++) //没有到达叶子结点,考虑i到n的活动
        {
            //第i层结点选择活动x[j]
            swap(x[i], x[j]); //排序树问题递归框架:交换x[i],x[j]
            int sum1 = sum; //保存sum,laste以便回溯
            int laste1 = laste;
            if (A[x[j]].b >= laste) //活动x[j]与前面兼容
            {
                sum++; //兼容活动个数增1
                laste = A[x[j]].e; //修改本方案的最后兼容时间
            }
            dfs(i + 1); //排序树问题递归框架:进入下一层
            swap(x[i], x[j]); //排序树问题递归框架:交换x[i],x[j]
            sum = sum1; //回溯
            laste = laste1; //即撤销第i层结点对活动x[j]的选择
        }
    }
}

回溯法实例-求解流水作业调度问题

void dfs(int i) //从第i层开始搜索
{
    if (i > n) //到达叶结点,产生一种调度方案
    {
        if (f2[n] < bestf) //找到更优解
        {
            bestf = f2[n];
            for (int j = 1; j <= n; j++) //复制解向量
                bestx[j] = x[j];
        }
    }
    else
    {
        for (int j = i; j <= n; j++) //没有到达叶结点,考虑可能的作业
        {
            swap(x[i], x[j]);
            f1 += a[x[i]]; //选择作业x[i],在M1上执行完的时间
            f2[i] = max(f1, f2[i - 1]) + b[x[i]];
            if (f2[i] < bestf) //剪枝
                dfs(i + 1);
            f1 -= a[x[i]]; //回溯
            swap(x[i], x[j]);
        }
    }
}

六、分支限界(bfs找一个解)

什么是分枝限界法

分枝限界法类似于回溯法,也是一种在问题的解空间树上搜索问题解的算法。
但在一般情况下,分枝限界法与回溯法的求解目标不同回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分枝限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解

分枝限界法与回溯法的主要区别

分支限界法的设计思想

  1. 设计合适的限界函数
    1. 目标是求最大值
    2. 目标是求最小值
  2. 组织活结点表
    1. 队列式分枝限界法
    2. 优先队列式分枝限界法
  3. 确定最优解的解向量
    1. 对每个扩展结点保存从根结点到该结点的路径。
      每个结点带有一个可能的解向量。这种做法比较浪费空间,但实现起来简单,后面的示例均采用这种方式。
    2. 在搜索过程中构建搜索经过的树结构。
      每个结点带有一个双亲结点指针,当找到最优解时,通过双亲指针找到对应的最优解向量。这种做法需保存搜索经过的树结构,每个结点增加一个指向双亲结点的指针。

分支限界法的3个关键问题

  1. 如何确定合适的限界函数
  2. 如何组织待处理结点的活结点表
  3. 如何确定解向量的各个分量

参考书

《算法设计与分析(第2版)》 李春葆 ISBN:9787302500988

posted @ 2023-05-04 00:58  尚方咸鱼  阅读(187)  评论(0编辑  收藏  举报