【ONE·基础算法 || BFS扩展问题】

在这里插入图片描述

总言

  主要内容:编程题举例,学习BFS解决单源最短路径、BFS解决多源最短路径(多源BFS)、BFS解决拓扑排序问题(拓扑排序相关知识)。
  
  
  
  


  
  
  
  

1、BFS 解决单源最短路问题

  1、关于图的BFS遍历思想,详情见博文:图(graph):广度优先遍历
在这里插入图片描述
  
  
  
  2、BFS解决单源最短路径:若图G=(V,E)为非带权图,定义从顶点u到顶点v的最短路径d(u,v),为从uv的任意一条路径中最少的边数。若从uv没有路径,则d(u,v)= ∞

  虽然BFS(广度优先搜索)算法通常用于遍历或搜索图,但对于上述定义的非带权图的单源最短路径问题(无权图,所有边权重都相同,可以认为是权重为1),可以使用使用BFS可求解,这是由广度优先搜索总是按照距离由近到远来遍历图中每个顶点的性质决定的。
  
  
  
  
  
  

1.1、迷宫中离入口最近的出口(medium)

  题源:链接

在这里插入图片描述

  
  

1.1.1、题解

  1)、思路分析
  问题转换:由于BFS遍历的顺序起点到每格的距离递增的顺序相同,因此可以使用BFS,计算从入口到任意出口的最短路径步数。只需要在搜索过程中记录经过的步数即可。在这里插入图片描述

  如何统计整体需要的步数? 可将步数初始化为 0,表示尚未访问任何位置。在BFS每层遍历时,将步数加 1。
  
  如何确定迷宫出口? 根据题目,出口格子无非在最上、最下、最左、最右四条边。因此,可在BFS向四周扩展时,判断一下待入队格子(设其为(x,y))是否为边界格子:x == 0 || x == m-1 || y == 0 || y == n-1。若满足条件,则说明已经走到边界出口处,返回此时的步数;否则,当整个队列为空BFS结束,表明没有出口。
  
  
  2)、题解
  

class Solution {
    bool visited[101][101];// 标记数组
    int dx[4]={-1,1,0,0};
    int dy[4]={0,0,-1,1};
public:
    int nearestExit(vector<vector<char>>& maze, vector<int>& entrance) {
        int m = maze.size();
        int n = maze[0].size();
        
        queue<pair<int,int>> q;// 队列:用于辅助BFS遍历(这里存的是矩阵下标)
        q.push({entrance[0],entrance[1]});// 先将迷宫入口入队
        visited[entrance[0]][entrance[1]] = true;// 标记
        int shortpath = 0;
        while(q.size())//队列不为空时
        {
            shortpath++;
            int levelsize = q.size();// 统计当前层的个数
            for(int i = 0; i < levelsize; ++i)// 每次只能出一层
            {
                auto [a,b] = q.front();// 取队头元素(下标)
                q.pop();
                // 将四个方位中,符合条件的方位入队
                for(int k = 0; k < 4; ++k)
                {
                    int x = a + dx[k];
                    int y = b + dy[k];
                    if(x >= 0 && x < m && y >=0 && y < n && maze[x][y] == '.' && !visited[x][y])
                    {
                        // 判断当前选中的(x,y)方位是否为出口
                        if(x == 0 || x == m-1 || y == 0 || y == n-1) return shortpath;

                        q.push({x,y});// 入队
                        visited[x][y] = true;// 标记

                    }
                }
            }

        }
        return -1;// 找不到出口
    }
};

  
  
  
  
  
  
  

1.2、最小基因变化(medium)

  题源:链接

在这里插入图片描述

  
  

1.2.1、题解

  1)、思路分析
  问题转换: 首先要清楚题目考察的是什么。如果将每次字符串的变换抽象成图中的两个顶点和一条边的话,问题就变成了边权为1的最短路问题。因此,可以从起始的字符串开始,来一次BFS即可。

在这里插入图片描述

  如何进行路径选择? 根据题目要求,碱基间的变化无非是ACGT四种形式, 而只有在基因库中的变化,才是一次有效变化。因此,对给定序列,从序列的首端碱基到序列的末端碱基,穷举出每个碱基的变化情况(除自身外,一个碱基可变化为3种形式,这里不考虑生物学上碱基配对的规则,直接暴力穷举)。对每次变化结果,判断其是否有效(是否在基因库中)。当其有效时,再加入队列,进行后续层的BFS(第二、第三、第n次变化)。

