硅基计划4.0 算法 BFS最短路问题&多源BFS&拓扑排序


文章目录


1. 最短路问题

我们先来说说什么是最短路问题,我们之前写的BFS代码,里面都是求的一个点到另一个点的最短距离

我们现在谈论的是边权为1的图,比如下面一个例子

一、迷宫离入口最近的出口

题目链接

这道题我们要注意,人站立的地方是不可以当做出口的,只有和边界相连的地方才可以算作出口

因此仅仅需要我们以人物站立的位置为起点,对迷宫中不是墙的位置一层层向外搜索,直到扩散到边界为止(只需要走到边界就好,不需要走出去)

java 复制代码
class Solution {
    int [] x = {0,0,1,-1};
    int [] y = {1,-1,0,0};
    public int nearestExit(char[][] maze, int[] entrance) {
        //如何处理人本身就站在边界上(不算出口)的情况呢
        int height = maze.length;
        int wide = maze[0].length;
        boolean [][] isUse = new boolean[height][wide];
        int startX = entrance[0];
        int startY = entrance[1];
        //统计结果
        int level = 0;
        //起点不算出口
        isUse[startX][startY] = true;
        Queue<Pair<Integer,Integer>> queue = new LinkedList<>();
        queue.offer(new Pair<>(startX, startY));
        //已经添加过的格子不能再添加
        while(!queue.isEmpty()){
            //每扩展一次,先统计一下
            level++;
            //扩展这一层需要几次
            int size = queue.size();
            for(int i = 0;i < size;i++){
                Pair<Integer,Integer> tmp = queue.poll();
                int posx = tmp.getKey();
                int posy = tmp.getValue();
                //如果在这里判断出口,就会导致起点在边界的情况被误判
                //扩展,向外扩展时候才去判断出口
                for(int distance = 0;distance < 4;distance++){
                    int curX = posx+x[distance];
                    int curY = posy+y[distance];
                    if(curX >= 0 && curX < height && curY >= 0 && curY < wide && !isUse[curX][curY] && maze[curX][curY] == '.'){
                        //判断出口
                        if(curX == 0 || curX == height-1 || curY == 0 || curY == wide-1){
                            //代表找到出口了
                            return level;
                        }
                        //此时当前位置才符合扩展需求
                        queue.offer(new Pair<>(curX, curY));
                        //标记
                        isUse[curX][curY] = true;
                    }
                }
            }  
        }
        return -1;
    }
}

二、最小基因变化

题目链接

这一题题意有点难理解,我们来详细解读下

我们要从start基因变换成end基因,并且每一次变化的结果都要在基因库bank中存在才可以,如果无法从start变成end,返回-1

这一题我们来模拟一下,对于以下基因

复制代码
A A C C G G T T  start基因

A A A C G G T A  end基因

此时如果我变换start中的第三个基因,C-->A,此时变化后的结果并不在我们的bank基因库中,因此不能这么变化

改变策略,如果我变换start中的最后一个基因,T-->A,此次变化结果在bank基因库中存在,因此此时我继续变化,变化start基因中的第三个基因,C-->A,此时我的start基因就和end基因重合了,总计变化两次

说白了这种问题就是边权为一的最短路问题,你看,如果我们的start基因是AAAAend基因是TTTT

此时我们AAAA第一个字符A就有四种变化选择A,C,G,T(顺序不可颠倒),同理第二个字符A也有四种变化选择A,C,G,T...


我们来说说一些细节问题

第一,在变化过程中,很可能会重复,比如AAAA->GAAA->AAAA,又回来了,因此我们需要使用哈希表记录我们已经变化过的情况!!

第二,你可能会好奇,因为我们要对整个字符串的每个字符都要变化,如果我AAAA->AAAA,这么变化是不是毫无意呢?其实不会,因为我们有哈希表判断重复情况,因此这种情况会直接略过

第三,请注意我们只有将变化结果在基因库bank中存在的 才能加入到队列中,不存在的就是非法变换

第四,如果我们每次变化后都要去扫描bank数组,是不是太耗时间,因此我们可以提前处理bank数组,把其丢入哈希表

