代码随想录算法训练营第二十五天 | 491. 非递减子序列、46. 全排列、47.全排列 II、332. 重新安排行程、51. N 皇后、37. 解数独

491. 非递减子序列

题目链接:https://leetcode.cn/problems/non-decreasing-subsequences/description/
文档讲解:https://programmercarl.com/0491.%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97.html
状态:已完成

思路:本题考察的是在无序数组中可能有重复元素的情况下,如何避免结果中出现重复的递增子序列。已知,避免重复的关键是跳过同一层递归中的重复元素。通常,通过对数组排序来简单跳过重复元素,然而,本题求解的递增子序列必须基于原数组获得,因此不能使用数组排序实现跳过重复元素的逻辑,只能使用Set记录已遍历的元素。
时间复杂度:递增子序列数量上限接近于 2 n 2^n 2n,如果将子序列的构建成本简化为 O ( N ) O(N) O(N),那么最坏情况下的时间复杂度为 O ( N ∗ 2 N ) O(N*2^N) O(N2N)
空间复杂度: O ( N ) O(N) O(N)

// 关键考点:数组中可能有重复元素,如何避免结果中出现重复的递增子序列?
// 同一层递归跳过重复元素
class Solution {
    List<List<Integer>> result;

    public List<List<Integer>> findSubsequences(int[] nums) {
        result = new ArrayList();
        Set<Integer> set = new HashSet();
        for(int i=0;i<nums.length-1;i++){
            if(set.contains(nums[i]))
                continue;
            else
                set.add(nums[i]);
            List<Integer> tmpList = new ArrayList();
            tmpList.add(nums[i]);
            backtrack(i+1, nums, tmpList);
        }

        return result;
    }

    public void backtrack(int idx, int[] nums, List<Integer> tmpList){
        if(tmpList.size()>=2)
            result.add(new ArrayList(tmpList));
        
        Set<Integer> set = new HashSet();
        for(int i=idx;i<nums.length;i++){
            if(set.contains(nums[i]))
                continue;
            else
                set.add(nums[i]);
            if(nums[i] >= tmpList.get(tmpList.size()-1)){
                tmpList.add(nums[i]);
                backtrack(i+1, nums, tmpList);
                tmpList.remove(tmpList.size()-1);
            }
        }
    }
}

46. 全排列

题目链接:https://leetcode.cn/problems/permutations/
文档讲解:https://programmercarl.com/0046.%E5%85%A8%E6%8E%92%E5%88%97.html
状态:已完成

思路:使用visited数组标记当前元素的选择情况,在每轮递归中遍历所有未被选择的元素,因此无需传递startIdx
时间复杂度: O ( N ∗ N ! ) O(N*N!) O(NN!)
空间复杂度: O ( N ) O(N) O(N)

class Solution {
    List<List<Integer>> result;

    public List<List<Integer>> permute(int[] nums) {
        result = new ArrayList();
        boolean[] visited = new boolean[nums.length];
        backtrack(nums, visited, new ArrayList());
        return result;
    }

    public void backtrack(int[] nums, boolean[] visited, List<Integer> tmpList){
        if(tmpList.size() == nums.length){
            result.add(new ArrayList(tmpList));
            return;
        }

        for(int i=0;i<nums.length;i++){
            if(visited[i])
                continue;
            tmpList.add(nums[i]);
            visited[i] = true;
            backtrack(nums, visited, tmpList);
            visited[i] = false;
            tmpList.remove(tmpList.size()-1);
        }
    }
}

47.全排列 II

题目链接:https://leetcode.cn/problems/permutations-ii/description/
文档讲解:https://programmercarl.com/0047.%E5%85%A8%E6%8E%92%E5%88%97II.html
状态:已完成

思路

  • 使用visited数组标记当前元素的选择情况,在每轮递归中遍历所有未被选择的元素
  • 未被选择的元素中可能存在重复,为了跳过重复元素避免重复全排列,首先对数组进行排序,然后通过记录同层递归遍历时的上一个元素 l a s t last last,与当前元素进行对比,相同则跳过,反之则选择该元素
    时间复杂度: O ( N ∗ N ! ) O(N*N!) O(NN!)
    空间复杂度: O ( N ) O(N) O(N)
// 数组排序+同层跳重复元素
class Solution {
    List<List<Integer>> result;

    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);				// O(nlogn)
        result = new ArrayList();
        boolean[] visited = new boolean[nums.length];
        backtrack(nums, visited, new ArrayList());
        return result;
    }

    public void backtrack(int[] nums, boolean[] visited, List<Integer> tmpList){
        if(tmpList.size() == nums.length){
            result.add(new ArrayList(tmpList));
            return;
        }
        
        int last = -100;
        for(int i=0;i<nums.length;i++){
            if(visited[i] || nums[i] == last) 
                continue;
            visited[i] = true;
            tmpList.add(nums[i]);
            backtrack(nums, visited, tmpList);
            tmpList.remove(tmpList.size()-1);
            visited[i] = false;
            last = nums[i];
        }
    }
}

