跳至主要內容

第15章 图

Weiser算法图的搜索拓扑并查集大约 22 分钟

第15章 图

图的搜索

解题小经验:

  • 如果面试题要求在无权图中找出两个节点之间的最短距离,那么广度优先搜索可能是更适合的算法。
  • 如果面试题要求找出符合条件的所有路径,那么深度优先搜索可能是最合适的算法。

剑指offerⅡ105:最大的岛屿

给定一个由 01 组成的非空二维数组 grid ,用来表示海洋岛屿地图。

一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

找到给定的二维数组中最大的岛屿面积。如果没有岛屿,则返回面积为 0

示例 :

img

方法一:广度优先搜索

class Solution {
    public int maxAreaOfIsland(int[][] grid) {
        int maxArea = 0;
        int row = grid.length;
        int col = grid[0].length;
        boolean[][] visited = new boolean[row][col];
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == 1 && !visited[i][j]) {
                    maxArea = Math.max(maxArea, getArea(grid, visited, i, j));
                }
            }
        }
        return maxArea;
    }

    private int getArea(int[][] grid, boolean[][] visited, int i, int j) {
        Deque<int[]> q = new LinkedList<>();
        q.offer(new int[]{i, j});
        visited[i][j] = true;
        
        int area = 0;
        int[][] dirs = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}};
        
        while (!q.isEmpty()) {
            int[] pos = q.pollFirst();
            area++;
            for (int[] dir : dirs) {
                int r = pos[0] + dir[0];
                int c = pos[1] + dir[1];
                if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length && grid[r][c] == 1 && !visited[r][c]) {
                    q.offer(new int[]{r, c});
                    visited[r][c] = true;
                }
            }
        }
        return area;
    }
}

方法二:深度优先搜索

将方法二代码第26行改成int[] pos = q.pollLast();即可。

方法三:基于递归的深度优先搜索

private int getArea(int[][] grid, boolean[][] visited, int i, int j) {
        int area = 1;
        visited[i][j] = true;
        int[][] dirs = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}};

        for (int[] dir : dirs) {
            int r = i + dir[0];
            int c = j + dir[1];
            if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length && grid[r][c] == 1 && !visited[r][c]) {
                visited[r][c] = true;
                area += getArea(grid, visited, r, c);
            }
        }
        return area;
    }

剑指offerⅡ106:二分图

存在一个 无向图 ,图中有 n 个节点。其中每个节点都有一个介于 0n - 1 之间的唯一编号。

给定一个二维数组 graph ,表示图,其中 graph[u] 是一个节点数组,由节点 u 的邻接节点组成。形式上,对于 graph[u] 中的每个 v ,都存在一条位于节点 u 和节点 v 之间的无向边。该无向图同时具有以下属性:

  • 不存在自环(graph[u] 不包含 u)。
  • 不存在平行边(graph[u] 不包含重复值)。
  • 如果 vgraph[u] 内,那么 u 也应该在 graph[v] 内(该图是无向图)
  • 这个图可能不是连通图,也就是说两个节点 uv 之间可能不存在一条连通彼此的路径。

二分图 定义:如果能将一个图的节点集合分割成两个独立的子集 AB ,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为 二分图

如果图是二分图,返回 true ;否则,返回 false

示例 1:

img
输入:graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
输出:false
解释:不能将节点分割成两个独立的子集,

示例 2:

img
输入:graph = [[1,3],[0,2],[1,3],[0,2]]
输出:true
解释:可以将节点分成两组: {0, 2} 和 {1, 3} 。

分析:

将该图所有节点染成两种颜色,只要每一条边连接的两个节点异色,那么就是二分图。

colors[]为-1代表没有被着色,0和1分别代表两种颜色

方法一:BFS