在这里插入图片描述

  
  
  2)、题解

class Solution {
public:
    int minMutation(string startGene, string endGene, vector<string>& bank) {
        if (startGene == endGene) return 0;
        if (!bank.size()) return -1;

        unordered_set<string> visited;
        unordered_set<string> hashbank(bank.begin(),bank.end()); // 用于统计基因库

        string change = "ACGT";
        queue<string> q;           // BFS使用的队列
        q.push(startGene);         // 入队
        visited.insert(startGene); // 标记

        int change_size = 0; // 统计变化次数
        while (q.size()) 
        {
            ++change_size;// 每一层统计一次
            int queue_size = q.size();// 当前层队列元素总数

            while (queue_size--) 
            {
                string gene = q.front(); // 出队
                q.pop();

                // 对序列从头到尾重新遍历,穷举每个位置,碱基突变成每种碱基的情况
                int length = gene.size();
                for (int i = 0; i < length; ++i) 
                {
                    string tmp = gene;
                    for (int j = 0; j < 4; ++j) 
                    {
                        tmp[i] = change[j];
                        if (hashbank.count(tmp) && !visited.count(tmp)) // 当前碱基改变结果在基因库中,且未被历史标记过
                        {
                            if (tmp == endGene) return change_size;
                            visited.insert(tmp); // 标记
                            q.push(tmp);
                        }
                    }
                }
            }
        }
        return -1;
    }
};

  
  
  
  
  
  
  

1.3、单词接龙(hard)

  题源:链接

在这里插入图片描述

  
  

1.3.1、题解

  1)、思路分析
  此题思路同上一题,区别在于上一题只变化四种形式(ACGT),这题是变化26个小写字母。

  2)、题解

class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {


        unordered_set<string> hashword(wordList.begin(),wordList.end());// 单词表的哈希隐射
        unordered_set<string> visited;// 用于标记出现在转换序列中的单词

        if(!hashword.count(endWord)) return 0;// endword不再字典中,无法转换
        if(beginWord == endWord) return 0;// 无需转换

        queue<string> q;// BFS使用队列
        q.push(beginWord);// 入队
        visited.insert(beginWord);// 标记

        int wordnum = 1;
        while(!q.empty())
        {
            ++wordnum;// 每层,统计一次
            int queuesize = q.size();// 单层数目
            while(queuesize--)// 一层一层出队
            {
                string word = q.front();// 出队
                q.pop();

                // 对序列中的每一位字母,从a~z变化单词,判断变换后单词是否出现在字典中(是否是一次有效变化)
                int size = word.size();
                for(int i = 0; i < size; ++i)
                {
                    string tmp = word;
                    for(char j = 'a'; j <= 'z'; ++j)
                    {
                        tmp[i] = j;
                        if(hashword.count(tmp) && !visited.count(tmp))
                        {
                            if(tmp == endWord) return wordnum;
                            q.push(tmp);
                            visited.insert(tmp);
                        }
                    }
                }
            }
        }
        return 0;

    }
};

  
  
  
  
  
  
  

1.4、为高尔夫比赛砍树(hard)

  题源:链接

在这里插入图片描述
  
  

1.4.1、题解

  1)、思路分析
  分析题目,题目要求从 (0, 0) 点开始,按照树的高度从低向高砍掉所有的树。 而forest矩阵中树的高度是随机分布的,因此, 我们可以先统计一下该森林中树存在的位置(矩阵下标),并对每棵树进行排序(排升序)。
在这里插入图片描述

  之后,我们就可按照排序后树的高度进行砍树:
  ①起点位置为(0,0),假设当前最小树高度在(x1,y1)位置处,这意味着我们需要求出从(0,0)→(x1,y1)的最少步数(最短路径);
  ②之后设最小高度树在(x2,y2)位置,则要求出(x1,y1)→(x2,y2)的最少步数(最短路径);
  ……
  如此,上述一个个子问题,实则就是1.1中的迷宫问题,求入口到出口的最短距离,就可以使用BFS来解决。
在这里插入图片描述

  
  
  2)、题解

class Solution {
    int m,n;

public:
    int cutOffTree(vector<vector<int>>& forest) {
        // 1、找出存在树的位置
        vector<pair<int,int>> tree;// 存储树的下标
        m = forest.size(), n = forest[0].size();// 森林(矩阵)的行、列
        for(int i = 0; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(forest[i][j] > 1)// (i,j)下标位置处非障碍,有树
                {
                    tree.push_back({i,j});
                }
            }
        }

