22. 括号生成

 

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList();
        if (n == 0) {
            return result;
        }
        // 必须要用字符串,每次拼接要产生新对象。不能用 StringBuffer StringBuilder 之类,栈里不能都操作一个对象
        dfs(n, n, "", result);
        return result;
    }

    private void dfs(int leftN, int rightN, String currentNodeStr, List<String> result) {
        // 左右括号都用完
        if (leftN == 0 && rightN == 0) {
            result.add(currentNodeStr);
            return;
        }

        // 剩余的右括号不仅要 cover 掉剩余的左括号,还要 cover 掉现有没匹配上的左括号
        // 所以剩余的右括号一定要大于剩余的左括号
        if (rightN < leftN) {
            return;
        }

        // 左、右递归
        if (leftN > 0) {
            // 必须要 leftN-1 ,不能 leftN--
            dfs(leftN-1, rightN, currentNodeStr + "(", result);
        }
        if (rightN > 0) {
            // 必须要 rightN-1 ,不能 rightN--
            dfs(leftN, rightN-1, currentNodeStr + ")", result);
        }
    }
}

 

 

51. N 皇后

在类中定义一个全局的结果

树的最大层数是行,每个分支是列,每次递归行数加一,循环对每一列进行递归

递归函数的最开始,判断 如果行数已经到了最后一行(=n),添加到全局结果中,return

判断可以放置,就先在棋盘上放置好(设为 "Q"),再进行递归

注意如果 判断不可以放置,不用 return,而是回来之后恢复现场,重新设为 ".",也就是回溯恢复现场

判断能否放置:检查此次皇后放置位置的同一列的上半部分。以及左上斜对角和右上斜对角(下半部分不用检查),检查斜对角的时候注意是单层循环,ij行列同时加减,不是双层循环

 

关于回溯

先将第一个皇后放在第一行的第一列上,符合题目要求

开始放置第二个皇后。放在第二行的第一个与第一行的皇后为同一列,不符合题意,继续向后搜素,放在第二列上面与第一个皇后在同一斜线上,不符合题意,继续向后搜素,发现放在第三列符合题意

开始放置第三个皇后。放在第三行的任意位置都会出现冲突,此时需要回溯,将第二个皇后放置在第四列,此时符合题意,继续放置第三个皇后,发现第三个皇后放置在第三行的第二列符合题意

继续放置第四个皇后。放在第四行的任意位置都会出现冲突,此时需要回溯,第三个皇后向后移动,发现依然不符合题意,继续回溯,第二行的皇后无法再向后移动,继续回溯,将第一个皇后向后移动到第二列,符合题意

移动第二个皇后,发现放在第四列符合题意

移动第三个皇后,发现放在第一列符合题意

移动第四个皇后,发现放在第三列符合题意

回溯结束

 

class Solution {


    private static List<List<String>> res = new ArrayList();


    public Solution() {
        // 不知道为啥,提交答案的时候似乎有之前用例的残余
        res.clear();
    }

    public List<List<String>> solveNQueens(int n) {
        dfs(n, getInitChessBoard(n), 0);
        return res;
    }

    private void dfs(int n, char[][] chessBoard, int newQueenRow) {
        if (newQueenRow == n) {
            res.add(char2DArray2StringList(chessBoard, n));
            return;
        }


         // 对每个分支 dfs。每个分支是每一列。(列的变化通过在 chessBoard 的放置传递进去,而层数即行的变化需要作为显式参数)
        for (int newQueenCol=0;newQueenCol<n;++newQueenCol) {
            // 不能 return 返回,因为对那些不满足条件的,要回溯,即回去恢复现场
            /* if (!canSet(n, chessBoard, newQueenRow, newQueenCol)) {
                return;
            }
            */
            if (canSet(n, chessBoard, newQueenRow, newQueenCol)) {
                // 可以设置了再设置
                chessBoard[newQueenRow][newQueenCol] = 'Q';
                dfs(n, chessBoard, newQueenRow + 1);
                // 回溯就指的是这一步,不满足条件的不要 return退出搜索,而是到这里回来后恢复现场
                chessBoard[newQueenRow][newQueenCol] = '.';
            }
        }
    }