java 复制代码
class Solution {
    //使用向量数组
    char [] change = {'A','C','G','T'};
    public int minMutation(String startGene, String endGene, String[] bank) {
        //标记一种枚举情况是否已经被枚举过
        HashSet<String> isUse = new HashSet<>();
        //预处理bank基因库
        HashSet<String> bankHash = new HashSet<>();
        for(int i = 0;i < bank.length;i++){
            bankHash.add(bank[i]);
        }
        if(startGene.equals(endGene)){
            return 0;
        }
        if(!bankHash.contains(endGene)){
            return -1;
        }
        //统计结果
        int ret = 0;
        //创建队头元素
        Queue<String> queue = new LinkedList<>();
        //起点也要标记
        isUse.add(startGene);
        queue.offer(startGene);
        while(!queue.isEmpty()){
            int size = queue.size();
            for(int level = 0;level < size;level++){
                String tmp = queue.poll();
                //判断是否和题目字符串匹配
                if(tmp.equals(endGene)){
                    return ret;
                }
                //循环改变每个下标值
                char [] tmpArray = tmp.toCharArray();
                //遍历数组每一个下标的值
                for(int i = 0;i < tmpArray.length;i++){
                    //每次枚举4种结果,并保存原有字符
                    char primitive = tmpArray[i];
                    for(int j = 0;j < 4;j++){
                        //如果枚举的是原有字符,直接回退,因为这是一次无意义变化
                        if(change[j] == primitive){
                            continue;
                        }
                        tmpArray[i] = change[j];
                        //看看是不是合法结果
                        String newGene = new String(tmpArray);
                        if(!bankHash.contains(newGene)){
                            continue;
                        }
                        //再看看这个变化是否已经被枚举过
                        if(isUse.contains(newGene)){
                            continue;
                        }
                        //来到这里就代表是一次有效的变化,并且是第一次被枚举到
                        isUse.add(newGene);
                        queue.add(newGene);
                    }
                    //恢复现场
                    tmpArray[i] = primitive;
                }
            }
            ret++;
        }   
        //到了这里说明根本没有结果
        return -1;
    }
}

三、单词接龙

题目链接

这一题和上面那一题一模一样,只不过每次每个字符变化可以在a~z中选择一个,变换后的字符还是要在词典中存在

注意题目中问的是转换过程一共有多少个单词,因此起点本身单词也要算上

java 复制代码
class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        HashSet<String> isUse = new HashSet<>();
        //判断转换前后是否相等
        if(beginWord.equals(endWord)){
            return 1;
        }
        //初始化字典
        HashSet<String> dictionary = new HashSet<>();
        for(int i = 0;i < wordList.size();i++){
            dictionary.add(wordList.get(i));
        }
        //判断最终字符是否在字典中存在
        if(!dictionary.contains(endWord)){
            return 0;
        }
        //接下来再去广度优先搜索
        //起点单词也算一个
        int ret = 1;
        Queue<String> queue = new LinkedList<>();
        queue.offer(beginWord);
        //起点标记
        isUse.add(beginWord);
        while(!queue.isEmpty()){
            int size = queue.size();
            for(int i = 0;i < size;i++){
                String tmp = queue.poll();
                //判断是否是结果字符
                if(tmp.equals(endWord)){
                    return ret;
                }
                char [] tmpArray = tmp.toCharArray();
                for(int j = 0;j < tmpArray.length;j++){
                    //遍历数组每个字符
                    //每个字符遍历26个字母
                    //记录当前下标字符,以便恢复现场
                    char c = tmpArray[j];
                    for(char ch = 'a'; ch <= 'z';ch++){
                        //重复情况返回
                        if(ch == c){
                            continue;
                        }
                        tmpArray[j] = ch;
                        //判断是否是合法结果
                        String newWord = new String(tmpArray);
                        //不在字典中出现
                        if(!dictionary.contains(newWord)){
                            continue;
                        }
                        //已经被枚举过了
                        if(isUse.contains(newWord)){
                            continue;
                        }
                        //接下来就是第一次被枚举,并且有效的变化结果
                        isUse.add(newWord);
                        queue.offer(newWord);
                    }
                    //恢复现场
                    tmpArray[j] = c;
                }
            }
            ret++;
        }
        return 0;
    }
}

四、为高尔夫比赛砍树

题目链接

这一题题意就是我们砍树只能按照从小到大的顺序看,如果不能砍完所有的树,就返回-1

我们举个例子,顺便再讲讲算法原理

你看,这不就是不同起点和终点的最短路问题吗?

我们每次都有一个新的起点和终点,只要走完就好了,顺便再统计层数(步数)

因此我们可以先处理原始数组,根据高度值找到我们要砍树的坐标顺序(映射的值要从小到大)