332. 重新安排行程

题目链接:https://leetcode.cn/problems/reconstruct-itinerary/description/
文档讲解:https://programmercarl.com/0332.%E9%87%8D%E6%96%B0%E5%AE%89%E6%8E%92%E8%A1%8C%E7%A8%8B.html
状态:需二刷,如何通过设计剪枝避免超时

思路

  • 对列表进行升序排序,使得回溯过程中按照字典序逐个尝试答案
  • 回溯过程中需要针对地点A,搜索所有以A为起点的机票,因此使用Map构建邻接表
    • Value并非简单的String列表,因为需要标记地点是否已经被选择,所以设计了Middle类(String+boolean)记录每个终点的选择情况
  • 回溯算法设计:
    • 参数设计:存储最终路径的列表path,邻接表map,目标路径长度len
    • 退出条件:当path达到目标长度返回true,表示成功找到路径;当Map中不存在以中间点为起点的机票时,返回false进行回溯
    • 单层递归逻辑:针对中间点M,遍历以M为起点的机票列表,选择所有未被选择的地点作为下一站,如果递归返回值为true说明找到了目标路径,继续返回true,反之则进行回溯
      • 由于力扣测试用例的设计,需要增加剪枝策略避免超时:遍历机票列表时,如果前后两个地点相同且均未被选择,当前地点可跳过
// 1.数组排序,先ele1后ele2,按照字典序来升序排序
// 2.针对有序数组,构建HashMap,Key是左节点,Value是右节点列表(含Boolean)
// 3.从JFK开始回溯

class Solution {
    public List<String> findItinerary(List<List<String>> tickets) {
        // 1.数组排序,先ele1后ele2,按照字典序来升序排序
        Collections.sort(tickets, new Comparator<List<String>>(){
            public int compare(List<String> l1, List<String> l2){
                if(l1.get(0).equals(l2.get(0)))
                    return l1.get(1).compareTo(l2.get(1));
                return l1.get(0).compareTo(l2.get(0));
            }
        });
        
        // 2.针对有序数组,构建HashMap,Key是左节点,Value是右节点列表(含Boolean)
        Map<String, List<Middle>> map = new HashMap();
        for(int i=0;i<tickets.size();i++){
            String start = tickets.get(i).get(0);
            String end = tickets.get(i).get(1);

            if(!map.containsKey(start))
                map.put(start, new ArrayList());
            map.get(start).add(new Middle(end));
        }

        List<String> path = new ArrayList();
        path.add("JFK");
        backtrack(path, map, tickets.size()+1);
        return path;
    }
    
    public boolean backtrack(List<String> path, Map<String, List<Middle>> map, int len){
        if(path.size() == len)
            return true;
        if(!map.containsKey(path.get(path.size()-1)))
            return false;
        
        List<Middle> tmpList = map.get(path.get(path.size()-1));
        for(int i=0;i<tmpList.size();i++){
            Middle middle = tmpList.get(i);
            if(middle.used || (i>0 && middle.point.equals(tmpList.get(i-1).point) && !tmpList.get(i-1).used))
                continue;
            path.add(middle.point);
            middle.used = true;
            if(backtrack(path, map, len))
                return true;
            else{
                middle.used = false;
                path.remove(path.size()-1);
            }
        }

        return false;
    }
}

class Middle{
    String point;
    boolean used;

    public Middle(){}

    public Middle(String point){
        this.point = point;
    }

    public Middle(String point, boolean used){
        this.point = point;
        this.used = used;
    }
}

51. N 皇后

题目链接:https://leetcode.cn/problems/n-queens/
文档讲解:https://programmercarl.com/0051.N%E7%9A%87%E5%90%8E.html
状态:已完成

思路:放置规则为每个皇后不同行不同列不同斜线(正&反)

  • 每层递归逻辑:决定第i行的皇后放置在哪一列不违反规则(重复列/重复斜线)
  • 关键:正/反斜线坐标映射
    • 正斜线坐标映射:[i, j] ——> i+j
    • 反斜线坐标映射:[i, j] ——> i+(n-1)-j
    • 注:正反斜线的数组长度为2n-1
  • 使用int数组记录中间结果,arr[i]表示第i行皇后所在列,初始值为-1

时间复杂度:如果规则为不同行不同列,那么所有可能的结果为 O ( N ! ) O(N!) O(N!),构建单个结果的时间复杂度为 O ( N ) O(N) O(N),因此总的时间复杂度为 O ( N ∗ N ! ) O(N*N!) O(NN!),实际的时间复杂度小于这个值
空间复杂度: O ( N ) O(N) O(N)