        // 2、排序树的高度:sort默认排升序,这里Compare comp传入的是lambda表达式
        sort(tree.begin(), tree.end(), [&](const pair<int,int>&a,const pair<int,int>&b) 
        {
            return forest[a.first][a.second] < forest[b.first][b.second];
        });


        // 3、从(x1,y1)到(x2,y2),找树与树之间的最短路径:转换为迷宫问题
        int x1 = 0, y1 = 0;
        int allsteps = 0;
        for(auto& [x2,y2] : tree)
        {
            int tmp = BFS(forest,x1,y1,x2,y2);
            if(tmp == -1) return -1;// 若某一次无法到达,说明砍树无解
            allsteps += tmp;
            x1 = x2, y1 = y2;// 迭代更新
        }

        return allsteps;

    }

    int dx[4] = {1,-1,0,0}; // 用于BFS向四周遍历
    int dy[4] = {0,0,1,-1};
    bool visited[51][51];// 标记数组

    int BFS(vector<vector<int>>& forest, int x1, int y1, int x2, int y2)
    {
        if(x1==x2 && y1==y2) return 0;// 有可能起始位置就有树
        memset(visited,0,sizeof(visited));// 每次BFS找(x1,y1)到(x2,y2),要清除先前数据
        queue<pair<int,int>> q;// 用于BFS的队列
        q.push({x1,y1}); // 入队
        visited[x1][y1] = true; // 标记

        int countsteps = 0;
        while(!q.empty())
        {
            ++countsteps;
            int size = q.size();
            while(size--)
            {
                auto [a,b] = q.front();// 出队
                q.pop();

                for(int k = 0; k < 4; ++k)
                {
                    int x = a + dx[k], y = b + dy[k];
                    if(x >=0 && x < m && y >=0 && y < n && forest[x][y] && !visited[x][y])
                    {
                        if(x == x2 && y == y2) return countsteps;// 已经到达当前(x2,y2)位置,返回路径长度
                        q.push({x,y});// 入队
                        visited[x][y] = true;// 标记
                    }
                }
            }
        }
        return -1;// 无法到达

    }
};

  
  
  
  
  
  
  
  

2、多源最短路径问题(多源BFS)

  何谓多源BFS: 这里我们讲的仍旧是无向图,或者说权值相同(权值均为1)的无向图。如果说单源最短路径只有一个源点(终点),那么多源最短路径是指找出图中任意两个顶点之间的最短路径。
在这里插入图片描述

  
  如何使用BFS解决多源最短路径:
  ①一种方法是,暴力进行n次BFS,将多源最短路问题转化成若干个单源最短路问题。但这种方法大概率是会超时。
在这里插入图片描述

  
  ②另一种方法:把所有的源点当成一个"超级源点",那么问题就转化为了一个单源最短路径问题。从 “超级源点”到任何顶点的最短路径长度,就等于原始图中从某个源点到该顶点的最短路径长度(如果原始图中存在这样的路径)。
在这里插入图片描述
  1、关于这种方法有相关证明,可自行查阅。
  2、如何写代码?(在队列中,宏观情况如下)
在这里插入图片描述

  
  
  
  
  
  
  

2.1、01矩阵(medium)

  题源:链接

在这里插入图片描述
  
  

2.1.1、题解

  1)、思路分析
  思路一: 单源BFS走N遍,对每个位置,都来一次BFS,一个位置一个位置的求解。这种情况下,大概率会超时。
  
  思路二: 多源BFS+正难则反。将所有起始点当成一个超级源点,都入队,然后进行BFS,这种情况下,我们只需要一次BFS即可完成求解。相比于思路一会快很多。
  但这里有一个问题,0、1矩阵,选谁做起点?
  按照题目要求,mat 中填的是对应下标位置的元素到最近的 0 的距离。那么以1作为起点,0作为终点看似顺理成章,实则这种写法存在一定缺陷。
在这里插入图片描述

  因此,不如选择以0作为起点,以1为终点,更新1位置处的最短距离。
在这里插入图片描述

  
  2)、题解

  注意对比这里多源BFS和单源BFS中,队列的使用细节。注意这里ret数组的用法,实则单源最短路径的问题也可以搞ret数组来做。