java 复制代码
class Solution {
    int height;
    int wide;
    public int cutOffTree(List<List<Integer>> forest) {
        height = forest.size();
        wide = forest.get(0).size();
        //我们先要去明确砍树顺序,因此使用数对去映射原数组下标
        List<Pair<Integer,Integer>> cutOffOrder = new ArrayList<>();
        //遍历整个数组,存入下标
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(forest.get(i).get(j) > 1){
                    cutOffOrder.add(new Pair<>(i, j));
                }
            }
        }
        //排序我们的砍树顺序,每次取出这个队列中两个数对
        Collections.sort(cutOffOrder,(a,b) ->{
            return forest.get(a.getKey()).get(a.getValue())-forest.get(b.getKey()).get(b.getValue());
        });
        //接下来我们就按照这个砍树顺序去砍树
        int startX = 0;
        int startY = 0;
        //接收结果
        int ret = 0;
        //遍历砍树顺序
        for(int i = 0;i < cutOffOrder.size();i++){
            int distanceX = cutOffOrder.get(i).getKey();
            int distanceY = cutOffOrder.get(i).getValue();
            int step = bfs(forest,startX,startY,distanceX,distanceY);
            //看结果是不是-1,如果是则代表没办法砍完全部的树,直接返回
            if(step == -1){
                return -1;
            }
            //到这里说明可以正常返回最小路径,添加到结果中
            ret += step;
            //此时更新起点
            startX = distanceX;
            startY = distanceY;
        }
        return ret;
    }

    //同理使用向量数组
    int [] x = {0,0,1,-1};
    int [] y = {1,-1,0,0};

    private int bfs(List<List<Integer>> forest,int startX,int startY,int endX,int endY){
        if(startX == endX && startY == endY){
            //说明本身就在终点,直接返回
            return 0;
        }
        //使用标记数组
        boolean [][] isUse = new boolean[height][wide];
        //统计当前最小路径步数
        int steps = 0;
        Queue<Pair<Integer,Integer>> queue = new LinkedList<>();
        queue.offer(new Pair<>(startX,startY));
        //标记
        isUse[startX][startY] = true;
        //队列操作主逻辑
        while(!queue.isEmpty()){
            int size = queue.size();
            //也就是说我们在使用向量数组的时候,curX和curY判断的是下一层的情况,因此step要提前++
            steps++;
            for(int i = 0;i < size;i++){
                //出队
                Pair<Integer,Integer> tmp = queue.poll();
                int posx = tmp.getKey();
                int posy = tmp.getValue();
                //使用向量数组枚举四个方向
                for(int j = 0;j < 4;j++){
                    int curX = posx+x[j];
                    int curY = posy+y[j];
                    //当前位置要合法,并且不能是障碍物,也必须是第一次遍历
                    if(curX >= 0 && curX < height && curY >= 0 && curY < wide && !isUse[curX][curY] && forest.get(curX).get(curY) != 0){
                        //可以提前判断终点
                        if(curX == endX && curY == endY){
                            return steps;
                        }
                        queue.offer(new Pair<>(curX, curY));
                        isUse[curX][curY] = true;
                    }
                }
            }
        }
        //说明我们从当前起点无法到达终点
        return -1;
    }
}

2. 多源BFS问题

我们之前讲的都是只有一个起点和一个终点,现在我们讲的是有多个起点,一个终点,当然还是有向图哈

如果我们把多源BFS转换成多个单源BFS,效率未免会太低下,因此我们的策略就是把所有起点都只当成一个起点 ,即合并为一个超级起点

我们画个图,顺便讲下原理

一、01矩阵

题目链接

这一题就是求出每个非0单元格的单元格离最近的0的距离

还记得我么正难则反的原则吗,因此我们这一题可以转换成统计每个0单元格到各个陆地单元格距离

因此这一题我们只需要遍历这个表,把所有为0位置的单元格添加进来,即多起点的多源BFS

注意0单元格在结果中也是0,因为起点不算距离

这一题我写了进阶版,就是不使用标记数组,也不是用变量,因为我们每一层的距离都是基于上一层 得来的,因此我么只要把上一层的距离+1即可

复制代码
1 ?         1 2 3
1 ? ?  ---> 1 2 2
1 1 1       1 1 1

这些?就是当前层,1代表上一层,我们计算?值只需要1+1=2即可

java 复制代码
class Solution {
    public int[][] updateMatrix(int[][] mat) {
        //尝试不去使用标记数组
        int height = mat.length;
        int wide = mat[0].length;
        int [][] distance = new int[height][wide];
        //结果数组初始化为-1
        for(int i = 0;i < height;i++){
            Arrays.fill(distance[i], -1);
        }
        //使用队列进行广度优先遍历
        Queue<Pair<Integer,Integer>> queue = new LinkedList<>();
        //再去遍历原数组,先把0的位置填上
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(mat[i][j] == 0){
                    distance[i][j] = 0;
                    queue.offer(new Pair<>(i, j));
                }
            }
        }
        //使用向量数组
        int [] x = {0,0,1,-1};
        int [] y = {1,-1,0,0};
        //开始正式的广度优先搜索
        while(!queue.isEmpty()){
            int size = queue.size();
            for(int i = 0;i < size;i++){
                Pair<Integer,Integer> tmp = queue.poll();
                int posx = tmp.getKey();
                int posy = tmp.getValue();
                for(int d = 0;d < 4;d++){
                    int curX = posx+x[d];
                    int curY = posy+y[d];
                    if(curX >= 0 && curX < height && curY >= 0 && curY < wide && distance[curX][curY] == -1){
                        //获取上一层层数,加1就好
                        distance[curX][curY] = distance[posx][posy]+1;
                        //入队
                        queue.offer(new Pair<>(curX, curY));
                    }
                }
            }
        }
        return distance;
    }
}