class Solution {
    public boolean isBipartite(int[][] graph) {
        int[] colors = new int[graph.length];
        Arrays.fill(colors, -1);
        for (int i = 0; i < graph.length; i++) {
            if (colors[i] == -1) {
                //对每个连通子图进行BFS搜索
                if (!setColor(graph, colors, i, 0)) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean setColor(int[][] graph, int[] colors, int i, int color) {
        Deque<Integer> queue = new LinkedList<>();
        queue.offer(i);
        colors[i] = color;
        while (!queue.isEmpty()) {
            int v = queue.pollFirst();
            //判断节点v的每个邻接节点
            for (int neighbour : graph[v]) {
                if (colors[neighbour] == -1) {
                    colors[neighbour] = 1 - colors[v];
                    queue.offer(neighbour);
                } else {
                    if (colors[neighbour] == colors[v]) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
}

方法二:DFS

    //递归地给i染色
    private boolean setColor(int[][] graph, int[] colors, int i, int color) {
        if (colors[i] != -1) {
            return colors[i] == color;
        }
        colors[i] = color;
        for (int neighbour : graph[i]) {
            if (!setColor(graph, colors, neighbour, 1 - color)) {
                return false;
            }
        }
        return true;
    }

剑指offerⅡ107:矩阵中的距离

给定一个由 01 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。

两个相邻元素间的距离为 1

示例 1:

img
输入:mat = [[0,0,0],[0,1,0],[0,0,0]]
输出:[[0,0,0],[0,1,0],[0,0,0]]

示例 2:

img
输入:mat = [[0,0,0],[0,1,0],[1,1,1]]
输出:[[0,0,0],[0,1,0],[1,2,1]]

算法:

class Solution {
    public int[][] updateMatrix(int[][] mat) {
        int row = mat.length;
        int col = mat[0].length;
        int[][] distances = new int[row][col];
        Deque<int[]> queue = new LinkedList<>();
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (mat[i][j] == 0) {
                    queue.offer(new int[]{i, j});
                    distances[i][j] = 0;
                } else {
                    distances[i][j] = Integer.MAX_VALUE;
                }
            }
        }

        int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        while (!queue.isEmpty()) {
            int[] v = queue.pollFirst();
            int distance = distances[v[0]][v[1]];
            for (int[] dir : dirs) {
                int r = v[0] + dir[0];
                int c = v[1] + dir[1];
                if (r >= 0 && r < row && c >= 0 && c < col) {
                    if(distances[r][c] > distance + 1){
                        distances[r][c] = distance + 1;
                        queue.offer(new int[]{r, c});
                    }
                }
            }
        }
        return distances;
    }
}

剑指offerⅡ108:单词演变

在字典(单词列表) wordList 中,从单词 beginWordendWord转换序列 是一个按下述规格形成的序列:

  • 序列中第一个单词是 beginWord
  • 序列中最后一个单词是 endWord
  • 每次转换只能改变一个字母。
  • 转换过程中的中间单词必须是字典 wordList 中的单词。

给定两个长度相同但内容不同的单词 beginWordendWord 和一个字典 wordList ,找到从 beginWordendWord最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。

**分析:**将每个单词看成是图中的一个节点,

单向BFS:

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        int length = 0;
        Deque<String> queue = new LinkedList<>();
        Set<String> notVisited = new HashSet<>(wordList);
        queue.offer(beginWord);
        while (!queue.isEmpty()) {
            int size = queue.size();
            length++;
            for (int i = 0; i < size; i++) {
                String word = queue.pollFirst();
                if (word.equals(endWord)) {
                    return length;
                }
                List<String> neighbors = getNeighbors(word);
                for (String neighbor : neighbors) {
                    if (notVisited.contains(neighbor)) {
                        queue.offer(neighbor);
                        notVisited.remove(neighbor);
                    }
                }
            }
        }
        return 0;
    }

    private List<String> getNeighbors(String word) {
        List<String> neighbors = new ArrayList<>();
        char[] chars = word.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char temp = chars[i];
            for (char ch = 'a'; ch <= 'z'; ch++) {
                if (temp != ch) {
                    chars[i] = ch;
                    neighbors.add(new String(chars));
                }
            }
            chars[i] = temp;
        }
        return neighbors;
    }
}

双向BFS:

下面的算法并没有用到队列,因为从一个set中得到是否包含一个元素,要比队列更快且更方便。

    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Set<String> notVisited = new HashSet<>(wordList);
        if (!notVisited.contains(endWord)) {
            return 0;
        }
        int length = 2;
        Set<String> set1 = new HashSet<>();
        Set<String> set2 = new HashSet<>();
        set1.add(beginWord);
        set2.add(endWord);

        while (!set1.isEmpty() && !set2.isEmpty()) {
            if (set1.size() > set2.size()) {
                Set<String> temp = set1;
                set1 = set2;
                set2 = temp;
            }
            Set<String> set3 = new HashSet<>();
            for (String word : set1) {
                List<String> neighbors = getNeighbors(word);
                for (String neighbor : neighbors) {
                    if (set2.contains(neighbor)) {
                        return length;
                    }
                    if (notVisited.contains(neighbor)) {
                        set3.add(neighbor);
                        notVisited.remove(neighbor);
                    }
                }
            }
            length++;
            set1 = set3;
        }
        return 0;
    }

剑指offerⅡ109:开密码锁

一个密码锁由 4 个环形拨轮组成,每个拨轮都有 10 个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0''0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。

锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。

列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

字符串 target 代表可以解锁的数字,请给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1

示例 :

输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,因为当拨动到 "0102" 时这个锁就会被锁定。

**思路:**这个题和面试题108:单词演变很像,可以对比学习。但需要注意的是,该题统计的是跳转次数,面试题108:单词演变统计的是路径的单词数目。

单向BFS算法:

class Solution {
    public int openLock(String[] deadends, String target) {
        Set<String> dead = new HashSet<>();
        for (String deadend : deadends) {
            dead.add(deadend);
        }
        Set<String> visited = new HashSet<>();
        if (dead.contains("0000") || dead.contains(target)) {
            return -1;
        }
        Deque<String> queue = new LinkedList<>();
        int ans = 0;
        queue.offer("0000");
        visited.add("0000");
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String number = queue.pollFirst();
                if (number.equals(target)) {
                    return ans;
                }
                List<String> neighbors = findNeighbors(number);
                for (String neighbor : neighbors) {
                    if (!visited.contains(neighbor) && !dead.contains(neighbor)) {
                        queue.offer(neighbor);
                        visited.add(neighbor);
                    }
                }
            }
            ans++;
        }
        return -1;
    }

    private List<String> findNeighbors(String number) {
        List<String> neighbors = new ArrayList<>();
        char[] chars = number.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char old = chars[i];

            chars[i] = old == '9' ? '0' : (char) (old + 1);
            neighbors.add(new String(chars));
            chars[i] = old == '0' ? '9' : (char) (old - 1);
            neighbors.add(new String(chars));

            chars[i] = old;
        }
        return neighbors;
    }
}

