算法学习(八)递归和回溯法

这样的算法思想通常都应用在树形问题上

leetcode17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

转换为树

 

 

 

回溯算法,复杂度O(2^n)

class Solution {
    private String letterMap[] = {
        " ",    //0
        "",     //1
        "abc",  //2
        "def",  //3
        "ghi",  //4
        "jkl",  //5
        "mno",  //6
        "pqrs", //7
        "tuv",  //8
        "wxyz" //9
    };
    private ArrayList<String> res = new ArrayList<>();

    public List<String> letterCombinations(String digits) {
        res.clear();
        if (digits.isEmpty()) {
            return res;
        }
        findCombination(digits, 0, "");
        return res;
    }

    //s中保存了此时从digits[0...index-1]翻译得到的一个字母字符串
    //寻找gidits[index]匹配的字母,获得digits[0...index]翻译得到的解
    private void findCombination(String digits, int index, String s) {
        
        if(index == digits.length()){
            res.add(s);
            return;
        }

        Character c = digits.charAt(index);
        String letters = letterMap[c - '0'];
        for(int i = 0 ; i < letters.length() ; i ++){
            findCombination(digits, index+1, s + letters.charAt(i));
        }
        return;
    }
}

相关问题,93,131

93. 复原IP地址

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
输入: "25525511135"
输出: ["255.255.11.135", "255.255.111.35"]

class Solution {
    public List<String> restoreIpAddresses(String s) {
        List<String> res = new ArrayList<>();
        getRes(s, 0, new StringBuilder(), res, 0);
        return res;
    }

    // s:输入字符串
    // start:开始位置
    // temp:已经划分的部分
    // res:保存的解
    // count:划分到第几个部分
    private void getRes(String s, int start, StringBuilder temp, List<String> res, int count) {
        // 剩余部分长度大于剩下的部分都为3位的长度,结束
        // 4-count是剩下几组
        if (s.length() - start > 3 * (4 - count)) {
            return;
        }
        if (start == s.length()) {
            // 如果位置正好到了字符串长度,并且分为4组,加入结果
            if (count == 4) {
                res.add(new String(temp.substring(0, temp.length() - 1)));
            }
            return;
        }

        // 当前超过末位,或者已经到达了 4 部分结束掉
        if (start > s.length() || count == 4) {
            return;
        }

        // 保存的当前的解
        StringBuilder before = new StringBuilder(temp);

        // 加一位数
        temp.append(s.charAt(start) + "" + '.');
        getRes(s, start + 1, temp, res, count + 1);

        // 2位数以上的ip的开头是 0,直接结束
        if (s.charAt(start) == '0') {
            return;
        }

        // 加2位数
        if (start + 1 < s.length()) {
            temp = new StringBuilder(before);// 恢复为之前的解
            temp.append(s.substring(start, start + 2) + "" + '.');
            getRes(s, start + 2, temp, res, count + 1);
        }

        // 加3位数
        if (start + 2 < s.length()) {
            temp = new StringBuilder(before);// 恢复为之前的解
            int num = Integer.parseInt(s.substring(start, start + 3));
            // 3位数要判断是否>255
            if (num >= 0 && num <= 255) {
                temp.append(s.substring(start, start + 3) + "" + '.');
                getRes(s, start + 3, temp, res, count + 1);
            }
        }

    }
}

131. 分割回文串

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
输入: "aab"
输出:
[
["aa","b"],
["a","a","b"]
]

class Solution {
    List<List<String>> res = new ArrayList();

    public List<List<String>> partition(String s) {
        backtrack(0, s, new ArrayList<>());
        return res;
    }

    //回溯
    private void backtrack(int index, String s, List<String> tmp) {
        if (index == s.length()) {
            res.add(new ArrayList(tmp));
            return;
        }
        for (int i = index; i < s.length(); i++) {
            if (isPalidrome(index, i, s)) {
                tmp.add(s.substring(index, i + 1));
                backtrack(i + 1, s, tmp);
                tmp.remove(tmp.size() - 1);
            }
        }
    }

