四、回溯算法
可能不是最优解法,目的是为了理解回溯算法
1、总结
常见的回溯算法通用模板:
1、确定结束条件,什么时候回溯结束:
- 通常若满足要求,会把路径(List<>)、记数(int)进行添加,然后return
- 若不满足要求,直接return
2、选择列表:代表着每一次可以都选择的选项(不管可不可行),所以具有通用性适合于每一次:
- 可能是一个循环for(int i ~9)进行选择
- 也可能是某几种情况进行选择,例如有效括号,是对’(‘和’)'进行选择
3、判断可行性:可行即放,不可行就continue
- 数组表达-这个数有没有被用过:visited[]?
- 函数表达-数放在这里可不可行:N皇后中某个点的放置是否可行
4、迭代下一次:这个是个关键
- 若任意的排列组合都行(全排列),直接backTrack(形参)
- 若排列组合对全局可能性存在某种要求(若N皇后),backTrack是有boolean的返回值的,
- 此时这一步为:if(backTrack(形参) return true;)
- 在循环列表结束之后,要return false,相当于在这个点,所有可能的情况都装不进去(回溯到上一点,上一点重新选择)
5、去除这个可能性
public void(boolean) backTrack(选择列表){ // 1、结束条件 if(结束?){ if(满足要求?) { 添加路径or记数加一; } return (boolean); } // 2、选择列表 for(int i = 0;i < grap.length;i++){ // 3、判断当前位置可不可以是这个数 if(isValid(当前i))或visite[]? // 添加值,该点是用这个数 grap[curRow][i] = 1; // 4、下一个点的迭代 -------backTrack(形参); -------if(backTrack(形参)) return true; // 5、去除该可能 grap[curRow][i] = 0; } (return false;) }
2、例题分类
1、记数-循环列表-可行性函数-组合有条件
2.1 N皇后2
题目描述
n 皇后问题 研究的是如何将 n 个皇后放置在 n × n 的棋盘上,并且使皇后彼此之间不能相互攻击。 给你一个整数 n ,返回 n 皇后问题 不同的解决方案的数量。 示例 1: 输入:n = 4 输出:2 解释:如上图所示,4 皇后问题存在两个不同的解法。 示例 2: 输入:n = 1 输出:1 提示: 1 <= n <= 9
代码
class Solution { // 运行函数 public int totalNQueens(int n) { // 创建n*n的矩阵,方便某个点放置位置是否可行的判断 int[][] data = new int[n][n]; // 调用回溯算法,从第0行开始 backTrack(data,0); // 返回“记数”值 return res; } int res = 0; public boolean backTrack(int[][] grap,int curRow){ // 1、是否结束的判断--结束与条件满足是同一个判断语句 // 因为能到grap.length行说明矩阵已经放好了n个皇后的位置 if(curRow == grap.length){ res++; // 进行下一个可能性,所以是false return false; } // 2、循环列表---这一行的所有列都可以选择 for(int i = 0;i < grap.length;i++){ // 3、判断当前位置可不可以是这个数,是一个函数 if(!isValid(grap,curRow,i)) continue; // 放置 grap[curRow][i] = 1; // 4、有条件的迭代 if(backTrack(grap,curRow+1)) return true; // 5、去除此可能性 grap[curRow][i] = 0; } // 此行所有位置都不能放,让上一行进行重新选择 return false; } // 判断位置可不可放----皇后的攻击范围 private boolean isValid(int[][] grap,int row,int cloum){ int n = grap.length; for(int i = 0; i < row; i++){ if(grap[i][cloum]!=0) return false; } for(int i = row - 1,j = cloum - 1; i >= 0 && j >=0;i--,j--){ if(grap[i][j]!=0) return false; } for(int i = row - 1,j = cloum + 1; i >= 0 && j < n;i--,j++){ if(grap[i][j]!=0) return false; } return true; } }
2、路径-循环列表-可行性Visite-组合有条件
2.1 格雷编码
题目描述
n 位格雷码序列 是一个由 2n 个整数组成的序列,其中: 每个整数都在范围 [0, 2n - 1] 内(含 0 和 2n - 1) 第一个整数是 0 一个整数在序列中出现 不超过一次 每对 相邻 整数的二进制表示 恰好一位不同 ,且 第一个 和 最后一个 整数的二进制表示 恰好一位不同 给你一个整数 n ,返回任一有效的 n 位格雷码序列 。 示例 1: 输入:n = 2 输出:[0,1,3,2] 解释: [0,1,3,2] 的二进制表示是 [00,01,11,10] 。 - 00 和 01 有一位不同 - 01 和 11 有一位不同 - 11 和 10 有一位不同 - 10 和 00 有一位不同 [0,2,3,1] 也是一个有效的格雷码序列,其二进制表示是 [00,10,11,01] 。 - 00 和 10 有一位不同 - 10 和 11 有一位不同 - 11 和 01 有一位不同 - 01 和 00 有一位不同 示例 2: 输入:n = 1 输出:[0,1] 提示: 1 <= n <= 16 ``
代码
class Solution { HashSet<Integer> visit = new HashSet<>(); LinkedList<Integer> res = new LinkedList<>(); public List<Integer> grayCode(int n) { // 初始化,把第一个条件加入 res.addLast(0); visit.add(0); backTrack(n,0); return res; } private boolean backTrack(int n,int val){ // 1、结束条件,res里面已经装了2^n个值 if(res.size() == (int)Math.pow(2, n)){ // 2、组合是有一定条件的,所以返回时true和false // 最后一位与第一位也是相差1 for(int i = 0;i < n;i++){ if((val^(1<<i)) == 0){ return true; } } return false; } // 3、选择列表:每一个数之后的另一个数,只有一位不同,所以对每个位置进行按位异或,取不同的操作 for(int i = 0; i < n+1; i++){ int cur = val^(1<<i); // 4、不能放置的条件:这个数已经被使用了 if(visit.contains(cur)) continue; // 5、加入res并标记访问 res.addLast(cur); visit.add(cur); // 6、迭代下一个 if(backTrack(n,cur)) return true; res.removeLast(); visit.remove(val); } return false; } }
3、路径-循环列表可行函数-组合无条件
3.1 复原IP地址
题目描述
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。 例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。 给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。 示例 1: 输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"] 示例 2: 输入:s = "0000" 输出:["0.0.0.0"] 提示: 1 <= s.length <= 20 s 仅由数字组成
代码
class Solution { List<String> res = new LinkedList<>(); public List<String> restoreIpAddresses(String s) { // K是创建的结束条件,代表着做了几次划分(需要4次划分) backTrack(s,0,new StringBuffer(),4); return res; } private void backTrack(String s,int start,StringBuffer sb,int k){ // 1、结束条件,就是做了是次划分 if(k == 0){ // 2、符合条件的就是把数据全利用完了的sb if(start == s.length()){ // 3、因为每次都会加一个‘.’,所以需要把最后的.给删除了 sb.deleteCharAt(sb.length()-1); res.add(sb.toString()); } return; } // 4、循环列表是s中能取得数,因为有顺序,所以给予start依次顺下去 for(int i = start; i < start + 3 && i < s.length(); i++){ // 5、取出当前值的string与int,是因为得取出0打头得非0项 String next = s.substring(start,i+1); int cur = Integer.parseInt(next); // 6、取得数字是否合理,break得原因: // 前面的不合适后续一定不合适:在001中取 第二次00不行 后一次001一定不合适 if(cur < 0 || cur > 255) break; if(next.charAt(0) == '0'&&next.length() > 1) break; // 7、加入路径中,进行迭代,注意退路的时候得去除点位是有变化的 sb.append(next+"."); backTrack(s,i + 1,sb,k - 1); sb.delete(start + (4 - k),sb.length()); } } }
3.2 分割字符串
题目描述
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 示例 1: 输入:s = "aab" 输出:[["a","a","b"],["aa","b"]] 示例 2: 输入:s = "a" 输出:[["a"]] 提示: 1 <= s.length <= 16 s 仅由小写英文字母组成
代码
class Solution { List<List<String>> res; public List<List<String>> partition(String s) { res = new LinkedList<>(); backTrack(s,0,new LinkedList<>()); return res; } private void backTrack(String s,int start,LinkedList<String> list){ // 1、结束条件,已没有可选字母 if(start == s.length()){ res.add(new LinkedList<>(list)); return ; } // 2、可选列表,就是String s,不能重复且连续 // (1)从start开始,而不是0; // (2)连续取子集 for(int i = start;i<s.length();i++){ String cur = s.substring(start,i+1); // 3、可行性函数:判断这个子串是不是回文字符串 if(!isValid(cur)) continue; // 4、加入路径 list.addLast(cur); // 5、下一次迭代,从下一个i开始 backTrack(s,i + 1,list); // 6、返回加入的值 list.removeLast(); } } // 判断子集是否是回文字符串 private boolean isValid(String s){ int i = 0;int j = s.length() - 1; while(i < j){ if(s.charAt(i)!=s.charAt(j)) return false; i++; j--; } return true; } }