回溯算法学习:
找了几个相似的题目,统一学习了一下:
第一道题:大小字母的全排列
题目描述:
一开始想用回溯法,对回溯法了解的并不那么深入,去百度了一下,觉得这篇文章写得很好,
回溯法
现在理解到,回溯法就是需要有一个结束条件,满足结束条件,就添加或者返回元素,不满足时,去寻找满足的条件,然后按照题目的要求,去寻找满足条件的情况。
上面是形式上的,从具体的遍历方式上,类似于深度搜索,先一步一步遍历,知道找到满足条件的,然后开始往前倒推,倒到上一步,找到满足条件的,这样一直倒推到最后,所有的情况都被遍历完成,回溯完成。
这一题先自己思考,后面参考题解,最终大概有两种解法,思想都是一样的:
第一种:
public void letterSolvedTheEnd(String s, List<String> result, int index, String preString) {
if(index == s.length()) {
result.add(preString);
} else {
char temp = s.charAt(index);
if(Character.isLetter(s.charAt(index))) {
temp = Character.toUpperCase(s.charAt(index));
System.out.println("temp = " + temp + ", priStringUp = " + preString + ", index = " + index);
letterSolvedTheEnd(s, result, index + 1, preString + temp);
temp = Character.toLowerCase(s.charAt(index));
System.out.println("temp = " + temp + ", priStringDown = " + preString + ", index = " + index);
letterSolvedTheEnd(s, result, index + 1, preString + temp);
} else {
preString += s.charAt(index);
System.out.println("temp = " + temp + ", 数字 = " + preString + ", index = " + index);
letterSolvedTheEnd(s, result, index + 1, preString);
}
}
}
第二种:
public void letterSolvedAnother(char[] temp, List<String> result, int index) {
if(index == temp.length) {
result.add(String.valueOf(temp));
} else {
if(Character.isLetter(temp[index])) {
temp[index] = Character.toUpperCase(temp[index]);
letterSolvedAnother(temp, result, index+1);
temp[index] = Character.toLowerCase(temp[index]);
letterSolvedAnother(temp, result, index+1);
} else {
letterSolvedAnother(temp, result, index+1);
}
}
}
这两种本质都是一样的,上面给出的是用来递归的函数。
主函数就比较简单:
public List<String> letterCase(String s) {
List<String> result = new ArrayList<>();
String preString = "";
// letterSolvedTheEnd(s, result, 0, preString);
char[] temp = s.toCharArray();
letterSolvedAnother(temp, result, 0);
return result;
}
可以通过:
第二道题:
这个还要考虑包含了重复数字,返回所有不重复的全排列,这个就需要设置一个数据,标记那个数字有没有被使用过:
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> perm = new ArrayList<Integer>();
vis = new boolean[nums.length];
Arrays.sort(nums);
backtrack(nums, ans, 0, perm);
return ans;
}
public void backtrack(int[] nums, List<List<Integer>> ans, int idx, List<Integer> perm) {
if (idx == nums.length) {
ans.add(new ArrayList<Integer>(perm));
return;
}
for (int i = 0; i < nums.length; ++i) {
if (vis[i] || (i > 0 && nums[i] == nums[i - 1] && !vis[i - 1])) {
continue;
}
perm.add(nums[i]);
vis[i] = true;
backtrack(nums, ans, idx + 1, perm);
vis[i] = false;
perm.remove(idx);
}
}
这个题目还踩了坑:
第一个:
if (idx == nums.length) {
ans.add(new ArrayList<Integer>(perm));
return;
}
这个要这样处理,不然返回的是空
是因为回溯法回到上一步的时候,有remove操作,如果直接是不这样保存一份perm的复制值,就会把remove后的 [ ] 加载进去。
第二个:
这个判断条件
if (vis[i] || (i > 0 && nums[i] == nums[i - 1] && !vis[i - 1])) {
continue;
}
当时对为什么是:!vis[i - 1] 纠结了很久,后面看了一个视频,讲解的很好,这样处理时因为,如果之前这个值已经出现过了,那么它一定是 回置回false。
第三道题:
做完这个需要增加一个数组来标记数字有没有使用的题目,再去做他的上一个题目,就很简单了
同样的思路,不用增加额外的判断:
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>(new ArrayList<>());
List<Integer> temp = new ArrayList<>();
permuteHelp(nums, result, temp, 0);
return result;
}
public void permuteHelp(int[] nums, List<List<Integer>> result, List<Integer> temp, int index) {
if(index == nums.length) {
result.add(new ArrayList<>(temp));
}
for (int i = 0; i < nums.length; i++) {
if(temp.contains(nums[i])) {
continue;
}
temp.add(nums[i]);
permuteHelp(nums, result, temp, index+1);
temp.remove(temp.size()-1);
}
}
第四道题:
后面又做了这道题用来练习,还是有一丢丢难的(对我来说),对大佬来说很easy。
这个一开始想到了用回溯,使用各种判断条件,最后还是在看了讲解之后,写出来的:
public boolean exist(char[][] board, String word) {
if (board.length == 0 || word.length() == 0) {
return false;
}
int l1 = board.length;
int l2 = board[0].length;
boolean temp[][] = new boolean[l1][l2];
for (int i = 0; i < l1; i++) {
for (int j = 0; j < l2; j++) {
if(existHelper(board, word, temp, i, j, 0)) {
return true;
}
}
}
return false;
}
public boolean existHelper(char[][] board, String word, boolean[][] temp, int i, int j, int index) {
//退出递归的边界条件
if(index == word.length()) {
return true;
}
//剪枝操作,减少无效的递归
if(i < 0 || i >= board.length || j < 0 || j >= board[0].length || temp[i][j] || board[i][j] != word.charAt(index)) {
return false;
}
temp[i][j] = true;
//真正的递归函数
boolean result = existHelper(board, word, temp, i-1, j, index+1)
|| existHelper(board, word, temp, i, j-1, index+1)
|| existHelper(board, word, temp, i+1, j, index+1)
|| existHelper(board, word, temp, i, j+1, index+1);
temp[i][j] = false;
return result;
}
讲两个我遇到的坑吧:
第一个:
for (int i = 0; i < l1; i++) {
for (int j = 0; j < l2; j++) {
if(existHelper(board, word, temp, i, j, 0)) {
return true;
}
}
}
这个双层for循环要放在主函数中,每一个值都要进行回溯,这个当时没有想到,导致写的很艰难。
第二个:
if(index == word.length()) {
return true;
}
当时一开始写时候,懵圈了,导致判断回溯退出的条件都不知道咋找了
总结一下,通过回溯法解题的要领就是:
第一:一定要找到回溯结束的条件
第二:遇到给定条件中有重复元素,但是结果集中不包含重复元素的情况,需要增加一个数组用来标记,方便后面剪枝处理。