    // 判断是否是回文
    private boolean isPalidrome(int left, int right, String s) {
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

2、回溯算法的应用

排列问题

leetcode46. 全排列

给定一个没有重复数字的序列,返回其所有可能的全排列。
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

 

image.png

 

Perms(nums[0...n-1])={取出一个数字}+Perms(nums{0...n-1}-这个数字]}

class Solution {
    private List<List<Integer>> res = new ArrayList<>();
    private boolean[] used;
    public List<List<Integer>> permute(int[] nums) {
        res.clear();
        if (nums.length == 0) {
            return res;
        }
        used = new boolean[nums.length];
        LinkedList<Integer> p = new LinkedList<>();
       
        generatePermutation(0, nums, p);
        return res;
    }

    //p中保存了一个有index个元素的排列
    //向这个排列的末尾添加低index+1,获得一个有index+1个元素的排列
    private void generatePermutation(int index, int[] nums, LinkedList<Integer> p) {
        if (index == nums.length) {
            res.add((List<Integer>)p.clone());
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (!used[i]) {
                p.addLast(nums[i]);
                used[i] = true;
                generatePermutation(index + 1, nums, p);
                p.removeLast();
                used[i] = false;
            }
        }
        return;
    }
}

47. 全排列 II

class Solution {
    private List<List<Integer>> res = new ArrayList<>();
    private boolean[] used;

    public List<List<Integer>> permuteUnique(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return res;
        }
        // 首先排序,之后才有可能发现重复分支
        Arrays.sort(nums);
        used = new boolean[len];
        findPermuteUnique(nums, 0, new Stack<>());
        return res;
    }

    private void findPermuteUnique(int[] nums, int depth, Stack<Integer> stack) {
        if (depth == nums.length) {
            res.add(new ArrayList<>(stack));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (!used[i]) {
                // 排序以后重复的数一定不会出现在开始,故 i > 0
                // 和之前的数相等,并且之前的数还未使用过,
                // 只有出现这种情况,才会出现相同分支,跳过
                if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                    continue;
                }
                used[i] = true;
                stack.add(nums[i]);
                findPermuteUnique(nums, depth + 1, stack);
                stack.pop();
                used[i] = false;
            }
        }
    }
}


组合问题

leetcode77. 组合

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

 

 

class Solution {
    LinkedList<List<Integer>> res = new LinkedList();

    public List<List<Integer>> combine(int n, int k) {
        res.clear();
        if (n <= 0 || k <= 0 || k > n) {
            return res;
        }
        LinkedList<Integer> c = new LinkedList<>();
        findCombination(n, k, 1, c);
        return res;
    }
    
    //求解C(n,k),当前已经找到的组合存储在c中,需要从start开始搜索新元素
    private void findCombination(int n, int k, int start, LinkedList<Integer> c) {
        if (c.size() == k) {
            res.addLast((List<Integer>) c.clone());
            return;
        }
        //还有k-c.size()个空位,所以,[i...n]中至少要有k-c.size()个元素
        //i最多为n-(k-c.size())+1
        for (int i = start; i <= n-(k-c.size())+1; i++) {
            c.addLast(i);
            findCombination(n, k, i + 1, c);
            c.removeLast();
        }
        return;
    }
}

相关问题39,40,216,78,90,401

39. 组合总和

class Solution {
    private List<List<Integer>> res = new ArrayList<>();
    private int[] candidates;
    private int len;

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        int len = candidates.length;
        if (len == 0) {
            return res;
        }

        // 先对数组排序,可以提前终止判断,因为题目不允许重复
        Arrays.sort(candidates);
        this.len = len;
        this.candidates = candidates;
        findCombinationSum(target, 0, new Stack<>());
        return res;
    }

    //回溯法,residue是减去后剩余的值
    private void findCombinationSum(int residue, int start, Stack<Integer> pre) {
        //减去后正好=0,说明找到了一组
        if (residue == 0) {
            res.add(new ArrayList<>(pre));
            return;
        }
        //遍历,
        //residue - candidates[i] 表示下一轮的剩余,
        //如果下一轮的剩余都小于 0 ,就没有必要进行后面的循环了
        //因为原始数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小
        for (int i = start; i < len && residue - candidates[i] >= 0; i++) {
            pre.add(candidates[i]);
            // 【关键】因为元素可以重复使用,这里递归传递下去的是 i 而不是 i + 1
            findCombinationSum(residue - candidates[i], i, pre);
            //别忘了,要是不符合得把之前的出栈,比如2,2,2,然后1-2=-1,就退出了递归
            pre.pop();
        }
    }
}