class Solution {
    int dx[4] = {-1,1,0,0};
    int dy[4] = {0,0,-1,1};
public:
    vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
        int m = mat.size();
        int n = mat[0].size();
        vector<vector<int>> ret(vector(m,vector<int>(n,-1)));// 用于返回距离的矩阵:初始化为-1

        queue<pair<int,int>> q;// 用于BFS的矩阵
        
        // 正难则反:先将所有0元素的位置入队
        for(int i = 0; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(mat[i][j] == 0)
                {
                    q.push({i,j});// 入队
                    ret[i][j] = 0;// 顺带将最短路径填入返回数组中
                }
            }
        }

        while(q.size())// 队列不为空
        {
            // 取队头元素
            auto [a,b] = q.front();
            q.pop();

            // 向其四周扩展路径
            for(int k = 0; k < 4; ++k)
            {
                int x = a + dx[k];
                int y = b + dy[k];
                if(x >=0 && x < m && y >=0 && y < n && ret[x][y] == -1)// 当前扩展下标(x,y)合法,且其路径值尚未得出
                {
                    q.push({x,y});// 入队
                    ret[x][y] = ret[a][b] + 1;// 相当于在(a,b)位置的基础上,多走一步
                }
            }
        }

        return ret;
    }
};

  
  
  
  
  
  
  
  
  

2.2、飞地的数量(medium)

  题源:链接

在这里插入图片描述

  
  

2.2.1、题解

  1)、思路分析
  实则此题思路和洪水灌溉:被围绕的区域 思路类似。正难则反,从四个边上元素为1的位置(陆地)开始进行BFS/DFS遍历搜索,把与边缘1相连的联通区域全部标记一下。然后再重新遍历一遍矩阵,看看哪些位置的1没有被标记,进行统计。
  在被围绕的区域那题中,我们使用了DFS和单源BFS来解决,这里我们使用多源BFS解题。 可以对比参考一下三者的写法(实则宏观思路不变,只是实现细节各有不同)。
在这里插入图片描述

  
  
  
  2)、题解
  一个补充说明:下述为了寻找边缘1入队时,采用了找首行、尾行、首列、尾列的方法,相对而言代码数量较长。这里我们再提供一种暴力写法,直接遍历矩阵把边缘1入队。这种写法虽然代码简洁一些,但相应的时间成本比前者较高。

// 1. 把边上的 1 加⼊到队列中
for (int i = 0; i < m; i++)
    for (int j = 0; j < n; j++)
        if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
            if (grid[i][j] == 1) {
                q.push({i, j});
                vis[i][j] = true;
            }
        }

  
  使用多源BFS的解法如下:

class Solution {
    int dx[4] = { -1, 1, 0, 0};
    int dy[4] = { 0, 0, -1, 1};
public:
    int numEnclaves(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();

        queue<pair<int,int>> q;// 用于BFS的队列
        
        // 1、多源BFS:先将边界所有的陆地位置入队
        for(int j = 0; j < n; ++j)
        {
            if(grid[0][j] == 1)// 首行
            {
                q.push({0,j});// 入队
                grid[0][j] = -1;// 标记一下
            }
            if(grid[m-1][j] == 1)// 尾行
            {
                q.push({m-1,j});// 入队
                grid[m-1][j] = -1;// 标记一下
            }
        }

        for(int i = 0; i < m; ++i)
        {
            if(grid[i][0] == 1)// 首列
            {
                q.push({i,0});
                grid[i][0] = -1;
            }

            if(grid[i][n-1] == 1)// 尾列
            {
                q.push({i,n-1});
                grid[i][n-1] = -1;
            }
        }


        // 2、使用队列进行BFS
        while(q.size())
        {
            // 取队头元素
            auto [a,b] = q.front();
            q.pop();

            // 向四周扩展
            for(int k = 0; k < 4; ++k)
            {
                int x = a + dx[k];
                int y = b + dy[k];
                if(x >= 0 && x < m && y >=0 && y < n && grid[x][y] == 1)// 扩展位置为未标记过的陆地
                {
                    q.push({x,y});
                    grid[x][y] = -1;
                }
            }
        }

        // 3、来到此处,说明正难则反已经解决了边界陆地情况,从头遍历找其它陆地
        int num = 0;
        for(int i = 0; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(grid[i][j] == 1) ++num;
                if(grid[i][j] == -1) grid[i][j] = 1;// 恢复标记

            }
        }
        return num;
    }
};

  
  
  
  
  
  
  
  
  

