搜索与图论之拓扑排序

搜索与图论之拓扑排序

207. 课程表

课程表三剑客之课程表I[Ladybird]

210. 课程表 II

课程表三剑客之课程表II[Dung Beetle]

269.火星字典

    public String alienOrder(String[] words) {
        Map<Character, Set<Character>> g = new HashMap<>();
        int[] indegree = new int[26];
        boolean isSamePrefix = buildGraph(g, indegree, words);
        if (isSamePrefix) return "";
        return bfs(g, indegree);

    }


    private boolean buildGraph(Map<Character, Set<Character>> g, int[] indegree, String[] words) {
        for (String word : words) for (char w : word.toCharArray()) g.putIfAbsent(w, new HashSet<>());
        boolean isSamePrefix = true;
        for (int i = 1; i < words.length; i++) {
            String fw = words[i - 1], sw = words[i];//前后两个相邻的单词
            int len = Math.min(fw.length(), sw.length());
            int j = 0;
            for (; j < len; j++) {
                if (fw.charAt(j) != sw.charAt(j)) {//两个单词的字符不相等
                    char u = fw.charAt(j), v = sw.charAt(j);//u是出 v是入 u->v
                    if (!g.get(u).contains(v)) {//图里没有的话加入,更新入度
                        g.get(u).add(v);
                        indegree[v - 'a']++;
                    }
                    isSamePrefix = false;
                    break;//之后的已经没有比较意义了
                }
            }
            if (j == len && fw.length() > sw.length()) {
                isSamePrefix = true;
                return isSamePrefix;//处理["abc", "ab"]
            }
        }
        return isSamePrefix;
    }

    private String bfs(Map<Character, Set<Character>> g, int[] indegree) {
        StringBuilder sb = new StringBuilder();
        Queue<Character> q = new LinkedList<>();
        for (char c : g.keySet()) {
            if (indegree[c - 'a'] == 0) {
                q.offer(c);
                sb.append(c);
            }
        }

        while (!q.isEmpty()) {
            char u = q.poll();
//            if (g.get(u) == null || g.get(u).isEmpty()) continue;
            for (char v : g.get(u)) {
                indegree[v - 'a']--;
                if (indegree[v - 'a'] == 0) {
                    q.offer(v);
                    sb.append(v);
                }
            }
        }
        return sb.toString().length() == g.size() ? sb.toString() : "";
    }

444.序列重建

一些case

edge case inputs:

  1. seqs为空 [1], [] ->
  2. both org and seqs 为空: [] , [[]]
  3. seqs 不是两个数,只有一个: [1] , [[1]]
  4. seqs 为空: [1] , [[],[]] ArrayIndexOutOfBoundsException or return true -> 不能用org来初始化map for edges and degree,而要用seqs来做初始化,而且在get degrees里面排除edge cases.
  5. edge的顺序是倒的:[1,2,3] , [[3,2],[2,1]]  ->创建result,最后与org比较是否相同,不同就return false.
  6. edge 多于两个[4,1,5,2,6,3] , [[5,2,6,3],[4,1,5,2]]
  7. edge里面有invalid 的数字: [5,3,2,4,1] , [[5,3,2,4],[4,1],[1],[3],[2,4], [1000000000]] -> 在处理edge时进行判断。
方法1
public boolean sequenceReconstruction(int[] org, int[][] seqs) {
    List<Integer> topoOrder = getTopoOrder(seqs);
    if (topoOrder == null || topoOrder.size() != org.length) return false;
    for (int i = 0; i < org.length; i++) {//比较构建的序列是否唯一
        if (org[i] != topoOrder.get(i)) return false;
    }
    return true;
}


/**
 * 收集topoOrder的序列,在之后与org一一比较
 *
 * @param seqs
 * @return
 */
private List<Integer> getTopoOrder(int[][] seqs) {
    Map<Integer, Set<Integer>> g = buildGraph(seqs);
    Map<Integer, Integer> indegrees = getIndegrees(g);
    List<Integer> topoOrder = new ArrayList<>();
    Queue<Integer> q = new LinkedList<>();
    for (Integer curr : g.keySet()) {
        if (indegrees.get(curr) == 0) {//找入度为0的点
            q.offer(curr);
            topoOrder.add(curr);
        }
    }
    while (!q.isEmpty()) {
        if (q.size() > 1) return null;//当前的q中不止一个,说明不唯一,返回,要求graph里只有一个排列结果
        Integer curr = q.poll();
        for (Integer next : g.get(curr)) {
            indegrees.put(next, indegrees.get(next) - 1);//弹出一个,入度-1
            if (indegrees.get(next) == 0) {
                q.offer(next);
                topoOrder.add(next);
            }
        }
    }
    return topoOrder;
}


/**
 * 构建graph
 * set去重用的 4-> 5  如果出现在不同的seq中,只记录一次
 * 如 [[2,4,5,7],[1,4,5,8]]这种,那么(4,5)就出现了两次
 *
 * @param seqs
 * @return
 */
private Map<Integer, Set<Integer>> buildGraph(int[][] seqs) {
    Map<Integer, Set<Integer>> g = new HashMap<>();
    for (int[] edge : seqs) {//拿到每一条边
        for (int i = 0; i < edge.length; i++) {
            g.putIfAbsent(edge[i], new HashSet<>());
            if (i == 0) continue;//i从0开始的
            g.get(edge[i - 1]).add(edge[i]);//只做前后的两个数字,挨着的
        }
    }
    return g;
}


/**
 * 拿到入度
 *
 * @param g
 * @return
 */
private Map<Integer, Integer> getIndegrees(Map<Integer, Set<Integer>> g) {
    Map<Integer, Integer> indegrees = new HashMap<>();
    for (Integer curr : g.keySet()) {
        indegrees.putIfAbsent(curr, 0);
        for (Integer next : g.get(curr)) {//遍历当前点的所有邻居节点,更新邻居节点的入度
            indegrees.put(next, indegrees.getOrDefault(next, 0) + 1);
        }
    }
    return indegrees;
}
方法2
public boolean sequenceReconstruction(int[] org, int[][] seqs) {
    Map<Integer, Set<Integer>> g = new HashMap<>();
    Map<Integer, Integer> indegrees = new HashMap<>();
    int n = org.length;
    int count = 0;
    for (int[] edge : seqs) {
        count += edge.length;
        if (edge.length >= 1 && outArea(edge[0], 0, n)) return false;
        if (edge.length == 1) {
            g.putIfAbsent(edge[0], new HashSet<>());
            indegrees.put(edge[0], indegrees.getOrDefault(edge[0], 0));
        }
        for (int i = 1; i < edge.length; i++) {
            if (outArea(edge[i], 0, n)) return false;
            int from = edge[i - 1], to = edge[i];
            g.putIfAbsent(from, new HashSet<>());
            if (g.get(from).add(to)) {
                indegrees.put(to, indegrees.getOrDefault(to, 0) + 1);
            }
            indegrees.putIfAbsent(from, 0);
        }
    }
    if (count < n) return false;
    Queue<Integer> q = new LinkedList<>();
    for (int curr : g.keySet()) {
        if (indegrees.get(curr) == 0) q.offer(curr);
    }
    int idx = 0;
    while (!q.isEmpty()) {
        if (q.size() > 1) return false;
        int curr = q.poll();
        if (g.get(curr) == null || g.get(curr).isEmpty()) {
            idx++;
            continue;
        }
        for (int next : g.get(curr)) {
            indegrees.put(next, indegrees.get(next) - 1);
            if (indegrees.get(next) == 0) q.offer(next);
        }
        if (curr != org[idx]) return false;
        idx++;
    }
    return idx == org.length;
}

private boolean outArea(int t, int lower, int upper) {
    return t <= lower || t > upper;
}

802. 找到最终的安全状态

题解链接:图论与搜索之拓扑排序-找到最终的安全状态
理解题意

对于题干:【对于一个起始节点,如果从该节点出发,无论每一步选择沿哪条有向边行走,最后必然在有限步内到达终点,则将该起始节点称作是 安全 的。】

当一个节点,在某个环内,其是不安全的,道理也很简单,如果遇到环,该节点在环上饶了多少圈不得而知,也就无法在固定的K步能走到终点
请添加图片描述

方法1:DFS+三色标记+找环

白色(用 0表示):该节点尚未被访问;
灰色(用 1表示):该节点位于递归栈中,或者在某个环上;
黑色(用 2表示):该节点搜索完毕,是一个安全节点。

public List<Integer> eventualSafeNodes(int[][] graph) {
    List<Integer> res = new ArrayList<>();
    int n = graph.length;
    int[] color = new int[n];
    for (int i = 0; i < n; i++) {
        if (dfs(graph, color, i)) res.add(i);
    }
    return res;
}

public boolean dfs(int[][] graph, int[] color, int x) {
    if (color[x] > 0) return color[x] == 2;
    color[x] = 1;
    for (int y : graph[x]) {
        if (!dfs(graph, color, y)) return false;
    }
    color[x] = 2;
    return true;
}
方法2:DFS+标记+找环
int[][] graph;

public List<Integer> eventualSafeNodes(int[][] graph) {
    this.graph = graph;
    List<Integer> res = new ArrayList<>();
    int n = graph.length;
    boolean[] vis = new boolean[n];
    boolean[] stk = new boolean[n];
    for (int i = 0; i < n; i++) {
        if (!isCyclic(i, vis, stk)) res.add(i);//不是环上的,表示这个节点是安全的
    }
    return res;
}

private boolean isCyclic(int i, boolean[] vis, boolean[] stk) {
    if (stk[i]) return true;
    if (vis[i]) return false;
    stk[i] = true;
    vis[i] = true;
    for (int x : graph[i]) {
        if (isCyclic(x, vis, stk)) return true;
    }
    stk[i] = false;
    return false;
}
方法3:拓扑排序
       public List<Integer> eventualSafeNodes(int[][] graph) {
            int n = graph.length;
            //原图是从u->v 存的反图 v->u
            List<List<Integer>> reverseGraph = new ArrayList<>();
            for (int i = 0; i < n; i++) reverseGraph.add(new ArrayList<>());
            int[] indegrees = new int[n];//入度数组
            for (int u = 0; u < n; u++) {
                for (int v : graph[u]) {
                    reverseGraph.get(v).add(u);
                }
                indegrees[u] = graph[u].length;//u节点原图的出度,即为反图u节点的入度
            }
            Queue<Integer> q = new LinkedList<>();
            for (int u = 0; u < n; u++) {
                if (indegrees[u] == 0) q.offer(u);//将入度为0的节点加入到队列中,该节点是「安全点」
            }
            while (!q.isEmpty()) {
                int v = q.poll();
                for (int u : reverseGraph.get(v)) {//开始遍历q
                    if (--indegrees[u] == 0) q.offer(u);
                }
            }
            List<Integer> res = new ArrayList<>();
            for (int u = 0; u < n; u++) {//入度为0的点为「安全点」
                if (indegrees[u] == 0) res.add(u);
            }

            return res;
        }

1743. 从相邻元素对还原数组

方法1:类拓扑排序
public int[] restoreArray(int[][] edges) {
    int n = edges.length;
    Map<Integer, Set<Integer>> g = new HashMap<>();//构建graph,做无向图
    for (int[] edge : edges) {
        int u = edge[0], v = edge[1];
        g.putIfAbsent(u, new HashSet<>());
        g.putIfAbsent(v, new HashSet<>());
        g.get(u).add(v);
        g.get(v).add(u);
    }
    Set<Integer> vis = new HashSet<>();//控制元素重复访问的set
    int start = 0;//处理当前的点
    int[] ans = new int[n + 1];//结果数组
    int idx = 0;
    for (Integer curr : g.keySet()) {
        if (g.get(curr).size() == 1) {//找一个size为1的
            start = curr;
            vis.add(start);
            ans[idx++] = start;
            break;
        }
    }
    while (vis.size() < n + 1) {
        for (int next : g.get(start)) {//遍历当前点的邻居节点
            if (!vis.contains(next)) {
                vis.add(next);
                ans[idx++] = next;
                start = next;
                break;
            }
        }
    }
    return ans;
}

329. 矩阵中的最长递增路径

方法1:拓扑排序
       int[][] dirs = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
        int R, C;


        public int longestIncreasingPath(int[][] matrix) {
            R = matrix.length;
            C = matrix[0].length;
            int[][] outdegrees = new int[R][C];
            for (int r = 0; r < R; r++) {
                for (int c = 0; c < C; c++) {
                    for (int[] d : dirs) {
                        int nr = r + d[0], nc = c + d[1];
                        if (!inArea(nr, nc)) continue;//顺着方向找出度
                        if (matrix[r][c] < matrix[nr][nc]) outdegrees[r][c]++;
                    }
                }
            }
            Queue<int[]> q = new LinkedList<>();
            for (int r = 0; r < R; r++) {
                for (int c = 0; c < C; c++) {
                    if (outdegrees[r][c] == 0) q.offer(new int[]{r, c});
                }
            }
            int ans = 0;
            while (!q.isEmpty()) {
                ans++;
                int size = q.size();
                for (int i = 0; i < size; i++) {
                    int[] curr = q.poll();
                    int r = curr[0], c = curr[1];
                    for (int[] d : dirs) {
                        int nr = r + d[0], nc = c + d[1];
                        if (!inArea(nr, nc)) continue;
                        if (matrix[nr][nc] < matrix[r][c]) {//逆着方向找
                            outdegrees[nr][nc]--;
                            if (outdegrees[nr][nc] == 0) q.offer(new int[]{nr, nc});
                        }
                    }
                }
            }
            return ans;
        }


        private boolean inArea(int r, int c) {
            return r >= 0 && r < R && c >= 0 && c < C;
        }
方法2:记忆化DFS
int[][] memo;
int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
int m, n;

public int longestIncreasingPath(int[][] matrix) {
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return 0;
    m = matrix.length;
    n = matrix[0].length;
    memo = new int[m][n];
    int res = 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            res = Math.max(res, dfs(matrix, i, j));
            PrintUtils.printMatrix(memo);
        }
    }
    return res;
}

private int dfs(int[][] matrix, int i, int j) {
    if (memo[i][j] != 0) return memo[i][j];
    memo[i][j] += 1;
    for (int[] dir : directions) {
        int nextI = i + dir[0], nextJ = j + dir[1];
        if (inArea(nextI, nextJ) && matrix[i][j] < matrix[nextI][nextJ]) {
            memo[i][j] = Math.max(memo[i][j], dfs(matrix, nextI, nextJ) + 1);
        }
    }
    return memo[i][j];
}


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

1203. 项目管理