40. 组合总和 II

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        int len = candidates.length;
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }
        // 先将数组排序,这一步很关键
        Arrays.sort(candidates);
        findCombinationSum2(candidates, 0, len, target, new Stack<>(), res);
        return res;

    }

    private void findCombinationSum2(int[] candidates, int begin, int len, int residue, Stack<Integer> stack,
            List<List<Integer>> res) {
        if (residue == 0) {
            res.add(new ArrayList<>(stack));
            return;
        }
        for (int i = begin; i < len && residue - candidates[i] >= 0; i++) {
            // 在数组排序的前提下,在这一层的递归中,一个元素只能使用一次
            if (i > begin && candidates[i] == candidates[i - 1]) {
                continue;
            }
            stack.add(candidates[i]);
            // 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
            findCombinationSum2(candidates, i + 1, len, residue - candidates[i], stack, res);
            stack.pop();
        }
    }
}

216. 组合总和 III

class Solution {
    List<List<Integer>> res = new ArrayList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        traceBack(k, n, 1, 0, new Stack<>());
        return res;
    }

    private void traceBack(int k, int n, int start, int sum, Stack<Integer> stack) {
        if (k == 0) {
            if (n == sum) {
                res.add(new ArrayList<>(stack));
            }
            return;
        }

        for (int i = start; i < 10; i++) {
            stack.add(i);
            traceBack(k - 1, n, i + 1, sum + i, stack);
            stack.pop();
        }
    }
}

78. 子集

class Solution {
    List<List<Integer>> res = new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0, res, new Stack<>());
        return res;
    }

    private void backtrack(int[] nums, int index, List<List<Integer>> res, Stack<Integer> stack) {
        res.add(new ArrayList<>(stack));
        for (int i = index; i < nums.length; i++) {
            stack.add(nums[i]);
            backtrack(nums, i + 1, res, stack);
            stack.pop();
        }
    }
}

90. 子集 II

class Solution {
    List<List<Integer>> res = new ArrayList();
    
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums); // 排序
        backtrack(nums, res, 0, new Stack<>());
        return res;
    }
    
    private void backtrack(int[] nums, List<List<Integer>> res, int start, Stack<Integer> stack) {
        res.add(new ArrayList<>(stack));

        for (int i = start; i < nums.length; i++) {
            //这道题又重复元素,需要去重
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }
            stack.add(nums[i]);
            backtrack(nums, res, i + 1, stack);
            stack.pop();
        }
    }
}

3、二维平面上使用回溯法

leetcode79. 单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.

 

 

class Solution {
    //        x-1,y
    // x,y-1  x,y    x,y+1
    //        x+1,y
    private int[][] d = { { -1, 0 }, { 0, -1 }, { 0, 1 }, { 1, 0 } };
    // 盘面上有多少行
    private int m;
    // 盘面上有多少列
    private int n;
    private boolean[][] visited;//被访问过

    public boolean exist(char[][] board, String word) {
        m = board.length;
        if (m == 0) {
            return false;
        }
        n = board[0].length;
        visited=new boolean[m][n];
        char[] wordChar=word.toCharArray();
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[i].length; j++) {
                if (searchWord(board, wordChar, 0, i, j)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    //从board的[startx][starty]开始,寻找word[index...word.length]
    private boolean searchWord(char[][] board, char[] word, int index, int startx, int starty) {
        if (index == word.length - 1) {
            return board[startx][starty] == word[index];
        }
        if (board[startx][starty] == word[index]) {
            visited[startx][starty] = true;
            //从startx,starty出发,向四个方向寻找
            for (int i = 0; i < 4; i++) {
                int newx = startx + d[i][0];
                int newy = starty + d[i][1];
                if (inArea(newx, newy) && !visited[newx][newy]) {
                    if (searchWord(board, word, index + 1, newx, newy)) {
                        return true;
                    }
                }
            }
            visited[startx][starty] = false;//回溯
        }

        return false;
    }

    private boolean inArea(int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n;
    }

}

floodfill算法,一类经典问题
这个算法的本质是深度优先遍历

leetcode200. 岛屿数量

给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
输入:
11110
11010
11000
00000
输出: 1

class Solution {
    private int[][] d = { { -1, 0 }, { 0, -1 }, { 0, 1 }, { 1, 0 } };
    int m, n;
    private boolean[][] visited;//被访问过

    public int numIslands(char[][] grid) {
        m = grid.length;
        if (m == 0) {
            return 0;
        }
        n = grid[0].length;
        visited = new boolean[m][n];
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                //没有被标记过的陆地
                if (grid[i][j] == '1' && !visited[i][j]) {
                    res++;
                    visited[i][j] = true;//将陆地标记
                    dfs(grid, i, j);//从这个陆地开始找,和他相连的陆地都标记上
                }
            }
        }
        return res;
    }

