2021-03-22

本文详细介绍了回溯算法,将其与决策树遍历相结合,通过全排列和N皇后问题实例阐述其工作原理。回溯算法的核心在于递归过程中做选择与撤销选择,其时间复杂度通常较高,不适用于所有问题。文章还对比了回溯算法与动态规划的差异,强调动态规划的优化可能性。
摘要由CSDN通过智能技术生成

回溯算法详解

一.介绍

解决一个回溯问题,实际上就是一个决策树的遍历过程。只需要思考3个问题:
1.路径: 也就是已经做出的选择。
2.选择列表: 也就是你当前可以做的选择。
3.结束条件: 也就是达到决策树底层,无法再做选择的条件。

后面会通过全排列N皇后问题来加深理解。

回溯算法的基本框架:

int[] result=new int [n+1];
backtrack(路径,选择列表){
	if 满足结束条件:
		result.add(路径);
		return;
	for 选择 in 选择列表{
		做选择
		backtrack(路径,选择列表)
		撤销选择
		}
} 

核心就是for循环里的递归,递归调用之前【做选择】,递归调用之后【撤销选择】。

二.全排列问题

已知n个不重复的数,进行全排列。
如:给三个数[1,2,3],先固定第一位是1,这样第二位可以是2,第三位只能是3;然后可以把第二位变成3,这样第三位就是2;然后再变第一位,再穷举后面两位。。。

在这里插入图片描述
实际上,这就是回溯算法。
只要从根遍历这课树,记录路径上的数字,其实就是所有的全排列。将这棵树称为回溯算法的决策树

从图上来解释几个名词。以根节点为例,[2]就是路径,记录已经做过的选择;[1,3]就是选择列表,表示当前可以做出的选择;[结束条件]就是遍历到树的底层,在这里就是选择列表为空的时候。

backtrack函数就像一个指针,在这棵树上游走,同时正确维护每个节点的属性。

多叉树的遍历框架:

void traverse(TreeNode root){
	for(TreeNode child:root.children)
		{
			//前序遍历操作
			traverse(child);
			//后序遍历操作
		}
}

前序遍历的代码在进入某个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行。
在这里插入图片描述
在这里插入图片描述

for 选择 in 选择列表:
	#做选择
	将该选择从选择列表移除
	路径.add(选择)
	backtrack(路径,选择列表)
	#撤销选择
	路径.remove(选择)
	将该选择再加入选择列表

全排列代码:

List<List<Integer>> res = new LinkedList<>();
/* 主函数,输入一组不重复的数字,返回它们的全排列*/
List<List<Integer>> permute(int []nums){
	//记录路径
	LinkedList<Integer> track = new LinkedList<>();
	backtrack(nums,track);
	return res;
}

void backtrack(int[]nums,LinkedList<Integer>track)
{
	//触发结束条件
	if(track.size() == nums.length)
		res.add(new LinkedList(track)); //可以这样新建一个列表
		return;
	for(int i =0;i<nums.length;i++)
	{
		//排除不合理的选择
		if(track.contains(nums[i]))
			continue;
		//做选择
		track.add(nums[i]);
		//进入下一层决策树
		backtrack(nums,track);
		//取消选择
		track.removeLast();
	}
}

至此,就使用回溯算法解决了全排列问题。但是这个算法并不是很高效,因为对链表使用contains方法需要O(N)时间复杂度。

但是,不管如何优化,时间复杂度都不会低于O(N!),因为穷举整颗决策树是无法避免的。
这就是回溯算法的特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度高

三.N皇后问题

给你一个NxN的棋盘,让你放置N个皇后,使得它们不能互相攻击。
ps:皇后可以攻击同一行,同一列,左上左下右上右下四个方向的任意单位。

本质上,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。

LinkedList<List<String>> res = new LinkedList<>();
LinkedList<List<String>> solveQ(int n)
{
	//Q表示皇后,‘.’表示空,初始化棋盘
	String [][] board = new String [n][n];
	Arrays.fill(board,'.');
	backtrack(board,0)return res;
}

void backtrack(String[][]board,int row){
	//触发结束条件
	int n = board.length;
	if(row==n)
		res.add(board);
	for(int col = 0;col<n;col++)
	{
		//排除不合理的选择
		if(!isvalid(board,row,col))
			continue;
		board[row][col] ='Q';
		//进入下一层决策
		backtrack(board,row+1);
		board[row][col]='.';
	}
}

boolean isvalid(String[][]board, int row,int col)
{
	int n= board.length;
	//检查是否有皇后冲突
	for(int i =0;i<n;i++)
	{
		if(board[i][col]='Q')
			return false;
	}
	//检查右上方是否有冲突
	for(int i = row -1,j=col+1;i>=0&&j<n;i--,j++){
		if(board[i][j] == 'Q')
			return false;
		}
	//检查左上方是否有皇后冲突
	for(int i = row-1,j=col-1;i>=0&&j>=0;i--.j--)
		{
			if(board[i][j] == 'Q')
				return false;
		}
	return true;
}

四.总结

某种程度上,回溯算法有点动态规划的味道,但是动态规划有重叠子问题的情况,可以用dp table或者备忘录优化。

动态规划的三个需要明确的点 【状态】、【选择】、和【base case】对应着回溯算法中的【路径】、【选择列表】、【结束条件】。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值