算法:BFS 解决拓扑排序

目录

拓扑排序

题目一:课程表

题目二:课程表II

题目三:火星词典


拓扑排序

有向无环图(DAG图)

有向无环图也就是有几个点,每个点之间的线段都是有向的,且任意拿出来几个点,都是无环的,这里的无环指的不仅仅是围成一个圈,而是这个圈的方向要一致,只有围成一个圈且方向一致的情况才叫有环,否则就是有向无环图,即下图就是有向无环图, 即使①②③围成了圈,但是方向不一致:

下面说明两个概念:

入度:指有多少边指向该点,例如③就有两边指向它,所以③的入度为2
出度:指有多少边从该点出去,例如①就有两边出去,所以①的出度为2


AOV网:顶点活动图

AOV网也就是在有向无环图中,用一个顶点来表示一个活动,用边来表示活动的先后顺序的图的结构

AOV网是有实际意义的


拓扑排序

拓扑排序就是找到做事情的先后顺序,这里的顺序可能不是唯一的

因为每次排序时,可能有多个顶点入度为0,此时选择哪个顶点都可以,所以顺序可能不是唯一的

如何排序

①找到图中入度为0的点
②删除与改点相连的点
③重复上面的①②操作,直到图中没有点或是没有入度为0的点为止

如果有环就会出现没有入度为0的点

所以拓扑排序的重要应用就是:判断图中是否有环


实现拓扑排序

拓扑排序就是借助队列,进行一次bfs即可

1、初始化,将所有入度为0的点入队列

2、当队列不为空的时候:
①拿出队头元素,加入到最终结果中
②删除与该元素相连的边
③判断:与删除边相连的点,是否入度变为0,如果入度为0,加入到队列中


题目一:课程表

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

其实说的简单一点,就是判断题目中给我们的这个有向图中,是否有环存在

题目中给的 [1, 0] 就表示在有向图中,方向是由0指向1的

那么如何建图呢

如果点比较稀疏,就采用邻接表建图,如果比较稠密,就采用邻接矩阵建图

邻接表就是每一个点都拿出来,在后面加上它连接的点

可以采用 vector<vector<int>> 或是 unordered_map<int, vector<int>> 这两种方式创建

第一种方式:就相当于创建一个二维数组,类似于实现了一个链式结构

第二种方式:相当于每个int后面都挂了一个数组,是一样的

因为拓扑排序最关键的就是要知道每一个顶点的入度,每个顶点的入度就使用vector<int>表示即可

代码如下:

class Solution 
{
public:
    bool canFinish(int num, vector<vector<int>>& prerequisites) 
    {
        // 1、准备工作
        unordered_map<int, vector<int>> edges; // 邻接表
        vector<int> in(num); // in数组存储每一个顶点的入度

        // 2、建图
        for(auto& it : prerequisites)
        {
            // 这里需要注意,是b指向a
            int a = it[0], b = it[1];
            // b的后面加上a,a的入度++
            edges[b].push_back(a);
            in[a]++;
        }

        // 3、拓扑排序
        queue<int> q;
        for(int i = 0; i < num; i++)
        {
            // 入度为0的顶点全部入队列
            if(in[i] == 0) q.push(i);
        }

        // 4、bfs
        while(!q.empty())
        {
            int top = q.front();
            q.pop();
            for(auto& it : edges[top])
            {
                in[it]--;
                if(in[it] == 0) q.push(it);
            }
        }

        // 5、判断是否有环
        for(int i = 0; i < num; i++)
            if(in[i]) return false;
        return true;
    }
};

题目二:课程表II

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

  • 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。

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

示例 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,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3]

示例 3:

输入:numCourses = 1, prerequisites = []
输出:[0]

这道题和上道题几乎一模一样,只不过上道题判断的是是否有环,只需要将是否有环的结果返回即可, 这道题如果无环,需要将无环的其中一种顺序返回,不需要多做说明,代码如下:

class Solution 
{
public:
    vector<int> findOrder(int num, vector<vector<int>>& prerequisites) 
    {
        // edges是邻接表,in是每个点的入度情况,ret是最终返回的上课顺序
        unordered_map<int, vector<int>> edges;
        vector<int> in(num);
        vector<int> ret;

        for(auto& it : prerequisites)
        {
            // b指向a
            int a = it[0], b = it[1];
            edges[b].push_back(a);
            in[a]++;
        }
        // 入度为0的顶点全部入队列
        queue<int> q;
        for(int i = 0; i < num; i++)
        {
            if(in[i] == 0) q.push(i);
        }
        // bfs
        while(!q.empty())
        {
            int top = q.front();
            q.pop();
            ret.push_back(top);
            for(auto& it : edges[top])
            {
                in[it]--;
                if(in[it] == 0) q.push(it);
            }
        }
        // 判断是否有环
        if(ret.size() == num) return ret;
        return {};
    }
};