2.3、地图中的最高点(medium)

  题源:链接

在这里插入图片描述
  
  

2.3.1、题解

  1)、思路分析
  此题实则为01矩阵的变型题。题目要求水域的高度必须为 0,因此水域的高度是已经确定的值,我们可以从水域出发,使用多源BFS,推导获取其余格子的高度。
在这里插入图片描述

  
  
  2)、题解

class Solution {
    int dx[4] = {-1, 1, 0, 0};
    int dy[4] = {0, 0, -1, 1};

public:
    vector<vector<int>> highestPeak(vector<vector<int>>& isWater) {
        int m = isWater.size();
        int n = isWater[0].size();
        queue<pair<int,int>> q;// 用于多源BFS的队列
        vector<vector<int>> ret(m,vector<int>(n,-1));// 用于记录返回的矩阵:统计高度

        // 遍历一遍矩阵,将水域找出,存入队列中
        for(int i = 0; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(isWater[i][j] == 1)// 当前位置是水域
                {
                    q.push({i,j});
                    ret[i][j] = 0;//如果一个格子是水域,它的高度必须为 0
                }
            }
        }


        while(q.size())// 队列不为空
        {
            auto [a,b] = q.front();// 出队
            q.pop();

            // 向四周扩展
            for(int k = 0; k < 4; ++k)
            {
                int x = a + dx[k];
                int y = b + dy[k];
                if(x >=0 && x < m && y >=0 && y < n && ret[x][y] == -1)// 下标合法且未被标记
                {
                    q.push({x,y});// 入队
                    ret[x][y] = ret[a][b] + 1;
                }
            }
        }
        return ret;
    }
};

  
  
  
  
  
  
  
  

2.4、地图分析(medium)

  题源:链接

在这里插入图片描述
  
  

2.4.1、题解

  1)、思路分析
  先来理解一下这里的曼哈顿距离:
在这里插入图片描述

  正难则反。我们可以从陆地位置出发,使用多源BFS,遍历搜索海洋单元格,记录下每个海洋单元格距离陆地的距离即可。
  
  2)、题解

class Solution {
    int dx[4] = {-1, 1, 0, 0};
    int dy[4] = {0, 0, -1, 1};

public:
    int maxDistance(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();

        queue<pair<int,int>> q;// 用于多源BFS的队列
        int maxdist = -1;// 用于记录返回值
        vector<vector<int>> dist( m, vector<int>(n, -1));// 辅助矩阵
        
        // 遍历,找到所有陆地,入队
        for(int i = 0; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(grid[i][j] == 1)
                {
                    q.push({i,j});// 入队
                    dist[i][j] = 0;// 标记为0
                }
            }
        }

        while(q.size())// 队列不为空
        {
            auto [a,b] = q.front();
            q.pop();

            for(int k = 0; k < 4; ++k)
            {
                int x = dx[k] + a;
                int y = dy[k] + b;
                if(x >= 0 && x < m && y >= 0 && y <n && dist[x][y] == -1)
                {
                    q.push({x,y});
                    dist[x][y] = dist[a][b] + 1;
                    maxdist = max(maxdist, dist[x][y]);
                }
            }
        }

        return maxdist;
    }
};

  
  
  
  
  
  
  

3、BFS 解决拓扑排序

3.1、预备知识

3.1.1、有向无环图

  1)、什么是有向无环图
  有向无环图(DAG图): 有向无环图(Directed Acyclic Graph,简称DAG图)是一种特殊的图结构,其中每个边都是有方向的,并且图中不存在任何环(即没有路径可以从一个顶点出发并最终回到该顶点)。
在这里插入图片描述

  既然是有向图,便具有入度和出度之分。顶点 v v v 的入度是以该顶点为终点的有向边的条数。出度则是以该顶点为起始点的有向边的条数。(图论相关概念见文章:图的基本概念
  
  
  
  

3.1.2、AOV网

  1)、什么是AOV网
  AOV网(Activity On Vertex Network): 顶点活动图的简称。它是一种特殊的有向无环图(DAG),用顶点代表一个活动(Activity),用有向边代表活动之间的先后顺序的图结构。这种图结构常用于描述和分析一项工程的计划和实施过程。