双向BFS算法:

剑指offerⅡ110:所有路径

给定一个有 n 个节点的有向无环图,用二维数组 graph 表示,请找到所有从 0n-1 的路径并输出(不要求按顺序)。

graph 的第 i 个数组中的单元都表示有向图中 i 号节点所能到达的下一些结点(译者注:有向图是有方向的,即规定了 a→b 你就不能从 b→a ),若为空,就是没有下一个节点了。

示例 :

img
输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3

思路:和[回溯法专题](../#第13章 回溯法)对比学习,回溯法本质上就是DFS

算法:

class Solution {
    public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
        List<List<Integer>> ans = new ArrayList<>();
        LinkedList<Integer> path = new LinkedList<>();
        dfs(0, ans, path, graph);
        return ans;
    }

    private void dfs(int i, List<List<Integer>> ans, LinkedList<Integer> path, int[][] graph) {
        path.add(i);
        if (i == graph.length - 1) {
            ans.add(new LinkedList<>(path));
            return;
        }
        for (int next : graph[i]) {
            dfs(next, ans, path, graph);
            path.removeLast();
        }
    }
}

剑指offerⅡ111:计算除法

给定一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi]values[i] 共同表示等式 Ai / Bi = values[i] 。每个 AiBi 是一个表示单个变量的字符串。

另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。

返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。

**注意:**输入总是有效的。可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。

示例 1:

输入:equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解释:
条件:a / b = 2.0, b / c = 3.0
问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
结果:[6.0, 0.5, -1.0, 1.0, -1.0 ]

示例 2:

输入:equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]
输出:[3.75000,0.40000,5.00000,0.20000]

示例 3:

输入:equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]]
输出:[0.50000,2.00000,-1.00000,-1.00000]

代码:

剑指offerⅡ112:最长递增路径

给定一个 m x n 整数矩阵 matrix ,找出其中 最长递增路径 的长度。

对于每个单元格,你可以往上,下,左,右四个方向移动。 不能对角线 方向上移动或移动到 边界外(即不允许环绕)。

示例 1:

img
输入:matrix = [[9,9,4],[6,6,8],[2,1,1]]
输出:4 
解释:最长递增路径为 [1, 2, 6, 9]
class Solution {
    public int longestIncreasingPath(int[][] matrix) {
        int row = matrix.length;
        int col = matrix[0].length;
        if(row == 0 || col == 0){
            return 0;
        }
        int[][] lengths = new int[row][col];
        int ans = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                int length = dfs(i, j, matrix, lengths);
                ans = Math.max(length, ans);
            }
        }
        return ans;
    }

    private int dfs(int i, int j, int[][] matrix, int[][] lengths) {
        if (lengths[i][j] != 0) {
            return lengths[i][j];
        }
        int length = 1;
        int row = matrix.length;
        int col = matrix[0].length;
        int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
        for (int[] dir : dirs) {
            int r = i + dir[0];
            int c = j + dir[1];
            if (r >= 0 && r < row && c >= 0 && c < col && matrix[r][c] > matrix[i][j]) {
                int path = dfs(r, c, matrix, lengths);
                length = Math.max(path + 1, length);
            }
        }
        lengths[i][j] = length;
        return length;
    }
}

LC79:矩阵中的路径

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

例如,在下面的 3×4 的矩阵中包含单词 "ABCCED"(单词中的字母已标出)。

img
img

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false
class Solution {
    public boolean exist(char[][] board, String word) {
        char[] words = word.toCharArray();
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, words, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean dfs(char[][] board, char[] words, int i, int j, int k) {
        if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || board[i][j] != words[k]) {
            return false;
        }
        if (k == words.length - 1) {
            return true;
        }
        board[i][j] = '1';
        boolean ans = dfs(board, words, i + 1, j, k + 1) ||
                      dfs(board, words, i - 1, j, k + 1) ||
                      dfs(board, words, i, j + 1, k + 1) ||
                      dfs(board, words, i, j - 1, k + 1);
        board[i][j] = words[k];
        return ans;
    }
}

剑指offerⅠ13:机器人的运动范围

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:

输入:m = 2, n = 3, k = 1
输出:3

示例 2:

输入:m = 3, n = 1, k = 0
输出:1

代码:

class Solution {
    public int movingCount(int m, int n, int k) {
        Deque<int[]> queue = new LinkedList<>();
        boolean visited[][] = new boolean[m][n];
        queue.offer(new int[]{0, 0});
        visited[0][0] = true;
        int ans = 0;
        while(!queue.isEmpty()){
            int[] node = queue.pollFirst();
            ans++;
            int[][] dirs = {{0 ,1}, {0, -1}, {1, 0}, {-1, 0}};
            for(int[] dir : dirs){
                int r = node[0] + dir[0];
                int c = node[1] + dir[1];
                if(check(r, c, m, n, k, visited)){
                    queue.offer(new int[]{r, c});
                    visited[r][c] = true;
                }
            }
        }
        return ans;
    }

    private boolean check(int i, int j, int m, int n, int k, boolean[][] visited){
        if(i >= 0 && i < m && j >= 0 && j < n && getSum(i) + getSum(j) <= k && visited[i][j] == false)
            return true;
        return false;
    }

    private int getSum(int num){
        int sum = 0;
        while(num > 0){
            sum = sum + num % 10;
            num = num / 10;
        }
        return sum;
    }
}

剑指offerⅠ14:课程顺序

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:

输入:m = 2, n = 3, k = 1
输出:3

示例 2:

输入:m = 3, n = 1, k = 0
输出:1

拓扑排序

剑指offerⅡ113:课程顺序

现在总共有 numCourses 门课需要选,记为 0numCourses-1

给定一个数组 prerequisites ,它的每一个元素 prerequisites[i] 表示两门课程之间的先修顺序。 例如 prerequisites[i] = [ai, bi] 表示想要学习课程 ai ,需要先完成课程 bi

请根据给出的总课程数 numCourses 和表示先修顺序的 prerequisites 得出一个可行的修课序列。

可能会有多个正确的顺序,只要任意返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

示例 1:

输入: numCourses = 2, prerequisites = [[1,0]] 
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
 因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

示例 3:

输入: numCourses = 1, prerequisites = [] 
输出: [0]
解释: 总共 1 门课,直接修第一门课就可。

