前言
开学啦,没时间搞这个了!!
要开始刷点算法题准备一些比赛和实习啦!!
内容
学会建图,学会用邻接表&&邻接矩阵存图。
图的遍历方式,BFS && DFS。
图的最小生成树,Prim && Kruscal。
图的拓扑排序。
图的最大路径与最小路径,CriticalPath && Dijkstra。
了解图的一些基本概念,出度入度,连通分量等等······
代码
困难的地方基本有标注其余的都是一些比较容易的算法思想。
有些算法直接采用实例给出测试效果可以自己进行验证。
可以自己修改为输入的模式即可。
代码如下:
/*
这里只涉及数据结构中一些基础的图算法
实际上看算法导论中还会有更多的图算法如:
最大流等等、、、(又忘得差不多了,真惭愧)
*/
#include<iostream>
#include<algorithm>
#include<string>
#include<vector>
#include<stack>
#include<queue>
#include<deque>
#include<map>
#include<cmath>
#include<string.h>
using namespace std;
struct Node{
Node *next;
int data;
};
struct ArcNode{
ArcNode *next;
int data;
int Info;
};
struct HeadNode{
ArcNode *next;
int data;
};
class GRAPH{
private:
// 图的长宽
int row;
int col;
// 二维数组存储方式
int **p;
// 邻接表形式
vector<HeadNode *> v;
// 用于深度优先遍历的标志数组
bool *visit;
// 用于广度优先遍历的队列
queue<int> que;
queue<HeadNode *> qu;
// 作为最小生成树的测试数据
int **weight;
// 设立拓扑排序的测试矩阵 以及保存顶点度的容器
vector<int> degree;
int **topo_test;
// 设立单源最短路径的测试矩阵
int **road;
// 设立最大路径测试数据数组
int **cri;
public:
GRAPH(int _row, int _col) : row(_row), col(_col){};
void CreateGraph(vector<vector<int>> &vec, int arc);
void CreateGraph_Linklist(vector<vector<int>> &vec, int arc);
void TraverseArray();
void TraverseLinklist();
void BFSTraverse();
void DFSTraverse();
void DFS(int cur);
void RecoverVisit();
void Test();
void MSCTree(); // 最小生成树
void Prim();
void Kruscal();
void CalDegree();
void TopologicalSort(); // 拓扑排序
void Dijkstra(); // 图的最短路径
void InitRoad();
void CriticalPath(); // 图的最长路径
~GRAPH();
};
void GRAPH::RecoverVisit(){
for (int i = 0; i < 6; i ++)
visit[i] = false;
}
void GRAPH::CreateGraph(vector<vector<int>> &vec, int arc){
p = new int *[row];
visit = new bool[6];
for (int i = 0; i < row; i ++){
visit[i] = false;
p[i] = new int[col];
for (int j = 0; j < col; j ++){
p[i][j] = 0;
}
}
for (int i = 0; i < arc; i ++){
// 由于构造是无向图,于是相互都得设置为1
p[vec[i][0]][vec[i][1]] = p[vec[i][1]][vec[i][0]] = 1;
}
}
void GRAPH::TraverseArray(){
for (int i = 0; i < row; i ++){
for (int j = 0; j < col; j ++){
cout << p[i][j] << " ";
}
cout << endl;
}
}
void GRAPH::CreateGraph_Linklist(vector<vector<int>>& vec, int arc){
// 建立头节点
for (int i = 0; i < row; i ++){
HeadNode *temp = new HeadNode;
temp->next = NULL;
temp->data = i;
v.push_back(temp);
}
for (int i = 0; i < arc; i ++){
HeadNode *q = v[vec[i][0]];
ArcNode *t = new ArcNode;
t->next = NULL;
t->data = vec[i][1];
if(!q->next) q->next = t;
else{
ArcNode *p = q->next;
while(p->next) p = p->next;
p->next = t;
}
}
}
void GRAPH::TraverseLinklist(){
for (int i = 0; i < row; i ++){
HeadNode *temp = v[i];
cout << temp->data << "->";
ArcNode *q = temp->next;
while(q){
cout << q->data << "->";
q = q->next;
}
cout << "NULL";
cout << endl;
}
}
void GRAPH::BFSTraverse(){
// 广度优先搜索是一层一层遍历
// 可以用到队列
for (int i = 0; i < row; i ++){
if(!visit[i]){
visit[i] = true;
cout << i << " ";
que.push(i);
while(!que.empty()){
int temp = que.front();
que.pop();
for (int j = 0; j < row; j ++){
if(j != temp && !visit[j] && p[temp][j] == 1){
visit[j] = true;
cout << j << " ";
que.push(j);
}
}
}
}
}
cout << endl;
}
void GRAPH::DFSTraverse(){
// 深度优先搜索就是一遇到邻接点就往下层跑
int cnt = 0;
for (int i = 0; i < row; i ++){
if(!visit[i]){
DFS(i);
cnt++;
}
}
cout << '\n';
cout << "连通分量为: " << cnt << endl;
}
void GRAPH::DFS(int cur){
cout << cur << " ";
visit[cur] = true;
for (int i = 0; i < col; i ++){
if(p[cur][i] == 1 && !visit[i])
DFS(i);
}
}
void GRAPH::Test(){
weight = new int*[6];
for (int i = 0; i < 6; i ++){
weight[i] = new int[6];
for (int j = 0; j < 6; j ++){
weight[i][j] = INT_MAX;
}
}
weight[0][1] = 6;
weight[0][2] = 1;
weight[0][3] = 5;
weight[1][2] = 5;
weight[1][4] = 3;
weight[2][3] = 5;
weight[2][4] = 6;
weight[2][5] = 4;
weight[3][5] = 2;
weight[4][5] = 6;
weight[1][0] = 6;
weight[2][0] = 1;
weight[3][0] = 5;
weight[2][1] = 5;
weight[4][1] = 3;
weight[3][2] = 5;
weight[4][2] = 6;
weight[5][2] = 4;
weight[5][3] = 2;
weight[5][4] = 6;
}
void GRAPH::MSCTree(){
char ch;
cout << "请输入字母选择最小生成树算法(\n P as Prim \n K as Kruscal \n Q as Quit \n 如果想要同时测试两种最小生成树算法得先使用Kruscal 否则Prim会改变weight权值矩阵 \n): ";
while(cin >> ch){
switch(ch){
case 'P':
Prim();
break;
case 'K':
Kruscal();
break;
case 'Q':
return;
}
}
}
void GRAPH::Prim(){
// Prim的思想是:
// 从初始顶点开始,不断去找到距离自己权值最小的一条边
// 第一重循环表示总共要生成的边的数目
int path = 0;
string route = "0";
visit[0] = true;
for (int i = 1; i < 6; i ++){
int minn = INT_MAX;
int k = 0;
// 第二重循环就是找权值最小的边
for (int j = 0; j < 6; j ++){
if(weight[0][j] < minn && !visit[j]){
minn = weight[0][j];
k = j;
}
}
path += minn;
route += "->";
route += to_string(k);
visit[k] = true;
for (int j = 0; j < 6; j ++){
if(!visit[j] && weight[k][j] < weight[0][j]){
weight[0][j] = weight[k][j];
}
}
}
cout << "最小生成树的路径为:" << route << endl;
cout << "最小生成树的代价为:" << path << endl;
}
void GRAPH::Kruscal(){
// 此算法的核心思想在于
// 每次都从图中取权值最小的边且两者连通分量不同
// 且那一个边的的两顶点分量值都会被更新会较大顶点值
string route = "";
int path = 0;
int *v = new int[6];
for (int i = 0; i < 6; i ++) v[i] = i;
for (int i = 1; i < 6; i ++){
int minn = INT_MAX;
int min_1 = 0, min_2 = 0;
for (int j = 0; j < 6;j ++){
for (int k = 0; k < 6;k ++){
if(weight[j][k] < minn && v[j]!=v[k]){
minn = weight[j][k];
min_1 = j, min_2 = k;
}
}
}
int temp = min(v[min_1], v[min_2]);
v[min_1] = v[min_2] = max(v[min_1], v[min_2]);
path += minn;
route += (to_string(min_1) + "->" + to_string(min_2));
route += " ";
for (int j = 0; j < 6; j ++){
if(v[j] == temp) v[j] = v[min_1];
}
}
cout << "最小生成树的路径为:" << route << endl;
cout << "最小生成树的代价为:" << path << endl;
delete[] v;
}
void GRAPH::CalDegree(){
topo_test = new int *[6];
for (int i = 0; i < 6; i++){
degree.push_back(0);
topo_test[i] = new int[6];
for (int j = 0; j < 6; j ++){
topo_test[i][j] = 0;
}
}
// 利用教材上的拓扑排序有向图实例进行赋值
topo_test[0][1] = topo_test[0][2] = topo_test[0][3] = 1;
topo_test[2][1] = topo_test[2][4] = 1;
topo_test[3][4] = 1;
topo_test[5][3] = topo_test[5][4] = 1;
// 根据邻接矩阵进行度的计算
for (int i = 0; i < 6; i ++){
for (int j = 0; j < 6; j ++){
if(topo_test[i][j] == 1)
degree[j]++;
}
}
}
void GRAPH::TopologicalSort(){
// 拓扑排序的核心思想就是
// 不断从图中找出入度为0的结点!
// 然后删去该结点以及从该节点出发出去的边
// 重复上述操作!!
// 如果最后全部结点统统输出则可以验证没有环的存在
RecoverVisit();
CalDegree();
string path = "";
queue<int> q2;
for (int i = 0; i < 6; i ++){
if(degree[i] == 0){
q2.push(i);
visit[i] = true;
}
}
while(!q2.empty()){
int temp = q2.front();
q2.pop();
path += to_string(temp) + "->";
for (int i = 0; i < 6; i ++){
if(topo_test[temp][i] == 1)
degree[i]--;
if(degree[i] == 0 && !visit[i]){
visit[i] = true;
q2.push(i);
}
}
}
path = path.substr(0, 16);
cout << path << endl;
for (int i = 0; i < 6; i ++){
if(!visit[i]){
cout << "此有向图中存在环" << endl;
break;
}
}
}
void GRAPH::InitRoad(){
road = new int *[6];
for (int i = 0; i < 6; i ++){
road[i] = new int[6];
for (int j = 0; j < 6; j ++){
road[i][j] = INT_MAX;
}
}
road[0][2] = 10;
road[0][4] = 30;
road[0][5] = 100;
road[1][2] = 5;
road[2][3] = 50;
road[3][5] = 10;
road[4][5] = 60;
road[4][3] = 20;
}
void GRAPH::Dijkstra(){
InitRoad();
string *path = new string[6];
for (int i = 0; i <= 5; i ++){
path[i] = "0"; // 全部初始化为 出发源结点
}
visit[0] = true;
// 控制循环次数总共要找出源结点到其余5个点的最小距离
for (int i = 0; i < 5; i ++){
int minn = INT_MAX;
int temp = 0;
for (int j = 1; j < 6; j ++){
if(!visit[j] && road[0][j] <= minn){
minn = road[0][j];
temp = j;
}
}
visit[temp] = true;
path[temp] += ("->" + to_string(temp));
for (int j = 1; j < 6; j ++){
if(!visit[j] && road[temp][j] <= INT_MAX - minn && road[0][j] > minn + road[temp][j]){
road[0][j] = road[temp][j] + minn;
path[j] = path[temp];
}
}
}
// 对到不同顶点的路径进行输出
for (int i = 1; i < 6; i ++){
if(road[0][i] != INT_MAX){
cout << "从源结点0到结点" << to_string(i) << "的路径为: " << path[i] << endl;
cout << "且长度为: " << road[0][i] << endl;
}
else{
cout << "从源结点0到结点" << to_string(i) << "不存在路径" << endl;
}
}
delete[] path;
}
void GRAPH::CriticalPath(){
// 关键路径又可以说是找到图中一条最大的通路
// 我们在工程中有些工作是可以并行的,于是完成一项工作的最短时间就是找图中一条最长的路
// 图的关键路径的寻找思想是 找时间余量为0的活动
// 有些活动由于可以与其他活动并行,可以推迟几天完成,于是就产生了时间余量
// 但有些工作如果推迟完成就会影响总工程的总时间于是这些工作就成为关键活动
// 一条充满了关键活动的路径就称为关键路径
// 此处设置 e为一项活动的最早发生时间 d为一项活动发生的最晚发生时间
// 假设两者差值为0,则是一项没有余量的活动
// 我们通过拓扑排序和逆拓扑排序的方式来计算两个数组
// 测试数据仍然直接采用教材中给出的例子
// 用的是Page 186 图7-30的例子
// 虽然书中给出的是邻接表的算法,但这儿仍然用邻接矩阵
// 毕竟只是看操作,不用太在意图的存储方式
// 最后我直接默认拓扑排序后无环也就是可以操作!!!!!否则拓扑排序发现有环的话无法操作!!!
vector<int> e, d;
// 由于用到拓扑排序,于是要设立一个装度数的容器
vector<int> deg;
cri = new int *[6];
for (int i = 0; i < 6; i ++){
cri[i] = new int[6];
deg.push_back(0), e.push_back(0);
d.push_back(INT_MAX);
for (int j = 0; j < 6; j ++){
cri[i][j] = INT_MAX;
}
}
cri[0][1] = 3;
cri[0][2] = 2;
cri[1][3] = 2;
cri[1][4] = 3;
cri[2][3] = 4;
cri[2][5] = 3;
cri[3][5] = 2;
cri[4][5] = 1;
// 我们要按照拓扑排序从只有出度结点一直运行到只有入度结点
queue<int> q;
stack<int> s; // 因为要用到逆拓扑排序
for (int i = 0; i < 6; i ++){
for (int j = 0; j < 6; j ++){
if(cri[i][j] != INT_MAX)
deg[j]++;
}
}
for (int i = 0; i < 6; i ++){
if(deg[i] == 0)
q.push(i);
}
// 用这样拓扑排序的方式求取e
// e代表每个顶点开始的最早时间 一定要max
// 假如多个结点汇到同一结点,肯定要都完成才能进行下一步!!于是要取最大值!
while(!q.empty()){
int temp = q.front();
visit[temp] = true;
s.push(temp);
q.pop();
for (int i = 0; i < 6; i ++){
if(cri[temp][i] !=INT_MAX){
deg[i]--;
e[i] = max(e[i], e[temp] + cri[temp][i]);
}
if(!visit[i] && deg[i] == 0) q.push(i);
}
}
d[s.top()] = e[s.top()]; // 最后一个活动开始时间是一样的!!
// 通过一个逆拓扑排序得到每个结点的最晚开始时间
// 利用我们的栈获取到的顺序拓扑排序直接调用即可
// 最晚开始就是说有些工作实际上是可以拖延的
// 比如你写完数学作业可以同时写语文和英语(fantanstic thinking!!)
// 现在你发现写语文要30mins,写英语要60mins,你被规定写完语文的时间是22:00,写完英语的时间是23:00
// 那么你自然会发现我们应该取相减后更早的那个时间为最晚写完数学作业的时间,即21:30,不然就会有一项超时
while(!s.empty()){
int temp = s.top();
s.pop();
for (int i = 0; i < 6; i ++){
if(cri[i][temp] !=INT_MAX){ // 注意我们是逆序于是i索引要在之前
d[i] = min(d[i], d[temp] - cri[i][temp]);
}
}
}
// 通过求得获取的东西我们去判断哪些活动是关键的,是不可拖延的!!
// 我们的活动实际上就是一条连接两个结点的边
// 那么假如 一项活动的最早开始时间和最晚开始时间相同则称为一条关键路径
// 最后就是遍历搜索关键路径
string critical_path = "";
int max_time = 0;
for (int i = 0; i < 6; i ++){
for (int j = 0; j < 6; j ++){
if(cri[i][j] != INT_MAX && e[i] == d[j] - cri[i][j]){
critical_path += (to_string(i) + "->" + to_string(j) + '\n');
max_time += cri[i][j];
}
}
}
cout << "关键路径为: \n" << critical_path;
cout << "最长路径值为: " << max_time << endl;
}
GRAPH::~GRAPH(){
for (int i = 0; i < row; i ++){
delete[] p[i];
}
delete[] p;
for (int i = 0; i < 6; i ++){
delete[] weight[i];
delete[] topo_test[i];
delete[] cri[i];
}
delete[] weight;
delete[] topo_test;
delete[] cri;
delete[] visit;
}
int main(){
int r, c;
cout << "请输入图的长和宽: " << endl;
cin >> r >> c;
GRAPH graph(r, c); // 初始化图
int n1, n2; // 用以表示某两点之间有边
int arc_num;
cout << "请输入图的边数目:" << endl;
cin >> arc_num;
vector<vector<int>> vec;
vector<int> *v = new vector<int>[arc_num];
for (int i = 0; i < arc_num ; i ++){
cin >> n1 >> n2;
v[i].push_back(n1);
v[i].push_back(n2);
vec.push_back(v[i]);
}
graph.CreateGraph(vec, arc_num);
graph.TraverseArray();
graph.CreateGraph_Linklist(vec, arc_num);
graph.TraverseLinklist();
graph.DFSTraverse();
graph.RecoverVisit();
graph.BFSTraverse();
graph.RecoverVisit();
graph.Test();
graph.MSCTree();
graph.TopologicalSort();
graph.RecoverVisit();
graph.Dijkstra();
graph.RecoverVisit();
graph.CriticalPath();
graph.RecoverVisit();
return 0;
}
测试效果图如下: