文章目录
- 回溯算法
- 1 基本内容
- 2 经典力扣题
- 2.1 全排列问题
- 2.2 子集问题
- 2.3 组合问题
- [77. 组合](https://leetcode-cn.com/problems/combinations/)
- [39. 组合总和](https://leetcode-cn.com/problems/combination-sum/)
- [40. 组合总和 II](https://leetcode-cn.com/problems/combination-sum-ii/)
- [216. 组合总和 III](https://leetcode-cn.com/problems/combination-sum-iii/)
- [17. 电话号码的字母组合](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/)
- 2.4 分割问题
- 2.5 棋盘问题
回溯算法
1 基本内容
1.1 回溯算法的框架
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
result = []
public List<Integer> backtrack(路径, 选择列表){
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
}
1.2 回溯核心思想
1、每一次的backtrack,是在回溯深度,如二叉树的深度遍历,所以我们要知道深度在这道题中的意义
- 比如对于N皇后问题,深度就是二维数组的行,所以每一次是backtrack(row+1)
- 比如分割字符串问题,深度就是字符串的长度,所以每一次是backtrack(start+1)
- 比如全排列问题,深度就是字符串的长度,所以每一次是backtrack(i+1)
back(s,start+i,path); trackBack(nums,target-nums[i],i,path);
2、在每一个backTrace中的for循环代表什么意思,就是在当前状态下你的所有选择
- 比如恢复IP中,你的选择是 用1还是2 还是 3作为这一段的长度
- 对于N皇后问题,你的选择就是这一层的那一个列的位置 col作为皇后的位置
for (int i = start; i < num.length ; i++) {//长度的选择 ... } for (int i = 1; i <=3 ; i++) {//切分的选择 ... }
3、剪枝
不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
把没有必要的枝叶剪去的操作就是剪枝,在代码中一般通过
break
或者contine
和return
(表示递归终止)实现。if(i>0 && nums[i]==nums[i-1] && !isVisit[i-1]){ continue; } if(list.contains(num[i])){ continue; } if(i==3 && temp.compareTo("255")>0){ return ; }
1.3 回溯法解决的问题
回溯法,一般可以解决如下几种问题:
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 组合问题:N个数里面按一定规则找出k个数的集合
- 分割问题:一个字符串按一定规则有几种切割方式
- 棋盘问题:N皇后,解数独
注意组合和排列的区别:
组合是不强调元素顺序的,排列是强调元素顺序。
{1, 2} 和 {2, 1} 在组合上,就是同一个,而要是排列的话,{1, 2} 和 {2, 1} 就是两个排列
1.4 题目列表
全排列
子集
组合
分割
棋盘
1.4 常见问题分析
明明设置了起始位置,但是结果中还是加入了起始位置前的元素——回溯起始位置问题的是i
但是填成了start
输入:nums = [1,2,3]
输出:[[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3],[3,2]]//[3,2]这种情况
输出的集合中的元素,远超过限制的元素,忘记在回溯的时候撤销选择了
path.removeLast()
输出的组合比答案少——是否剪枝的时候没有排序?
if(target-nums[i]<0){
break;
}
输出下面这种情况,是因为char[]
没有初始化
[[".Q\u0000\u0000","\u0000\u0000.Q","Q.\u0000\u0000",
"\u0000\u0000Q\u0000"],["..Q\u0000","Q\u0000..",
"..\u0000Q","\u0000Q.\u0000"]]
String知识
//String.compareTo()方法
//如果第一个字符和参数的第一个字符不等,结束比较,返回第一个字符的ASCII码差值。
//如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至不等为止,返回该字符的ASCII码差值。 //如果两个字符串不一样长,可对应字符又完全一样,则返回两个字符串的长度差值。
@Test
public void test(){
//输出 5,第一个字符相同,返回第二个字符差值的ASCII码值 5
System.out.println("15".compareTo("10"));
//输出 1,第一个字符不同,返回第一个字符差值的ASCII码值 1
System.out.println("35".compareTo("255"));
// 输出 4,第一个字符不同,返回第一个字符差值的ASCII码值 4
System.out.println("5".compareTo("10"));
// 输出 -1,第一个字符不同,返回第一个字符差值的ASCII码值 -1
System.out.println("15".compareTo("25"));
}
//将二维字符数组转化成String,用到的String.copyValueOf(char[])的
public List<String> char2List(char[][] path){
LinkedList<String> list = new LinkedList<>();
for (char[] ch : path) {
list.add(String.copyValueOf(ch));
}
return list;
}
2 经典力扣题
2.1 全排列问题
2.1.1 没有重复元素的全排列
剑指 Offer II 083. 没有重复元素集合的全排列==46. 全排列
给定一个不含重复数字的整数数组
nums
,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
List<List<Integer>> res;
public List<List<Integer>> permute(int[] nums) {
// 1.创建存放结果集的List
res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
trackBack(nums,track);
return res;
}
public void trackBack(int[] num, LinkedList<Integer> list){
// 1.结束条件,全排列,全部元素都在里面
if(list.size()==num.length){
//为什么要 new LinkedList<>(list),因为list是一个引用,不创建新的话,还是会
res.add(new LinkedList<>(list));
return;
}
// 2.确定遍历的集合时全部元素
for (int i = 0; i < num.length; i++) {
// 3.首先需要将已经在列表中的元素排除
if(list.contains(num[i])){
continue;
}
// 4.做出选择
list.add(num[i]);
// 5.继续递归下去
trackBack(num,list);
// 6.撤销选择
list.removeLast();
}
}
2.1.2 含重复元素的递归全排列
给定一个可包含重复数字的序列
nums
,按任意顺序 返回所有不重复的全排列。输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
思路:这里所给元素是重复的,所以如何去处理重复元素
① 进行排序,那么相同的元素就会排在一起
if(i>0 && nums[i]==nums[i-1] && !isVisit[i-1]){ continue; }
- 保证相同的元素只会在根节点回溯时只回溯一次
nums[i]==nums[i-1]
,如图①- 为了避免[1,1,2]这种情况被剪枝,再加一个限制条件
!isVisit[i-1]
,既前一个元素不在遍历的路径上② 对使用过的元素进行标记
List<List<Integer>> ans;
boolean[] isVisit;
public List<List<Integer>> permuteUnique(int[] nums) {
// 1.前期处理
if(nums==null || nums.length==0){
return null;
}
ans = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
// 2.排序
Arrays.sort(nums);
// 3.回溯穷举
trackBack2(nums,path);
return ans;
}
public void trackBack(int[] nums,LinkedList<Integer> path){
// 1.结束条件,找到一组合格的解
if(path.size()==nums.length){
ans.add(new LinkedList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 1.首先需要将已经在列表中的元素排除
if(isVisit[i]){
continue;
}
// 2.重复数字只会在第一次出现时填入一次,[1 1 2]这种情况还必须加上前一个元素没有被选上的条件
if(i>0 && nums[i]==nums[i-1] && !isVisit[i-1]){
continue;
}
// 3.做出选择
path.add(nums[i]);
isVisit[i] = true;
// 4.回溯
trackBack(nums,path);
// 5.撤销选择
path.removeLast();
isVisit[i] = false;
}
}
2.2 子集问题
2.2.1 不含重复元素的子集
给你一个整数数组
nums
,数组中的元素 互不相同 。返回该数组所有可能的子集输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
List<List<Integer>> res;
public List<List<Integer>> subsets(int[] nums) {
res = new LinkedList<>();
if(nums==null || nums.length==0){
res.add(new LinkedList<>());
return res;
}
LinkedList<Integer> set = new LinkedList<>();
trackBack(nums,0,set);
return res;
}
public void trackBack(int[] num, int start, LinkedList<Integer> set){
// 1.因为是子集,所以结束条件不是长度,而是直接入结果集
res.add(new LinkedList<>(set));
// 2.起始位置保证了不会有重复元素
for (int i = start; i < num.length ; i++) {
set.add(num[i]);
// 3.注意!!!这里是 i+1 而不是 start+1,找了半天的错误找不到
trackBack(num,i+1,set);
set.removeLast();
}
}
2.2.2 含重复元素的子集个数
给你一个整数数组
nums
,其中可能包含重复元素,请你返回该数组所有可能的子集输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
思路:
① 排序,使相同的元素在一堆
② 同全排列的思考,标记,当前一个相同的元素没选中,并且当前元素等于前一个元素,那么剪枝就可以去重了
List<List<Integer>> ans;
boolean[] isVisit;
public List<List<Integer>> subsetsWithDup(int[] nums) {
ans = new LinkedList<>();
isVisit = new boolean[nums.length];
if(nums==null || nums.length==0){
ans.add(new LinkedList<>());
return ans;
}
Arrays.sort(nums);
LinkedList<Integer> path = new LinkedList<>();
backTrack(nums,0,path);
return ans;
}
public void backTrack(int[] nums,int start,LinkedList<Integer> path){
ans.add(new LinkedList<>(path));
for (int i = start; i < nums.length; i++) {
// 1.思路同全排列,剪枝去重
if(i>0 && nums[i]==nums[i-1] && !isVisit[i-1]){
continue;
}
path.add(nums[i]);
isVisit[i] = true;
backTrack(nums,i+1,path);
isVisit[i] = false;
path.removeLast();
}
}
2.2.3 递增子序列
给你一个整数数组
nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。**注意:**子序列是不能对它进行排序的会破坏原来的相对位置
输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
思路:
这里使用到
HashSet
就非常好的印证了,在每一个for循环中,是表示同一层的元素,而回溯的代码中显示的则是深度,所以在此时使用的HashSet
可以记录这一层已经遍历了哪些元素比如[1,3,3,7]——这一层 HashSet [3]
,表示已经遍历了3
,就不会出现[1,3,7],[1,3,7]
这种重复的现象
public List<List<Integer>> findSubsequences(int[] nums) {
res = new LinkedList<>();
if(nums==null || nums.length==0){
return res;
}
LinkedList<Integer> path = new LinkedList<>();
back(nums,0,path);
return res;
}
public void back(int[] nums,int start,LinkedList<Integer> path){
if(path.size()>1){
res.add(new LinkedList<>(path));
}
// 利用哈希表去重,同一层里面不能有两个相同的元素,相同既代表已经遍历过了
HashSet<Integer> set = new HashSet<>();
for (int i = start; i < nums.length; i++) {
if(set.contains(nums[i])){
continue;
}
set.add(nums[i]);
if(path.size()==0 || nums[i]>=path.getLast()){
path.add(nums[i]);
back(nums,i+1,path);
path.removeLast();
}
}
}
2.3 组合问题
77. 组合
给定两个整数
n
和k
,返回范围[1, n]
中所有可能的k
个数的组合输入:n = 4, k = 2 输出:[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4]]
思路:
① 为了防止
[1,2][2,1]
这种重复,也是通过起始位置 start进行限制② 回溯的终止条件是长度==输入要求的长度
List<List<Integer>> res;
public List<List<Integer>> combine(int n, int k) {
res = new LinkedList<>();
if(k<=0 || n<k){
return res;
}
int[] nums = new int[n];
for (int i = 1; i <= n; i++) {
nums[i-1] = i;
}
LinkedList<Integer> path = new LinkedList<>();
backTrack(nums,k,0,path);
return res;
}
public void backTrack(int[] num,int k,int start,LinkedList<Integer> path){
if(path.size()==k){
res.add(new LinkedList<>(path));
return;
}
for (int i = start; i <num.length ; i++) {
path.add(num[i]);
backTrack(num,k,i+1,path);
path.removeLast();
}
}
39. 组合总和
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates
中的数字可以无限制重复被选取。输入: candidates = [2,3,6,7], target = 7 输出: [[7],[2,2,3]]
方法一:回溯,不断的在元素中循环进行回溯,每一次的起始位置都不变
这里尤其需要主要排序+target-nums[i]<0
剪枝部分的优化,不排序剪枝会少结果,剪枝效率高些
List<List<Integer>> ans;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates==null || candidates.length==0){
return null;
}
ans = new LinkedList<>();
//排序,因为后面用到了 sum + candidates[i]>target就结束本轮for循环的遍历。如果不排序的话回导致结果少
//而这样会做到剪枝的效果,所以还是这样
Arrays.sort(candidates);
LinkedList<Integer> path = new LinkedList<>();
trackBack(candidates,target,0,path);
return ans;
}
public void trackBack(int[] nums,int target,int start,LinkedList<Integer> path){
//结束回溯的条件
if(target==0){
ans.add(new LinkedList<>(path));
return;
}
for (int i = start; i < nums.length; i++) {
//剪枝
if(target-nums[i]<0) {
break;
}
//做出选择
path.add(nums[i]);
//回溯
trackBack(nums,target-nums[i],i,path);
//撤销选择
path.removeLast();
}
}
方法二:递归的形式,如下图,有点类似于上台阶,两种情况加起来,选或者不选
List<List<Integer>> ans;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates==null || candidates.length==0){
return null;
}
ans = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
Arrays.sort(candidates);
trackBack(candidates,target,0,path);
return ans;
}
public void dfs(int[] nums,int target,int start,LinkedList<Integer> path){
if(start>=nums.length){
return;
}
if(target==0){
ans.add(new LinkedList<>(path));
return;
}
//不将当前的元素加入
dfs(nums,target,start+1,path);
//正常加入
if(target-nums[start]<0) {
return;
}
path.add(nums[start]);
//因为元素是不限数量的,下次还是从start位置开始
dfs(nums,target-nums[start],start,path);
path.removeLast();
}
40. 组合总和 II
给定一个数组
candidates
和一个目标数target
,找出candidates
中所有可以使数字和为target
的组合。candidates
中的每个数字在每个组合中只能使用一次。输入: candidates = [10,1,2,7,6,1,5], target = 8, 输出:[[1,1,6],[1,2,5],[1,7],[2,6]]
思路:
① 输入有重复的元素,需要去重,使用
boolean[]
数组进行标记② 选择过的元素不能再选择,需要start去控制起始遍历位置
List<List<Integer>> res;
boolean[] isVisited;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
res = new LinkedList<>();
if(candidates==null || candidates.length==0){
return null;
}
isVisited = new boolean[candidates.length];
LinkedList<Integer> path = new LinkedList<>();
Arrays.sort(candidates);
track(candidates,target,0,path);
return res;
}
public void track(int[] nums,int target,int start,LinkedList<Integer> path){
if(target==0){
res.add(new LinkedList<>(path));
return ;
}
for (int i = start; i < nums.length; i++) {
if(target-nums[i]<0){
break;
}
if(i>0 && nums[i]==nums[i-1]&& !isVisited[i-1]){
continue;
}
path.add(nums[i]);
isVisited[i] = true;
track(nums,target-nums[i],i+1,path);
path.removeLast();
isVisited[i] = false;
}
}
216. 组合总和 III
找出所有相加之和为 n 的 k个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
public List<List<Integer>> combinationSum3(int k, int n) {
res = new LinkedList<>();
if(k==0 || n==0){
return res;
}
LinkedList<Integer> path = new LinkedList<>();
int[] nums = {1,2,3,4,5,6,7,8,9};
backTrack(nums,0,k,n,path);
return res;
}
public void backTrack(int[] nums,int start,int k,int target,LinkedList<Integer> path){
// 回溯结束的条件,和与长度
if(path.size()==k){
if(target==0){
res.add(new LinkedList<>(path));
}
return;
}
for (int i = start; i <nums.length ; i++) {
if(target-nums[i]<0){
break;
}
path.add(nums[i]);
backTrack(nums,i+1,k,target-nums[i],path);
path.removeLast();
}
}
17. 电话号码的字母组合
给定一个仅包含数字
2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
List<String> ans;
public List<String> letterCombinations(String digits) {
ans = new LinkedList<>();
if(digits==null || digits.length()==0){
return ans;
}
HashMap<Character,String> map = new HashMap<>();
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
StringBuffer path = new StringBuffer();
traceBack(map,digits,0,path);
return ans;
}
public void traceBack(Map<Character,String> map, String digits,int index, StringBuffer path){
// 1.结束的条件
if(index== digits.length()){
ans.add(path.toString());
return ;
}
char digit = digits.charAt(index);
String s = map.get(digit);
// 1.遍历所有数字
for (int i = 0; i < s.length(); i++) {
// 选择当前数字的一位
path.append(s.charAt(i));
// 再去选择下一个数字的元素
traceBack(map,digits,index+1,path);
path.deleteCharAt(index);
}
}
2.4 分割问题
131. 分割回文串
给你一个字符串
s
,请你将s
分割成一些子串,使每个子串都是 回文串 。返回s
所有可能的分割方案。
List<List<String>> res;
public List<List<String>> partition(String s) {
res = new LinkedList<>();
LinkedList<String> path = new LinkedList<>();
backTrack(s,0,path);
return res;
}
public void backTrack(String s,int index,LinkedList<String> path){
// 1.回溯结束的条件,这一部分需要注意,我想不到是 >= 的关系
if(index>=s.length()){
res.add(new LinkedList<>(path));
return ;
}
// 遍历,判断什么时候成回文
for (int i = index; i < s.length(); i++) {
// 2.当前切分的区间是回文时才进入
if(isCyc(s.substring(index,i+1))){
// 注意subString是左闭右开的区间
path.add(s.substring(index,i+1));
backTrack(s,i+1,path);
path.removeLast();
}
}
}
public boolean isCyc(String s){
int end = s.length()-1;
int begin = 0;
while(begin<end){
if(s.charAt(begin)!=s.charAt(end)){
return false;
}
begin++;
end--;
}
return true;
}
93. 复原 IP 地址
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"]
思路:
① 一个字符有三种切法:2 25 255,所以回溯中就按1-3的循环来
② 回溯结束的条件,字段长为4,并且刚刚用完所有字符
③ 有关剪枝
- 第一种情况是 前导为0的情况
- 第二种是长度为3的切片>255的范围
- 超出长度
List<String> ans;
final int COUNT = 4;
public List<String> restoreIpAddresses(String s) {
ans = new LinkedList<>();
LinkedList<String> path = new LinkedList<>();
// 特殊情况排除
if(s.length()<4 || s.length()>12){
return ans;
}
back(s,0,path);
return ans;
}
public void back(String s,int start,LinkedList<String> path){
// 1.回溯结束的条件,找到四段,或者索引超长
if(start>=s.length()){
if(path.size() == COUNT){
StringBuilder str = new StringBuilder();
for (int i = 0; i < path.size(); i++) {
if(i<COUNT-1){
str.append(path.get(i)).append(".");
}else{
str.append(path.get(i));
}
}
ans.add(str.toString());
}
return ;
}
// 2.枚举出选择,三种切割长度
for (int i = 1; i <COUNT ; i++) {
//不判断会导致 indexOutOfBound 错误
if(start+i>s.length()){
return;
}
// 3.不能有前导 0,不能切出'0x'、'0xx'
if(i!=1 && s.charAt(start)=='0'){
return;
}
// 4.要在0-255范围内
String temp = s.substring(start,i+start);
// 注意我原来的思路是,temp.compareTo("0")>=0 &&temp.compareTo("255")<=0
// 这样会导致不必要的剪枝,比如 "35".compareTo("255")=1,就被剪枝了
if(i==3 && temp.compareTo("255")>0){
return ;
}
path.add(temp);
back(s,start+i,path);
path.removeLast();
}
}
2.5 棋盘问题
51. N 皇后
将
n
个皇后放置在n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上该方案中
'Q'
和'.'
分别代表了皇后和空位。输入:n = 4 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 解释:如上图所示,4 皇后问题存在两个不同的解法。
List<List<String>> res;
public List<List<String>> solveNQueens(int n) {
res = new LinkedList<>();
if(n==0){
return res;
}
char[][] path = new char[n][n];
for (char[] ch : path) {
Arrays.fill(ch,'.');
}
backTrace(0,path);
return res;
}
public void backTrace(int row,char[][] path){
// 二维矩阵中矩阵的高就是这颗树的高度,回溯的是树的高度
// 1.回溯结束的条件:所有深度都填上了皇后
if(path.length==row){
res.add(char2List(path));
return ;
}
// 矩阵的宽就是树形结构中每一个节点的宽度,在每一次回溯里面遍历的是数的宽度
int n = path[row].length;
for (int col = 0; col < n; col++) {
// 2.判断当前位置是否合法
if(!isValid(path,row,col)){
continue;
}
path[row][col] = 'Q';
backTrace(row+1,path);
path[row][col] = '.';
}
}
public boolean isValid(char[][] path,int row,int col){
// 因为是深度回溯,所以当前行不可能有其他元素,不需要判断
// 1.遍历所有列是否冲突
for (int i = 0; i < row; i++) {
if(path[i][col]=='Q'){
return false;
}
}
// 2.遍历45°斜线是否有元素
for (int i = row-1,j=col-1; i >=0 && j>=0; i--,j--) {
if(path[i][j]=='Q'){
return false;
}
}
// 3.遍历135°斜线是否有元素
for (int i = row-1,j=col+1; i >=0 && j<path.length; i--,j++) {
if(path[i][j]=='Q'){
return false;
}
}
return true;
}
public List<String> char2List(char[][] path){
LinkedList<String> list = new LinkedList<>();
for (char[] ch : path) {
list.add(String.copyValueOf(ch));
}
return list;
}
37. 解数独
编写一个程序,通过填充空格来解决数独问题。数独部分空格内已填入了数字,空白格用 ‘.’ 表示。数独的解法需 遵循如下规则:
- 数字 1-9 在每一行只能出现一次。
- 数字 1-9 在每一列只能出现一次。
- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
输入:board = [["5","3",".",".","7",".",".",".","."], ["6",".",".","1","9","5",".",".","."], [".","9","8",".",".",".",".","6","."], ["8",".",".",".","6",".",".",".","3"], ["4",".",".","8",".","3",".",".","1"], ["7",".",".",".","2",".",".",".","6"], [".","6",".",".",".",".","2","8","."], [".",".",".","4","1","9",".",".","5"], [".",".",".",".","8",".",".","7","9"]] 输出: [["5","3","4","6","7","8","9","1","2"], ["6","7","2","1","9","5","3","4","8"], ["1","9","8","3","4","2","5","6","7"], ["8","5","9","7","6","1","4","2","3"], ["4","2","6","8","5","3","7","9","1"], ["7","1","3","9","2","4","8","5","6"], ["9","6","1","5","3","7","2","8","4"], ["2","8","7","4","1","9","6","3","5"], ["3","4","5","2","8","6","1","7","9"]]
思路:
- 先每一行进行填充,行填充玩了,再进入下一行填充,范围都是0-9
- 如果当前位置已经有元素了,直接跳过
- 在填充元素时需要进行判断填充的元素是否合法
- 行是否有重复元素
- 列是否有重复元素
- 小三角块是否有重复
// 对于3*3 小块的索引技巧,如下表示(4,5)位置处的小块 // i/3在0-9的范围是(0 0 0,1 1 1,2 2 2),所以出现(3 3 3,4 4 4,5 5 5),符合行规律 // i%3在0-9的范围是(0 1 2,0 1 2,0 1 2),所以出现(3 4 5,3 4 5,3 4 5),符合列规律 for (int i = 0; i < COUNT; i++) { System.out.print("行:"+((4/3)*3+i/3)+" "); System.out.println("列:"+((5/3)*3+i%3)); } 输出: 行:3 列:3 行:3 列:4 行:3 列:5 行:4 列:3 行:4 列:4 行:4 列:5 行:5 列:3 行:5 列:4 行:5 列:5
/**
* 解数独问题
* @param board
*/
static final int COUNT=9;
public void solveSudoku(char[][] board) {
if(board==null || board.length==0){
System.out.println(Arrays.deepToString(board));
}
back(board,0,0);
System.out.println(Arrays.deepToString(board));
}
public boolean back(char[][] board,int row,int col){
// 1.如果列达到限制了,从下一行重新开始
if(col==COUNT){
return back(board,row+1,0);
}
// 2.如何此时行也到达了最后一行,那么找到一组解
if(row==COUNT){
return true;
}
// 3.当前位置已经有数字了,直接跳过
if(board[row][col]!='.'){
return back(board,row,col+1);
}
// 4.万事具备,开填数字
for(char i='1';i<='9';i++){
if(!isVal(board,row,col,i)){
continue;
}
board[row][col] = i;
// 5.只要找到一个,可以直接返回
if(back(board,row,col+1)){
return true;
}
board[row][col] = '.';
}
return false;
}
public boolean isVal(char[][] board,int row,int col,char ch){
for (int i = 0; i < COUNT; i++) {
// 1.行是否有重复
if(board[row][i]==ch){
return false;
}
// 2.列是否有重复
if(board[i][col]==ch){
return false;
}
// 3.所属小三角块是否重复,(row/3)*3 == 小块的行
if(board[(row/3)*3+i/3][(col/3)*3+i%3]==ch){
return false;
}
}
return true;
}