二、飞地的数量

题目链接

这一题统计的就是看看那几块陆地(连通块)没有紧挨着边界,统计它们的面积

这一题暴力写法就是扫描边界每一个陆地单元格,来一次BFS或者是DFS,但是这样未免太慢

我们可以这样子,我们遍历整个表的边缘,把所有陆地单元格都加入队列,便利完成后把原单元格的边缘陆地区域全部标记为-1

此时我们再去扫描整个表,剩下的陆地单元格全都是没有紧挨着边缘的陆地单元格(即题目中说的飞地)

这里我给出三种版本

1. 方法一:深度优先搜索DFS

java 复制代码
class Solution {
    int height;
    int wide;
    public int numEnclaves(int[][] grid) {
        //我们反着来,我们可以从边缘的陆地开始向里搜索,把搜索到的都标记
        //剩下地我们再去遍历整个数组,把是陆地但是没有被标记的统计上就好
        height = grid.length;
        wide = grid[0].length;
        //深度优先搜索
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(i == 0 || i == height-1 || j == 0 || j == wide-1){
                    if(grid[i][j] == 1){
                        grid[i][j] = -1;
                        dfs(grid,i,j);
                    }
                }
            }
        }
        //再去扫描最终剩下的1
        int ret = 0;
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(grid[i][j] == 1){
                    ret++;
                }
            }
        }
        return ret;
    }

    //使用向量数组
    int [] x = {0,0,1,-1};
    int [] y = {1,-1,0,0};

    //同理posx表示行,posy表示列
    private void dfs(int [][] grid,int posx,int posy){
        for(int d = 0;d < 4;d++){
            int curX = posx+x[d];
            int curY = posy+y[d];
            if(curX >= 0 && curX < height && curY >= 0 && curY < wide && grid[curX][curY] == 1){
                grid[curX][curY] = -1;
                dfs(grid,curX,curY);
            }
        }
    }
}

2. 方法二:广度优先搜索------单源BFS

java 复制代码
class Solution {
    int height;
    int wide;
    public int numEnclaves(int[][] grid) {
        //我们反着来,我们可以从边缘的陆地开始向里搜索,把搜索到的都标记
        //剩下地我们再去遍历整个数组,把是陆地但是没有被标记的统计上就好
        height = grid.length;
        wide = grid[0].length;
        //先搜索第一行和最后一行
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(i == 0 || i == height-1){
                    if(grid[i][j] == 1){
                        //宽度优先搜索
                        grid[i][j] = -1;
                        bfs(grid,i,j);
                    }
                }
            }
        }
        //再搜索第一列和最后一列
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(j == 0 || j == wide-1){
                    if(grid[i][j] == 1){
                        //宽度优先搜索
                        grid[i][j] = -1;
                        bfs(grid,i,j);
                    }
                }
            }
        }
        //再去扫描最终剩下的1
        int ret = 0;
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(grid[i][j] == 1){
                    ret++;
                }
            }
        }
        return ret;
    }

    //使用向量数组
    int [] x = {0,0,1,-1};
    int [] y = {1,-1,0,0};

    //同理posx代表行,posy代表列
    private void bfs(int [][] grid,int posx,int posy){
        Queue<Pair<Integer,Integer>> queue = new LinkedList<>();
        queue.offer(new Pair<>(posx,posy));
        while(!queue.isEmpty()){
            int size = queue.size();
            for(int i = 0;i < size;i++){
                Pair<Integer,Integer> tmp = queue.poll();
                int nowX = tmp.getKey();
                int nowY = tmp.getValue();
                for(int d = 0;d < 4;d++){
                    int curX = nowX+x[d];
                    int curY = nowY+y[d];
                    if(curX >= 0 && curX < height && curY >= 0 && curY < wide && grid[curX][curY] == 1){
                        //标记
                        grid[curX][curY] = -1;
                        //入队
                        queue.offer(new Pair<>(curX,curY));
                    }
                }
            }
        }
    }
}

3. 方法三:广度优先搜索------多源BFS

