回溯算法杂谈

1、概念

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

2、基本思想

在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。

若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

3、用回溯法解题的一般步骤:

(1)针对所给问题,确定问题的解空间:首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

在计算机问题中,大量的问题都需要使用递归算法,上一篇博客我们介绍了一下二叉树中的递归问题。现在我们来看递归算法中非常经典的思想回溯法,这样的算法思想通常都应用在一类问题上,这类问题叫做树型问题,这类问题他本身没有定义在一颗二叉树中,但我们具体分析这个问题时就会发现解决这个问题的思路本质是一颗树的形状。

树形问题

现在我们来看递归算法中非常经典的思想回溯法,这样的算法思想通常都应用在一类问题上,这类问题叫做树型问题,这类问题他本身没有定义在一颗二叉树中,但我们具体分析这个问题时就会发现解决这个问题的思路本质是一颗树的形状。

leetcode 17. 电话号码的字母组合

在这里插入图片描述

解题思路

比如我们输入的digits=“23”,2能代表abc三个字母,当2代表a时,3代表def,同理我们就可以画出一棵树。
递归过程:
digits是数字字符串
s(digits)是digits所能代表的字母字符串
s(digits[0…n-1]) = letter(digits[0]) + s(digits[1…n-1]) = letter(digits[0]) + letter(digits[1]) + s(digits[2…n-1]) = …

代码实现
/**
 * Created by weidiezeng on 2019.12.01 15:50
 */
class Solution {
    static List<String>res=new ArrayList<>();
	static final String[] list={
		 " ",    //0
         "",     //1
         "abc",  //2
         "def",  //3
         "ghi",  //4
         "jkl",  //5
         "mno",  //6
         "pqrs", //7
         "tuv",  //8
         "wxyz"  //9
			
	};
	public List<String> letterCombinations(String digits) {
		res.clear();
		if(digits.length()==0) {
			return res;
		}
		findCombinations(digits,0,"");
		return res;
        
    }
	private void findCombinations(String digits, int index, String s) {
		// TODO Auto-generated method stub
		if(index==digits.length()) {
			res.add(s);
			return ;
		}
		//获得数字
		char c=digits.charAt(index);
		//获得对应的字符串
		String letters=list[c-'0'];
		for(int i=0;i<letters.length();i++) {
			findCombinations(digits,index+1,s+letters.charAt(i));
		}
	}
}

什么是回溯

递归调用的一个重要特征-要返回。回溯法是暴力解法的一个主要实现手段。

思考题

  • leetcode 93
  • leetcode 131

排列问题(Permutations)

leetcode 46. 全排列

在这里插入图片描述

解题思路

回溯算法能处理一类重要的问题是排列问题,如果我们要用1,2,3进行排列,我们可以先抽出一个元素,比如我们现在抽出1,那么我们下面要做的事就是使用2,3两个元素构造排列。我们又需要抽出一个元素,如果我们抽出2,我们剩下唯一的元素就是3,我们通过这个路径获得排列123,用23排列如果选3,那么就剩下2我们得到排列132。相应的我们考虑最开始选择2或者选择3。

这也是一个树形问题
Perms(nums[0…n-1]) = {取出一个数字} + Perms(nums[{0…n-1} - 这个数字])

代码实现

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

//leetcode 46
/**
 * Created by weidiezeng on 2019.12.01 21:50
 */
public class LeetCode46 {

	static List<List<Integer>> res;
	static List<Boolean>visitor;
	public List<List<Integer>> permute(int[] nums) {
		res=new ArrayList<List<Integer>>();
		res.clear();
		if(nums.length==0) {
			return res;
		}
		visitor=new ArrayList<>();
		for(int i=0;i<nums.length;i++) {
			visitor.add(false);
		}
		LinkedList<Integer>p=new LinkedList<>();
		generatePermute(nums,0,p);
		return res;
        
    }
	private void generatePermute(int[] nums, int index, LinkedList<Integer> p) {
		// TODO Auto-generated method stub
		if(index==nums.length) {
			res.add(new LinkedList(p));
			return;
		}
		for(int i=0;i<nums.length;i++) {
			if(! visitor.get(i)) {
				p.add(nums[i]);
				visitor.set(i,true);
				generatePermute(nums,index+1,p);
				
				///回溯
				p.removeLast();
				visitor.set(i, false);
			}
		}
	}	
}

相似问题

  • leetcode 47

组合问题 Combinations

回溯的意思就是要回去,递归函数自动保证了回去,但是我们设置的其他变量如果有必要的话也必须要回到原位。

leetcode 77. 组合

在这里插入图片描述

解题思路

我们在1,2,3,4中取出你两个数。在第一步时如果我们取1,那么接下来就在2,3,4中取一个数,我们可以得到组合12,13,14。如果第一步取2,那么第二步在3,4中取一个数,可以得到组合23,24。如果我们第一步取3,那么第二步只能取4,得到组合34。

代码实现
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
//leetcode 77
/**
* Created by weidiezeng on 2019.12.02 10:30
*/
public class LeetCode77 {

