【算法分析】回溯法详解+范例+习题解答

🦄1.回溯法

1.1回溯法的设计思想

以深度优先方式搜索问题解的算法【回溯法是优化的暴力遍历,即一棵树在特定条件作为剪枝函数,树可以提前截掉,省去一些子节点。完全暴力遍历则是需要全部叶子节点都考虑】

回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。具有限界函数的深度优先生成法称为回溯法

  • 有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
  • 回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
  • 回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。

1.2回溯法的基本思想

(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索

常用剪枝函数:
用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。

1.3回溯法的空间复杂度

用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到当前扩展结点的路径。如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2h(n))或O(h(n)!)内存空间。

🦄2.范例

2.1 0-1背包问题

2.2 装载问题

有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且
在这里插入图片描述
装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有,找出一种装载方案。
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近。由此可知,装载问题等价于以下特殊的0-1背包问题
在这里插入图片描述

2.2.1 基本思想

  • 解空间:子集树
  • 可行性约束函数(选择当前元素)
  • 上界函数(不选择当前元素)
  • 当前载重量cw+剩余集装箱的重量r ≤ 当前最优载重量bestw

2.2.2 伪代码实现

void backtrack (int i){// 搜索第i层结点
    if (i > n){//到达叶结点更新最优解bestx;return;}
    r -= w[i];
    if (cw + w[i] <= c) {// 搜索左子树
       x[i] = 1;
       cw += w[i];
       backtrack(i + 1);
       cw -= w[i];      
    }
    if (cw + r > bestw)  {// 搜索右子树
       x[i] = 0;         
       backtrack(i + 1); 
    }
    r += w[i];
}

在这里插入图片描述

2.3 n后问题

在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。
在这里插入图片描述

2.3.1基本思想1

  • 解向量:(x1, x2, … , xn)

  • 显约束:xi=1,2, … ,n

  • 隐约束:
    1)不同列:
    在这里插入图片描述

    2)不处于同一正、反对角线:
    在这里插入图片描述

2.3.2 伪代码1【时间复杂度2n

boolean place (int k) {
      for (int j=1;j<k;j++)
        if ((abs(k-j)==abs(x[j]-x[k]))||(x[j]==x[k])) return false;
      return true;
   } 

   void backtrack (int t) {
      if (t>n) output(x);
      else
          for (int i=1;i<=n;i++) {
            x[t]=i;
            if (place(t)) backtrack(t+1);
          }
   }

2.3.3基本思想2【第2种比第1种时间复杂度低】

解向量:(x1, x2, … , xn)是1,2, … ,n的排列
约束:
不处于同一正、反对角线
在这里插入图片描述

2.3.4伪代码2【时间复杂度n!】

boolean place (int k) {
      for (int j=1;j<k;j++)
        if ((abs(k-j)==abs(x[j]-x[k]))) return false;
      return true;
   } 

   void backtrack (int t) {
      if (t>n) output(x);
      else
         for (int i=t;i<=n;i++) {
            swap(x[t],x[i]);
            if (place(t)) backtrack(t+1);
            swap(x[t],x[i]);
         }
   }

🦄3.习题

3.1 子集树【时间复杂度O(2n)空间复杂度O(n)】

  1. 已知集合S={a,b,c,d,e,f,g},请编程输出S的所有子集
#define n 7
char s[n] = {a,b,c,d,e,f,g};
int x[n];
void output(int x[]);
void all_subset ( ){
    backtrack(0);
}
void backtrack (int t){
   if (t>=n) output(x);
   else
      for (int i=0;i<=1;i++) {
        x[t]=i;
        backtrack(t+1);
      }
}

在这里插入图片描述

  1. 找出{1,2,3}的所有子集
void backtrack (int t)
{
  if (t>n) output(x);
    else
      for (int i=0;i<=1;i++) {
        x[t]=i;
        if (legal(t)) backtrack(t+1);
      }
}

在这里插入图片描述

3.2排列树【时间复杂度O(n!)空间复杂度O(n)】

  1. 已知集合S={1,2,3,4,5,6,7},请编程输出S的所有排列。
