拓扑排序——《数据结构》课设

拓扑排序

相关概念

先给出百度百科的有关概念:

  对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

  举个通俗的例子——排课方案。只有学习了相应的基础课(如高数、线代)后,才能学习进阶课(比如人工智能),而拓扑序列正是因此产生——拓扑序列满足一个事件发生的条件一定排列在这个事件之前。比如有课程及其先修课程如下:

课程 先修课程
数据结构 离散数学
离散数学 高等数学
高等数学

显然,想要学习数据结构,就要先学习离散数学,想要学习离散数学,就先要知道基础的高数知识,而高数作为大学基础课,则不需要先修。
  那么,依据此课程表排出的一个拓扑序列则是:

高等数学→离散数学→数据结构

  拓扑排序得到的拓扑序列不一定唯一,例如,在上边中加入不需要先修知识的《程序设计基础》课程,此课程与高数的层级是并列关系,那么,先修这两个的哪个都是一样的。因此拓扑序列存在不唯一性。

算法设计

  按照先修课程的思想,我们应当从依赖课程较少的课开始,逐渐学习。就像玩DNF给角色升级技能一样,不同角色觉醒后拥有不同技能,而想要获得高级技能,就要先学习其依赖的低级技能。
  因此,不妨从无依赖的课程开始学起,逐渐解锁“技能树”。那么,生成拓扑序列的一种方案是这样的(使用栈或队列存储信息均可):

  1. 遍历所有课程,将无先修依赖(入度为 000)的点全部压入栈中;
  2. 判断栈是否为空,如果不为空,弹出一个课程并将其加入拓扑序列;
  3. 由于此课程被解锁,以它为依赖的课程便均没有了这个依赖。因此,以它为依赖的课程入度均−1-1−1;
  4. 判断以它为依赖的课程在经过入度−1-1−1后,入度是否变为 000,如果是,将课程压栈;
  5. 重复 2 4 2→4 ,直至栈为空为止。

  另外,拓扑排序图中不能存在回路——其实很好理解,想象一下,回路的存在意味着一个课程以它自己为先修条件——这显然很荒谬。


代码实现

  本次《数据结构》课设我选择了有关拓扑排序的题目,题目大意如下:
  给定课程编号、课程名称、学分、先修条件以及约束条件:学期数和每学期学分上限,给出一个排课方案。

1.设计的逻辑结构:
在这里插入图片描述
  邻接表部分表示了我做的题目的课程依赖关系。这里我使用了哈希表的方式,来减少通过搜索课程编号进而索引其邻接表表头下标带来的 O ( n ) O(n) 时间复杂度。

2. A O V AOV 网络图

在这里插入图片描述
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;
}

猜你喜欢

转载自blog.csdn.net/cprimesplus/article/details/94365048