// 每层递归逻辑:决定第i行的皇后放置在哪一列
// 关键:记录每列放置情况,正斜线放置情况,反斜线放置情况
// 正斜线坐标映射:[i, j] ---> i+j
// 反斜线坐标映射:[i, j] ---> i+(n-1)-j
// 注:正反斜线的数组长度为2n-1
// 使用int数组记录中间结果,arr[i]表示第i行皇后所在列,初始值为-1
class Solution {
    List<List<String>> result;

    public List<List<String>> solveNQueens(int n) {
        result = new ArrayList();
        boolean[] col = new boolean[n];
        boolean[] arrow = new boolean[2*n-1];
        boolean[] dearrow = new boolean[2*n-1];
        int[] tmpCol = new int[n];
        for(int i=0;i<n;i++)
            tmpCol[i] = -1;
        backtrack(0, col, arrow, dearrow, tmpCol, n);
        return result;
    }

    public void backtrack(int row, boolean[] col, boolean[] arrow, boolean[] dearrow, int[] tmpCol, int n){
        if(row == n){
            List<String> tmpResult = new ArrayList();
            char[] res = new char[n];
            for(int i=0;i<n;i++)
                res[i] = '.';
            for(int i=0;i<n;i++){
                res[tmpCol[i]] = 'Q';
                tmpResult.add(new String(res));
                res[tmpCol[i]] = '.';
            }
            result.add(tmpResult);
            return;
        }

        for(int i=0;i<n;i++){
            if(col[i] || arrow[row+i] || dearrow[row+n-1-i])
                continue;
            col[i] = true;
            arrow[row+i] = true;
            dearrow[row+n-1-i] = true;
            tmpCol[row] = i;
            backtrack(row+1, col, arrow, dearrow, tmpCol, n);
            tmpCol[row] = -1;
            col[i] = false;
            arrow[row+i] = false;
            dearrow[row+n-1-i] = false;
        }
    }
}

37. 解数独

题目链接:https://leetcode.cn/problems/sudoku-solver/description/
文档讲解:https://programmercarl.com/0037.%E8%A7%A3%E6%95%B0%E7%8B%AC.html
状态:需二刷,因为忘记处理某个分支导致长时间debug

思路:使用List记录每行的数字,每列的数字,每格的数字集合,通过回溯寻找唯一解

  • 回溯算法:
    • 参数设计:当前下标(x, y),数独数组board,记录每行/列/格的列表
    • 结束条件&返回值类型:一旦抵达坐标(9,0),说明找到唯一解,需要一路返回,因此回溯算法的返回值类型为boolean
    • 单层递归逻辑:
      • 如果当前位置已经存在数字,直接返回下一层递归的返回值
      • 如果当前位置没有数字,遍历1-9,尝试那些满足规则的数字,如果递归返回值为true,则一路返回,反之则回溯并尝试下一个满足规则的数字
// List> 记录每行的数字,每列的数字,每格的数字
// 格的坐标映射:(i,j) ---> (i/3)*3+(j/3)
// 每层递归逻辑:确定(i,j)的数字
// 一旦抵达坐标(9,0),说明找到唯一解,需要一路返回,因此回溯算法的返回值类型为boolean
// 注:对于分支 board[x][y] != '.',直接返回回溯算法即可,不要忘记处理这个分支
class Solution {
    public void solveSudoku(char[][] board) {
        List<List<Integer>> row = new ArrayList();
        List<List<Integer>> col = new ArrayList();
        List<List<Integer>> grid = new ArrayList();
        for(int i=0;i<9;i++){
            row.add(new ArrayList());
            col.add(new ArrayList());
            grid.add(new ArrayList());
        }

        for(int i=0;i<9;i++){
            for(int j=0;j<9;j++){
                row.get(i).add(board[i][j] - '0');
                col.get(j).add(board[i][j] - '0');
                grid.get((i/3)*3+(j/3)).add(board[i][j] - '0');
            }
        }

        backtrack(0, 0, board, row, col, grid);
    }

    public boolean backtrack(int x, int y, char[][] board, List<List<Integer>> row, List<List<Integer>> col, List<List<Integer>> grid){
        if(y == 9){
            x++;
            y = 0;
        }
        if(x == 9)
            return true;

        if(board[x][y] != '.')
            return backtrack(x, y+1, board, row, col, grid);
        else{
            for(int i=1;i<=9;i++){
                if(row.get(x).contains(i) || col.get(y).contains(i) || grid.get((x/3)*3+(y/3)).contains(i))
                    continue;
                board[x][y] = (char)('0'+i);
                row.get(x).add(i);
                col.get(y).add(i);
                grid.get((x/3)*3+(y/3)).add(i);
                if(backtrack(x, y+1, board, row, col, grid))
                    return true;
                grid.get((x/3)*3+(y/3)).remove(grid.get((x/3)*3+(y/3)).size()-1);
                col.get(y).remove(col.get(y).size()-1);
                row.get(x).remove(row.get(x).size()-1);
                board[x][y] = '.';
                
            }
        }
        
        return false;
    }
}

你可能感兴趣的:(代码随想录算法训练营,算法)