    private boolean canSet(int n, char[][] chessBoard, int newQueenRow, int newQueenCol) {
        // 不用检查同一行(每次 dfs 层数 +1 也就是说限制了一行只能放置一个)

        // 检查同一列上面
        for (int i=0; i<newQueenRow; i++) {
            // 不是自己且为 Q
            if (chessBoard[i][newQueenCol] == 'Q') {
                return false;
            }
        }

        // 检查左上斜对角(不用检查下半截)
        // 注意不是双层循环,斜对角要横纵坐标同进同退,是单层循环
        for (int i=newQueenRow-1,j=newQueenCol-1;i>=0 && j>=0; i--,j--) {
            if (chessBoard[i][j] == 'Q') {
                return false;
            }
        }

        // 检查右上斜对角(不用检查下半截)
        // 注意不是双层循环,斜对角要横纵坐标同进同退,是单层循环
        for (int i=newQueenRow-1,j=newQueenCol+1;i>=0 && j<n; i--,j++) {
            if (chessBoard[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    } 

    private char[][] getInitChessBoard(int n) {
        char[][] initChessBoard = new char[n][n];
        for (int i=0;i<n;i++) {
            for (int j=0;j<n;j++) {
                initChessBoard[i][j] = '.';
            }
        }
        return initChessBoard;
    }

    private List<String> char2DArray2StringList(char[][] charArray, int n) {
        List<String> list = new ArrayList();
        for (int i=0;i<n;i++) {
            // copy 一行的字符 到 字符串 s
            String s = String.copyValueOf(charArray[i], 0, n);
            list.add(s);
        }
        return list;
    }

   
}

 

 

17. 电话号码的字母组合

递归树的层数是按钮的总个数,每次递归层数加一
递归树的分支是每个按钮上对应的那几个字符,因此每次递归对这几个字符分别递归

class Solution {

    public Solution() {
        resList.clear();
    }

    private List<String> resList = new ArrayList();

    private static Map<Character, List<Character>> buttonMap = new HashMap();

    static {
        buttonMap.put('2', Arrays.asList('a', 'b', 'c'));
        buttonMap.put('3', Arrays.asList('d', 'e', 'f'));
        buttonMap.put('4', Arrays.asList('g', 'h', 'i'));
        buttonMap.put('5', Arrays.asList('j', 'k', 'l'));
        buttonMap.put('6', Arrays.asList('m', 'n', 'o'));
        buttonMap.put('7', Arrays.asList('p', 'q', 'r', 's'));
        buttonMap.put('8', Arrays.asList('t', 'u', 'v'));
        buttonMap.put('9', Arrays.asList('w', 'x', 'y', 'z'));
    }


    public List<String> letterCombinations(String digits) {
        if (digits == null || "".equals(digits)) {
            return resList;
        }
        dfs(digits, 0, "");
        return resList;
    }

    private void dfs(String digits, int curButtonLayer, String curRes) {
        // 走到最后一层叶子节点,结果获得,递归结束
        if (curButtonLayer == digits.length()) {
            resList.add(curRes);
            return;
        }

        //根据层数获得当前按钮
        Character curButton = digits.charAt(curButtonLayer);
        //根据当前按钮获得当前按钮上的字符
        List<Character> buttonChars = buttonMap.get(curButton);

        // 递归树的分支是每个按钮上对应的那几个字符,因此每次递归对这几个字符分别递归
        for (int i=0;i<buttonChars.size();i++) {
            // 递归树的层数是按钮的总个数,每次递归层数加一
            dfs(digits, curButtonLayer + 1, curRes + buttonChars.get(i));
        }
    }
}

 

46. 全排列

就是 dfs & 回溯,注意点:

  • 由于每一个排列不能有重复字符,所以用一个 boolean[] used = new boolean[nums.length]; 来标记当前数字是否被添加到了结果中
  • 如果层数达到 n,添加结果时,要添加深拷贝 resList.add(new LinkedList(curArray)); 而不能直接添加 curArray,要不然结果里就是curArray的复制,而curArray在后面的递归里还会变

递归 & 回溯过程:

    for (int i=0;i<nums.length;i++) {
            // 全排列要求不能有重复的数字,所以用 uesd 来标志某数字是否被使用过
            if (!used[i]) {
                curArray.addLast(nums[i]);
                used[i] = true;
                dfs(nums, curLayer+1, curArray, used);
                // dfs 完成后进行回溯
                curArray.removeLast();
                used[i] = false;
            }
        }
class Solution {

    private List<List<Integer>> resList = null;

    Solution() {
        resList = new ArrayList();
    }

    public List<List<Integer>> permute(int[] nums) {
        // 初始化标记数组
        boolean[] used = new boolean[nums.length];
        for (int i=0;i<nums.length;i++) {
            used[i] = false;
        }
        dfs(nums, 0, new LinkedList<Integer>(), used);
        return resList;
    }

    private void dfs(int[] nums, int curLayer, LinkedList<Integer> curArray, boolean[] used) {
        // 层数是固定的,为nums的长度。所以以层数达到最后一层作为递归终止的条件
        if (curLayer == nums.length) {
            // 这里添加时,要curArray的复制,要不然结果里就是curArray的复制,而curArray在后面的递归里还会变
            resList.add(new LinkedList(curArray));
            return;
        }

        for (int i=0;i<nums.length;i++) {
            // 全排列要求不能有重复的数字,所以用 uesd 来标志某数字是否被使用过
            if (!used[i]) {
                curArray.addLast(nums[i]);
                used[i] = true;
                dfs(nums, curLayer+1, curArray, used);
                // dfs 完成后进行回溯
                curArray.removeLast();
                used[i] = false;
            }
        }
    }

    
}

 

78. 子集

两种 dfs :

1.选或不选nums[层数]

总层数就是 nums 的长度

每个节点有两个分支:选nums[层数] 还是 不选nums[层数]

到最后一层所有的叶子节点就是结果,所以如果层数是最后一层,把结果加到结果集里

 

class Solution {

    private List<List<Integer>> res = null;

    Solution() {
        res = new ArrayList();
    }

    public List<List<Integer>> subsets(int[] nums) {
        //初始层数是0
        dfs(nums, new LinkedList(), 0);
        return res;
    }

    public void dfs(int[] nums, LinkedList<Integer> curRes, int curLayer) {
        // 叶子节点的值是最后的结果
        if (curLayer == nums.length) {
            res.add(new ArrayList(curRes));
            return;
        }
        // 在每个数字有两种选择:
        curRes.addLast(nums[curLayer]);
        // 1.有自己这个数字
        dfs(nums, curRes, curLayer+1);
        // 回溯
        curRes.removeLast();
        // 2.没自己这个数字
        dfs(nums, curRes, curLayer+1);
    }

}

 

2.每个节点可选的数字只有 nums[起始坐标~末尾]:

0 1 2 3 4 5 6 ......

1 2 3 4 5 6 7 .......

2 3 4 5 6 7 8 ......

根节点的起始坐标初始是0,每个节点的起始坐标都是:

  • 它的左边节点的起始坐标+1(每层第一个节点没有左边节点,不符合这条)
  • 它的父节点的起始坐标+1

如下面这样

0 1 2 3 4 5 6 ......

1 2 3 4 5 6 7 .......

2 3 4 5 6 7 8 ......

然后最终 所有节点都是结果,每次dfs都要把当前放入结果

class Solution {

    private List<List<Integer>> res = null;

    Solution() {
        res = new ArrayList();
    }

    public List<List<Integer>> subsets(int[] nums) {
        //初始起始坐标是0
        dfs(nums, new LinkedList(), 0);
        return res;
    }

    public void dfs(int[] nums, LinkedList<Integer> curRes, int startIndex) {
        // 每个节点的值都是最终的结果
        res.add(new ArrayList(curRes));
        // 每个节点有 起始坐标~nums.length 这么些选择
        for (int i=startIndex;i<nums.length;i++) {
            curRes.addLast(nums[i]);
            // 下一层的起始坐标是现在的起始数字再加1
            dfs(nums, curRes, i+1);
            // 回溯
            curRes.removeLast();
        }
    }
}

 

77. 组合

根节点的起始数字是1,每个节点的起始数字都是:

  • 它的左边节点的起始数字+1(每层第一个节点没有左边节点,不符合这条)
  • 它的父节点的起始数字+1

如下面这样

1 2 3 4 5 6 ......

2 3 4 5 6 7 .......

3 4 5 6 7 8 ......

类似于上面子集问题的解法2,但是子集问题是所有节点都是答案,而这个组合问题只有叶子节点是答案

class Solution {

    List<List<Integer>> res = null;

    Solution() {
        res = new ArrayList();
    }

    public List<List<Integer>> combine(int n, int k) {
        // 起始层数是0,起始数字是1
        dfs(n, k, new LinkedList(), 0, 1);
        return res;
    }

    public void dfs(int n, int k, LinkedList<Integer> curRes, int curLayer, int startIndex) {
        // 一共有 k 层,所有的叶子节点是答案
        // 很类似于子集问题,但子集问题所有的节点都是答案
        if (curLayer == k) {
            res.add(new ArrayList(curRes));
            return;
        }

        // 每个节点的选择是从 startIndex...n
        for (int i=startIndex;i<=n;i++) {
            curRes.addLast(i);
            // 层数+1,startIndex 是 i+1
            dfs(n,k,curRes,curLayer+1,i+1);
            curRes.removeLast();
        }
    }
}

 

39. 组合总和

 

类似于上面的组合,但是不是固定长度的,所以不只有叶子节点,而是所有的节点都有可能是结果

还有添加到结果的时候需要额外判断是否等于 target 

以及如果已经大于 target ,要进行剪枝,都是正数,再往下加还是大于了

另外还有一点是组合中数字可以重复

  • 虽然可以有重复的,但是 1,3,3 还是等价于 3,1,3 即 3,1 等价于 1,3。即还是要避免这种情况。
  • 因为可以有重复的,所以下一层的 startIndex 是 i 而不是 i+1
class Solution {

    List<List<Integer>> res = null;

    Solution() {
        res = new ArrayList();
    }

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(candidates, target, new LinkedList(), 0, 0);
        return res;
    }

    private void dfs(int[] candidates, int target, LinkedList<Integer> curRes, int startIndex, int curSum) {
        if (curSum == target) {
            res.add(new ArrayList(curRes));
            return;
        }
        else if (curSum > target) {
            // 由于都是正数,已经超过了。再往下加还是超过,所以先剪枝
            return;
        }
        for (int i=startIndex;i<candidates.length;i++) {
            curRes.addLast(candidates[i]);
            // 虽然可以有重复的,但是 1,3,3 还是等价于 3,1,3 即 3,1 等价于 1,3
            // 因为可以有重复的, 所以下一层 startIndex 是 i 而不是 i+1
            dfs(candidates, target, curRes, i, curSum+candidates[i]);
            curRes.removeLast();
        }
    }
}

 

 79. 单词搜索

 进行 dfs

  • dfs 的起点是什么?
    • 棋盘上所有元素值为 word.charAt(0) 的元素
  • 除了board和dfs到的当前元素i,j,还需要什么变量?
    • 由于同一个单元格里的字母不允许被重复使用,所以需要一个 boolean[][] visited 访问数组,来标志一次dfs中,这个元素有没有被访问过(初始false,访问过了标为true)
    • 需要一个变量k来表示前面已经完成了word从0到k-1的字母,现在需要寻找的是word.charAt(k)的字母
  • 怎样判断这一步的dfs不是有效的?
    1. 超过了board边界
    2. 已经被访问过
    3. 不等于当前期望的字符 board[i][j] != word.charAt(k)
  • 怎么判断当前dfs已经找到棋盘上等于word的路径?
    • 当 k==word.length 时
class Solution {

    Boolean res = false;

    Solution() {
        res = false;
    }

    public boolean exist(char[][] board, String word) {
        // 从那些和 word 第一个字符一样的元素开始 dfs
        for(int i=0;i<board.length;i++) {
            for(int j=0;j<board[0].length;j++) {
                if(word.charAt(0) == board[i][j]) {
                    dfs(board, getInitVisited(board), word, i, j, 0);
                    // 每次 dfs 完都判断一下,如果已经是true了,可以提前退出
                    if (res) {
                        break;
                    }
                }
            }
        }
        return res;
    }

    private void dfs(char[][] board, boolean[][] visited, String word, int i, int j, int k) {
        // 不是有效的,直接返回
        if (!isValid(board, visited, word, i, j, k)) {
            return;
        }
        // 到最后一个字符了,与word的相等, 就说明获得了结果,可以返回了
        if (k == word.length()-1) {
            res = true;
            return;
        }
        visited[i][j] = true;
        dfs(board, visited, word, i-1, j, k+1);
        dfs(board, visited, word, i+1, j, k+1);
        dfs(board, visited, word, i, j-1, k+1);
        dfs(board, visited, word, i, j+1, k+1);
        // 回溯
        visited[i][j] = false;
    }

    private boolean isValid(char[][] board, boolean[][] visited, String word,int i, int j, int k) {
        // 1.超出边界
        if (i<0 || i>=board.length) {
            return false;
        }
        else if (j<0 || j>=board[0].length) {
            return false;
        }
         // 2.已经参观过
        else if (visited[i][j]) {
            return false;
        }
        // 3.不是期望的那个字符
        else if (word.charAt(k) != board[i][j]) {
            return false;
        }
        return true;
    }

    private boolean[][] getInitVisited(char[][] board) {
        boolean[][] visited = new boolean[board.length][board[0].length];
        for (int i=0;i<board.length;i++) {
            for (int j=0;j<board[0].length;j++) {
                visited[i][j] = false;
            }
        }
        return visited;
    }
}

 

131. 分割回文串

有点像上面的组合

  • 起始位置是上一层的结束位置 +1
  • 判断是 回文串 之后,再往下 dfs
  • 每一层是 起始位置~结束位置 遍历
  • 当起始位置==s.length() 时添加结果,退出循环

由于每个节点都要判断是否是回文串,所以可以先对整个字符串求出 dp :

boolean dp[i][j] 表示字符串从 i 到 j 是否是回文串:

  • if i==j dp[i][j]=true
  • else if s.charAt(i) != s.charAt(j) dp[i][j]=false;
  • else if j-i==1(如"aa") dp[i][j]=true
  • else dp[i][j]=dp[i+1][j-1]

dp[i][j]=dp[i+1][j-1] 由于需要从左下角的元素推出现在的元素

所以遍历应该是从左下到右上——从下到上&从左到右

即从下到上: i 从 n-1 到 0

从左到右(又j>i):j 从 i 到 n

class Solution {

    List<List<String>> res = null;

    Solution() {
        res = new ArrayList();
    }

    public List<List<String>> partition(String s) {
        boolean[][] dp = getDP(s);
        dfs(s, dp, new LinkedList(), 0);
        return res;
    }

    private void dfs(String s, boolean[][] dp, LinkedList<String> curRes, int startIndex) {
        // startIndex 越界,添加结果集
        if (startIndex==s.length()) {
            res.add(new ArrayList(curRes));
        }
        for (int j0=startIndex;j0<s.length();j0++) {
            //substring 方法i,j不包含j,而其它地方包括dp认为的i,j都是包括j的。
            // 所以这里 substring 的时候要 j0+1
            String substr = s.substring(startIndex, j0+1);
            // 是回文串才往下
            // 和八皇后一样,这里不能在前面循环外面 return,而是符合条件才往下。因外外面没有 j0?
            if (dp[startIndex][j0]) {
                curRes.addLast(substr);
                // 如 aabcb, startIndex是0,j0现在等于1,那么已经分出来了是回文的aa
                // 那么后面就要继续分bcb,即startIndex是j+1,里面循环j+1到length分bcb
                dfs(s, dp, curRes, j0+1);
                curRes.removeLast();
            }
        }
    }


    private boolean[][] getDP(String s) {
        int n=s.length();
        boolean[][] dp = new boolean[n][n];
        // dp[i][j] s的i到j是否是回文
        // dp[i][j] = dp[i+1][j-1];
        // 需要通过左下角的结果推出来,所以遍历顺序应该是从左下到右上
        // i从下到上
        for (int i=n-1;i>=0;i--) {
            // 由于是从i到j,所以j永远是大于i的,所以j从i开始往上增
            // j从左到右
            for (int j=i;j<n;j++) {
                if (i==j) {
                    dp[i][j] = true;
                }
                else if (s.charAt(i) != s.charAt(j)) {
                    dp[i][j] = false;
                }
                // 只有两个元素时,而且此时已经通过了上面的s.charAt(i) != s.charAt(j)
                // 举例,如 aa
                else if (j-i==1) {
                    dp[i][j] = true;
                }
                else {
                    // 需要通过左下角的结果推到出来
                    // 所以 dp 遍历的顺序应该是从左下到右上
                    dp[i][j] = dp[i+1][j-1];
                }
            }
        }
        return dp;
    }
}
 

93. 复原 IP 地址 (和前面分割回文串方法类似,要在一起看)

和前面分割回文串方法类似,要在一起看

class Solution {
    List<String> res = null;

    public Solution() {
        res = new ArrayList();
    }

    public List<String> restoreIpAddresses(String s) {
        dfs(s, new LinkedList(), 0);
        return res;
    }

    private void dfs(String s, LinkedList<String> curRes, int startIndex) {
        // 起始下标超了(说明已经全部划分完了),并且当前结果中已有4个分段
        if (startIndex == s.length() && curRes.size() == 4) {
            // 拼接结果
            StringBuilder sb = new StringBuilder();
            for (String ss : curRes) {
                sb.append(ss).append(".");
            }
            res.add(sb.substring(0, sb.length()-1).toString());
            return;
        }
        // 前面结束了符合结果的加进去了,这些结束了却还没符合结果的,直接返回
        if (curRes.size() >= 4 || startIndex == s.length()) {
            return;
        }
        // end=startIndex 开始
        for (int end=startIndex;end<s.length();end++) {
            // subString 的第二个参数不包括结束字符,所以 end 要+1
            String subs = s.substring(startIndex, end+1);
            // 如果当前划分满足 0~255
            if (isIpSeg(subs)) {
                curRes.addLast(subs);
                // 进行递归,下次开始下标是 endIndex
                dfs(s, curRes, end+1);
                // 回溯,恢复现场
                curRes.removeLast();
            }
        }
    }

    private boolean isIpSeg(String s) {
        // 长度不符合,不用转换成数字,直接就先返回false
        if (s.length()<1 || s.length() > 3) {
            return false;
        }
        // 以0开头的非 0,也不符合,如 02, 023
        if (s.length() > 1 && s.charAt(0) == '0') {
            return false;
        }
        // 长度==3,但是开头数字大于2,还是先返回false
        if (s.length()==3 && s.charAt(0) - '0' > 2) {
            return false;
        }
        Integer a = Integer.valueOf(s);
        if (a>=0 && a<=255) {
            return true;
        }
        return false;
    }
}