在这里插入图片描述
  相关学习视频链接:数据结构-(51)AOV网与拓扑排序
  
  
  2)、AOV网特点

  • 顶点表示活动: 在AOV网中,每个顶点都代表一个具体的活动或任务。
  • 有向边表示优先关系: 有向边从一个顶点指向另一个顶点,表示这两个活动之间的先后顺序。例如,如果边从顶点A指向顶点B,则活动A必须在活动B之前完成。
  • 无环: AOV网中不存在环,即不存在一个顶点可以通过一系列有向边最终回到自己。这是为了确保活动之间的依赖关系是合理和可执行的。

  
  
  3)、拓朴排序与AOV网的关系
  与AOV网密切相关的是拓扑排序(Topological Sorting)算法。拓扑排序可以将AOV网中的顶点(代表各个活动)排列成一个线性有序的序列,使得对于任何一条有向边 (u, v),u 在序列中都出现在 v 的前面。 这样的序列可以表示项目中所有活动的一个合理执行顺序。

在这里插入图片描述

  
  

3.1.3、拓扑排序

  1)、拓扑排序基本思想
  1、从AOV网中选择一个入度为0的顶点,将其输出;
  2、删去该顶点,并且删除与该顶点连接的边;
  3、重复上述两步,直到图中没有顶点,或没有入度为0的顶点为止(后者主要是图有环的情况)。

在这里插入图片描述

  关于拓扑排序的思想,上述说了结束重复操作的情况有二:①直到图中没有顶点,②没有入度为0的顶点为止。这里我们对②,即图中带环的情况补充说明:
在这里插入图片描述

  
  2)、如何实现拓扑排序?

  借助队列,来一次BFS即可。
  1、初始化: 创建一个队列,把所有入度为0的点加入到队列中。
  2、当队列不为空时:
   ①、从队列中取出一个顶点(队头元素),并将其添加到拓扑排序结果列表中。
   ②、删除与该顶点相连的连接边;
  3、判断: 与删除边相连的其它顶点,其入度是否变成0?如果入度为0,将其加入到队列中。
  
  上述,是在有了图的基础上来完成的,而实际编程题中还要考虑建图以及如何存储入度问题(后续例题中会讲到)。此类拓扑排序的题目和之前的多源BFS类似,其代码实现均具有固定框架,关键在于碰到一道题时,我们要能识别挖掘出题干考察可以使用拓扑排序的思想解决。(下图仅供参考)

在这里插入图片描述

  
  
  
  

3.2、课程表(medium)

  题源:链接

在这里插入图片描述

  
  

3.2.1、题解

  1)、思路分析
  分析题目:题目要求我们判断是否可能完成所有课程的学习?且说明了要学习课程 ai 则 必须 先学习课程 bi ,这里就存在事件执行的先后顺序。因此,这题考察的实则是拓扑排序:能否完成课程学习→能否进行拓扑排序→是否是有向无环图→有向图中是否有环。
在这里插入图片描述

  
  
  拓扑排序的相关基础知识和思想我们在先前已经介绍过,这里补充说明如何建图的问题:(灵活使用语言提供的容器
  1、如何存储边? 根据图论中的学习,建图时,关于顶点与顶点之间边的关系,其存储结构无非两种,邻接表or邻接矩阵。可以根据数据稠密,选择合适的容器创建邻接表/邻接矩阵。
  
  比如此处编程题中,数据量不会特别稠密,那么可以使用邻接表,关键在于如何用代码实现?
  实际实现时,没有必要真的搞一个链表结构的邻接表,我们可以使用一个哈希表隐射:
  ①用数组模拟 vector<vector<顶点数据类型>> edges
  ②或者真实的STL提供的哈希表结构容器unordered_map<顶点数据类型, vector<顶点数据类型>> edges
在这里插入图片描述

  
  2、如何存储入度? 由于拓扑排序是根据每个顶点的入度关系来排序的,因此我们还需要知道顶点入度值。对此,可以使用一个与顶点数量相同大小的数组来记录每个顶点的入度值(vector<int> in)。
  
  3、综上,在建图时我们实际需要关注两点:

1、确定邻接边的指向
2、确定顶点的入度

  
  
  2)、题解
  有了上述建图的经验流程,拓扑排序解决此题的宏观步骤如下:注意这里的3步骤,拓扑排序只是我们用于判断是否成环的手段。

