回溯算法
简介
记录一下自己刷题的历程以及代码。写题过程中参考了 代码随想录的刷题路线。会附上一些个人的思路,如果有错误,可以在评论区提醒一下。
回溯题模板:
class Solution {
public 主方法(参数) {
//把递归参数传入递归函数
recursion(0);
return ans;
}
public void recursion(int n){
if(达到终止条件) {
//存放结果
ans.add(answer);
return;
}
for(循环调用后续的递归){
//递归前处理,比如把当前数加入答案集合
recursion(n + 1);
//递归后处理,回溯,撤销递归前处理的内容
}
}
}
[中等] 77. 组合
注意每次ans.add()时需要添加nums的深拷贝,而不是单纯把当前nums放入,否则放入的其实是nums数组的引用,最后ans的所有元素实际都指向了同一个数组。
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> nums = new ArrayList<>();
recursion(ans, nums, 1, n, k);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> nums, int p, int n, int k){
//终止条件
if(nums.size() == k){
ans.add(new ArrayList<>(nums));
return;
}
for(; p <= n; p++){
nums.add(p);
recursion(ans, nums, p+1, n, k);
nums.remove(nums.size() - 1);
}
return;
}
}
剪枝操作: k - nums.size()
是还需要多少个元素构成一组答案
for(; p <= n - (k - nums.size()) + 1; p++){
nums.add(p);
recursion(ans, nums, p+1, n, k);
nums.remove(nums.size() - 1);
}
[中等] 216. 组合总和 III
sum函数直接用参数替代也可以
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<Integer> nums = new ArrayList<>();
List<List<Integer>> ans = new ArrayList<>();
recursion (ans, nums, k, n, 1);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> nums, int k, int n, int p){
if(nums.size() == k){
if(sum(nums) == n)
ans.add(new ArrayList<>(nums));
return;
}
if(sum(nums) < n) {
for (; p <= 9; p++) {
nums.add(p);
recursion(ans, nums, k, n, p + 1);
nums.remove(nums.size() - 1);
}
}
}
public int sum(List<Integer> nums){
int sum = 0;
for(Integer i: nums){
sum += i;
}
return sum;
}
}
[中等] 17. 电话号码的字母组合
使用StringBuilder做字符串操作
class Solution {
public List<String> letterCombinations(String digits) {
StringBuilder sb = new StringBuilder();
List<String> ans = new ArrayList<>();
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
if(digits.length() == 0) return ans;
recursion(digits, sb, ans, numString, 0);
return ans;
}
public void recursion(String digits, StringBuilder sb, List<String> ans, String[] numString, int p){
if(p == digits.length()){
ans.add(sb.toString());
return;
}
for(int i = 0; i < numString[digits.charAt(p) - '0'].length(); i++) {
sb.append(numString[digits.charAt(p) - '0'].charAt(i));
recursion(digits, sb, ans, numString, p + 1);
sb.deleteCharAt(p);
}
}
}
[中等] 39. 组合总和
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> nums = new ArrayList<>();
recursion(ans, nums, candidates, target, 0, 0);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> nums, int[] candidates, int target, int sum, int p){
if(sum > target) return;
//终止条件
if(sum == target){
ans.add(new ArrayList<>(nums));
return;
}
for(int i = p; i < candidates.length; i++){
nums.add(candidates[i]);
recursion(ans, nums, candidates, target, sum+candidates[i], i);
nums.remove(nums.size() - 1);
}
return;
}
}
[中等] 40. 组合总和 II
对原数组进行排序,相同的数字顺序存放,对相同数字的使用保证从左到右。比如选择[1,1,6] 中的 [1,6] 作为答案,只会选到第一个1
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> ans = new ArrayList<>();
List<Integer> nums = new ArrayList<>();
int[] flag = new int[110];
recursion(ans, nums, candidates, flag, target, 0, 0);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> nums, int[] candidates, int[] flag, int target, int sum, int p){
if(sum > target) return;
//终止条件
if(sum == target){
ans.add(new ArrayList<>(nums));
return;
}
for(int i = p; i < candidates.length; i++){
//存在与之相同的前一个数,且前一个数还没被取过,则不会取当前数
if(i>0 && flag[i-1] == 0 && candidates[i-1] == candidates[i]) continue;
nums.add(candidates[i]);
flag[i] = 1;
recursion(ans, nums, candidates, flag, target, sum+candidates[i], i+1);
flag[i] = 0;
nums.remove(nums.size() - 1);
}
return;
}
}
[中等] 131. 分割回文串
这个递归自己想还想了挺久的,就是从左往右切割,终止条件就是判断有没有切到最右端
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> ans = new ArrayList<>();
List<String> group = new ArrayList<>();
if(s == null || s.length() == 0) return ans;
recursion(ans, group, s, 0);
return ans;
}
private void recursion(List<List<String>> ans, List<String> group, String s, int start) {
if(start == s.length()) {
ans.add(new ArrayList<>(group));
return;
}
for(int i = start; i < s.length(); i++) {
if(isPalindrome(s, start, i)) {
group.add(s.substring(start, i + 1));
recursion(ans, group, s, i + 1);
group.remove(group.size() - 1);
}
}
}
private boolean isPalindrome(String s, int begin, int end) {
while(begin < end) {
if(s.charAt(begin++) != s.charAt(end--)) return false;
}
return true;
}
}
[中等] 93. 复原 IP 地址
注意分割段到4时也需要return,往后的递归都是无意义的
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> ans = new ArrayList<>();
List<String> answer = new ArrayList<>();
if(s == null || s.length() == 0) return ans;
//p表示目前划分到第几段,ip地址总共四段, index表示s下标
recursion(ans, s, 0, answer);
return ans;
}
public void recursion(List<String> ans, String s, int index, List<String> answer){
if(answer.size() == 4 && index == s.length()){
ans.add(createAnswer(answer));
}
if(index == s.length() || answer.size() == 4) return;
if(s.charAt(index) == '0'){
answer.add("0");
recursion(ans, s, index + 1, answer);
answer.remove(answer.size() - 1);
}else {
for (int i = index; i < s.length() && i - index <= 3; i++) {
String string = s.substring(index, i + 1);
int num = Integer.parseInt(string);
if (num >= 0 && num <= 255){
answer.add(string);
recursion(ans, s, i + 1, answer);
answer.remove(answer.size() - 1);
}
}
}
}
public String createAnswer(List<String> answer){
StringBuilder sb = new StringBuilder();
for(int i = 0; i < answer.size(); i++){
if(i != 0){
sb.append(".");
}
sb.append(answer.get(i));
}
return sb.toString();
}
}
[中等] 78. 子集
一道标准的子集问题模板题,子集是收集树形结构中树的所有节点的结果。
而组合问题、分割问题是收集树形结构中叶子节点的结果。子集问题不需要在找到一组答案之后就进行回退。
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> answer = new ArrayList<>();
recursion(ans, answer, nums, 0);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> answer, int[] nums, int index){
ans.add(new ArrayList<>(answer));
for(int i = index; i < nums.length; i++){
answer.add(nums[i]);
recursion(ans, answer, nums, i + 1);
answer.remove(answer.size() - 1);
}
}
}
[中等] 90. 子集 II
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> answer = new ArrayList<>();
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
recursion(ans, answer, used, nums, 0);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> answer, boolean[] used, int[] nums, int index){
ans.add(new ArrayList<>(answer));
for(int i = index; i < nums.length; i++){
if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
continue;
}
used[i] = true;
answer.add(nums[i]);
recursion(ans, answer, used, nums, i + 1);
answer.remove(answer.size() - 1);
used[i] = false;
}
}
}
[中等] 491. 非递减子序列
不能简单的强制类似[1,1,1,1,1]
序列从左到右选,这道题不能排序,有可能会有[1,1,2,1,1]
,这样2之前的1没取的时候2后面的1也可以取,需要用集合set来做去重
class Solution {
public List<List<Integer>> findSubsequences(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> answer = new ArrayList<>();
recursion(ans, answer, nums, 0);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> answer, int[] nums, int index){
if(answer.size() > 1)
ans.add(new ArrayList<>(answer));
HashSet<Integer> hs = new HashSet<>();
for(int i = index; i < nums.length; i++){
if(hs.contains(nums[i]) || (answer.size() > 0 && nums[i] < answer.get(answer.size() - 1)))
continue;
hs.add(nums[i]);
answer.add(nums[i]);
recursion(ans, answer, nums, i + 1);
answer.remove(answer.size() - 1);
}
}
}
[中等] 46. 全排列
开一个数组标记不重复取数,每次循环都从头开始即可
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> answer = new ArrayList<>();
boolean[] used = new boolean[nums.length];
recursion(ans, answer, used, nums);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> answer, boolean[] used, int[] nums){
if(answer.size() == nums.length) {
ans.add(new ArrayList<>(answer));
return;
}
for(int i = 0; i < nums.length; i++){
if(used[i]) continue;
used[i] = true;
answer.add(nums[i]);
recursion(ans, answer, used, nums);
answer.remove(answer.size() - 1);
used[i] = false;
}
}
}
[中等] 47. 全排列 II
对数组排序,并对相同数字,保证从左到右取数,同时不重复取数
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> answer = new ArrayList<>();
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
recursion(ans, answer, used, nums);
return ans;
}
public void recursion(List<List<Integer>> ans, List<Integer> answer, boolean[] used, int[] nums){
if(answer.size() == nums.length) {
ans.add(new ArrayList<>(answer));
return;
}
for(int i = 0; i < nums.length; i++){
if(used[i]) continue;
if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
used[i] = true;
answer.add(nums[i]);
recursion(ans, answer, used, nums);
answer.remove(answer.size() - 1);
used[i] = false;
}
}
}
[困难] 51. N 皇后
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> ans = new ArrayList<>();
//初始化棋盘,声明一个flag数组,标记每一行填充的元素在哪一列,初始化为-1
int[] flag = new int[n];
for(int i = 0; i < n; i++){
flag[i] = -1;
}
recursion(ans, flag, 0);
return ans;
}
public void recursion(List<List<String>> ans, int[] flag, int row){
if(row == flag.length){
ans.add(createAnswer(flag));
return;
}
for(int i = 0; i < flag.length; i++){
if(isFree( flag, row, i)){
flag[row] = i;
recursion(ans, flag, row + 1);
flag[row] = -1;
}
}
}
//可否放置皇后棋子的判定
public boolean isFree(int[] flag, int row, int p){
//对角线判定 以及 列未判定
for(int i = 0; i < row; i++ ) {
if(flag[i] == p) return false;
if (flag[i] - i == p - row || flag[i] + i == row + p) {
return false;
}
}
return true;
}
//创建答案
public List<String> createAnswer(int[] flag) {
List<String> answer = new ArrayList<>();
for (int i = 0; i < flag.length; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < flag.length; j++) {
if (flag[i] == j) {
sb.append("Q");
} else {
sb.append(".");
}
}
answer.add(sb.toString());
}
return answer;
}
}
[困难] 37. 解数独
代码随想录给到的是一种暴力二维搜索,是比较容易想到的,每次递归都去判断行、列、九宫格里是否重复,这样效率会比较低,可以牺牲空间来简化这部分的判断,分别开设三个二维数组,记录每一行每一列每一个九宫格,1-9的数字是否被使用过,后续比较直接从数组里去读取信息就可以。
class Solution {
public void solveSudoku(char[][] board) {
//行、列、九宫格是否被占用
boolean[][] rowOccupy = new boolean[9][9];
boolean[][] colOccupy = new boolean[9][9];
boolean[][] nineOccupy = new boolean[9][9];
char a = board[1][2];
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j++){
char ch = board[i][j];
if(ch != '.') {
rowOccupy[i][ch - '1'] = true;
colOccupy[j][ch - '1'] = true;
nineOccupy[i / 3 * 3 + j / 3][ch - '1'] = true;
}
}
}
recursion(rowOccupy, colOccupy, nineOccupy, board, 0, 0);
}
public boolean recursion(boolean[][] rowOccupy, boolean[][] colOccupy, boolean[][] nineOccupy, char[][]board, int i, int j){
if(i > 8 || j > 8) return true;
if(board[i][j] != '.'){
if(j == 8) {
if (recursion(rowOccupy, colOccupy, nineOccupy, board, i + 1, 0)) {
return true;
}
}
else if (recursion(rowOccupy, colOccupy, nineOccupy, board, i, j + 1)) {
return true;
}
}else {
for (int k = 1; k <= 9; k++) {
if (!rowOccupy[i][k - 1] && !colOccupy[j][k - 1] && !nineOccupy[i / 3 * 3 + j / 3][k - 1]) {
board[i][j] = String.valueOf(k).charAt(0);
rowOccupy[i][k - 1] = true;
colOccupy[j][k - 1] = true;
nineOccupy[i / 3 * 3 + j / 3][k - 1] = true;
if (j == 8) {
if (recursion(rowOccupy, colOccupy, nineOccupy, board, i + 1, 0)) {
return true;
}
}
else if (recursion(rowOccupy, colOccupy, nineOccupy, board, i, j + 1)) {
return true;
}
board[i][j] = '.';
rowOccupy[i][k - 1] = false;
colOccupy[j][k - 1] = false;
nineOccupy[i / 3 * 3 + j / 3][k - 1] = false;
}
}
}
return false;
}
}