java 复制代码
class Solution {
    //使用向量数组
    int [] x = {0,0,1,-1};
    int [] y = {1,-1,0,0};
    public int numEnclaves(int[][] grid) {
        //我们反着来,我们可以从边缘的陆地开始向里搜索,把搜索到的都标记
        //剩下地我们再去遍历整个数组,把是陆地但是没有被标记的统计上就好
        int height = grid.length;
        int wide = grid[0].length;
        //多源BFS
        Queue<Pair<Integer,Integer>> queue = new LinkedList<>();
        //把所有的边缘是1的加入到队列中,第一列和最后一列
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(i == 0 || i == height-1 || j == 0 || j == wide-1){
                    if(grid[i][j] == 1){
                        grid[i][j] = -1;
                        queue.offer(new Pair<>(i,j));
                    }
                }
            }
        }
        //同时进行宽度优先搜索
        while(!queue.isEmpty()){
            int size = queue.size();
            for(int i = 0;i < size;i++){
                Pair<Integer,Integer> tmp = queue.poll();
                int posx = tmp.getKey();
                int posy = tmp.getValue();
                for(int d = 0;d < 4;d++){
                    int curX = posx+x[d];
                    int curY = posy+y[d];
                    if(curX >= 0 && curX < height && curY >= 0 && curY < wide && grid[curX][curY] == 1){
                        //标记
                        grid[curX][curY] = -1;
                        //入队
                        queue.offer(new Pair<>(curX,curY));
                    }
                }
            }
        }
        //再去扫描最终剩下的1
        int ret = 0;
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(grid[i][j] == 1){
                    ret++;
                }
            }
        }
        return ret;
    }
}

三、地图中的最高点

题目链接

这一题给了我们一个矩阵,1是原有的水域,0为原有的陆地

现在呢要求我们返回一个新的表,这个表中0是水域,其他则是陆地高度

说白了就是从原有的水域出发,向陆地扩展高度,每个陆地之间的高度差不能超过1

要想创造最高点,那就是尽量使得每个陆地之间高度差为1

这个不就是我们01矩阵那道题吗,一模一样啊,因此就不多说了

java 复制代码
class Solution {
    public int[][] highestPeak(int[][] isWater) {
        int height = isWater.length;
        int wide = isWater[0].length;
        int [][] ret = new int[height][wide];
        //初始化结果数组,并且进行多源BFS
        for(int i = 0;i < height;i++){
            Arrays.fill(ret[i],-1);
        }
        //使用向量数组
        int [] x = {0,0,1,-1};
        int [] y = {1,-1,0,0};
        //开始广度优先搜索
        Queue<Pair<Integer,Integer>> queue = new LinkedList<>();
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(isWater[i][j] == 1){
                    //标记水域
                    ret[i][j] = 0;
                    //添加水域位置
                    queue.offer(new Pair<>(i,j));
                }
            }
        }
        //开始正式广度优先搜索
        while(!queue.isEmpty()){
            int size = queue.size();
            for(int i = 0;i < size;i++){
                Pair<Integer,Integer> tmp = queue.poll();
                int posx = tmp.getKey();
                int posy = tmp.getValue();
                for(int d = 0;d < 4;d++){
                    int curX = posx+x[d];
                    int curY = posy+y[d];
                    if(curX >= 0 && curX < height && curY >= 0 && curY < wide && ret[curX][curY] == -1){
                        ret[curX][curY] = ret[posx][posy]+1;
                        queue.offer(new Pair<>(curX,curY));
                    }
                }
            }
        }
        return ret;
    }
}

四、地图分析

题目链接

我们把题目解析和方法都画一张图表示

java 复制代码
class Solution {
    public int maxDistance(int[][] grid) {
        //这一题是要求曼哈顿距离,即只能上下左右移动
        //那我们是不是可以反着思路,既然求海洋到最近的陆地的最远距离
        //那是不是就可以转换成从各个陆地到各个海洋方格的最远距离
        //把结果数组(数组中每个元素代表和最近的陆地的距离)填充好后,再去遍历
        //看看有没有最大值,如果整个数组全是0,说明全是陆地,直接返回-1
        int height = grid.length;
        int wide = grid[0].length;
        int [][] ret = new int[height][wide];
        //初始化结果数组
        for(int i = 0;i < height;i++){
            Arrays.fill(ret[i],-1);
        }
        //使用向量数组
        int [] x = {0,0,1,-1};
        int [] y = {1,-1,0,0};
        Queue<Pair<Integer,Integer>> queue = new LinkedList<>();
        //扫描寻找陆地
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(grid[i][j] == 1){
                    //标记
                    ret[i][j] = 0;
                    //入队
                    queue.offer(new Pair<>(i,j));
                }
            }
        }
        //统计结果
        int result = 0;
        //开始宽度优先搜索
        while(!queue.isEmpty()){
            Pair<Integer,Integer> tmp = queue.poll();
            int posx = tmp.getKey();
            int posy = tmp.getValue();
            for(int d = 0;d < 4;d++){
                int curX = posx+x[d];
                int curY = posy+y[d];
                if(curX >= 0 && curX < height && curY >= 0 && curY < wide && ret[curX][curY] == -1){
                    ret[curX][curY] = ret[posx][posy]+1;
                    queue.offer(new Pair<>(curX,curY));
                    result = Math.max(result,ret[curX][curY]);
                }
            }
        }
        return result == 0 ? -1 : result;
    }
}

