回溯可以看成是一个树的结构,for循环是树层(即树的宽度遍历),递归函数是进行树枝(即树的深度遍历)。如果给的数组有重复的数据,那么就会涉及到去重。
树层去重代码:为了防止结果上同一个位置的值相同,这样可能会造成相同的结果[[1,1],[1,1]],树层表示的就是数组当前位置的值,树枝就是沿着数组往后走。
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {//事先需要对数组进行排序
continue;
}
树枝去重:防止得到的结果中存在重复[1,1,2]
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == true) {//事先需要对数组进行排序
continue;
}
组合总和(无重复)
class Solution {
class Solution {
List<List<Integer>> res = new LinkedList<>();
int t;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
t = target;
List<Integer> path = new LinkedList<>();
backTracking(0, candidates, path, 0);
return res;
}
void backTracking(int startIndex, int[] nums, List<Integer> path, int sum) {
if (sum > t) return;
if (sum == t) {
res.add(new LinkedList<>(path));
return;
}
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]);
sum += nums[i];
backTracking(i, nums, path, sum);//这里不用i + 1,因此此题要求,每个元素可以无限次数使用
sum -= nums[i];
path.remove(path.size() - 1);
}
}
}
组合总和(有重复)
class Solution {
List<List<Integer>> res = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
boolean[] used = new boolean[candidates.length];
backTracking(0, 0, candidates, used, target);
return res;
}
//确定 回溯函数返回值
void backTracking(int sum, int startIndex, int[] nums, boolean[] used, int target) {
//确定终止条件
if (sum > target) return;
if (sum == target) {
res.add(new LinkedList<>(path));
return;
}
for (int i = startIndex; i < nums.length && sum + nums[i] <= target; i++) {
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
sum += nums[i];
path.add(nums[i]);
used[i] = true;
backTracking(sum, i + 1, nums, used, target);
sum -= nums[i];
used[i] = false;
path.remove(path.size() - 1);
}
}
}
全排列(无重复)
全排列和组合的区别就是,每次startIndex从0开始,并且用used[i]记录某个数据是否北方问过,如果访问过就跳过
class Solution {
List<List<Integer>> res = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
backTracking(nums, used);
return res;
}
void backTracking(int[] nums, boolean[] used) {
if (nums.length == path.size()) {
res.add(new LinkedList(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i] == true) continue;
used[i] = true;
path.add(nums[i]);
backTracking(nums, used);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
全排列有重复
class Solution {
List<Integer> path = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backTracking(nums, used);
return res;
}
//确定回溯函数的返回值 和 待定参数
void backTracking(int[] nums, boolean[] used) {
//确定终止条件
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
//去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue; //树层去重
if (used[i] == false) {
used[i] = true;
path.add(nums[i]);
backTracking(nums, used);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
}
分割问题
分割问题和组合问题相似,不同的地方在于分割问题有一个分割点起始点问题,即startIndex会作为分割起始点,然后遍历的值假设x,那么x-startIndex
这一段就是我们要分割的目标,递归函数就是在剩余的数据中找新的起始分割点。而组合就是每次只选取1个,for循环的作用,就是不断改变当前位置的值。
class Solution {
List<String> path = new LinkedList<>();
List<List<String>> res = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracking(s, 0);
return res;
}
//确定 回溯函数的返回值
void backTracking(String s, int startIndex) {
//确定终止条件
if (startIndex >= s.length()) {
res.add(new LinkedList<>(path));
return;
}
for (int i = startIndex; i < s.length(); i++) {
if (isPalindrome(s, startIndex, i) == true) {
path.add(s.substring(startIndex, i + 1));
} else {
continue;
}
backTracking(s, i + 1);
path.remove(path.size() - 1);
}
}
boolean isPalindrome(String s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) return false;
}
return true;
}
}
复原Ip(分割)
class Solution {
List<String> res = new LinkedList<>();
public List<String> restoreIpAddresses(String s) {
if (s == null || s.length() == 0) return res;
StringBuilder sb = new StringBuilder(s);
backTracking(sb, 0, 0);
return res;
}
//用"."分割IP串
void backTracking(StringBuilder sb, int startIndex, int sum) {
if (sum == 3) {
if (checkIp(sb, startIndex, sb.length() - 1)) {
res.add( sb.toString());
return;
}
}
for (int i = startIndex; i < sb.length(); i++) {
if (checkIp(sb, startIndex, i)) {
sb.insert(i + 1, ".");
sum++;
backTracking(sb, i + 2, sum);
sum--;
sb.deleteCharAt(i + 1);
} else {
break; //一旦不满足Ip说明在往后走还是会大于255,所以用break
}
}
}
boolean checkIp(StringBuilder sb, int start, int end) {
if (start > end)
return false;
if (sb.charAt(start) == '0' && start != end)
return false;
int num = 0;
for (int i = start; i <= end; i++) {
if (!Character.isDigit(sb.charAt(i)))
return false;
num = num * 10 + sb.charAt(i) - '0';
if (num > 255)
return false;
}
return true;
}
}
深搜和回溯
由从新安排行程问题有以下思考,重新安排行程就是每一个起点对应一个集合,集合中的每个地点又对应另一个集合,所以可以看成是一个深搜问题,但是深搜问题也可以用回溯的方式写,因为深搜的实现本身就是回溯。
个人的理解:回溯是一种思想,深搜是一种搜索方式,深搜的实现用到了回溯的思想,对于一些组合,排序,分割数组,子集问题都有自己对应的模板,相当于直接利用回溯思想解题,这种问题往往是从集合中选元素的问题,他们还不能称为深搜,只是用回溯的思想解题。回溯问题可以看成是一个树的枝干问题。深搜也是当前的位置到下一个位置有几条路径,然后分别去走这几条路,达到条件就回退,这里就隐含回溯的意思,并且一般要带一些变量来判读是否经过这个位置的变量,这个在回溯解题问题中也存在(去重,树层去重,树枝去重,排列问题防止重复元素)。
用不用startIndex?
startIndex:代码随想录中说对于组合问题,一个集合就要有startIndex,不同集合就不用startIndex(手机号码问题,不同数字对应不同的字母集合)。自己想了想,说的真对。。。。,对于排列问题,每次选的时候,所有的元素都可以选,就可以等价于,每次都是新的集合和之前不相关,只是随着树枝的深入,判断某个值是否取过就可以(不同于树层去重和树枝去重,因为排列组合也可以出现[1,1,2]的情况)。此外,还有N皇后问题,看着是棋盘问题,但是解决的思路确是确实从每一层进行深入,每一层的数据都可以看成是不同的集合,所以每次层的树层遍历都是i=0开始的。
数读
一个纸老虎题
class Solution {
/**
1. 棋盘格本身就是去重的作用,以及本身就是一个结束条件(都满了)
2. 每次递归时,就要判断哪个棋盘格是空的,虽然每次进函数都是一个双重遍历,但是也只是找到下一个“落脚点”而已,不要怕!
3. 在二位遍历中如果遇到可以落脚的“.”处,就遍历1-9,看哪个可以放到当前位置,如果都不能说明,当前的方案不行,就要return false。
4.如果能就一直递归,当棋盘格满了以后,其实就自动出来了,return true
5. 总之很巧妙。。。和之前的套路都不太一样,外面的两层for循环是纸老虎,实际干活的是里面的1-9的遍历递归,这里是符合回溯的套路的。
*/
public void solveSudoku(char[][] board) {
backTracking(board);
}
boolean backTracking(char[][] board) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (board[i][j] != '.') //这里如果棋盘满了,那就结束,相当于结束条件
continue;
for (char k = '1'; k <= '9'; k++) {
if (isValid(i, j, board, k))
{
board[i][j] = k;
if (backTracking(board)) return true;
board[i][j] = '.';
}
}
return false;//这里就是如果当前的位置从1-9都不满足条件,那么就需要回退了所以返回false
}
}
return true;
}
boolean isValid(int row, int col, char[][] board, char k) {
//检查行
for (int i = 0; i < 9; i++) {
if (board[row][i] == k)
return false;
}
//检查列
for (int i = 0; i < 9; i++) {
if (board[i][col] == k)
return false;
}
//检查九宫格
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++) {
for (int j = startCol; j < startCol + 3; j++) {
if (board[i][j] == k)
return false;
}
}
return true;
}
}