#define n 7
int s[n] = {1,2,3,4,5,6,7};
int x[n];
void output(int x[]);
void all_ permutation( ){
    backtrack(0);
}
void backtrack (int t){
   if (t>=n) output(x);
   else
      for (int i=t; i<n; i++) {
	       swap(x[t],x[i]);
	 	   backtrack(t+1);
	       swap(x[t],x[i]);
      }
}

在这里插入图片描述

  1. 已知集合S={1,2,3,4,5,6,7,8},请编程输出S的所有满足下列条件的排列:奇偶数相间出现
#define n 7
int s[n] = {1,2,3,4,5,6,7};
int x[n];
void output(int x[]);
void all_subset ( ){
    backtrack(0);
}
void backtrack (int t){
   if (t>=n) output(x);
   else
      for (int i=t;i<n;i++) {
        swap(x[t],x[i]);
        if(legal(t)) backtrack(t+1);
        swap(x[t],x[i]);
      }
bool legal(int t){
  bool bRet = true;
   for (int i=0;i<t;i++) {
       bRet &&= ((x[i]-x[i+1])%2==1);
    }
    return bRet;
}

剪枝剪剪【legal函数】

3.3子集以及排序

  1. 已知集合S={1,2,3,4,5,6,7},请编程输出S的满足特定约束的子集和排列。
#define n 7
int  x[n], s[n] = {1,2,3,4,5,6,7};
void main ( ){
    backtrack1(0);
    backtrack2(0);
}
void backtrack1 (int t){
   if (t>=n) output(x);
   else
      for (int i=0;i<=1;i++) {
        x[t]=i;
        if(legal(t)) backtrack1(t+1);
      }
}
void backtrack2 (int t){
   if (t>=n) output(x);
   else
      for (int i=t;i<n;i++) {
        swap(x[t],x[i]);
        if(legal(t))  backtrack2(t+1);
        swap(x[t],x[i]);
      }
}

🦄4.书后习题

5-1, 5-2, 5-3,5-4, 5-5

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
背包问题是一个经典的组合优化问题,它可以用回溯算法决。回溯算法是一种通过不断地尝试所有可能的决方案来找到最优算法。在背包问题中,回溯算法的基本思路是:对于每个物品,都有两种选择,即将其放入背包或不放入背包。我们可以通过递归的方式来实现这个过程,每次递归时,我们都需要考虑当前物品是否放入背包,如果放入背包,则需要更新背包的容量和价值,然后继续递归下一个物品;如果不放入背包,则直接递归下一个物品。 以下是背包问题的回溯算法C++代码实现: ``` #include <iostream> using namespace std; const int MAXN = 100; int w[MAXN], v[MAXN]; // 物品的重量和价值 int n, c; // 物品的数量和背包的容量 int bestv = 0; // 最优的价值 int x[MAXN], bestx[MAXN]; // 当前和最优 void backtrack(int i, int cw, int cv) { if (i > n) { // 达到叶节点 if (cv > bestv) { // 更新最优 bestv = cv; for (int j = 1; j <= n; j++) { bestx[j] = x[j]; } } return; } if (cw + w[i] <= c) { // 放入背包 x[i] = 1; backtrack(i + 1, cw + w[i], cv + v[i]); } x[i] = 0; // 不放入背包 backtrack(i + 1, cw, cv); } int main() { cin >> n >> c; for (int i = 1; i <= n; i++) { cin >> w[i] >> v[i]; } backtrack(1, 0, 0); cout << bestv << endl; for (int i = 1; i <= n; i++) { cout << bestx[i] << " "; } cout << endl; return 0; } ``` 其中,w和v数组分别表示物品的重量和价值,n和c分别表示物品的数量和背包的容量,bestv和bestx分别表示最优的价值和物品的选择情况。在回溯算法中,我们通过递归函数backtrack来实现对所有可能搜索。在每次递归时,我们需要考虑当前物品是否放入背包,然后继续递归下一个物品。当搜索到叶节点时,我们需要判断当前是否为最优,并更新最优

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

司六米希

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值