3. 多源BFS的拓扑排序

什么是拓扑排序呢,我们先来说几个概念


第一个是有向无环图(DAG图)

什么是入度呢?入度就是指的是有多少条边指向当前节点

那什么又是出度呢?就是由这个点发散出去的边有多少条


第二个是顶点活动图(AOV图)和拓扑排序

说白了就是一个有向无环图,每一个顶点代表一个活动或者是事件,用边代表各个姐活动(事件)之间的联系


第三,如何实现拓扑排序

我们可以找到一个活多个入度为0的点,把它添加到队列中

然后弹出这个点,删除这个点发散出去的所有的边

也就是说比如A点连接了B,C,D三个点,此时假设B,C,D三个点入度都是1,此时我们删除与A点关联到点的边,此时B,C,D三个点入度都变成了了,把它们加入队列,一直重复执行这个操作,直到最后图中没有点位置

但是请注意,不一定图中的点都遍历完,比如可能存在一个环

因此我们通过拓扑排序还可以判断一个图中是否存在环


我们以一个题为例,将如何去建造图

一、课程表

题目链接

这一题是使用数字代表的课程,为0~n-1号课程

给出一个数组,[1,0]表示学习顺序是0-->1

因此这一题可以把题意转换成判断题目中建立的图中是否存在环

比如我们的prerequisites数组是这样的,我们画图来讲解

那我们要如何去实现邻接表呢?

我们可以用一个List<List<Integer>> edges(不推荐,如果节点使用字符或者是其他表示)

我们更推荐使用HashMap<Integer,List<Integer>> edges

那我们如何去找到每个节点的入读情况呢,我们可以再搞一个哈希表HashMap<Integer,Integer>,当然此题由于节点使用数字表示的,因此可以去使用int[]表示

java 复制代码
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        //准备容器
        //in表示每个节点的入度数
        int [] in = new int[numCourses];
        //edges表示每个节点出度与之相连的节点
        HashMap<Integer,List<Integer>> edges = new HashMap<>();
        //统计题目给的课程信息,创建图
        for(int i = 0;i < prerequisites.length;i++){
            //start表示两个节点的起始节点,end表示两个节点的终止节点
            int start = prerequisites[i][1];
            int end = prerequisites[i][0];
            //把与起始节点相连的节点全部加入edges中,并且还要判断是不是第一次加入
            if(!edges.containsKey(start)){
                edges.put(start, new ArrayList<>());
            }
            //此时就说明已经创建好了这个key值,此时加入到图中
            edges.get(start).add(end);
            //不要忘记给入度数组也添加,注意是start,因为此时end指向start
            in[end]++;
        }
        //此时可以开始进行宽度优先搜索了,先把所有入度为0的节点号添加
        Queue<Integer> queue = new LinkedList<>();
        for(int i = 0;i < in.length;i++){
            if(in[i] == 0){
                //注意此时不是添加的in[i],而是添加i
                //因为in[i]存的是该节点的入度值,并不是存入度为0的节点号
                queue.offer(i);
            }
        }
        //开始宽度优先搜索
        while(!queue.isEmpty()){
            int node = queue.poll();
            //此时要把与这个节点相关联的出度节点边全部删除
            //说白了就是把与这个点相关联的全部节点入度减去1即可
            //但是首先要判断下这个节点可能没有与其他节点相连,比如最后一个节点
            for(int tmp : edges.getOrDefault(node,new ArrayList<>())){
                //减去入度值
                in[tmp]--;
                //如果此时入度值为0,则表明这号节点可以入队
                if(in[tmp] == 0){
                    queue.offer(tmp);
                }
            }
        }
        //此时遍历入度数组,如果存在值不为0,则表明有环
        //因为如果课程修完,每一门课都被抵消干净了,入度就会全变成0
        //反之如果修不完,就说明有前置课程死活减不下来,队列又空了,入度永远大于0
        for(int tmp : in){
            if (tmp != 0) {
                return false;
            }
        }
        return true;
    }
}

二、课程表II

题目连接

这一题就是在上一题中返回任何一种学习课程的顺序,代码大差不差

