目录
拓扑排序
题目一:课程表
题目二:课程表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 解决拓扑排序到此结束