	 static LinkedList<List<Integer>>res;
	 public List<List<Integer>> combine(int n, int k) {
		 res=new LinkedList<List<Integer>>();
		 res.clear();
		 if(n<=0||k<=0) {
			 return res;
		 }
		 LinkedList<Integer>c=new LinkedList<>();
		 generateCombinations(n,k,1,c);
		return res;
	        
	 }
	private void generateCombinations(int n, int k, int start, LinkedList<Integer> c) {
		// TODO Auto-generated method stub
		if(c.size()==k) {
			res.push(new LinkedList(c));
			return;
		}
		for(int i=start;i<=n;i++) {
			c.add(i);
			generateCombinations(n,k,i+1,c);
			
			//回溯
			c.pop();
		}
	}
}

回溯法解决组合问题的优化

在这里插入图片描述

这是我们对这道题递归树建立的模型,在这个模型里存在一个地方我们是明显没必要去走的,就是在于最后的地方,我们根本不需要去尝试取4,这是因为我们取4之后无法再取任意一个数了。在我们上面的算法中我们还是尝试取了4,取完4之后当取第二个数时发现我们什么都取不了了,所以只好再返回回去,对于这一部分我们完全可以把它剪掉。换句话说,我们只尝试取1,2,3。

回溯法的剪枝

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
//leetcode 77
/**
* Created by weidiezeng on 2019.12.02 10:30
*/
public class LeetCode77 {

	 static LinkedList<List<Integer>>res;
	 public List<List<Integer>> combine(int n, int k) {
		 res=new LinkedList<List<Integer>>();
		 res.clear();
		 if(n<=0||k<=0) {
			 return res;
		 }
		 LinkedList<Integer>c=new LinkedList<>();
		 generateCombinations(n,k,1,c);
		return res;
	        
	 }
	private void generateCombinations(int n, int k, int start, LinkedList<Integer> c) {
		// TODO Auto-generated method stub
		if(c.size()==k) {
			res.push(new LinkedList(c));
			return;
		}
		 // 还有k - c.size()个空位, 所以,[i...n]中至少要有k-c.size()个元素
         // i最多为 n - (k-c.size()) + 1
		for(int i=start;i<=n-(k-c.size())+1;i++) {
			c.add(i);
			generateCombinations(n,k,i+1,c);
			
			//回溯
			c.pop();
		}
	}
}

相似问题

  • leetcode 39
  • leetcode 40
  • leetcode 216
  • leetcode 78
  • leetcode 90
  • leetcode 401

二维平面上的回溯法

leetcode79. 单词搜索

在这里插入图片描述

解题思路

在这里插入图片描述

对于每一个位置,我们按照上右下左从四个方向寻找,当选择的方向匹配时,则选择这个位置继续进行上右下左寻找,如果四个方向都不匹配,则退回上一步的位置寻找下一个方向。

代码实现
import java.util.ArrayList;
import java.util.List;

/**
* Created by weidiezeng on 2019.12.02 15:15
*/
public class LeetCode79 {

	static Boolean [][] visited;
	static int m;
	static int n;
	static int [][] d= {{-1,0},{0,1},{1,0},{0,-1}};
	public boolean exist(char[][] board, String word) {
		m=board.length;//行
		n=board[0].length;//列
		visited=new Boolean[m][n];
		
		//初始化visited
		for(int i=0;i<m;i++) {
			for(int j=0;j<n;j++) {
				visited[i][j]=false;
			}
			
		}
		for(int i=0;i<m;i++) {
			for(int j=0;j<n;j++) {
				if(searchWord(board,word,0,i,j)){
					return true;
				}
			}
		}
		return false;
        
    }
	private boolean searchWord(char[][] board, String word, int index, int startx, int starty) {
		// TODO Auto-generated method stub
		if(index==word.length()-1) {
			return board[startx][starty]==word.charAt(index);
		}
		if(board[startx][starty]==word.charAt(index)) {
			
			visited[startx][starty]=true;
		// 从startx, starty出发,向四个方向寻
		for(int i=0;i<4;i++) {
			int newx=startx+d[i][0];
			int newy=starty+d[i][1];
			if(inArea(newx,newy)&&!visited[newx][newy]) {
				if(searchWord(board,word,index+1,newx,newy)){
					return true;
				}
			}
		}
		//回溯
		visited[startx][starty]=false;
	}
		return false;
	}
	private boolean inArea(int x, int y) {
		// TODO Auto-generated method stub
		return x >= 0 && x < m && y >= 0 && y < n;
	}
}


floodfill算法,一类经典问题

leetcode 200. 岛屿的个数

在这里插入图片描述

解题思路

在这里插入图片描述

首先我们从二维数组最开始的地方(0,0)找起,这个地方是1,我们就找到了一个新的岛屿,但我们需要标记和这块陆地同属于一个岛屿的陆地,当我们寻找下一个岛屿的时候才不会重复。那么这个过程就是floodfill过程。其实就是从初始点开始进行一次深度优先遍历,和上面那道题的寻找很相似,对每一个岛屿进行四个方向寻找。

代码实现
/**
* Created by weidiezeng on 2019.12.02 21:10
*/
public class LeetCode200 {
 