java 复制代码
class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        //准备容器
        //统计结果,每个下表代表修课程的顺序,也就是节点的顺序
        int [] ret = new int[numCourses];
        int index = 0;
        //in表示每个节点的入度数
        int [] in = new int[numCourses];
        //edges表示每个节点出度与之相连的节点
        HashMap<Integer,List<Integer>> edges = new HashMap<>();
        //统计题目给的课程信息,创建图
        for(int i = 0;i < prerequisites.length;i++){
            //start表示两个节点的起始节点,end表示两个节点的终止节点,start-->end
            int start = prerequisites[i][1];
            int end = prerequisites[i][0];
            //把与起始节点相连的节点全部加入edges中,并且还要判断是不是第一次加入
            if(!edges.containsKey(start)){
                edges.put(start, new ArrayList<>());
            }
            //此时就说明已经创建好了这个key值,此时加入到图中
            edges.get(start).add(end);
            //不要忘记给入度数组也添加,注意是start,因为此时end指向start
            in[end]++;
        }
        //此时可以开始进行宽度优先搜索了,先把所有入度为0的节点号添加
        Queue<Integer> queue = new LinkedList<>();
        for(int i = 0;i < in.length;i++){
            if(in[i] == 0){
                //注意此时不是添加的in[i],而是添加i
                //因为in[i]存的是该节点的入度值,并不是存入度为0的节点号
                queue.offer(i);
            }
        }
        //开始宽度优先搜索
        while(!queue.isEmpty()){
            int node = queue.poll();
            //此时的课程顺序是一个合法的顺序,把这个顺序加入到结果数组中,即加入节点号
            ret[index] = node;
            //index++表示下一门合法顺序课程的位置
            index++;
            //此时要把与这个节点相关联的出度节点边全部删除
            //说白了就是把与这个点相关联的全部节点入度减去1即可
            //但是首先要判断下这个节点可能没有与其他节点相连,比如最后一个节点
            for(int tmp : edges.getOrDefault(node,new ArrayList<>())){
                //减去入度值
                in[tmp]--;
                //如果此时入度值为0,则表明这号节点可以入队
                if(in[tmp] == 0){
                    queue.offer(tmp);
                }
            }
        }
        //此时遍历入度数组,如果存在值不为0,则表明有环
        //因为如果课程修完,每一门课都被抵消干净了,入度就会全变成0
        //反之如果修不完,就说明有前置课程死活减不下来,队列又空了,入度永远大于0
        //查看我们的index下标是不是到了最后一个位置,如果是,则代表所有课程都被加入了结果数组
        //如果不是则代表有的课程并没有加入,说明存在环
        if(index == numCourses){
            return ret;
        }
        return new int[0];
    }
}

三、火星词典------Hard

题目链接

这一题难就难在题意理解上,并且建图过程也比较考验代码能力

我们地球上的字母顺序是a,b,c....,但是在火星上就不一样,我们需要找到它们星球上的字典顺序

题目中给了一个字典表,我们要在这个表中挖掘出火星上的部分字典顺序


好,我们拿出了字典中任意两个字符,那我们要如何获取字典顺序呢

请注意,题目中的字段顺序是已经排好序了,因此整体上前面一个字符串字典顺序是比后一个字符串的字典顺序大的

比如

复制代码
a b c e g  顺序在前的字符串

a b f e b  顺序在后的字符串

这两个字符串第一个不同的地方c f,由于上面字符串顺序在前,因此我们可以判定c < f,即c在f前面

注意注意,此时后面的e g不用再判断了,我们只需要记录两个字符串的第一个不同的位置的字符顺序

当我们把题目中给的字典全部不重不漏的枚举完毕后,得到部分字符顺序,这就是我们要返回的结果

同时,对于矛盾顺序,比如第一个字符串和第二个字符串比较出字符顺序z < x,但是第二个字符串和第三个字符串比较出字符顺序是x < z,矛盾,这就是非法顺序,直接返回空的结果即可


好,我们现在来看下如何实现,我们就拿这个字典words=["wrt","wrf","er","ett","rftt"]为例

我们不重不漏地比较后,得到了一个顺序

复制代码
w -> e
↘️   ↓
  ↘️ r -> t -> f

此时我们就进行正常的拓扑排序,找到入读为0节点,添加,后面就是重复内容了...


好,我们来讲几个要注意的问题

第一,如何建图。我们可以使用哈希表HashMap<Character,char[]>

但是可能会出现重复情况,比如w < e这个关系已经存在了,如果再次枚举就会有重复,因此我们把char[]换成HashSet<Character>达到去重效果

第二,入度信息如何统计。我们发现我们现在每一个节点都是字符,因此我们要使用哈希表HashMap<Character,Integer>存信息,记得全部初始化成0

第三,收集字符顺序信息,我们可以使用一个指针去扫描,但是注意,有一种特殊情况abc ab(abc在前,ab在后),此时这种就是非法序列,直接返回空结果

但是如果是ab abc(ab在前,abc在后)这种虽然不是非法序列,但是由于共同部分ab相同,并且越界无法比较,因此这种情况在比较中直接跳过就好


这一题非常考验代码能力,建议看了思路后,先自己写一下代码