    //从gird[x][y]的位置开始,进行floodfill
    private void dfs(char[][] grid, int x, int y) {
        visited[x][y]=true;//将陆地标记
        for (int i = 0; i < 4; i++) {
            int newx = x + d[i][0];
            int newy = y + d[i][1];
            //保证(x,y)合法,且grid[x][y]是没有被访问过的陆地,这个也是递归终止条件
            if (inArea(newx, newy) && !visited[newx][newy] && grid[newx][newy] == '1') {
                dfs(grid, newx, newy);
            }
        }
        return;
    }
    private boolean inArea(int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n;
    }
}

相关问题,130,417

130. 被围绕的区域

class Solution {

    public void solve(char[][] board) {
        if (board == null || board.length == 0) {
            return;
        }

        // 盘面上有多少行
        int m = board.length;
        // 盘面上有多少列
        int n = board[0].length;
        // 从边界出发先把边界上和 O 连通点找到, 把这些变成 #,然后遍历整个 board 把 O 变成 X, 把 # 变成 O
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 从边缘o开始搜索
                boolean isEdge = (i == 0 || j == 0 || i == m - 1 || j == n - 1);
                if (isEdge && board[i][j] == 'O') {
                    dfs(board, i, j);
                }
            }
        }
        // 然后遍历整个 board 把 O 变成 X, 把 # 变成 O
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                }
            }
        }

    }

    private void dfs(char[][] board, int i, int j) {
        if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || board[i][j] == 'X' || board[i][j] == '#') {
            // board[i][j] == '#' 说明已经搜索过了.
            return;
        }
        board[i][j] = '#';
        dfs(board, i + 1, j);
        dfs(board, i - 1, j);
        dfs(board, i, j + 1);
        dfs(board, i, j - 1);
    }
}

4、回溯法是经典人工智能的基础

leetcode51. N皇后

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。(即任意两个皇后都不能处于同一行、同一列或同一斜线上)

 

 

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],

["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。

 

 

快速判断不合法的情况
竖向:col[i]表示第i列被占用
对角线1:dia1[i]表示在对角线1中,第i个元素被占用
对角线2:dia2[i]表示在对角线2中,第i个元素被占用
可以用横纵坐标相加的值表示对角线1

 

 


可以用横纵坐标相减的值表示对角线2,为了方便用数组表示还要+n-1

 

class Solution {
    private List<List<String>> res = new ArrayList();
    private boolean[] colUsed;// 纵方向
    private boolean[] dia1, dia2;// 2个斜对角线

    public List<List<String>> solveNQueens(int n) {
        res.clear();
        colUsed = new boolean[n];
        dia1 = new boolean[2 * n - 1];
        dia2 = new boolean[2 * n - 1];
        List<Integer> row = new ArrayList();
        putQueen(n, 0, row);
        return res;
    }

    // 尝试在一个n皇后问题中,摆放第index行的皇后位置,结果存在row
    private void putQueen(int n, int index, List<Integer> row) {
        if (index == n) {
            res.add(generateBoard(n, row));
            return;
        }

        for (int i = 0; i < n; i++) {
            // 尝试将第index行的皇后摆放在第i列
            if (!colUsed[i] && !dia1[index + i] && !dia2[index - i + n - 1]) {
                row.add(i);
                colUsed[i] = true;
                dia1[index + i] = true;
                dia2[index - i + n - 1] = true;
                putQueen(n, index + 1, row);
                colUsed[i] = false;
                dia1[index + i] = false;
                dia2[index - i + n - 1] = false;
                row.remove(row.size() - 1);
            }
        }
        return;
    }

    private List<String> generateBoard(int n, List<Integer> row) {
        List<String> list = new ArrayList<>();
        
        for (int i = 0; i < n; i++) {
            String s = "";
            for (int j = 0; j < n; j++) {
                if (j == row.get(i)) {
                    s += "Q";
                } else {
                    s += "."; 
                }
            }
            list.add(s);
        }
       
        return list;
    }

}

相关问题,52,37

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值