代码:

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        Map<Integer, List<Integer>> graph = new HashMap<>();
        for (int i = 0; i < numCourses; i++) {
            graph.put(i, new LinkedList<>());
        }
        int[] inDegrees = new int[numCourses];
        for (int[] prerequisite : prerequisites) {
            graph.get(prerequisite[1]).add(prerequisite[0]);
            inDegrees[prerequisite[0]]++;
        }
        Deque<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegrees[i] == 0) {
                queue.offer(i);
            }
        }
        List<Integer> ans = new ArrayList<>();
        while (!queue.isEmpty()) {
            int node = queue.pollFirst();
            ans.add(node);
            for (int next : graph.get(node)) {
                inDegrees[next]--;
                if (inDegrees[next] == 0) {
                    queue.offer(next);
                }
            }
        }
        return ans.size() == numCourses
                ? ans.stream().mapToInt(i -> i).toArray()
                : new int[0];
    }
}

剑指offerⅡ114:外星文字典

现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。

给定一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经 按这门新语言的字母顺序进行了排序

请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 "" 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。

字符串 s 字典顺序小于 字符串 t 有两种情况:

  • 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t
  • 如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t

示例 1:

输入:words = ["wrt","wrf","er","ett","rftt"]
输出:"wertf"

示例 2:

输入:words = ["z","x"]
输出:"zx"

示例 3:

输入:words = ["z","x","z"]
输出:""
解释:不存在合法字母顺序,因此返回 "" 。

代码:

class Solution {
    public String alienOrder(String[] words) {
        Map<Character, Set<Character>> graph = new HashMap<>();
        Map<Character, Integer> inDegrees = new HashMap<>();
        for (String word : words) {
            for (char ch : word.toCharArray()) {
                graph.put(ch, new HashSet<>());
                inDegrees.put(ch, 0);
            }
        }

        for (int i = 1; i < words.length; i++) {
            String word1 = words[i - 1];
            String word2 = words[i];
            if (word1.startsWith(word2) && !word1.equals(word2)) {
                return "";
            }
            for (int j = 0; j < word1.length() && j < word2.length(); j++) {
                char ch1 = word1.charAt(j);
                char ch2 = word2.charAt(j);
                if (ch1 != ch2) {
                    if (!graph.get(ch1).contains(ch2)) {
                        graph.get(ch1).add(ch2);
                        inDegrees.put(ch2, inDegrees.get(ch2) + 1);
                    }
                    break;
                }
            }
        }

        Deque<Character> queue = new LinkedList<>();
        StringBuilder sb = new StringBuilder();
        for (char ch : graph.keySet()) {
            if (inDegrees.get(ch) == 0) {
                queue.offer(ch);
            }
        }
        while (!queue.isEmpty()) {
            char character = queue.pollFirst();
            sb.append(character);
            for (char next : graph.get(character)) {
                inDegrees.put(next, inDegrees.get(next) - 1);
                if (inDegrees.get(next) == 0) {
                    queue.offer(next);
                }
            }
        }
        return sb.length() == inDegrees.size() ? sb.toString() : "";
    }
}

剑指offerⅡ115:重建序列

请判断原始的序列 org 是否可以从序列集 seqs 中唯一地 重建

序列 org 是 1 到 n 整数的排列,其中 1 ≤ n ≤ 104。重建 是指在序列集 seqs 中构建最短的公共超序列,即 seqs 中的任意序列都是该最短序列的子序列。

示例 1:

输入: org = [1,2,3], seqs = [[1,2],[1,3]]
输出: false
解释:[1,2,3] 不是可以被重建的唯一的序列,因为 [1,3,2] 也是一个合法的序列。

示例 2:

输入: org = [1,2,3], seqs = [[1,2]]
输出: false
解释:可以重建的序列只有 [1,2]。

示例 3:

输入: org = [1,2,3], seqs = [[1,2],[1,3],[2,3]]
输出: true
解释:序列 [1,2], [1,3] 和 [2,3] 可以被唯一地重建为原始的序列 [1,2,3]。

示例 4:

输入: org = [4,1,5,2,6,3], seqs = [[5,2,6,3],[4,1,5,2]]
输出: true

代码:

class Solution {
    public boolean sequenceReconstruction(int[] org, List<List<Integer>> seqs) {
        if (seqs.size() == 0) {
            return false;
        }
        int n = org.length;
        Map<Integer, Set<Integer>> graph = new HashMap<>();
        for (int i = 1; i <= n; i++) {
            graph.put(i, new HashSet<>());
        }
        int[] inDegrees = new int[n];//inDegrees[i] 对应i + 1的入度
        for (List<Integer> seq : seqs) {
            for (int num : seq) {
                if (num < 1 || num > n) {
                    return false;
                }
            }
            for (int i = 1; i < seq.size(); i++) {
                int first = seq.get(i - 1);
                int second = seq.get(i);
                if (!graph.get(first).contains(second)) {
                    graph.get(first).add(second);
                    inDegrees[second - 1]++;
                }
            }
        }
        Deque<Integer> queue = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            if (inDegrees[i] == 0) {
                queue.offer(i + 1);
            }
        }
        int length = 0;
        int[] ans = new int[n];
        while (queue.size() == 1) {
            int first = queue.pollFirst();
            ans[length++] = first;
            for (int next : graph.get(first)) {
                inDegrees[next - 1]--;
                if (inDegrees[next - 1] == 0) {
                    queue.offer(next);
                }
            }
        }
        return Arrays.equals(org, ans);
    }
}

