算法-----回溯算法

一   什么是回溯算法

回溯算法(Backtracking Algorithm)是一种用于解决组合优化问题的算法,它通过逐步构建候选解并进行验证,以寻找所有满足特定条件的解。回溯算法通常应用于在给定约束条件下枚举所有可能解的问题,如组合、排列、子集等。

回溯算法的基本思想是通过递归的方式进行搜索,每一步都尝试扩展当前的解,直到找到满足条件的解或者确定无解。在搜索的过程中,如果当前的解不满足约束条件,就会回溯到上一步进行其他选择,继续搜索。

回溯算法的一般步骤如下:

  1. 定义问题的解空间,确定问题的约束条件。
  2. 通过递归的方式搜索解空间,每一步都进行选择,并进行约束条件的检查。
  3. 如果当前的选择满足约束条件,则继续递归地进行下一步选择。
  4. 如果当前的选择不满足约束条件,进行回溯,撤销当前选择,返回上一步继续搜索其他选择。
  5. 当搜索完成后,得到所有满足条件的解。

回溯算法的时间复杂度通常较高,因为它需要枚举所有可能的解。在某些情况下,可以通过剪枝等优化策略来减少搜索空间,提高算法效率。

回溯算法在很多问题中都有应用,例如八皇后问题、0-1背包问题、图的遍历等。它是一种非常经典和常用的算法思想,对于解决组合优化问题具有重要的作用。

通常解该类题目时,我们要确定解的空间,从而很好的利用回溯算法来解决该类题目。


二   何为解空间

      解空间(Solution Space)是指在给定问题的约束条件下,所有可能的解的集合。它包含了问题的所有合法解。

      解空间的具体形式取决于问题的性质和约束条件。对于某些问题,解空间可能是一个有限的集合,例如在数独游戏中,解空间是由符合数独规则的所有数字填充方案组成的集合。而对于其他问题,解空间可能是一个无限的集合,例如在连续优化问题中,解空间是由实数构成的无限维空间。

     解空间是问题求解的关键概念之一。在解决问题时,我们通常需要在解空间中搜索满足特定条件的解。回溯算法、枚举法、剪枝算法等求解方法都是基于对解空间的搜索。

      解空间的大小直接影响了问题的复杂性和求解算法的效率。如果解空间非常大,问题的求解可能会非常困难,需要耗费更多的时间和资源。因此,在实际应用中,优化算法常常通过剪枝、启发式搜索等技术来减小解空间的规模,以提高求解效率。

总之,解空间是问题求解中描述所有可能解的概念,通过搜索解空间,我们可以找到满足问题要求的解或者找到最优解。

对于大多数该类问题的解空间都是树的形式,集合的大小构成了树的宽度,递归的深度构成的树的深度。


三   回溯算法的模板

下面是回溯算法的一般模板:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {//横向遍历
        处理节点;//这里一般可能进行剪枝操作
        backtracking(路径,选择列表); // 递归,纵向遍历
        回溯,撤销处理结果
    }
}

这是一个基本的回溯算法模板,其中的关键点包括:

  1. 定义终止条件:当满足终止条件时,表示找到了一个解或者不再需要继续搜索,可以进行相应的操作,如输出结果或返回。

  2. 遍历选择列表:遍历所有可能的选择,通常使用循环结构,对于每个选择,进行相应的操作。

  3. 做出选择:根据当前选择,更新状态或路径,表示对问题的一次选择。

  4. 递归进入下一层决策树:根据当前选择,进入下一层决策树,即进行下一步的选择。

  5. 撤销选择:在回溯到上一层之前,需要撤销当前选择,恢复状态或路径,以便进行下一个选择。

在实际应用中,根据具体问题的不同,模板中的代码需要进行相应的修改和扩展,以适应问题的特点和约束条件。同时,通过剪枝、优化等技巧,可以对模板进行改进,提高算法的效率。

需要注意的是,回溯算法是一种暴力搜索的方法,解空间的规模很大时,可能会导致算法效率低下。因此,在使用回溯算法时,需要根据问题的规模和特点进行合理的优化和剪枝,以提高算法的性能。


四   回溯算法例题之N皇后问题

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

4*4的棋盘摆放结果如下:

解题思路:

  1. 定义一个 N × N 的棋盘,使用一个二维数组或其他数据结构表示。初始化棋盘的所有位置为空。

  2. 从第一行开始,逐行放置皇后。对于每一行,遍历该行的每一个位置,尝试将皇后放置在当前位置。

  3. 在放置皇后之前,检查是否满足以下条件:

    • 当前位置的同一列没有其他皇后。
    • 当前位置的左上方和右上方(对角线)没有其他皇后。
  4. 如果满足上述条件,将皇后放置在当前位置,并将该位置标记为已占用。

  5. 继续递归地处理下一行,重复步骤 3 和步骤 4。

  6. 如果已经放置了 N 个皇后,表示找到了一个可行解,将该解保存起来。

  7. 回溯到上一步,撤销对当前位置的选择,继续尝试下一个位置。

  8. 当所有的位置都尝试完毕或者已经找到了所有的可行解时,算法结束。

  9. 返回所有的可行解。

    我们先以3*3的棋盘来看它的解空间,如图所示:

不难看出解空间对应的是树的结构,我们可以套用模板进行解决:

void Backtrack(char bord[][N],int n,int row){
	if(row>=n){//递归出口
		print(bord,n);//如果满足直接打印结果
		count++;//用来记录解的数量
		printf("\n");
		return ;
	}else{
		for(int colum=0;colum<n;colum++){//横向遍历
			if(isselect(bord,row,colum,n)){//如果满足放置条件
				bord[row][colum] = 'Q';
				Backtrack(bord,n,row+1);//进行递归,选择下一行的格子
				bord[row][colum] = '*';//回溯
			}
		}
	}
}

 【完整代码】

#include <stdio.h>
#include <stdbool.h>
#define N 100

int count=0;
void print(char bord[][N],int n){
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			if(bord[i][j] == 'Q'){
				printf("  Q");
			}else{
				printf("  *");
			} 
		}
		printf("\n");
	} 
}

bool isselect(char bord[][N], int row, int colum,int n) {
	for(int j=0;j<row;j++){//判断列
		if(bord[j][colum]=='Q'){
			return false;
		}
	}
	
	for(int i=row,j=colum;i>=0&&j>=0;i--,j--){//判断左上角
		if(bord[i][j]=='Q'){
			return false;
		}
		
	}
	
	for(int i=row,j=colum;i>=0&&j<n;i--,j++){//判断右上角
		if(bord[i][j]=='Q'){
			return false;
		}
		
	}
	return true;
	
}



void Backtrack(char bord[][N],int n,int row){
	if(row>=n){
		print(bord,n);
		count++;
		printf("\n");
		return ;
	}else{
		for(int colum=0;colum<n;colum++){
			if(isselect(bord,row,colum,n)){
				bord[row][colum] = 'Q';
				Backtrack(bord,n,row+1);
				bord[row][colum] = '*';
			}
		}
	}
}

int main()
{
	int n;
	printf("请输入皇后个数:\n");
	scanf("%d",&n);
	char bord[N][N]={'*'};
	printf("皇后摆放形式如下:\n");
	Backtrack(bord,n,0);
	printf("%d后问题共有%d种摆放方案 ",n,count);
	return 0;
}

【运行效果】

部分资料参考:代码随想录; 

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回溯算法是一种解决问题的算法思想,它通过不断地尝试所有可能的解决方案来寻找问题的最优解。而装载问题是回溯算法的一个经典应用,它是指在给定一些集装箱和一艘载重量为C的轮船的情况下,如何将这些集装箱装载到轮船上,使得轮船的载重量最大。 具体来说,装载问题可以通过回溯算法来解决。我们可以定义一个递归函数来尝试所有可能的装载方案,每次递归时,我们可以选择将当前集装箱装载到轮船上或者不装载,然后继续递归下去。当所有集装箱都被考虑过后,我们就可以得到一个装载方案,然后比较这个方案的载重量和当前最优解的载重量,如果更优则更新最优解。 下面是一个简单的装载问题的回溯算法实现: ```c #include <stdio.h> #define MAX_N 100 int n; // 集装箱数量 int c; // 轮船载重量 int w[MAX_N]; // 集装箱重量 int best[MAX_N]; // 当前最优解 int cur[MAX_N]; // 当前解 int cur_weight; // 当前载重量 int best_weight; // 当前最优解的载重量 void backtrack(int i) { if (i == n) { // 所有集装箱都被考虑过 if (cur_weight > best_weight) { // 更新最优解 best_weight = cur_weight; for (int j = 0; j < n; j++) { best[j] = cur[j]; } } return; } if (cur_weight + w[i] <= c) { // 装载当前集装箱 cur[i] = 1; cur_weight += w[i]; backtrack(i + 1); cur_weight -= w[i]; } cur[i] = 0; // 不装载当前集装箱 backtrack(i + 1); } int main() { scanf("%d%d", &n, &c); for (int i = 0; i < n; i++) { scanf("%d", &w[i]); } backtrack(0); printf("最大载重量为:%d\n", best_weight); printf("装载方案为:"); for (int i = 0; i < n; i++) { if (best[i]) { printf("%d ", i + 1); } } printf("\n"); return 0; } ``` 在这个实现中,我们使用了一个数组`cur`来记录当前的解,使用`cur_weight`来记录当前的载重量,使用`best`来记录当前最优解,使用`best_weight`来记录当前最优解的载重量。在回溯函数`backtrack`中,我们首先判断是否可以装载当前集装箱,如果可以则装载,然后递归下去;如果不可以则不装载,然后递归下去。当所有集装箱都被考虑过后,我们就可以得到一个装载方案,然后比较这个方案的载重量和当前最优解的载重量,如果更优则更新最优解。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值