1、建图
2、进行拓扑排序
3、判断是否成环

  代码如下:这里邻接表也可以是vector<vector<int>> edges(numCourses);

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {

        // 1、建图:以邻接表的方式,建立一个有向图
        // 1.1、准备工作:确定需要的容器
        unordered_map<int, vector<int>> edges;// 邻接表。key值:某一顶点。value值:从该顶点出发能到达的其它顶点 key -> other_key
        vector<int> in(numCourses);// 记录顶点入度

        // 1.2、建图
        for(auto& indexs: prerequisites)// 取课程:prerequisites[i] = [ai, bi],这里是按行取(实际每行只有两列)
        {
            // 课程指向关系:bi-> ai
            int ai = indexs[0];// 第一个元素
            int bi = indexs[1];// 第二个元素
            // 构建图:确定边的指向、确定入度
            edges[bi].push_back(ai);
            in[ai]++;
        }

        // 2、进行拓扑排序:借助队列。取出图中入度为0的顶点,删除该顶点的邻接边,重复操作
        // 2.1、把所有入队为0的顶点存入队列中
        queue<int> q;// 题中顶点为每门课程,其已经使用整型标记,即 0 ~ numCourses-1,故类型为int
        for(int i = 0; i < numCourses; ++i)
        {
            if(in[i] == 0)//入度为0
                q.push(i);// 将该顶点入队(注意,这里存的是顶点,不是顶点对应的入度值in[i])
        }

        // 2.2、BFS遍历进行拓扑排序
        while(q.size())// 队列不为空时
        {
            int bi = q.front();// 取队头元素,出队
            q.pop();

            // bi-> ai,删除顶点bi的邻接边,意味着ai的入度减少
            for(auto& ai : edges[bi])
            {
                --in[ai];
                if(in[ai] == 0)// 若邻接表的另一侧顶点ai,其入度减少到0,将其入队
                    q.push(ai);
            }
        }

        // 3、利用拓扑排序判断是否有环,由此便可知晓是否能够完成所有课程的学习
        // 体现在代码上:判断入度表是否仍存在非0的顶点
        for(auto e : in)
        {
            if(e != 0) return false;
        }

        return true;
    }
};

  
  
  
  
  
  
  
  

3.3、课程表II(medium)

  题源:链接

在这里插入图片描述
  
  

3.3.1、题解

  1)、思路分析
  此题实则为上题的延伸,区别在于我们需要记录拓扑排序的结果。因此,只需要额外搞一个vector<int>数组,在BFS进行拓扑排序时,记录每次出队的值,即拓扑排序的序列。
  
  2)、题解

class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        // 1、建图
        // 1.1、准备工作
        unordered_map<int, vector<int>> edges;// 邻接表:存储bi->ai的关系
        vector<int> in(numCourses);// 入度表:存储0~numCourses - 1每个顶点的入度

        // 1.2、建图
        for(auto& indexs : prerequisites)
        {
            // 关系:bi->ai
            int ai = indexs[0], bi = indexs[1];
            // 填充邻接边、入度
            edges[bi].push_back(ai);
            ++in[ai];
        }

        // 2、拓扑排序:借助队列进行BFS
        // 2.1、找出所有入度为0的顶点,将其入队
        queue<int> q;
        for(int i = 0; i < numCourses; ++i)
        {
            if(in[i] == 0) q.push(i);
        }

        // 2.2、BFS
        vector<int> courses;// 用于记录正确的课程顺序
        while(q.size())// 队列不为空
        {
            // 取队头顶点
            int bi = q.front();
            q.pop();
            courses.push_back(bi);// 将该课程存入结果集中

            // bi->ai,删除顶点bi的邻接边,修改ai的入度
            for(auto& ai : edges[bi])
            {
                --in[ai];
                if(in[ai] == 0) q.push(ai);
            }
        }

        // 3、判断是否有环
        if(courses.size() == numCourses)return courses;
        else return {};
    }
};

  
  
  
  
  
  
  
  
  

3.4、火星词典(hard)

  题源:链接

在这里插入图片描述

  
  

3.4.1、题解

  1)、思路分析
  1、如何比较给定的字符串列表 vector<string>& words,搜索字符排序信息? 可以使用两层循环:

  外层: 从头到尾,对字符串逐一两两比较。words[0]&words[1]、words[0]&words[2]、words[0]&words[3]、……
  内层: 对两个字符串str1、str2,逐个比较字符,当两字符串出现首次不相同的位置,即可获取得这两个字符的顺序。例如:wrt和wrf,比较可得顺序为:t > f (t 在 f 的前面)
