拓扑排序
相关概念
先给出百度百科的有关概念:
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
举个通俗的例子——排课方案。只有学习了相应的基础课(如高数、线代)后,才能学习进阶课(比如人工智能),而拓扑序列正是因此产生——拓扑序列满足一个事件发生的条件一定排列在这个事件之前。比如有课程及其先修课程如下:
课程 | 先修课程 |
---|---|
数据结构 | 离散数学 |
离散数学 | 高等数学 |
高等数学 | 无 |
显然,想要学习数据结构,就要先学习离散数学,想要学习离散数学,就先要知道基础的高数知识,而高数作为大学基础课,则不需要先修。
那么,依据此课程表排出的一个拓扑序列则是:
拓扑排序得到的拓扑序列不一定唯一,例如,在上边中加入不需要先修知识的《程序设计基础》课程,此课程与高数的层级是并列关系,那么,先修这两个的哪个都是一样的。因此拓扑序列存在不唯一性。
算法设计
按照先修课程的思想,我们应当从依赖课程较少的课开始,逐渐学习。就像玩DNF给角色升级技能一样,不同角色觉醒后拥有不同技能,而想要获得高级技能,就要先学习其依赖的低级技能。
因此,不妨从无依赖的课程开始学起,逐渐解锁“技能树”。那么,生成拓扑序列的一种方案是这样的(使用栈或队列存储信息均可):
- 遍历所有课程,将无先修依赖(入度为 000)的点全部压入栈中;
- 判断栈是否为空,如果不为空,弹出一个课程并将其加入拓扑序列;
- 由于此课程被解锁,以它为依赖的课程便均没有了这个依赖。因此,以它为依赖的课程入度均−1-1−1;
- 判断以它为依赖的课程在经过入度−1-1−1后,入度是否变为 000,如果是,将课程压栈;
- 重复 ,直至栈为空为止。
另外,拓扑排序图中不能存在回路——其实很好理解,想象一下,回路的存在意味着一个课程以它自己为先修条件——这显然很荒谬。
代码实现
本次《数据结构》课设我选择了有关拓扑排序的题目,题目大意如下:
给定课程编号、课程名称、学分、先修条件以及约束条件:学期数和每学期学分上限,给出一个排课方案。
1.设计的逻辑结构:
邻接表部分表示了我做的题目的课程依赖关系。这里我使用了哈希表的方式,来减少通过搜索课程编号进而索引其邻接表表头下标带来的
时间复杂度。
2. 网络图
3.代码
#include<iostream>
#include<map>
#include<fstream>
#include<stack>
#include<vector>
#include<string>
#include<sstream>
#include<windows.h>
using namespace std;
typedef int ScoreType;
typedef string Number;
struct CourseInfo { // 单个课程的信息
string courseName; // 课程名称
ScoreType credit; // 课程学分
int inDgree; // 每个点的入度
CourseInfo(string courseName = "NULL", ScoreType credit = 0, int inDgree = 0) {
this->courseName = courseName;
this->credit = credit;
this->inDgree = inDgree;
}
};
struct CourseNode { // 单个课程节点
Number courseNumber; // 课程编号
struct CourseNode *next;
CourseNode(Number courseNumber = "NULL") {
this->courseNumber = courseNumber;
next = NULL;
}
};
struct CourseList {
vector<CourseNode*> courseNet; // 课程邻接表
vector<CourseNode*> tailPoint; // 记录每行的尾指针
vector<CourseInfo> courseInfo; // 课程信息
map<Number, int> hm; // 哈希表,用来记录编号与下标的关系,如201706020115对应下标 1号
int vertexNum; // 当前课程数量
CourseList() {
vertexNum = 0;
}
void CreateHashMap(stringstream &ss); // 建立索引哈希表
void CreateAdjList(string buf, int index); // 建立邻接表
};
struct UserInterface {
CourseList course;
vector<string> topoOrder; // 拓扑序列:C1->C2->C3...
bool JudgeLogicError(int count); // 判断课程逻辑是否有误,拓扑图不能有环
void LoadingCourseInfo();
void ReadFromFile();
void GetTopologicalOrder();
void ShowTopoOrder(); // 显示拓扑序列方案
void ShowCourseInfo();
void UserOption();
char ShowOption();
};
int main()
{
UserInterface user;
user.UserOption();
system("pause");
return 0;
}
void CourseList::CreateHashMap(stringstream &ss) { // 记录编号与下标的映射、课程名称、课程学分
string cName;
Number cNumber;
CourseInfo cInfo;
ss >> cNumber >> cInfo.courseName >> cInfo.credit;
courseInfo.push_back(cInfo);
hm[cNumber] = vertexNum++; // 将课程号与下标建立哈希映射,减少顺序检索带来的时间复杂度
CourseNode *cn = new CourseNode(cNumber);
courseNet.push_back(cn);
tailPoint.push_back(cn);
}
void CourseList::CreateAdjList(string buf, int index) {
vector<string> mark;
string str;
// 解析字符串
int len = buf.size(), c = 0, flag = 1;
if (buf[0] != 'C') {
flag = 0;
}
for (int i = 0;flag && i < len;i++) {
if (buf[i] != ',') {
str += buf[i];
}
else {
mark.push_back(str);
str.clear();
}
}
if (flag) {
mark.push_back(str);
}
// 根据 hm[courseNumber] 进行下标索引
int size = mark.size(), cnt = 0;
for (int i = 0;flag && i < size;i++) {
cnt++;
CourseNode * newNode = new CourseNode(courseNet[index]->courseNumber);
int n = hm[mark[i]];
tailPoint[n]->next = newNode;
tailPoint[n] = tailPoint[n]->next;
}
courseInfo[index].inDgree += cnt;
}
bool UserInterface::JudgeLogicError(int count) { // 判断课程是否存在回路
return bool(count != course.vertexNum);
}
void UserInterface::GetTopologicalOrder() {
stack<CourseNode *> st;
for (int i = 0;i < course.vertexNum;i++) { // 将入度为 0 的点压入栈中
if (!course.courseInfo[i].inDgree)
st.push(course.courseNet[i]);
}
int cnt = 0;
while (!st.empty()) { // 拓扑的核心,也可用队列存储
CourseNode *p, *temp = st.top(); // 若优化方案,考虑到先修课程并行学习,可以做多个辅助栈以达到模拟并行的目的
st.pop();
topoOrder.push_back(temp->courseNumber);
p = temp->next;
while (p) {
int index = course.hm[p->courseNumber];
if (!(--course.courseInfo[index].inDgree)) {
st.push(course.courseNet[index]);
}
p = p->next;
}
cnt++;
}
if (JudgeLogicError(cnt)) {
cout << "There are logical errors in course information!";
system("pause");
exit(0);
}
}
char UserInterface::ShowOption() {
system("cls");
char ch;
cout << " 请选择操作: " << endl;
cout << "1.显示全部课程信息" << endl;
cout << "2.显示学期课程安排" << endl;
cout << "3.退出自动排课系统" << endl;
cin >> ch;
return ch;
}
void UserInterface::UserOption() {
char op;
int flag = 1;
LoadingCourseInfo();
while (op = ShowOption()) {
switch (op)
{
case '1':
ShowCourseInfo();Sleep(5000);break;
case '2':
if (flag) {
GetTopologicalOrder();
flag = 0;
}
ShowTopoOrder();Sleep(10000);break;
case '3':
cout << "谢谢使用,再见!" << endl;
system("pause");exit(0);break;
default:
break;
}
}
}
void UserInterface::LoadingCourseInfo() {
ReadFromFile();
}
void UserInterface::ReadFromFile() {
const string fileName = "courseInfo.txt";
fstream fin;
try {
fin.open(fileName, ios::in | ios::out);
if (!fin)
throw "File Open Error!";
}
catch (const char *Warning)
{
cout << Warning << endl;
system("pause");
exit(0);
}
//成功打开文件
string line;
int flag = 0;
while (getline(fin, line))
{
if (!flag) { // 跳过第一行(标题属性)
flag = 1;
continue;
}
stringstream ss(line);
course.CreateHashMap(ss);
}
fin.clear();
fin.seekg(0);
// 再读一遍,此时根据课程依据的先序,创建邻接表
flag = 0;
int st = 0;
while (getline(fin, line))
{
if (!flag) { // 跳过第一行(标题属性)
flag = 1;
continue;
}
string buf;
stringstream ss(line);
for (int j = 1;j <= 4;j++) ss >> buf; // 吞掉前面3个字符串,只留下先修课程做解析
course.CreateAdjList(buf, st);
st++;
}
fin.close();
}
void UserInterface::ShowCourseInfo() {
//显示全部信息
for (int i = 0;i < course.vertexNum;i++) {
string name = course.courseNet[i]->courseNumber;
cout << course.hm[name] << ": " << name << " " << course.courseInfo[i].courseName << " ";
cout << course.courseInfo[i].credit << endl;;
}
}
void UserInterface::ShowTopoOrder() {
int verNum = course.vertexNum, size; // verNum 和 Size 均为总共课程数量
int flag = 0;
size = verNum;
cout << "课程学习序列为:" << endl;
for (int i = 0;i < size;i++) {
int index = course.hm[topoOrder[i]];
cout << course.courseInfo[index].courseName << "(" << topoOrder[i] << ") ";
if (!((i + 1) % 5))
cout << endl;
if (--verNum)
cout << "→ ";
}
cout << endl;
// 增加学期数和每学期学分上限的约束
int semester = 7, hasLearned = 0, ceiling = 9;
int curScore = 0, index = 0, t = 1;
for (int i = 0;i < size;i++) {
if (t) {
cout << "第 " << ++index << " 学期:" << endl;
t = 0;
}
int temp = course.courseInfo[course.hm[topoOrder[i]]].credit;
if ((hasLearned <= size - semester)) {
if (curScore <= ceiling - temp) { // 预判
cout << course.courseInfo[course.hm[topoOrder[i]]].courseName << " ";
hasLearned++;
curScore += temp;
}
else { // 如果不满足上面的约束条件,回退到上一次并清空状态,开始下一学期的排课
cout << endl;
hasLearned--;
i--;
curScore = 0;
t = 1;
}
}
else {
cout << endl;
hasLearned--;
i--;
curScore = 0;
t = 1;
}
}
cout << endl;
}