日升时奋斗,日落时自省
目录
回溯不用直接去理解词名,实际上类似枚举,不断尝试各种情况,在尝试不同情况中寻找问题的解,当发现当前情况已经不满足求解需要了,就产生“回溯” 返回 (直白说就是递归条件到头了,返回),尝试别的路径
(1)回溯法是一种选优先搜索法,按选优条件向前搜索,以达到目标,但当探索到某一步时,,发现原先选择并不优或达不到目标,就回退一步重新选择(排除当前选择),这种走不通过就回退再走的技术为回溯法,而满足回溯条件的某个状态的点成为“回溯点”,也可以成为剪枝点,所谓的剪枝,指的是不必要的步骤或者路径去掉
(2)复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称
(3)在包含问题的所有解空间树中个,按照深度优先搜索的策略,从根节点出发深度探索解空间树,当探索到某一节点时,要先判断该节点是否包含问题的解,如果包含就从该节点出发,继续探索下去,如果该节点不被包含,则逐层向其祖先节点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)
(4)若用回溯法求问题的所有解时,要回溯到根,且根节点的所有可行的子树都要已被搜索遍历才结束
(5)若用回溯法求问题只有一个解,只要深度优先搜索到问题的解就可以结束了
文字叙述总显得很浅显,以下用例题详细解析 “回溯” 如何深度理解
1、深度优先解题
1.1、一条到走到黑
使用方法:深度优先搜索
假如有编号为1~3 的3张扑克牌和编号为1~3的3个盒子,现在需要将3张牌分别放到3个盒子中去,且每个盒子只能做一张牌,一共有多少种不同的放法(图解)
代码解析(附加注释):
public static void main(String[] args) {
int n;
Scanner scanner=new Scanner(System.in);
n=scanner.nextInt();
//为什么数组要多创建一个空间 因为0下标的用不上 从1开始计数并且使用
//此处也是看需求而定
int [] boxs=new int[n+1]; //盒子
int [] books=new int[n+1]; //准备一数组标记值
Dfs(1,n,boxs,books); //深度优先遍历
}
private static void Dfs(int index, int n, int[] boxs, int[] books) {
//判定盒子是否是装满的 为啥index == n+1 因为 Dfs要多执行一次 所以index会在原有个数上多一
if(index==n+1){
//以下是为了打印 能到底层的路径
for(int i=1;i<=n;i++){
System.out.print(boxs[i]+" ");
}
System.out.println();
return ;
}
for(int i=1;i<=n;i++){
if(books[i]==0){ //第 i 号牌仍在手上 说明还没有被用,就是还没有被标记
// 把当前还没有进行标记的值 赋值给盒子
boxs[index]=i;
books[i]=1; //这个值 已经给了一个盒子,这个值就不能再用了,需要标记一下
//处理下一个盒子 下面参数 index+1 就表示下一个盒子
Dfs(index+1,n,boxs,books);
//无论什么情况回溯 都需要 将本值的标记取消,因为该值能回退,说明已经用过了
books[i]=0;
}
}
}
1.2、员工的重要性
使用方法:深度优先搜索
题目源于力扣:力扣
题目描述:给定一个保存员工信息的数据结构,它包含了员工 唯一的 id ,重要度 和 直系下属的 id 。
比如,员工 1 是员工 2 的领导,员工 2 是员工 3 的领导。他们相应的重要度为 15 , 10 , 5 。那么员工1的数据结构是[1,15,[2]],员工2的数据结构是[2,10,[3]],员工3的数据结构是[3,5,[]]。(图解)
题目最终求解:所有员工重要度一共是多少(也就是所有员工重要度加起来)
代码解析(附加注释):
class Employee {
public int id; //员工号
public int importance; //该员工的重要度
public List<Integer> subordinates; //存放下级员工id
};
public class GetImportanceTest {
public int getImportance(List<Employee> employees,int id){
// 每次都是向下级递归 如果当前级别已经没有下级了也就是空 此处结束 回退
if(employees.isEmpty()){
return 0;
}
//以下的 Map 存放的是 的是员工的 id 和 员工的整个信息
Map<Integer,Employee> info=new HashMap<>();
//存入所有的员工的信息
for(Employee e:employees){
//key存放员工 id 同时value对应员工的所有信息
info.put(e.id, e);
}
//使用DFS 实现回溯 加 每个员工的重要度
return DFS(info,id);
}
private int DFS(Map<Integer, Employee> info, int id) {
//首先获得下级的id 准备向下递归
Employee cur=info.get(id);
//获取当前员工的 重要度
int curSum=cur.importance;
// 将下级进行深度优先搜索 如果没有下级就 “回溯”
for(int curd:cur.subordinates){
curSum+=DFS(info,curd);
}
return curSum;
}
}
1.3、图像渲染
使用方法:深度优先搜索
题目源于力扣:力扣
题目描述:我们照相的话知道像素,如果使用ps这样的工具把图片拉大了,能看见一小块一小块的图案带有颜色,对题的理解基本如上,如果能够实现相同点数的位置用同一种颜色 ,就是图像渲染
如何完成上色,从初识像素开始,把和初始坐标开始,颜色值相同的点的颜色全部改为新的颜色,并且只要某个点颜色被更改,则继续以此点周围渲染
代码解析(附加注释):
//图像渲染
//对应参数分别是 当前颜色值使用二维数组表示 当前颜色点的位置 行:srow 列:scol
//newColor 是新颜色位置
//四个方位 顺时针建立
int[][] nextPosition={{0,1},{1,0},{0,-1},{-1,0}}; //从左向右分别代表上右下左
public int[][] floodFill(int[][] image,int srow,int scol,int newColor){
//要知道原来的颜色是什么值
int oldColor =image[srow][scol];
//当前渲染的 行 和 列 有多大
int row=image.length; //行
int col=image[0].length; //列
//建立标记
int[][] book=new int[row][col];
//深度遍历同时 回溯
DFS(image,row,col,book,srow,scol,oldColor,newColor);
return image;
}
private void DFS(int[][] image, int row, int col, int[][] book, int srow, int scol, int oldColor, int newColor) {
// 修改当前位置的颜色 并且做已标记 ,标记表示已经修改过了
image[srow][scol]=newColor;
book[srow][scol]=1;
for(int k=0;k<4;k++){
//k 在这里是以4为限制 表示给四个方位逐一加一次
//处理行 新的行坐标
int newSrow=srow+nextPosition[k][0];
//处理列 新的列坐标
int newScol=scol+nextPosition[k][1];
//防止向四周进行越界
if(newSrow>=row||newSrow>0||newScol<0||newScol>=col){
continue;
}
//满足两个条件就可以改色了
// 第一个就是该颜色是要改的颜色
// 第二个就是该颜色没有被改过 也就就是没有被标记过
if(image[newSrow][newScol]==oldColor&&book[newSrow][newScol]!=1){
//只有进入递归才能 在下一次进行改色
DFS(image,row,col,book,newSrow,newScol,oldColor,newColor);
}
}
}
1.4、被围绕的区域
使用方法:深度优先搜索
题目源于力扣:力扣
本题的意思被包围的区间不会存在于边界上,所以边界上的o以及与o联通的都不算做包围,只要把边界上的o以及yu之联通的o进行特殊处理,剩下的o替成x即可。故问题转化为,如何寻找和边界联通的o,我们需要考虑如下情况。
代码解释(附加注释):
//四个方位 移动
int[][] nextPosition={{0,1},{1,0},{0,-1},{-1,0}};
public void dfs(char[][] board,int row,int col,int i,int j){
//当前位置设为 ‘*’ 说了将边界上的o 改为特殊值 进行保存
board[i][j]='*';
for(int k=0;k<4;k++){
//向四个方向扩散
int ni=i+nextPosition[k][0];
int nj=j+nextPosition[k][1];
//判断边界
if(ni<0||ni>=row||nj<0||nj>=col){
continue;
}
//是 ‘o’ 说明和边界联通 继续搜索是否有联通的
//只要不等于 * 且不等于 X 一共只有三种标志
if(board[ni][nj]!='*'&&board[ni][nj]!='X'){
dfs(board,row,col,ni,nj);
}
}
}
public void solve(char[][] board){
if(board.length==0){
return ;
}
//寻找边上的每一个0 ,如果每一
//表示所有的0都被包围
int row =board.length;
int col =board[0].length;
//寻找第一行和最后一行
for(int j=0;j<col;j++){
//首先就是找第一行 的 o 进行处理
if(board[0][j]=='O'){
//将o改值 用来保存边界的o
dfs(board,row,col,0,j);
}
//然后就是最后一行 的 o 进行处理
if(board[row-1][j]=='O'){
//将o改值 用来保存边界的o
dfs(board,row,col,row-1,j);
}
}
//寻找第一列和最后一列
for(int i=0;i<row;i++){
//首先就是找第一列 的 o 进行处理
if(board[i][0]=='O'){
//将o改值 用来保存边界的o
dfs(board,row,col,i,0);
}
//然后就是最后一列 的 o 进行处理
if(board[0][col-1]=='O'){
//将o改值 用来保存边界的o
dfs(board,row,col,i,col-1);
}
}
//以上是处理好边界的 o 进行保存的操作
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
//从头到尾进行判断是否是 * 因为此时的边界相关的o 已经做了改值处理 进行保存
if(board[i][j]=='*'){
//将原来改为 ‘*’ 的位置 改回来O 因为都边界相关
board[i][j]='O';
}else if(board[i][j]=='O'){ //将中间部分并且是与边界无连接的O进行转变为 X
board[i][j]='X';
}
}
}
}
1.5、电话号码的字母组合
使用方法:深度优先搜索
题目源于力扣:力扣
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
代码解析(附加注释):
//对应不同编号的 2 - 9 电话的英文字母
String[] mapString={"","","abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
private void backTrace(String digits, List<String> ret, StringBuilder curStr, int curDepth) {
//边界 找到一种组合 ,放入数组中,结束此路径 向上回溯
if(curDepth==digits.length()){
if(curStr.length()!=0){
ret.add(curStr.toString());
}
return ;
}
//找到当前字符在String映射表中的位置 等到对应的下标
int curMapIndex=digits.charAt(curDepth)-'0';
//对应字符串
String curMap=mapString[curMapIndex];
//遍历每一种可能的组合
for(int i=0;i<curMap.length();i++){
//第一个参数 是输入的数字字符串
//第二个参数 存储所有结果集合
//第三个参数 是可变的字符串
//第四个参数 curDepth输入数字字符串的当前下标
backTrace(digits,ret,curStr.append(curMap.charAt(i)),curDepth+1);
//回溯的同时 要取消当前可变字符串进行处理 减少一个位置
curStr.deleteCharAt(curStr.length()-1);
}
}
public List<String> letterCombination(String digits){
//用来存储不同情况的子串
List<String> ret=new ArrayList<>();
//处理可变的字符串
StringBuilder curStr=new StringBuilder("");
//回溯位置
backTrace(digits,ret,curStr,0);
return ret;
}
1.6、组合总和
使用方法:深度优先搜索
题目源于力扣:力扣
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
此题相加的元素可以重复,所以去下一个元素的位置可以从当前位置开始,DFS+回溯为了保证组合不重复(顺序不同,元素相同,也算重复),不再从当前位置向前看
1.从第一个元素开始相加
2.让局部和继续累加候选的剩余值
3.局部和等于目标值,保存组合,向上回退,寻找其它组合
代码解析(附加注释):
public List<List<Integer>> combinationSum(int[] candidates,int target){
//存放能够组合起来等于 target的组合
List<Integer> solution=new ArrayList<>();
//存放不同情况的集合
List<List<Integer>> solutions=new ArrayList<>();
//如果这个数组没有值的话 就找不到目标
if(candidates.length==0){
return solutions;
}
int curSum=0; //计算相加值 与 target对比
dfs(candidates,solutions,solution,curSum,0,target);
return solutions;
}
private void dfs(int[] candidates, List<List<Integer>> solutions, List<Integer> solution, int curSum, int prevPosition, int target) {
//边界,如果大于等于目标,则结束
if(curSum>=target){
//等于目标 ,找到一个组合
if(curSum==target){
List<Integer> news=new ArrayList<>();
//如果值对应位置
for(int e: solution){
//需要进行拷贝
news.add(e);
}
//拷贝完成后 ,放入结果集中
solutions.add(news);
}
return ;
}
//可以从上一个位置开始 ,因为元素可以重复
for(int i=prevPosition;i< candidates.length;i++){
//单个值已经大于目标 ,直接跳过
if(candidates[i]>target){
continue;
}
//将当前 直接假如
solution.add(candidates[i]);
//继续进行可以执行重复数字
dfs(candidates,solutions,solution,curSum+candidates[i],i,target);
//回溯 向上回退 删除当前位置
solution.remove(solution.size()-1);
}
}
1.7、活字印刷
使用方法:深度优先搜索
题目源于力扣:力扣
有一套活字字模 tiles
,其中每个字模上都刻有一个字母 tiles[i]
。返回你可以印出的非空字母序列的数目。
此题组合的长度不唯一,最小组合长度为1,最大组合长度为tiles的长度
按照题意tiles中每一个位置的字符在组合中只能出现一次,所以可以用一个标记辅助
当去组合新的组合时,可以与tiles中的每一个位置组合,但是如果当前位置已经在当前组合中出现过,则跳过虽然此题中每一个位置字符在组合中只能出现一次,但是tiles中可能有相同的字符,所以需要考虑重复的组合
DFS + 回溯
1.当前组合不为空 ,则插入set中
2.继续给当前组合拼接新的组合,尝试拼接tiles每一个位置的字符
3.如果当前位置已在组合中出现过,返回到2,否则标记当前位置,继续拼接更长的组合的
4.回溯,尝试组合其它位置,返回2
当所有位置都已经使用过时,当前递归就结束了,继续向上层DFS回退(图解)
代码解析(附加注释):
public int numTilepossibilities(String tiles){
if(tiles.length()==0){
return 0;
}
//set 天生去重
Set<String> totalString =new HashSet<>();
//标记全部初始化为未使用
List<Integer> usedIndex=new ArrayList<>();
//针对 活字模板 将模板的值进行标记 为 0
for(int i=0;i<tiles.length();i++){
usedIndex.add(0);
}
// 可变字符串 存储当前的字符串
StringBuilder curStr=new StringBuilder("");
//将所有字符串进行遍历
dfs(tiles,curStr,usedIndex,totalString);
return totalString.size();
}
private void dfs(String tiles, StringBuilder curStr, List<Integer> usedIndex, Set<String> totalString) {
//是以不同个数模板进行
if(curStr.length()!=0){
totalString.add(curStr.toString());
}
//标记保证所有位都用完之后 就结束了
for(int i=0;i<tiles.length();i++){
//当前位置的字符已用过,直接跳过
if(usedIndex.get(i)==1){
continue;
}
//如果种类情况没有,那就设置为1
usedIndex.set(i,1);
//此处添加 字符,逐一枚举
dfs(tiles,curStr.append(tiles.charAt(i)),usedIndex,totalString);
//回溯 ,尝试其他字符
usedIndex.set(i,0);
//同时去除掉字符串的位置
curStr.deleteCharAt(curStr.length()-1);
}
}
1.8、N皇后
使用方法:深度优先搜索
题目源于力扣:力扣
题目描述:N皇后问题:把N个皇后放值N*N的二维矩阵中,保证他们相互不能攻击:既不在同一行,同一列,同一个斜线上(三种可能情况)
思想:DFS + 回溯
从第一行开始放置皇后,每确定一个位置,判断是否会冲突:是否在同一列,同一行,或者同一斜线,当前行位置确定之后,继续确定下一行的位置回退,尝试当前行的其他位置
代码解析(附加注释):
class pair{
public int x; //横坐标
public int y; //纵坐标
public pair(int x ,int y){//传参
this.x=x;
this.y=y;
}
}
public class SolveNQueensTest {
public List<List<String>> solveNQueens(int n){
//按坐标位置存放所有解决方案 存放结果集
List<List<pair>> solutions=new ArrayList<>();
//存放一种解决方案中的所有皇后的位置 存放结果
List<pair> solution=new ArrayList<>();
nQueensBackTrack(solutions,solution,0,n);
//把坐标位置转成String
return transResult(solutions,n);
}
private List<List<String>> transResult(List<List<pair>> solutions, int n) {
List<String> tmp=new ArrayList<>();
//把每一种解决方案都转换为String 形式 ,最终结果
List<List<String>> ret=new ArrayList<>();
for(List<pair> solution : solutions){
//n*n char : 每行有n个元素 把皇后的位置修改为 Q
List<StringBuilder> solutionString=new ArrayList<>();
//针对每一行
for(int i=0;i<n;i++){
StringBuilder sb=new StringBuilder();
//针对当前行的每个位置
for(int j=0;j<n;j++){
sb.append('.');
}
//每行的结果集进行存放
solutionString.add(sb);
}
//以所有行都已经赋值了 . 但是皇后还没有附上
for(pair i:solution){
solutionString.get(i.x).setCharAt(i.y,'Q');
}
//直接拷贝
List<String> curRet=new ArrayList<>();
//将所有的 集合中的每行都存放起来
for(StringBuilder sb : solutionString){
curRet.add(sb.toString());
}
//最终存储到结果集
ret.add(curRet);
}
return ret;
}
private boolean nQueensBackTrack(List<List<pair>> solutions, List<pair> solution, int curRow, int n) {
//如果可以话 说明当前情况已经符合条件了将符合条件进行拷贝存储
if(curRow==n){
List<pair> newS=new ArrayList<>();
//进行拷贝
for(pair p:solution){
newS.add(p);
}
//放入结果集中
solutions.add(newS);
}
//尝试当前行的每一个位置是否可以放置一个皇后
for(int col=0;col<n;col++){
//判断是否在 米 字 方向上 如果不在就向下进行
if(isValid(solution,curRow,col)){
//如果可以, 在保存当前位置 ,继续确定下一行皇后的位置
//直接调用构造函数 ,内部构造 pair 或者调用make_pair
solution.add(new pair(curRow,col));
nQueensBackTrack(solutions,solution,curRow+1,n);
//回溯 删除当前位置,尝试当前行的其他位置
solution.remove(solution.size()-1);
}
}
}
//solution : 一个解决方案 ,从第一行开始到当前行的上一行每一行已经放置皇后的点
private boolean isValid(List<pair> solution, int row, int col) { //判断当前行尝试的皇后位置是否和前面几行的皇后位置有冲突
// i.second == col 第i个皇后与当前这个点在同一列
// i.first+i.second =row + col 第i个皇后与当前点在反斜杠 横坐标 + 纵坐标值相同
// i.first+i.second =row - col 第i个皇后与当前点在正斜杠 横坐标 - 纵坐标值相同
for(pair i: solution){
if(i.y==col||i.x+i.y==row+col||i.x-i.y==row-col){
return false;
}
}
return true;
}
}
2、广度优先搜索
2.1、迷宫问题
使用方法:广度优先搜索
题目源于力扣:力扣
迷宫问题:假设有一个迷宫,里面有障碍物,迷宫用二维矩阵表示,标记为0的地方表示可以通过,标记为1的地方表示障碍物,不能通过。现在给一个迷宫出口,让你判断是否可以从入口进来之后,走出迷宫,每次可以向任意方向走。(图解)
代码解析(附加注释):
class node{
public int x; //节点 横坐标
public int y; //节点 纵坐标
public node(int x,int y){
this.x=x;
this.y=y;
}
}
public class MazeTest {
public static void main(String[] args) {
int sx,sy,ex,ey;
int m,n;
System.out.println("请输入迷宫的大小:行、列");
Scanner scanner=new Scanner(System.in);
m= scanner.nextInt();
n= scanner.nextInt();
//创建一个数组
int[][] graph=new int[m][n];
System.out.println("请输入迷宫的元素");
//给迷宫所有位置附加值
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
graph[i][j]=scanner.nextInt();
}
}
//入口 和 出口
while(true){
System.out.println("请输入迷宫入口和出口");
// 入口的下标
sx=scanner.nextInt();
sy=scanner.nextInt();
// 出口的下标
ex=scanner.nextInt();
ey=scanner.nextInt();
System.out.println("是否可以走出迷宫");
//进行广度优先搜索
BFS(graph,sx,sy,ex,ey);
System.out.println();
}
}
private static boolean BFS(int[][] graph, int startx, int starty, int destx, int desty) {
//迷宫的大小
int m=graph.length;
int n=graph[0].length;
//存储迷宫中的位置
Queue<node> q=new LinkedList<>();
//标记迷宫中的位置是否被走过
int[][] book=new int[m][n];
//将当前第一个节点 放入 队列中
q.offer(new node(startx,starty));
//标记已经走过
book[startx][starty]=1;
//四个行走的方向 左右下上
int[][] next={{-1,0},{1,0},{0,-1},{0,1}};
//标记是否可以出去
boolean flag=false; //此处用来判定 是否找到最终位置
while(!q.isEmpty()){
//当前位置带出所有新的位置,可以向上下左右走
for(int i=0;i<4;i++){
//计算新的位置
int nx=q.peek().x+next[i][0];
int ny=q.peek().y+next[i][1];
//新的位置越界,继续下一个
if(nx>=m||nx<0||ny>=n||ny<0){
continue;
}
//如果新的位置无障碍并且之前也没有走过,保存新的位置
if(graph[nx][ny]==0&&book[nx][ny]==0){
q.offer(new node(nx,ny));
//标记已被走过
book[nx][ny]=1;
}
//如果新的位置为目标位置,则结果查找
if(nx==destx&&ny==desty){
flag=true;
break;
}
}
//判定为了结束外界循环
if(flag){
break;
}
//否则,用新的位置继续向后走
q.poll();
}
return flag;
}
}
2.2、腐烂的橘子
使用方法:广度优先搜索
题目源于力扣:力扣
本题可以先找到所有的腐烂橘子,入队 ,用第一批带出新一批腐烂的橘子,每一批橘子都会在一分钟之内腐烂,每一次腐烂都是一个广度优先搜索,step表示时间 ,也就是每一次广度优先搜索针对题意都会花费1分钟 step++
最后广度优先遍历结束后,再去遍历原来的给定m*n数组,还有没有新鲜橘子(判定有没有数组值是1)
注:此处就不在进行图像演示了,打开力扣题后图解已经很清晰了。
代码解析(附加注释):
public int orangesRotting(int[][] grid){
//Entry存放位置
Queue<pair> q=new LinkedList<>();
int row=grid.length;
int col=grid[0].length;
//已经腐烂的位置入队
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
//为了一会的广度优先遍历 需要先收集起数组中的腐烂橘子
if(grid[i][j]==2){
//已入队列
q.offer(new pair(i,j));
}
}
}
//可以蔓延的方向 向四个方向进行扩张
int[][] nextp={{0,1},{1,0},{0,-1},{-1,0}};
int step=0;
while(!q.isEmpty()){
int n=q.size();
int flag=0;
//用当前这一批已经腐烂的橘子带出下一批要腐烂的橘子
//故要遍历队列中的所有位置 先处理四个位置
while(n--!=0){
//用来接收当前位置坐标
pair curpos=q.peek();
q.poll();
//当前位置向四个方向蔓延
for(int i=0;i<4;i++){
//向四个方向进行扩展
int nx= curpos.x+nextp[i][0];
int ny= curpos.y+nextp[i][1];
//如果位置越界或者是空格 ,或者已经是腐烂橘子的位置,则跳过
if(nx>=row||nx<0||ny>=col|ny<0||grid[nx][ny]!=1){
continue;
}
//标记有新的被腐烂的橘子
flag=1;
//如果前面的判定跳过了 说明当前橘子可以被腐烂
grid[nx][ny]=2;
//已经成为腐烂的橘子就放入队列
q.offer(new pair(nx,ny));
}
}
//如果有新的腐烂
if(flag==1){
step++;
}
}
//判断 是否 还有无法腐烂的
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(grid[i][j]==1){
return -1;
}
}
}
return step;
}
2.3、单词接龙
使用方法:广度优先搜索
题目源于力扣:力扣
(1) 通过广度优先搜索,首先将beginWord带出转换一个字母之后所有可能的结果
(2)每一步都要把队列中上一步添加的所有单词转换一遍,最短的转化肯定在这些单词中,所有这些单词的转换只能算一次转换,其实每次队列每次也只针对一个单词,单词的每个字符都会被26个字母换一遍直到对应词典找到了
(3)把转换成功的新词入队,进行下一步的转换
(4)最后整个转换的长度和广度优先搜索次数一致
代码解析(附加注释):
public int ladderLength(String beginword , String endWord, List<String> wordList){
//hash表的查询效率最高
Set<String> wordDict=new HashSet<>();
for(String wd: wordList){
wordDict.add(wd);
}
//标记单词是否已经访问过, 访问过的不再访问
Set<String> visited=new HashSet<>();
visited.add(beginword);
//初始化队列
Queue<String> q=new LinkedList<>();
//将首个单词 放入队列进行广度优先
q.offer(beginword);
int res=1;
while(!q.isEmpty()){
int nextSize=q.size();
//每一步都要把队列中上一步添加的所有单词转换一遍
//最短的转换肯定在这些单词当中,所有这些词的转换只能算一次转换
//因为都是上一步转换出来的
while(nextSize--!=0){
//顶部位置
String curword=q.peek();
q.poll();
//尝试转换当前单词的每一个位置
for(int i=0;i<curword.length();i++){
StringBuilder newWord=new StringBuilder(curword);
//每一个位置用26字母分别替换
for(char ch='a';ch<='z';ch++){
//替换第 i 位置的单词
newWord.setCharAt(i,ch);
//如果列表中没有此单词或者已经访问过,(它的转换已经遍历过,无需再次遍历)
String changeWord=newWord.toString();
//修改过后的单词是否包含
if(!wordDict.contains(changeWord)||visited.contains(changeWord)){
continue;
}
//转换成功,则再上一步转换的基础上+1 处理最后的情况
if(changeWord.equals(endWord)){
return res+1;
}
//到了这个位置 就说明 修改过后的单词是被包含在词典中
//还没有转换成功,则新的单词入队
visited.add(changeWord);
//存入队列
q.offer(changeWord);
}
}
}
//处理下一个已经处理好的单词
res++;
}
//转换不成功 返回 0
return 0;
}
2.4、打开转盘锁
使用方法:广度优先搜索
题目源于力扣:力扣
题目描述:
(1)锁的初始数字为 '0000'
,一个代表四个拨轮的数字的字符串。
(2)列表 deadends
包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
(3)字符串 target
代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1
。
代码解析(附加注释):
public int openLock(String[] deadends,String target){
Set<String> deadendsSet=new HashSet<>();
//将死亡数字进行存储,去重 后面进行判断拨动的转轮数字是不是被包含在这些死亡数字中
for(String str :deadends){
deadendsSet.add(str);
}
//如果“0000” 在死亡数字中,则永远开不了 直接结束
if(deadendsSet.contains("0000")){
return -1;
}
//初始化队列
Queue<String> que=new LinkedList<>();
//添加当前初识值
que.offer("0000");
//创建一个标记位,已经播过的数字不在进行重复出现
Set<String> book=new HashSet<>();
book.add("0000");
//需要拨动几次
int step=0;
while(!que.isEmpty()){
int n= que.size();
//从上一步转换之后的字符串都需要进行验证和转换
//并且只算做一次转换, 类似于层序遍历,转换的步数和层相同
//同一层的袁术都是经过一步转换得到的
for(int i=0;i<n;i++){
String curStr=que.peek();
//抛出是为了让后面的值一直都是第一个,也是最后一个
que.poll();
if(curStr.equals(target)){
return step;
}
//四位密码锁,每个位置每次都可以转一次
for(int j=0;j<4;j++){
//为啥是两个 其实两个都是同一个滑轮 表示向上转动和向下转动两种情况
char newc1=curStr.charAt(j);
char newc2=curStr.charAt(j);
//当前位置向上
if(newc1=='9'){
newc1='0';
}else{
newc1++;
}
//当前位置向下
if(newc2=='0'){
newc2='9';
}else{
newc2++;
}
//要改变字符串 就需要用到 StringBuild 类型
StringBuilder newStr1=new StringBuilder(curStr);
StringBuilder newStr2=new StringBuilder(curStr);
//借助StringBuild 的方法进行改变 字符串的值
newStr1.setCharAt(j,newc1);
newStr2.setCharAt(j,newc2);
//此处是进行判断 修改后的花轮值 是否 是死亡数字 或者是 被标记过的
//被标记过的也就是已滑过的数字
//除以上两种情况 其他情况皆可以存入队列进行下次修改 并且 进行标记
if(!deadendsSet.contains(newStr1.toString())&&!book.contains(newStr1.toString())){
que.offer(newStr1.toString());
book.add(newStr1.toString());
}
if(!deadendsSet.contains(newStr2.toString())&&!book.contains(newStr2.toString())){
que.offer(newStr2.toString());
book.add(newStr2.toString());
}
}
}
step++;
}
return -1;
}