java 复制代码
class Solution {
    public String alienOrder(String[] words) {
        //信息搜集,使用哈希表进行映射,第一个表示字符
        //第二个表示这个字符后面的字符可以是哪些
        HashMap<Character,HashSet<Character>> order = new HashMap<>();
        //统计入度次数
        HashMap<Character,Integer> in = new HashMap<>();
        //初始化所有字符的入度为0
        for (String word : words) {
            for (char ch : word.toCharArray()) {
                in.putIfAbsent(ch, 0);
            }
        }
        //只比较相邻单词
        for(int i = 0;i < words.length-1;i++){
            String str1 = words[i];
            String str2 = words[i+1];
            char [] strArray1 = str1.toCharArray();
            char [] strArray2 = str2.toCharArray();
            int minLength = Math.min(strArray1.length, strArray2.length);
            int pos = 0;
            while(pos < minLength && strArray1[pos] == strArray2[pos]){
                pos++;
            }
            //此时出循环遇到了第一个不同的字符,后面的字符相不相同不需要管
            //判断是哪种情况出的循环,此时是两个字符串部分相等,但是越界了
            if(pos == minLength){
                //只有长单词在前、短单词在后时才无效(如"abc"在"ab"前)
                if(str1.length() > str2.length()){
                    return "";
                }
                //短单词在前、长单词在后是合法前缀关系,跳过
                continue;
            }
            //此时记录字段顺序,str1在前,str2在后,因此顺序是str1字符 < str2字符
            if(!order.containsKey(strArray1[pos])){
                //说明是第一次建立关系,初始化一下
                order.put(strArray1[pos],new HashSet<Character>());
            }
            //避免重复添加相同边(导致入度重复计算)
            if(!order.get(strArray1[pos]).contains(strArray2[pos])){
                order.get(strArray1[pos]).add(strArray2[pos]);
                //统计入度次数
                in.put(strArray2[pos], in.getOrDefault(strArray2[pos], 0) + 1);
            }
        }
        //统计结果
        StringBuilder ret = new StringBuilder();
        //此时图已经建立好了,此时进行拓扑排序,先统计入度为0的点,进行入队操作
        Queue<Character> queue = new LinkedList<>();
        //遍历哈希表,获取key值
        for(char ch : in.keySet()){
            if(in.get(ch) == 0){
                //统计入队为0的字符
                queue.offer(ch);
            }
        }
        //进行广度优先遍历
        while(!queue.isEmpty()){
            int size = queue.size();
            for(int i = 0;i < size;i++){
                char tmp = queue.poll();
                //添加结果,因为弹出的都是合法结果
                ret.append(tmp);
                //接下来取出与tmp字符相连的所有节点的边减一
                HashSet<Character> wantRemoveOne = order.get(tmp);
                //避免空指针(没有出边的字符,order中无对应key)
                if(wantRemoveOne != null){
                    //遍历哈希表,每个入度值都减去1
                    for(char chs : wantRemoveOne){
                        in.put(chs, in.get(chs) - 1);
                        //如果此时入度是0,直接加入队列
                        if(in.get(chs) == 0){
                            queue.offer(chs);
                        }
                    }
                }
            }
        }
        //判断结果长度是否等于所有字符数(避免遗漏无依赖的字符)
        if(ret.length() != in.size()){
            return "";
        }
        //此时查看in表中是否还有不为0的值,说明存在环,即无法还原出正常序列
        for(char ch : in.keySet()){
            if(in.get(ch) != 0){
                return "";
            }
        }
        return ret.toString();
    }
}

感谢您的阅读,如果你您更好的建议,欢迎指出


最近由于学校要期末考试,因此先暂时不更新文章了QAQ


END

相关推荐
小刘不想改BUG2 小时前
LeetCode 56.合并区间 Java
java·python·leetcode·贪心算法·贪心
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于Java的星星儿童救助帮扶系统为例,包含答辩的问题和答案
java·开发语言
清晓粼溪2 小时前
SpringBoot3-02:整合资源
java·开发语言·spring boot
小尧嵌入式2 小时前
音视频入门基础知识
开发语言·c++·qt·算法·音视频
CoderYanger2 小时前
C.滑动窗口-求子数组个数-越短越合法——3134. 找出唯一性数组的中位数
java·开发语言·数据结构·算法·leetcode
_OP_CHEN2 小时前
【算法基础篇】(二十八)线性动态规划之基础 DP 超详解:从入门到实战,覆盖 4 道经典例题 + 优化技巧
算法·蓝桥杯·动态规划·运筹学·算法竞赛·acm/icpc·线性动态规划
ckhcxy2 小时前
继承和多态(二)
java·开发语言
ndzson2 小时前
从前序与中序遍历序列构造二叉树 与
数据结构·算法
HillVue3 小时前
夸克对话助手,填补了中国版 ChatGPT 的缺口
人工智能·chatgpt·宽度优先·推荐算法