方法1
public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {
        List<List<Integer>> groupItem = new ArrayList<>();
        for (int i = 0; i < (n + m); i++) groupItem.add(new ArrayList<>());
        //组间与组内的依赖图
        List<List<Integer>> groupGraph = new ArrayList<>();
        for (int i = 0; i < (n + m); i++) groupGraph.add(new ArrayList<>());
        List<List<Integer>> itemGraph = new ArrayList<>();
        for (int i = 0; i < n; i++) itemGraph.add(new ArrayList<>());
        //组间与组内的入度数组
        int[] groupDegree = new int[n + m];
        int[] itemDegree = new int[n];
        List<Integer> id = new ArrayList<>();
        for (int i = 0; i < n + m; i++) id.add(i);
        int leftId = m;
        //给未分配的item分配一个groupId
        for (int i = 0; i < n; i++) {
            if (group[i] == -1) {
                group[i] = leftId++;
            }
                groupItem.get(group[i]).add(i);
        }
        //依赖关系图
        for (int i = 0; i < n; i++) {
            int currGroupId = group[i];
            for (int item : beforeItems.get(i)) {
                int beforeGroupId = group[item];
                if (beforeGroupId == currGroupId) {
                    itemDegree[i] += 1;
                    itemGraph.get(item).add(i);
                } else {
                    groupDegree[currGroupId] += 1;
                    groupGraph.get(beforeGroupId).add(currGroupId);
                }
            }
        }
        //组间拓扑关系排序
        List<Integer> groupTopSort = topSort(groupDegree, groupGraph, id);
        // for(int item : groupTopSort){
        //             System.out.printf("%d ",item );
        // }
        if (groupTopSort.size() == 0) return new int[0];
        int[] ans = new int[n];
        int index = 0;
        for (int currGroupId : groupTopSort) {
            int size = groupItem.get(currGroupId).size();
            if (size == 0) continue;
            List<Integer> res = topSort(itemDegree, itemGraph, groupItem.get(currGroupId));
            if (res.size() == 0) return new int[0];
            for (int item : res) ans[index++] = item;
        }
        // for(int item : ans){
        //           System.out.printf("%d ",item );
        // }
        return ans;
    }

    private List<Integer> topSort(int[] degree, List<List<Integer>> graph, List<Integer> items) {
        Queue<Integer> queue = new LinkedList<>();
        for (int item : items) {
            if (degree[item] == 0) queue.offer(item);
        }
        List<Integer> res = new ArrayList<>();
        while (!queue.isEmpty()) {
            int u = queue.poll();
            res.add(u);
            for (int v : graph.get(u)) {
                if (--degree[v] == 0) queue.offer(v);
            }
        }
            for(int item : res){
                  System.out.printf("%d ",item );
        }
        return res.size() == items.size() ? res : new ArrayList<>();
    }

剑指 Offer II 114. 外星文字典

同269题

注意题意中这两点的描述

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

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

实际举例:

以下两种情况不存在合法字母顺序:

  • 字母之间的顺序关系存在由至少 22 个字母组成的环,例如words=[“a",“b",“a"];

  • 相邻两个单词满足后面的单词是前面的单词的前缀,且后面的单词的长度小于前面的单词的长度,例如 words=[“ab",“a"]。

方法1:拓扑排序+BFS
        Map<Character, List<Character>> edges = new HashMap<>();//字符间边的关系
        Map<Character, Integer> indegrees = new HashMap<>();//统计某个字符的入度
        boolean valid = true;//判断是否需要提前退出

        public String alienOrder(String[] words) {
            //建图,完成拓扑排序的准备工作
            int n = words.length;
            for (String word : words) {
                for (char c : word.toCharArray()) {
                    edges.putIfAbsent(c, new ArrayList<>());//给每个字符添加一个相邻边
                }
            }
            for (int i = 1; i < n && valid; i++) {
                addEdge(words[i - 1], words[i]);
            }
            if (!valid) return "";
            //bfs
            Queue<Character> q = new LinkedList<>();
            for (char u : edges.keySet()) {//将入度为0的字符加入到队列中
                if (!indegrees.containsKey(u)) {
                    q.offer(u);
                }
            }
            StringBuilder sb = new StringBuilder();//记录弹出的顺序
            while (!q.isEmpty()) {
                char u = q.poll();
                sb.append(u);
                for (char v : edges.get(u)) {//遍历u的邻居
                    indegrees.put(v, indegrees.get(v) - 1);
                    if (indegrees.get(v) == 0) {//入度为0后,该节点转入队列中
                        q.offer(v);
                    }
                }
            }
            //["z","x","a","zb","zx"]
            //对于前四个字符串 排序是zxab 但是来了zx后 x又得排序到b之后,但是b之前已经出现了x,是矛盾的
            //这个case范围的是"" 如果不加上判断,返回的是"b", "b"的邻居"x"在入度减一后并没有立马减少为0,bfs提前结束
            return sb.length() == edges.size() ? sb.toString() : "";
        }


        public void addEdge(String prev, String cur) {
            int m = prev.length(), n = cur.length();
            int len = Math.min(m, n);
            int i = 0;
            for (; i < len; i++) {
                char u = prev.charAt(i), v = cur.charAt(i);
                if (u != v) {
                    if (u == 'b') {
                        System.out.println();
                    }
                    edges.get(u).add(v);
                    indegrees.put(v, indegrees.getOrDefault(v, 0) + 1);
                    break;
                }
            }
            //如果"abc" "ab"的这种case,提前返回""
            if (i == len && m > n) {
                valid = false;
            }
        }
方法2:拓扑排序+DFS

官方dfs拓扑排序的思路:

由于拓扑排序的顺序和搜索完成的顺序相反,因此需要使用一个栈存储所有已经搜索完成的节点。深度优先搜索的过程中需要维护每个节点的状态,每个节点的状态可能有三种情况:「未访问」、「访问中」和「已访问」。初始时,所有节点的状态都是「未访问」。

每一轮搜索时,任意选取一个「未访问」的节点 u,从节点 u 开始深度优先搜索。将节点 u的状态更新为「访问中」,对于每个与节点 u 相邻的节点 v,判断节点 v 的状态,执行如下操作:

  • 如果节点 v的状态是「未访问」,则继续搜索节点 v;

  • 如果节点 v 的状态是「访问中」,则找到有向图中的环,因此不存在拓扑排序;

  • 如果节点 v 的状态是「已访问」,则节点 v 已经搜索完成并入栈,节点 u 尚未入栈,因此节点 u 的拓扑顺序一定在节点 v 的前面,不需要执行任何操作。

     static final int VISITING = 1, VISITED = 2;
        Map<Character, List<Character>> edges = new HashMap<>();//字符间边的关系
        Map<Character, Integer> states = new HashMap<>();//统计某个字符的状态
        boolean valid = true;
        char[] paths;
        int index;

        public String alienOrder(String[] words) {
            //建图,完成拓扑排序的准备工作
            int n = words.length;
            for (String word : words) {
                for (char c : word.toCharArray()) {
                    edges.putIfAbsent(c, new ArrayList<>());//给每个字符添加一个相邻边
                }
            }
            for (int i = 1; i < n && valid; i++) {
                addEdge(words[i - 1], words[i]);
            }
            if (!valid) return "";
            //dfs
            paths = new char[edges.size()];
            index = edges.size() - 1;
            for (char u : edges.keySet()) {
                if (!states.containsKey(u)) {
                    dfs(u);
                }
            }
            if (!valid) return "";
            return String.valueOf(paths);
        }

        private void dfs(char u) {
            states.put(u, VISITING);//当前节点标记为「访问中」
            for (char v : edges.get(u)) {
                if (!states.containsKey(v)) {//节点v没有被访问
                    dfs(v);//继续遍历v
                    if (!valid) return;//如果发现有不符合条件的 ,提前结束
                } else if (states.get(v) == VISITING) //第二次进入v,说明有环
                {
                    valid = false;
                    return;
                }
            }
            states.put(u, VISITED);//u这个节点是安全的,标记为「已访问」
            paths[index--] = u;//记录u在栈的位置
        }


        public void addEdge(String prev, String cur) {
            int m = prev.length(), n = cur.length();
            int len = Math.min(m, n);
            int i = 0;
            for (; i < len; i++) {
                char u = prev.charAt(i), v = cur.charAt(i);
                if (u != v) {
                    edges.get(u).add(v);
                    break;
                }
            }
            if (i == len && m > n) {
                valid = false;
            }
        }

1203 sort items by groups respecting dependencies

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值