LeetCode - 7. 回溯
刷题顺序来自:代码随想录
组合
77. 组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。
回溯
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
private void backtracking(int n, int k, int index) {
if(path.size() == k) {
res.add(new LinkedList<Integer>(path));
return;
}
// for第一层1, 2, 3, 4
for(int i = index; i <= n; i++) {
path.add(i);
backtracking(n, k, i+1); // 往第二层或者更深
path.removeLast(); // 回溯
}
}
回溯与剪枝
for(int i = index; i <= n - (k - path.size()) + 1; i++)
39. 组合总和
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。
对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
回溯
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates, target, 0);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
private void backtracking(int[] candidates, int target, int index) {
if(sum >= target) {
if(sum == target) {
res.add(new LinkedList<Integer>(path));
}
return;
}
for(int i = index; i < candidates.length; i++) {
path.add(candidates[i]);
sum += candidates[i];
backtracking(candidates, target, i);
path.removeLast();
sum -= candidates[i];
}
}
剪枝
由于数组是无序的,想要剪枝,必须先排序。
Arrays.sort(candidates);
for(int i = index; i < candidates.length && sum + candidates[i] <= target; i++)
40. 组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
这一题与I和III的区别是需要去重,去重需要去掉当前层已经使用过的元素。
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
int last;
private void backtracking(int[] candidates, int target, int index) {
if(sum == target) {
res.add(new LinkedList<Integer>(path));
return;
}
for(int i = index; i < candidates.length && sum + candidates[i] <= target; i++) {
// 去除当前层已经使用过的元素
if(i > index && candidates[i] == candidates[i-1]) {
continue;
}
sum += candidates[i];
path.add(candidates[i]);
backtracking(candidates, target, i+1);
sum -= candidates[i];
path.removeLast();
}
}
216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
回溯
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k, n, 1);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
private void backtracking(int k, int n, int index) {
if(path.size() == k) {
if(sum == n) {
res.add(new LinkedList(path));
}
return;
}
// 注意剪枝
for(int i = index; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
sum += i;
backtracking(k, n, i+1);
path.removeLast();
sum -= i;
}
}
17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
public List<String> letterCombinations(String digits) {
// 数字与字母的映射
HashMap<Character, Character[]> map = new HashMap<>();
map.put('2', new Character[]{'a', 'b', 'c'});
map.put('3', new Character[]{'d', 'e', 'f'});
map.put('4', new Character[]{'g', 'h', 'i'});
map.put('5', new Character[]{'j', 'k', 'l'});
map.put('6', new Character[]{'m', 'n', 'o'});
map.put('7', new Character[]{'p', 'q', 'r', 's'});
map.put('8', new Character[]{'t', 'u', 'v'});
map.put('9', new Character[]{'w', 'x', 'y', 'z'});
backtracking(digits, map, 0);
return res;
}
ArrayList<String> res = new ArrayList<>();
StringBuilder builder = new StringBuilder();
private void backtracking(String digits, HashMap<Character, Character[]> map, int index) {
if(digits.length() == 0) {
return;
}
if(builder.length() == digits.length()) {
res.add(builder.toString());
}
for(int i = index; i < digits.length(); i++) {
char digit = digits.charAt(index);
Character[] chars = map.get(digit);
for(Character ch: chars) {
builder.append(ch);
backtracking(digits, map, i+1);
builder.deleteCharAt(builder.length()-1);
}
}
}
131. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
与上面的组合不同,每次循环只有2种可能,即在/不在当前索引切割。
public List<List<String>> partition(String s) {
backtracking(s, 0, 0);
return res;
}
ArrayList<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
private void backtracking(String s, int index, int start) {
if(index >= s.length()) {
// 需要字符串被完整地切割,才收集此结果
if(index == start) {
res.add(new LinkedList<String>(path));
}
return;
}
// 不在当前索引切割
backtracking(s, index + 1, start);
// 在当前索引切割,则切割下来的字符串需要是回文串,否则剪枝
if(isPalindrome(s, start, index)) {
path.add(s.substring(start, index + 1));
backtracking(s, index + 1, index + 1);
path.removeLast();
}
}
// 判断字符串是否是回文串
private boolean isPalindrome(String s, int start, int end) {
while(start < end) {
if(s.charAt(start) != s.charAt(end)) {
return false;
}
start++;
end--;
}
return true;
}
占用内存更小的回溯:
List<List<String>> lists = new ArrayList<>();
Deque<String> deque = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracking(s, 0);
return lists;
}
private void backTracking(String s, int startIndex) {
//如果起始位置大于s的大小,说明找到了一组分割方案
if (startIndex >= s.length()) {
lists.add(new ArrayList(deque));
return;
}
for (int i = startIndex; i < s.length(); i++) {
//如果是回文子串,则记录
if (isPalindrome(s, startIndex, i)) {
String str = s.substring(startIndex, i + 1);
deque.addLast(str);
} else {
continue;
}
//起始位置后移,保证不重复
backTracking(s, i + 1);
deque.removeLast();
}
}
93. 复原 IP 地址
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你不能重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
public List<String> restoreIpAddresses(String s) {
backtracking(s, 0);
return res;
}
ArrayList<String> res = new ArrayList<>();
String path = "";
int count = 0; // 记录插入点的个数
private void backtracking(String s, int index) {
if(count == 3) { // 当已经插入了3个点之后,说明需要开始收集结果了
if(isValid(s, index, s.length())) { // 判断剩余子串是否是有效ip
path += s.substring(index, s.length());
res.add(path);
// 收集好结果之后,需要把刚刚加入的剩余第4位ip删除
path = path.substring(0, path.length()- s.length() + index);
}
return;
}
for(int i = index; i < s.length(); i++) {
if(isValid(s, index, i+1)) {
path += s.substring(index, i+1) + ".";
count++;
}
else {
continue;
}
backtracking(s, i+1);
// 在回溯时,需要多删除一位,即添加的点
path = path.substring(0, path.length() - i + index - 2);
count--;
}
}
// 判断ip是否是有效的
private boolean isValid(String s, int start, int end) {
if(start < 0 || end <= 0 || start >= s.length() || end > s.length() || end <= start || end - start > 3) {
return false;
}
if(end - start == 3) { // 如果长度为3,则必须介于100和255之间
if(s.charAt(start) == '1') {
return true;
}
else if(s.charAt(start) == '2') {
return s.charAt(start + 1) < '5' || (s.charAt(start + 1) == '5' && s.charAt(start + 2) <= '5');
}
return false;
}
else if(end - start == 2) { // 如果长度是2,第一位必须大于等于1
return s.charAt(start) >= '1';
}
return true;
}
78. 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
public List<List<Integer>> subsets(int[] nums) {
backtracking(nums, 0);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
private void backtracking(int[] nums, int index) {
res.add(new LinkedList<Integer>(path)); // 需要记录所有结果
for(int i = index; i < nums.length; i++) {
path.add(nums[i]);
backtracking(nums, i+1);
path.removeLast();
}
}
90. 子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums); // 由于需要去重,所以先排序
backtracking(nums, 0);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
private void backtracking(int[] nums, int index) {
res.add(new LinkedList<Integer>(path));
for(int i = index; i < nums.length; i++) {
// 去除同一层中相同的节点
if(i > index && nums[i] == nums[i-1]) {
continue;
}
path.add(nums[i]);
backtracking(nums, i+1);
path.removeLast();
}
}
491. 递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums, 0);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
private void backtracking(int[] nums, int index) {
if(path.size() >= 2) { // 有至少2个元素后,收集结果
res.add(new LinkedList<Integer>(path));
}
// 使用set去重
HashSet<Integer> set = new HashSet<>();
for(int i = index; i < nums.length; i++) {
// 去重,每一层相同的元素只收集一次
if(set.contains(nums[i])) {
continue;
}
set.add(nums[i]);
if(path.size() == 0 || nums[i] >= path.get(path.size() - 1)) {
path.add(nums[i]);
backtracking(nums, i + 1);
path.removeLast();
}
}
}
由于输入-100 <= nums[i] <= 100,可以使用一个大小为201的数组记录数字有没有使用过,占用内存更小。优化之后:
private List<Integer> path = new ArrayList<>();
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums,0);
return res;
}
private void backtracking (int[] nums, int start) {
if (path.size() > 1) {
res.add(new ArrayList<>(path));
}
int[] used = new int[201];
for (int i = start; i < nums.length; i++) {
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
(used[nums[i] + 100] == 1)) continue;
used[nums[i] + 100] = 1;
path.add(nums[i]);
backtracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
排列
46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
public List<List<Integer>> permute(int[] nums) {
used = new int[nums.length];
backtracking(nums);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int[] used; // 记录已经访问过的元素
private void backtracking(int[] nums) {
if(path.size() == nums.length) {
res.add(new LinkedList<>(path));
return;
}
for(int i = 0; i < nums.length; i++) {
if(used[i] == 0) {
used[i] = 1;
path.add(nums[i]);
backtracking(nums);
path.removeLast();
used[i] = 0;
}
}
}
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
public List<List<Integer>> permuteUnique(int[] nums) {
used = new int[nums.length];
Arrays.sort(nums); // 排序
backtracking(nums);
return res;
}
ArrayList<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int[] used;
private void backtracking(int[] nums) {
if(path.size() == nums.length) {
res.add(new LinkedList<Integer>(path));
return;
}
for(int i = 0; i < nums.length; i++) {
if(used[i] == 0) {
// 在同一层内去重
if(i > 0 && used[i-1] == 0 && nums[i] == nums[i-1]) {
continue;
}
used[i] = 1;
path.add(nums[i]);
backtracking(nums);
path.removeLast();
used[i] = 0;
}
}
}
332. 重新安排行程
public List<String> findItinerary(List<List<String>> tickets) {
used = new int[tickets.size()];
path.add("JFK");
tickets.sort(new Comparator<List<String>> () {
public int compare(List<String> l1, List<String> l2) {
return l1.get(1).compareTo(l2.get(1));
}
});
backtracking(tickets, "JFK");
return res;
}
LinkedList<String> res = null;
LinkedList<String> path = new LinkedList<>();
int[] used;
boolean find = false;
private void backtracking(List<List<String>> tickets, String target) {
if(find) {
return;
}
if(path.size() == tickets.size() + 1) {
res = new LinkedList<String>(path);
find = true;
return;
}
for(int i = 0; i < tickets.size(); i++) {
if(!find && used[i] == 0 && tickets.get(i).get(0).equals(target)) {
// 去重,加了之后效果一般
// if(i > 0 && used[i-1] == 0 && tickets.get(i-1).get(0).equals(target) && tickets.get(i).get(1).equals(tickets.get(i-1).get(1))) {
// continue;
// }
used[i] = 1;
path.add(tickets.get(i).get(1));
backtracking(tickets, tickets.get(i).get(1));
path.removeLast();
used[i] = 0;
}
}
}
优化版:
private Deque<String> res;
private Map<String, Map<String, Integer>> map;
private boolean backTracking(int ticketNum){
if(res.size() == ticketNum + 1){
return true;
}
String last = res.getLast();
if(map.containsKey(last)){//防止出现null
for(Map.Entry<String, Integer> target : map.get(last).entrySet()){
int count = target.getValue();
if(count > 0){
res.add(target.getKey());
target.setValue(count - 1);
if(backTracking(ticketNum)) return true;
res.removeLast();
target.setValue(count);
}
}
}
return false;
}
public List<String> findItinerary(List<List<String>> tickets) {
map = new HashMap<String, Map<String, Integer>>();
res = new LinkedList<>();
for(List<String> t : tickets){
Map<String, Integer> temp;
if(map.containsKey(t.get(0))){
temp = map.get(t.get(0));
temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1);
}else{
temp = new TreeMap<>();//升序Map
temp.put(t.get(1), 1);
}
map.put(t.get(0), temp);
}
res.add("JFK");
backTracking(tickets.size());
return new ArrayList<>(res);
}
51. N 皇后
public List<List<String>> solveNQueens(int n) {
backtracking(n);
return res;
}
ArrayList<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
StringBuilder builder = new StringBuilder();
private void backtracking(int n) {
if(path.size() == n) {
res.add(new LinkedList<String>(path));
return;
}
int layer = path.size(); // 当前层数
// 根据前面若干层的位置,找出当前层有哪些位置是不能放的
HashSet<Integer> invalid = new HashSet<Integer>();
for(int i = 0; i < path.size(); i++) {
// 根据字符串中Q的位置判断第i层的棋子位置
int pos = 0;
for(int j = 0; j < path.get(i).length(); j++) {
if(path.get(i).charAt(j) == 'Q') {
pos = j;
break;
}
}
// 根据第i层的棋子位置pos,找出哪些位置是当前层不能放的
invalid.add(pos);
if(pos - (layer - i) >= 0) {
invalid.add(pos - (layer - i));
}
if(pos + (layer - i) < n) {
invalid.add(pos + (layer - i));
}
}
// 如果当前层所有位置都不能放,说明这种情况失败了,直接return
if(invalid.size() >= n) {
return;
}
builder.setLength(0); // 清空字符串
for(int i = 0; i < n; i++) {
if(!invalid.contains(i)) {
builder.append('Q');
while(builder.length() < n) {
builder.append('.');
}
path.add(builder.toString());
backtracking(n);
path.removeLast();
builder.setLength(i); // 切割字符串,回溯
}
builder.append('.');
}
}
优化版:
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTrack(n, 0, chessboard);
return res;
}
public void backTrack(int n, int row, char[][] chessboard) {
if (row == n) {
res.add(Array2List(chessboard));
return;
}
for (int col = 0;col < n; ++col) {
if (isValid (row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
backTrack(n, row+1, chessboard);
chessboard[row][col] = '.';
}
}
}
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
public boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查列
for (int i=0; i<row; ++i) { // 相当于剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线
for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线
for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
37. 解数独
public void solveSudoku(char[][] board) {
backtracking(board, 0);
}
// 每次递归从row开始
private boolean backtracking(char[][] board, int row) {
for(int i = row; i < 9; i++) {
for(int j = 0; j < 9; j++) {
// 结束条件:已经找到了一组可行解
if(i == 8 && j == 8 && board[i][j] != '.') {
return true;
}
if(board[i][j] == '.') {
// 分别填入1-9
for(char k = '1'; k <= '9'; k++) {
if(isValid(board, i, j, k)) {
board[i][j] = k;
if(backtracking(board, i)) {
return true;
}
board[i][j] = '.';
}
}
return false; // 每次递归只填写一个数字
}
}
}
return false;
}
// 判断在[row][col]填写val是否可行
private boolean isValid(char[][] board, int row, int col, char val) {
// 当前行不能有重复
for(int j = 0; j < 9; j++) {
if(board[row][j] == val) {
return false;
}
}
// 当前列不能有重复
for(int i = 0; i < 9; i++) {
if(board[i][col] == val) {
return false;
}
}
// 当前九宫格不能有重复
int firstRow = row / 3 * 3;
int firstCol = col / 3 * 3;
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
if(board[firstRow+i][firstCol+j] == val) {
return false;
}
}
}
return true;
}

浙公网安备 33010602011771号