并查集

并查集是一种树型数据结构,用于处理一些不交集的合并及查询问题。

Find:确定元素属于哪一个自己。它可以被用来确定两个元素是否属于同一个子集;

Union:将两个自己合并成同一个集合。

生活中的例子:

  • 小弟->老大
  • 帮派识别
  • 两种优化方式

优化方法一:union的时候,将更深度次深的树的root指向更深的树的root;

优化方法二:这个更常用。每个Union的各个节点只指向root。

剑指offerⅡ116:省份数量

n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

示例 1:

img
img
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

示例 2:

img
img
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

方法一:广度优先搜索

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length;
        boolean[] isVisited = new boolean[n];
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (!isVisited[i]) {
                bfs(isConnected, isVisited, i);
                ans++;
            }
        }
        return ans;
    }

    private void bfs(int[][] isConnected, boolean[] isVisited, int city) {
        Deque<Integer> queue = new LinkedList<>();
        queue.offer(city);
        isVisited[city] = true;
        while (!queue.isEmpty()) {
            int thisCity = queue.pollFirst();
            for (int i = 0; i < isConnected.length; i++) {
                if (isConnected[thisCity][i] == 1 && !isVisited[i]) {
                    queue.offer(i);
                    isVisited[i] = true;
                }
            }
        }
    }
}

方法二:深度优先搜索

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int n = isConnected.length;
        boolean[] isVisited = new boolean[n];
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (!isVisited[i]) {
                dfs(isConnected, isVisited, i);
                ans++;
            }
        }
        return ans;
    }

    private void dfs(int[][] isConnected, boolean[] isVisited, int city) {
        isVisited[city] = true;
        for (int i = 0; i < isConnected.length; i++) {
            if (isConnected[city][i] == 1 && !isVisited[i]) {
                dfs(isConnected, isVisited, i);
            }
        }
    }
}

方法三:并查集

剑指offerⅡ写法:

极客时间写法:

剑指offerⅡ117:相似的字符串

LC 839

如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等,那么称 XY 两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。

例如,"tars""rats" 是相似的 (交换 02 的位置); "rats""arts" 也是相似的,但是 "star" 不与 "tars""rats",或 "arts" 相似。

总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"}{"star"}。注意,"tars""arts" 是在同一组中,即使它们并不相似。形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。

给定一个字符串列表 strs。列表中的每个字符串都是 strs 中其它所有字符串的一个 字母异位词 。请问 strs 中有多少个相似字符串组?

字母异位词(anagram),一种把某个字符串的字母的位置(顺序)加以改换所形成的新词。

示例 1:

输入:strs = ["tars","rats","arts","star"]
输出:2

示例 2:

输入:strs = ["omv","ovm"]
输出:1

提示:

  • 1 <= strs.length <= 300
  • 1 <= strs[i].length <= 300
  • strs[i] 只包含小写字母。
  • strs 中的所有单词都具有相同的长度,且是彼此的字母异位词。

剑指offerⅡ118:多余的边

LC 684

树可以看成是一个连通且 无环无向 图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edgesedges[i] = [ai, bi] 表示图中在 aibi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。

示例 1:

img
img
输入: edges = [[1,2],[1,3],[2,3]]
输出: [2,3]

示例 2:

img
img
输入: edges = [[1,2],[2,3],[3,4],[1,4],[1,5]]
输出: [1,4]

提示:

  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ai < bi <= edges.length
  • ai != bi
  • edges 中无重复元素
  • 给定的图是连通的

剑指offerⅡ119:最长连续序列

LC 128

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

示例 1:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:

输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

提示:

  • 0 <= nums.length <= 104
  • -109 <= nums[i] <= 109

**进阶:**可以设计并实现时间复杂度为 O(n) 的解决方案吗?