	static int d[][] = {{0,1}, {0, -1}, {1,0},{-1, 0}};
    static int m, n;
    static Boolean[][]  visited;

	public int numIslands(char[][] grid ) {
		m=grid.length;
		if(m==0) {
			return 0;
		}
		n=grid[0].length;
		if(n==0){
			return 0;
		}
		//初始化visited
		visited=new Boolean[m][n];
		for(int i=0;i<m;i++) {
			for(int j=0;j<n;j++) {
				visited[i][j]=false;
			}
		}
		int res=0;
		for(int i=0;i<m;i++) {
			for(int j=0;j<n;j++) {
				if(grid[i][j]=='1'&&!visited[i][j]) {
					dfs(grid,i,j);
					res++;
				}
			}
		}
		return res;
	}
	private void dfs(char[][] grid, int x, int y) {
		// TODO Auto-generated method stub
		visited[x][y]=true;
		//四个方向开始地柜
		for(int i=0;i<4;i++) {
			int newx=x+d[i][0];
			int newy=y+d[i][1];
			if(inArea(newx,newy)&&!visited[newx][newy]&&grid[newx][newy]=='1') {
				dfs(grid,newx,newy);
			}
		}
	}
	private boolean inArea(int x, int y){
	        return x >= 0 && x < m && y >= 0 && y < n;
	    }
 
	
}

在这里,我们似乎没有看见回溯的过程,也就是说我们不需要找到一个位置让visited[x][y]为false,这是因为我们的目的就是要把和最初我们运行的(i,j)这个点相连接的岛屿全部标记上,而不是在其中找到某一个特定的序列或者一个具体的值,所以我们只标记true,不会把它倒着标记成false。所以对于这个问题是否叫做回溯法,这是一个见仁见智的问题。在搜索的过程中一定会回去,这是递归的特性。但它没有对信息进行重置。不过它的解题思路是经典的floodfill。

相似问题

  • leetcode 130
  • leetcode 417

回溯法是经典人工智能的基础

回溯法师经典人工智能的基础

leetcode 51. N皇后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HrqBXrp9-1577786693457)(_v_images/20191202171159167_20373.png =700x)]

解题思路

在这里插入图片描述

**快速判断合法的情况

在这里插入图片描述

  • dia1: 横纵坐标相加相同
    加粗样式

  • dia2:横坐标-纵坐标相同
    在这里插入图片描述

对于四皇后为例看一下如何递归回溯。首先肯定每行都应该有一个皇后,否则就会有一行出现多个皇后。那么第二行只能在第三个位置或第四个位置,考虑第三个位置。那么第三行无论在哪都会有冲突。说明我们第二行的皇后不能放在第三个位置,我们回溯,在第四个位置放置皇后。
每一次在一行中尝试摆放一个皇后,来看我们能不能摆下这个皇后,如果不能摆下,回去上一行重新摆放上一行皇后的位置,直到我们在四行都摆放皇后。

代码实现
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
* Created by weidiezeng on 2019.12.03 09:10
*/
public class Leetcode51 {

	Boolean[] col;
	Boolean[] dia1;
	Boolean[] dia2;
	List<List<String>>res;
	public List<List<String>> solveNQueens(int n) {
		res=new ArrayList<List<String>>();
		res.clear();
		//初始化col,dia1,dia2;
		col=new Boolean[n];
		dia1=new Boolean[2*n-1];
		dia2=new Boolean[2*n-1];
		for(int i=0;i<n;i++){
			col[i]=false;
		}
		for(int i=0;i<2*n-1;i++) {
			dia1[i]=false;
			dia2[i]=false;
		}
		LinkedList<Integer>row=new LinkedList<>();
		putQueen(n,0,row);
		
		return res;
        
    }
	private void putQueen(int n, int index, LinkedList<Integer> row) {
		// TODO Auto-generated method stub
		if(index==n) {
			res.add(generateBoard(n,row));
			return;
		}
		// 尝试将第index行的皇后摆放在第i列
		for(int i=0;i<n;i++) {
			if(!col[i]&&!dia1[index+i]&&!dia2[index-i+n-1]) {
				row.push(i);
				col[i]=true;
				dia1[index+i]=true;
				dia2[index-i+n-1]=true;
				
				//递归,尝试下一行
				putQueen(n,index+1,row);
				//回溯,复原
				col[i]=false;
				dia1[index+i]=false;
				dia2[index-i+n-1]=false;
				row.pop();
			}
		}
	}
	private List<String> generateBoard(int n, LinkedList<Integer> row) {
		// TODO Auto-generated method stub
		List<String> board=new ArrayList<String>();
		//初始化棋盘
		for(int i=0;i<n;i++){
			char[] s=new char[n];
			for(int j=0;j<n;j++) {
				s[j]='.';
			}
			board.add(String.valueOf(s));
		}
		//根据row的位置变为Q
		for(int i=0;i<n;i++) {
			char [] str=board.get(i).toCharArray();
			str[row.get(i)]='Q';
			
			board.set(i, String.valueOf(str));
		}
		return board;
	}
}

相似问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值