在这里插入图片描述

  ①根据上述比较,我们可知建图关系:对比较的两字符,若t 在 f 的前面,则有t→f。
  ②需要注意,这里只用比较两字符串首次不同字母(因为字典序比较字符串时,我们也只是根据首次不同的字符进行比较的。)
  ③特殊情况:abcab,如果给定字符如左侧序列,这是一个不符合的字符,需要特殊处理。
在这里插入图片描述

  
  2、如何还原字典序? 根据上述比较方式建图后,我们只需要按照拓扑排序取字母,所得即这种外星语言的字典顺序。
在这里插入图片描述

  
  1)、细节一:关于建图
  ①由于这里进行排序的是字符串,建图时,邻接表可以使用hash<,> edges
  ②因我们在比较字符串找字典序时,所找出的字母存在冗余的情况。所以,对于每次找出的两个字符w->e,不能无脑入哈希表中(因为他们都是同一条邻接边)。一个方法设edges的类型为 hash<char, hash<char>>,这里key值表示顶点(即字典中的某个字符),value值表示边的关系(即两字母的先后顺序),将其设置为哈希表可以在每次存入邻接边时做判断。
  
  2)、细节二:统计入度信息
  ①由于本题并非统计26个字符的外星文顺序,只是统计题words中出现的字符顺序,因此,我们可以使用一个哈希表来统计入度,hash<char,int> in。这里,需要对其初始化(有别于先前两题,因为那里是用vector<int> in表示入度信息的,直接把数组下标作为了顶点。而这里如果不初始化,则哈希表中不存在对应数值)
  
  2)、题解

class Solution {
    // 1、建图
    // 1.1、准备工作
    unordered_map<char, unordered_set<char>>edges; // 邻接表:key值,匹配字母;value值,有key->other_key(使用哈希是为了避免重复录入)
    unordered_map<char, int> in; // 用于统计每个字母的入度
    bool is_illegal;// 用于检查特殊情况:abc ab(此情况不合法)
public:
    string alienOrder(vector<string>& words) {
        is_illegal = false;
        // 1.2、初始化入度表
        for(auto& str : words)// 获取到的是字符串
        {
            for(auto& ch : str)// 获取到的是字符
            {
                in[ch] = 0;// 先将所有字母的入度初始化为0
            }
        }

        // 1.3、根据给定字符串数组搜索信息建图
        int n = words.size();// 字符串数组个数
        for(int former = 0; former < n; ++former)
        {
            for(int latter = former + 1; latter < n; ++latter)
            {
                // 关系:former[ch]->latter[ch]
                compare(words[former],words[latter]);
                
                if(is_illegal) return "";// 处理特殊情况
            }
        }

        // 2、拓扑排序
        queue<char> q;
        // 找度为0的字符,入队
        for(auto& [ch, degree] : in)
        {
            if(degree == 0) q.push(ch);
        }

        string ret;// 用于记录返回的字母(有效顺序)
        while(!q.empty())
        {
            // 取队头字符
            char a = q.front();
            q.pop();
            ret += a;

            // 删除该字符的邻接边(a->b)
            for(auto& b : edges[a])
            {
                --in[b];
                if(in[b] == 0) q.push(b);
            }
        }

        // 3、判断是否成环
        for(auto& [ch, degree] : in)
        {
            if(degree != 0) return "";
        }

        return ret;

    } 

    void compare(string& s1, string& s2)  
    {   
        // 比较两个字符串的每个字母
        int n = min(s1.size(),s2.size());//只比较两字符串公共长度
        int ch = 0;
        for(; ch < n; ++ch)
        {   
            if(s1[ch] != s2[ch])// 当前比较的两字符不相等时:存在指向关系
            {
                // 关系:a->b
                char a = s1[ch], b = s2[ch];
                if( !edges.count(a)  || !edges[a].count(b))// a不存在或者,a存在,但a->b不存在
                {
                    edges[a].insert(b);
                    ++in[b];
                }
                break;// 字典排序,是比较两字符串首次不相等的情况。故这里首次比较结束,可以退出
            }
        }

        // 特殊情况处理:abc ab
        if(ch == s2.size() && ch < s1.size()) is_illegal = true;
    }
};

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

Fin、共勉。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值