题目三:火星词典

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

给定一个字符串列表 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"]
输出:""
解释:不存在合法字母顺序,因此返回 "" 。

这道题也就是告诉我们根据给出的words字符串数组,得到这个星球的字母顺序

在words数组中,先出现的字符串的字典序就小于后出现的字典序

举个例子,如果 words = ["th", "ta"]

根据words数组的顺序,得出 "th" 的字典序在 "ta" 之前, 而两个字符串的第一个字符t相等, 所以就根据第一个不同的字符比较字典序,"th"和"ta"的第二个字符不同,分别是 'h' 和 'a',又因为 "th"  在 "ta" 前,所以说明 h 的字典序小于 a

根据上面的例子,可以知道,想要得到字典序,只需要找到两个字符串第一个不同的字符,将这两个字符进行比较即可,所以在此题中,进行两次for循环,将每两个字符串都做比较,最终得到很多组指向,例如上述的例子中,就可以得到 h 的字典序在 a 之前,可以得到 h -> a

那么将这些所有得到的信息,形成一个有向无环图,最终进行拓扑排序即可

下面有几点需要注意的地方:

①建图的哈希表unordered_map,在之前的题目都是 unordered_map<char, vector<char>>这种的,但是此题一个顶点后面有可能会出现重复插入的情况,所以将后面的vector也替换为哈希表

②统计入度信息时,最好不使用vector<char>了,因为此题是char类型的数据,如果想使用char类型的数组,需要开26个空间,如果其中大部分的字符没有出现,会造成空间浪费的情况,所以这里统计入度信息时,也采用哈希表unordered_map<char, int>来统计

使用哈希表统计需要注意的是需要初始化,将所有出现的字符全部初始化为0,因为如果不初始化为0,哈希表只会统计有入度的字符

③收集信息时,需要使用双指针的操作,找到两个字符串第一个不同的字符

④如果有字符串是 "abc" , "ab",也是不合法的,因为如果这两个字符串前面的"ab"相等,那么"abc" 一定是在 "ab" 后面的,如果出现在 "ab" 前面,就是不合法的

代码如下:

class Solution 
{
    // edges邻接表,in存储每个顶点的入度数,ret存储最终结果
    unordered_map<char, unordered_set<char>> edges;
    unordered_map<char, int> in;
    bool flag;
public:
    string alienOrder(vector<string>& words) 
    {
        // 处理特殊情况
        if(words.size() == 1) return words[0];
        // 初始化入度哈希表
        for(int i = 0; i < words.size(); i++)
            for(auto& ch : words[i])
                in[ch] = 0;

        // 两层for循环,将words数组的字符串两两组合
        for(int i = 0; i < words.size(); i++)
        {
            for(int j = i + 1; j < words.size(); j++)
            {
                // 找两个字符串中不同的字符,添加进edges中
                add(words[i], words[j]);
                if(flag) return "";
            }
        }
        // 拓扑排序
        queue<char> q;
        for(auto& [a, b] : in)
        {
            if(b == 0) q.push(a);
        }
        string ret;
        while(!q.empty())
        {
            char top = q.front();
            q.pop();
            ret += top;
            for(auto& it : edges[top])
            {
                in[it]--;
                if(in[it] == 0) q.push(it);
            }
        }
        // 判断是否有环
        for(auto& [a, b] : in)
            if(b) return "";
        return ret;
    }

    void add(const string& s1, const string& s2)
    {
        int cur = 0;
        int n = min(s1.size(), s2.size());
        while(cur < n)
        {
            if(s1[cur] != s2[cur])
            {
                // a -> b
                char a = s1[cur], b = s2[cur];
                // a不存在,或a存在,但a对应的哈希表中没有b
                if(!edges.count(a) || !edges[a].count(b))
                {
                    edges[a].insert(b);
                    in[b]++;
                }
                break;
            }
            cur++;
        }
        // "abc"和"ab"的情况
        if(cur == s2.size() && cur < s1.size())
            flag = true;
    }
};

算法:BFS 解决拓扑排序到此结束

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值