离散数学中关于图的介绍
链表迭代器的实现
邻接链表迭代器
这里不使用指针 cur 指向已经遍历的节点是因为如果节点被删除就访问不到了, 所以只能每次都从头开始遍历
template<class T>
class iter {
public:
iter(graphChain<T>* list) {
cur = 1;
row = list;
}
chainNode<T>* next() {
chainNode<T>* res = nullptr,
// 从根节点开始遍历
* temp = row->root();
// 到 cur 前停下
for (int i = 0; i < cur && cur <= row->size(); i++) {
res = temp;
temp = temp->next;
}
if (res == nullptr) {
cur = 0; // 设置成 0 不能再迭代了
return nullptr;
}
else {
cur++;
return res; // 迭代器返回当前节点
}
}
protected:
graphChain<T>* row;
int cur; // 当前迭代位置
};
iter<Edge<T>>* iterator(Vertex<T>& v) {
// 返回顶点 theVertex 的迭代器
if (!checkVertex(v)) {
throw "顶点非法!";
}
return new iter<Edge<T>>(aList + v.getId());
}
邻接矩阵迭代器实现
class iter {
public:
iter(Edge<T>* r, Edge<T>& ne, int num) : row(r), noEdge(ne), n(num), cur(0) {};
Edge<T>& next() {
// 遍历邻接矩阵的列
// 不为 noEdge 代表有边
// 返回下一个邻接顶点
for (int j = cur; j < n; j++) {
if (row[j] != noEdge) {
cur = j + 1;
return row[j];
}
}
// 不存在邻接点
cur = n;
return noEdge;
}
protected:
Edge<T>* row; // 邻接矩阵的行
Edge<T>& noEdge; // 不存在边
int n; // 顶点数
int cur; // 当前位置
};
iter* iterator(Vertex<T> v) {
// 返回顶点 theVertex 的迭代器
if (!checkVertex(v)) {
throw "顶点非法!";
}
return new iter(a[v.getId() - 1], noEdge, n);
}
成本邻接矩阵
加权有向图
使用一个 二维矩阵 表示各边的关系, 0 表示无边, 数字代表 权
构造函数的时间复杂度:
出度和入度
由邻接矩阵可以很方便的计算出度和入度
// 顶点的出度
template<class T>
int adjacencyWDigraph<T>::outDegree(Vertex<T>& v) const {
if (!checkVertex(v)) throw "无效顶点";
// 顶点 v 的边数
int sum = 0;
// 只需要遍历邻接矩阵第 v 行即可
// 只要不等于 noEdge, 就代表有出度
for (int j = 0; j < n; j++) {
if (a[v.getId() - 1][j] != noEdge) {
sum++;
}
}
return sum;
}
// 顶点的入度
template<class T>
int adjacencyWDigraph<T>::inDegree(Vertex<T>& v) const {
if (!checkVertex(v)) throw "无效顶点";
// 顶点 v 的边数
int sum = 0;
// 只需要遍历邻接矩阵第 v 列即可
for (int j = 0; j < n; j++) {
if (a[j][v.getId() - 1] != noEdge) {
sum++;
}
}
return sum;
}
增加、删除
增加和删除一条边只需要把矩阵相应位置上的值改成 1 或 0 , 因此时间复杂度:
完整代码
template<class T>
class adjacencyWDigraph:public graph<T> {
protected:
int n; // 顶点个数
int e; // 边的个数
T** a; // 邻接矩阵
T noEdge; // 不存在的边
public:
adjacencyWDigraph(int numberOfVertices = 0, T theNoEdge = 0) {
if (numberOfVertices < 0) {
throw "顶点数必须大于等于 0";
}
n = numberOfVertices;
e = 0;
noEdge = theNoEdge; // 默认 0 代表无边
// 创建一个 (n+1) * (n+1) 的数组
a = new T * [n + 1];
for (int i = 0; i < n + 1; i++) {
a[i] = new T[n + 1];
}
// 初始化邻接矩阵
for (int i = 0; i < n + 1; i++) {
for (int j = 0; j < n + 1; j++) {
a[i][j] = 0;
}
}
}
~adjacencyWDigraph() {
for (int i = 0; i < n + 1; i++) {
delete[] a[i];
}
delete[] a;
a = nullptr;
}
int numberOfVertices() const {
return n;
}
int numberOfEdges() const {
return e;
}
// 加权有向图
bool directed() const {
return true;
}
bool weighted() const {
return true;
}
bool existsEdge(int i, int j) const {
// 当且仅当 (i, j) 存在边返回 true
if (i<1 || j<1 || i>n || j>n || a[i][j] == noEdge) {
return false;
}
else {
return true;
}
}
void insertEdge(Edge* theEdge) {
// 边存在, 则修改权
int v1 = theEdge->vertex1();
int v2 = theEdge->vertex2();
if (v1<1 || v2<1 || v1>n || v2>n || v1 == v2) {
throw "非法的边";
}
if (a[v1][v2] == noEdge) {
e++;
}
a[v1][v2] = theEdge->getWeight();
}
void eraseEdge(int i, int j) {
// 删除边
// 注意这是有向图, (i, j) 和 (j, i) 不等价
// 不应该删掉 a[j][i]
if (i >= 1 && j >= 1 && i <= n && j <= n && a[i][j] != noEdge) {
a[i][j] = noEdge;
e--;
}
}
bool checkVertex(int theVertex) const {
// 判断是否是有效顶点
if (theVertex < 1 || theVertex > n) {
return false;
}
else {
return true;
}
}
int degree(int theVertex) const {
throw "有向图无此方法";
}
// 返回顶点的出度
int outDegree(int theVertex) const {
if (!checkVertex(theVertex)) {
throw "无效顶点";
};
// 关联顶点 theVertex 的边数
int sum = 0;
// 只需要遍历邻接矩阵第 theVertex 行即可
// 只要不等于 0(即有权), 就代表有出度
for (int j = 1; j <= n; j++) {
if (a[theVertex][j] != noEdge) {
sum++;
}
}
return sum;
}
int inDegree(int theVertex) const {
if (!checkVertex(theVertex)) {
throw "无效顶点";
};
// 关联顶点 theVertex 的边数
int sum = 0;
// 只需要遍历邻接矩阵第 theVertex 列即可
for (int j = 1; j <= n; j++) {
if (a[j][theVertex] != noEdge) {
sum++;
}
}
return sum;
}
// 内部类
class iter:public vertexIterator<T> {
public:
iter(T* theRow, T theNoEdge, int numberOfVertics) {
row = theRow;
noEdge = theNoEdge;
n = numberOfVertics;
currentVertex = 1;
}
~iter() {}
int next(T& theWeight) {
// 返回下一个邻接顶点
for (int j = currentVertex; j <= n; j++) {
if (row[j] != noEdge) {
currentVertex = j + 1;
theWeight = row[j];
return j;
}
}
// 不存在邻接点
currentVertex = n + 1;
return 0;
}
int next() {
// 返回下一个邻接顶点
for (int j = currentVertex; j <= n; j++) {
if (row[j] != noEdge) {
currentVertex = j + 1;
return j;
}
}
// 不存在邻接点
currentVertex = n + 1;
return 0;
}
protected:
T* row; // 邻接矩阵的行
T noEdge; // 不存在边
int n; // 顶点数
int currentVertex; // 当前顶点
};
iter* iterator(int theVertex) {
// 返回顶点 theVertex 的迭代器
if (!checkVertex(theVertex)) {
throw "顶点非法!";
}
return new iter(a[theVertex], noEdge, n);
}
};
使用
// 10 个点
adjacencyWDigraph<int>* p = new adjacencyWDigraph<int>(10);
Edge* a = new Edge(1, 2, 3);
p->insertEdge(a);
Edge* b = new Edge(1, 3, 3);
p->insertEdge(b);
Edge* c = new Edge(1, 4, 5);
p->insertEdge(c);
adjacencyWDigraph<int>::iter* i = p->iterator(1);
cout << "1 的邻接点: " << i->next() << endl; // 2
return 0;
邻接压缩表
使用两个一维数组, h[1:n+1] 和 l[0:x]
其中 l 保存顶点的邻接顶点, 一个点的邻接顶点在 l 中是连续的, h[i] 表示顶点 i 的邻接顶点在 l 中的起始位置
对于有 e 条边的有向图, x=e-1 , 无向图 x=2e-1, 既有无向边(e), 又有有向边(e'), 则 x=2e+e'-1
某个点的出度为 h[i+1]-h[i]
边总数, 无向图为 h[n+1]/2
有向图 h[n+1]
增加和删除一条边
邻接链表
使用一个 n 维链表数组 表示点的关系
链表内元素表示与当前顶点相邻的点
构造函数的时间复杂度:
出度和入度
出度比较简单, 时间复杂度为 O(1), 直接获取对应链表的长度即可
int outDegree(Vertex<T>& v) const {
if (!checkVertex(v)) {
throw "顶点无效";
}
return aList[v.getId()].size();
}
入度需要遍历所有点的链表, 时间复杂度 O(n+e) ( e 是边数)
int inDegree(Vertex<T>& v) const {
if (!checkVertex(v)) {
throw "顶点无效";
}
int sum = 0, vv = v.getId();
Edge<T> e;
for (int j = 1; j <= n; j++) {
// 自己和自己不存在入度(不能成环)
if (vv == j) continue;
// 将 j 作为点1, 点 v 作为点2
// 即 v1->v2
e.setId(j, vv);
// 在 aList 中查找相应边
if (aList[j].indexOf(e) != -1) {
sum++;
}
}
return sum;
}
完整代码
#include<iostream>
using namespace std;
// 点类
template<class T>
class Vertex {
private:
T data; // 顶点所具有的数据
int id; // 顶点编号
public:
T& get() { return data; };
int getId() { return id; };
// 这里要求 T 类型重载 = 运算符
void set(const T& value) { data = value; };
void setId(int i) { id = i; };
Vertex<T>& operator=(const Vertex<T>& v) {
data = v.data;
id = v.id;
return *this;
}
// 这里要求 T 类型具有复制构造函数
Vertex(const T& value, int i = 1): data(data), id(i) {};
Vertex() { id = 1; };
Vertex(int i) :id(i) {};
bool operator==(const Vertex<T>& v) {
// 这里要求 T 类型重载 == 运算符
return (data == v.data && id == v.id);
}
bool operator!=(const Vertex<T>& v) {
return (data != v.data || id != v.id);
};
};
// 边类
template<class T>
struct Edge {
private:
// 边的方向 v1->v2
int v1_id; // 点 1 的编号
int v2_id; // 点 2 的编号
int weight; // 权重
public:
Edge() { v1_id = v2_id = 0; weight = 1; };
Edge(const Vertex<T>& v1, const Vertex<T>& v2, int w = 1)
:weight(w) {
v1_id = v1.getId();
v2_id = v2.getId();
};
int getWeight() const { return weight; };
void setWeight(int w) { weight = w; };
// 设置边对应的点(也可以用于重置)
void setId(int v1 = 0, int v2 = 0) { v1_id = v1; v2_id = v2; };
// 获取边对应顶点的编号
int v1() const { return v1_id; };
int v2() const { return v2_id; };
bool operator==(const Edge<T>& v) { return v1_id == v.v1_id && v2_id == v.v2_id; };
bool operator!=(const Edge<T>& v) { return v1_id != v.v1_id || v2_id != v.v2_id; };
};
// 链表节点
template<class T>
struct chainNode {
T element;
chainNode<T>* next;
chainNode(const T& e) :element(e), next(nullptr) {};
chainNode(const T& e, chainNode<T>* n) : element(e), next(n) {};
};
// 链表类
template<class T>
class graphChain {
public:
graphChain() : firstNode(nullptr), listSize(0) {};
int indexOf(const T&) const;
void insert(const T&);
bool erase(const T&);
int size() const { return listSize; };
chainNode<T>* root() const { return firstNode; };
private:
chainNode<T>* firstNode;
int listSize;
};
// 获取元素索引
template<class T>
int graphChain<T>::indexOf(const T& e) const {
chainNode<T>* cur = firstNode;
int index = 0;
while (cur != nullptr && cur->element != e) {
cur = cur->next;
index++;
}
if (cur == nullptr) return -1;
else return index;
}
// 插入节点
template<class T>
void graphChain<T>::insert(const T& e) {
// 总是插入到表头
firstNode = new chainNode<T>(e, firstNode);
listSize++;
}
// 删除节点, 删除成功返回 true
template<class T>
bool graphChain<T>::erase(const T& e) {
chainNode<T>* deleteNode;
// 如果要删除的是首节点
if (firstNode->element == e) {
deleteNode = firstNode;
firstNode = firstNode->next;
}
else
{
chainNode<T>* p = firstNode;
while (p->next != nullptr && p->next->element != e) {
p = p->next;
}
if (p->next == nullptr) return false;
deleteNode = p->next;
p->next = p->next->next;
}
listSize--;
delete deleteNode;
return true;
}
// 邻接链表(加权有向图)
template<class T>
class linkedDigraph {
protected:
int n; // 顶点数
int e; // 边数
/* 点集和边集索引一一对应 */
Vertex<T>* v; // 点集, 用于保存顶点的数据
graphChain<Edge<T>>* aList; // 对应的邻接链表, 保存边
public:
linkedDigraph(int numberOfVertices = 0) {
if (numberOfVertices < 0) {
throw "顶点数必须大于 0";
}
n = numberOfVertices;
e = 0;
aList = new graphChain<Edge<T>>[n + 1];
v = new Vertex<T>[n + 1];
}
~linkedDigraph() { delete[] aList; delete[] v; };
// 顶点数
int numberOfVertices() const { return n; };
// 边数
int numberOfEdges() const { return e; };
// 是否是有向图
bool directed() const { return true; };
// 是否是有权图
bool weighted() const { return false; };
bool exists(Edge<T>&) const;
void insert(Edge<T>&);
void erase(Edge<T>&);
// 检测顶点是否有效
bool checkVertex(Vertex<T>& v) const {
if (v.getId() < 1 || v.getId() > n) {
return false;
}
else {
return true;
}
}
// 出度
int outDegree(Vertex<T>& v) const {
if (!checkVertex(v)) {
throw "顶点无效";
}
return aList[v.getId()].size();
}
// 入度
int inDegree(Vertex<T>& v) const {
if (!checkVertex(v)) {
throw "顶点无效";
}
int sum = 0, vv = v.getId();
Edge<T> e;
for (int j = 1; j <= n; j++) {
// 自己和自己不存在入度(不能成环)
if (vv == j) continue;
// 将 j 作为点1, 点 v 作为点2
// 即 v1->v2
e.setId(j, vv);
// 在 aList 中查找相应边
if (aList[j].indexOf(e) != -1) {
sum++;
}
}
return sum;
}
// 内部类
template<class T>
class iter {
public:
iter(graphChain<T>* list) {
cur = 1;
row = list;
}
chainNode<T>* next() {
chainNode<T>* res = nullptr,
* temp = row->root();
for (int i = 0; i < cur && cur <= row->size(); i++) {
res = temp;
temp = temp->next;
}
if (res == nullptr) {
cur = 0; // 设置成 0 不能再迭代了
return nullptr;
}
else {
cur++;
return res; // 迭代器返回当前节点
}
}
protected:
graphChain<T>* row;
int cur; // 当前迭代位置
};
iter<Edge<T>>* iterator(Vertex<T>& v) {
// 返回顶点 theVertex 的迭代器
if (!checkVertex(v)) {
throw "顶点非法!";
}
return new iter<Edge<T>>(aList + v.getId());
}
};
// 边是否存在
template<class T>
bool linkedDigraph<T>::exists(Edge<T>& edge) const {
int i = edge.v1(), j = edge.v2();
if (i < 1 || j < 1 || i > n || j > n || aList[i].indexOf(edge) == -1) {
return false;
}
else {
return true;
}
}
// 插入边
template<class T>
void linkedDigraph<T>::insert(Edge<T>& edge) {
// 边存在, 则修改权
int i = edge.v1();
int j = edge.v2();
if (i < 1 || j < 1 || i > n || j > n || i == j) {
throw "非法的边";
}
// 链表插入新边
// 因为是有向图, 因此只需要插入 v1->v2 的边即可
if (aList[i].indexOf(edge) == -1) {
aList[i].insert(edge);
e++;
}
}
// 删除边
template<class T>
void linkedDigraph<T>::erase(Edge<T>& edge) {
int i = edge.v1(), j = edge.v2();
if (i >= 1 && j >= 1 && i <= n && j <= n) {
bool v = aList[i].erase(edge);
// 删除成功, 边减一
if (v) e--;
}
}
int main() {
linkedDigraph<string> ld(10);
// 插入下列点集的笛卡尔积
int v1[] = { 1,2,3,5,7 };
int v2[] = { 4,6,8,9 };
Edge<string> a;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 4; j++) {
a.setId(v1[i], v2[j]);
ld.insert(a);
}
}
Vertex<string> v("点1", 1);
// 生成点1的迭代器
linkedDigraph<string>::iter<Edge<string>>* i = ld.iterator(v);
int ii = 1;
chainNode<Edge<string>>* temp;
while (temp = i->next()) {
Edge<string> e = temp->element;
cout << "1 的邻接点(" << ii++ << "): " << e.v2() << endl;
}
cout << "-----分隔符-----\n";
a.setId(1, 6);
ld.erase(a); // 删除边 (1,6)
i = ld.iterator(v);
ii = 1;
while (temp = i->next()) {
Edge<string> e = temp->element;
cout << "1 的邻接点(" << ii++ << "): " << e.v2() << endl;
}
return 0;
}
仅提供添加边的代码, 省略了往邻接链表图里插入顶点...
广度优先搜索(bfs)
以下均使用 C++ 标准库
<stack>与<queue>实现, 且均使用上面邻接链表实现的图
出队时访问
与层次遍历类似, 对 1 进行广度优先遍历:
- 访问
1 - 依次访问
432 - 再访问下一层的
765 - 最后访问
98
template<class T>
void linkedDigraph<T>::bfs(int v, int* reach, int label) {
// v: 顶点编号
// reach: 保存顶点 v 所有可达顶点的数组
// label: 标记方式(一般用 1 标记可达)
queue<int> q;
q.push(v);
while (!q.empty()) {
int w = q.front();
q.pop();
cout << "访问到顶点: " << w << endl;
reach[w] = label; // 标记访问
chainNode<Edge<T>>* temp = aList[w].root();
// 这里默认无环图, 因此不会访问到访问过的顶点
// 如果是有环图, 还需要判断是否访问过, 访问过的不要入队
while (temp != nullptr) {
// 将点 w 的相邻节点依次入队
q.push(temp->element.v2());
temp = temp->next;
}
}
}
测试用例
int main() {
linkedDigraph<string> ld(9);
Edge<string> a;
a.setId(1, 2);
ld.insert(a);
a.setId(1, 3);
ld.insert(a);
a.setId(1, 4);
ld.insert(a);
a.setId(2, 5);
ld.insert(a);
a.setId(5, 8);
ld.insert(a);
a.setId(4, 6);
ld.insert(a);
a.setId(4, 7);
ld.insert(a);
a.setId(7, 9);
ld.insert(a);
// 下标从 1 开始
int reach[10] = { 0,0,0,0,0,0,0,0,0,0 };
ld.bfs(1, reach, 1);
cout << "1 的可达节点为: ";
for (int i = 1; i < 10; i++) {
if (reach[i] != 0) {
cout << i << " ";
}
}
return 0;
}
深度优先遍历(dfs)
与前序遍历类似, 使用栈实现 出栈时访问
template<class T>
void linkedDigraph<T>::dfs(int v, int* reach, int label) {
// v: 顶点编号
// reach: 保存顶点 v 所有可达顶点的数组
// label: 标记方式
stack<int> q;
q.push(v);
chainNode<Edge<T>>* temp;
while (!q.empty()) {
int w = q.top();
q.pop(); // 栈顶元素访问了一次就出栈
cout << "访问到顶点 " << w << endl;
reach[w] = label;
temp = aList[w].root();
// 把顶点对应的另一个点依次入栈
// 只要一个路径上还有其他顶点, 就会入栈顶
// 因此不会去访问其他分支(在栈更底部)
while (temp) {
q.push(temp->element.v2());
temp = temp->next;
}
}
}
拓扑排序
知识点: 拓扑排序
如果顶点无法遍历完(也就是说还存在入度不为0的边), 说明存在环, 该任务相互依赖, 不可能完成
基于深度优先的拓扑排序 使用栈实现
template<class T>
void linkedDigraph<T>::tSort(int* res) {
Vertex<T> v;
int* indegree = new int[n + 1];
for (int i = 1; i <= n; i++) {
// 存储每个点的入度
v.setId(i);
int in = inDegree(v);
int out = outDegree(v);
// 入度为 0 且出度为 0 的点是不存在的点, 不考虑
if (out == 0 && in == 0) indegree[i] = -1;
else indegree[i] = in;
}
stack<int> s; // 存储入度为 0 顶点编号的栈
for (int i = 1; i <= n; i++) {
// 入度为 0 的顶点入栈
if (indegree[i] == 0) s.push(i);
}
int i = 1; // 存储位置
while (!s.empty()) {
int t = s.top();
s.pop();
res[i++] = t;
// 将所以与 t 关联的顶点入度减一
chainNode<Edge<T>>* temp = aList[t].root();
while (temp) {
int v = temp->element.v2();
// 减一后入度为 0 就入栈
if (--indegree[v] == 0) s.push(v);
temp = temp->next;
}
}
}
基于广度优先的拓扑排序 使用队列实现
template<class T>
void linkedDigraph<T>::ttSort(int* res) {
Vertex<T> v;
int* indegree = new int[n + 1];
for (int i = 1; i <= n; i++) {
// 存储每个点的入度
v.setId(i);
int in = inDegree(v);
int out = outDegree(v);
// 入度为 0 且出度为 0 的点是不存在的点, 不考虑
if (out == 0 && in == 0) indegree[i] = -1;
else indegree[i] = in;
}
queue<int> s; // 存储入度为 0 顶点编号的队列
for (int i = 1; i <= n; i++) {
// 入度为 0 的顶点入队
if (indegree[i] == 0) s.push(i);
}
int i = 1; // 存储位置
while (!s.empty()) {
int t = s.front();
s.pop();
res[i++] = t;
// 将所以与 t 关联的顶点入度减一
chainNode<Edge<T>>* temp = aList[t].root();
while (temp) {
int v = temp->element.v2();
// 减一后入度为 0 就入队
if (--indegree[v] == 0) s.push(v);
temp = temp->next;
}
}
}
最短路径
DIJKSTRA算法
知识点: dijkstra 算法 图解
优先级队列实现
需要给 Edge 重载 < 运算符
template<class T>
struct Edge {
public:
// ....
bool operator < (const Edge<T>& v) const {
// weight 小的要在前面
// 而 stl 小的在后面
// 因此这里做相反处理
return weight > v.weight;
}
};
使用 stl 中的 priority_queue 实现
template<class T>
int linkedDigraph<T>::Dijkstra(int start, int end) {
priority_queue<Edge<T>> q;
// 顶点是否访问过
bool* vis = new bool[n + 1];
for (int i = 1; i <= n; i++) {
vis[i] = false;
}
// 从 start 出发到各个顶点的最短路径
int* distance = new int[n + 1];
// 初始化路径为无穷大(到 start 的路径为 0)
for (int i = 1; i <= n; i++) {
if (i == start) distance[i] = 0;
else distance[i] = 0x7fffffff;
}
Edge<T> a;
a.setId(0, start); // 顶点 0 是不存在的
a.setWeight(0); // 开始权值为 0
// 将一个不存在的点到 start 的路径入队
q.push(a);
while (!q.empty()) {
// 优先级队列(小根堆)会优先弹出路径 Edge 中 weight 小的
Edge<T> cur = q.top();
q.pop();
cout << "访问到: " << cur.v2() << endl;
if (vis[cur.v2()] == true) continue; // 顶点访问过, 就跳过
vis[cur.v2()] = true; // 标记访问过
// 访问所有与 cur 相邻的顶点
chainNode<Edge<T>>* temp = aList[cur.v2()].root();
while (temp) {
int w = temp->element.getWeight(); // 0->cur 路径的权值
// 0->cur + cur->temp 如果大于 0->temp
if (distance[cur.v2()] + w < distance[temp->element.v2()]) {
distance[temp->element.v2()] = distance[cur.v2()] + w;
// 将 0->temp 的路径入队
a.setId(0, temp->element.v2());
a.setWeight(distance[temp->element.v2()]);
q.push(a);
}
temp = temp->next;
}
}
int min = distance[end];
delete[] distance;
delete[] vis;
return min;
}
测试用例
int main() {
linkedDigraph<string> ld(9);
Edge<string> a;
a.setId(1, 2);
a.setWeight(4);
ld.insert(a);
a.setId(1, 3);
a.setWeight(2);
ld.insert(a);
a.setId(2, 4);
a.setWeight(3);
ld.insert(a);
a.setId(2, 5);
a.setWeight(3);
ld.insert(a);
a.setId(3, 5);
a.setWeight(3);
ld.insert(a);
a.setId(5, 6);
a.setWeight(1);
ld.insert(a);
a.setId(4, 6);
a.setWeight(2);
ld.insert(a);
int min = ld.Dijkstra(1, 6);
cout << "最短路径长度: " << min << endl;
return 0;
}
普通实现
顺便完成作业题...
顶点编号从 1 开始
#include<iostream>
#include<vector>
#include<stack>
#include<algorithm>
using namespace std;
#define INF 0x7fffffff
// 获取邻接矩阵
void init(vector<vector<int>>& matrix, int m) {
int i, j, k;
for (int z = 0; z < m; z++) {
cin >> i >> j >> k;
matrix[i][j] = k;
}
}
void Dijkstra(
int start,
vector<vector<int>>& matrix,
vector<int>& distance,
vector<int>& path,
vector<bool>& vis
) {
int n = matrix.size() - 1;
distance[start] = 0; // 起始点距离为 0
// 获取 distance 数组
for (int i = 1; i <= n; i++) {
int u = 0, min = INF;
// 从未访问过的顶点中找出一条最短路径
for (int j = 1; j <= n; j++) {
if (!vis[j] && distance[j] < min) {
u = j;
min = distance[j];
}
}
if (u == 0) break; // 没找到则说明已经到头了
vis[u] = true; // 找到后标记访问
// 遍历所有顶点, 找到最短路径上 u 的下一个顶点
for (int i = 1; i <= n; i++) {
// 要求顶点未访问过, 并且 u->i存在路径, 且加上路径权值后比原来的路径要短
if (!vis[i] && matrix[u][i] != INF && distance[i] > distance[u] + matrix[u][i]) {
path[i] = u; // 保存 i 的前驱顶点 u
distance[i] = distance[u] + matrix[u][i]; // 更新最短路径
}
}
}
}
void print(
int start,
vector<int>& distance,
vector<int>& path,
vector<bool>& vis
) {
int n = distance.size() - 1;
// 保存路径长度以及顶点的数组
vector<pair<int, int>> t(n + 1);
for (int i = 1; i <= n; i++) {
t[i].first = distance[i];
t[i].second = i;
}
// 路径升序
sort(t.begin() + 1, t.end());
int no = 1;
bool first = true;
stack<int> s;
for (int i = 1; i <= n; i++) {
// 会先从路径小的开始输出
if (t[i].second == start || !vis[t[i].second]) continue;
int temp = t[i].second;
while (temp != start) {
s.push(temp);
temp = path[temp];
}
if (first) {
cout << "No." << no++ << " : " << start;
first = false;
}
else {
cout << "\nNo." << no++ << " : " << start;
}
while (!s.empty()) {
cout << " -> " << s.top();
s.pop();
}
cout << " , d = " << distance[t[i].second];
}
// 不可达顶点
first = true;
for (int i = 1; i <= n; i++) {
if (!vis[i] && first) {
cout << "\nNo." << no++ << " : No Path to " << i;
first = false;
}
else if (!vis[i]) {
cout << " " << i;
}
}
}
int main() {
int n, m;
cin >> n >> m; // 输入顶点数与路径数
vector<vector<int>> matrix(n + 1, vector<int>(n + 1, INF)); // 邻接矩阵
init(matrix, m);
vector<int> distance(n + 1, INF); // 初始化 distance 数组为无穷大
vector<bool> vis(n + 1, false); // 用于判断顶点是否访问过
vector<int> path(n + 1, 0); // 保存每个顶点最短路径上的前驱节点, 用于回溯路径
int start;
cin >> start; // 输入起始点
Dijkstra(start, matrix, distance, path, vis);
print(start, distance, path, vis);
return 0;
}
floyd 算法
原理: floyd图解
#include<iostream>
#include<vector>
#include<stack>
using namespace std;
#define INF 0x7fffffff
// 获取邻接矩阵和路径矩阵
void init(vector<vector<int>>& distance, vector<vector<int>>& path, int m) {
int n = distance.size();
for (int i = 0; i < n; i++) {
distance[i][i] = 0; // 最短距离矩阵对角线元素均为 0
}
int i, j, k;
for (int z = 0; z < m; z++) {
cin >> i >> j >> k;
distance[i][j] = k; // i->j 路径长度
path[i][j] = -2; // 标记直接相邻
}
}
void Flody(
vector<vector<int>>& distance,
vector<vector<int>>& path
) {
int n = distance.size();
for (int k = 0; k < n; k++) { // 欲加入的中间点
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// 要求加入一个点后 i,j 两点能够连通, 并且如果 i->j 的距离比 i->k->j 大
if (!(distance[i][k] == INF || distance[k][j] == INF) && distance[i][j] > distance[i][k] + distance[k][j]) {
distance[i][j] = distance[i][k] + distance[k][j]; // 修改距离
path[i][j] = k; // 记录中转点
}
}
}
}
}
// 输出最短路径矩阵
void print(vector<vector<int>>& distance) {
int n = distance.size();
// 输出最短路径矩阵
for (int i = 0; i < n; i++) {
bool first = true;
for (int j = 0; j < n; j++) {
if (distance[i][j] == INF && first) {
cout << -1;
first = false;
}
else if (distance[i][j] == INF) {
cout << " " << - 1;
}
else if (first) {
cout << distance[i][j];
first = false;
}
else {
cout << " " << distance[i][j];
}
}
cout << endl;
}
}
// 获取两点直接的最短路径
void getMinPath(int start, int end, vector<vector<int>>& path) {
stack<int> p;
int temp = path[start][end];
for (int i = 0; i < path.size(); i++) {
if (temp == -1) {
cout << start << "到" << end << "不连通";
return;
}
if (temp == -2) break;
p.push(temp);
temp = path[start][temp];
}
cout << start << " -> ";
while (!p.empty()) {
cout << p.top() << " -> ";
p.pop();
}
cout << end;
}
int main() {
int n, m;
cin >> n >> m; // 输入顶点数与路径数
// 初始化 distance 数组为 INF
vector<vector<int>> distance(n, vector<int>(n, INF));
// 保存每个顶点最短路径上的前驱节点, 用于回溯路径
// -1 表示不直接相邻
vector<vector<int>> path(n, vector<int>(n, -1));
init(distance, path, m);
Flody(distance, path);
int start, end;
cin >> start >> end;
// 测试用例
// 8 10
// 0 5 10
// 0 4 5
// 5 2 1
// 5 4 2
// 2 3 4
// 3 0 7
// 3 2 6
// 4 5 3
// 4 2 9
// 4 3 2
// 0 2
getMinPath(start, end, path);
return 0;
}
最小生成树
知识点: 最小生成树
Kruskal算法破圈法prim算法
只适合无向图
测试用例
因为要求无向图, 但是
linkedDigraph是有向图, 为了偷懒, 就把两个方向一起加进去
int main() {
linkedDigraph<string> ld(9);
Edge<string> a;
a.setWeight(4);
a.setId(1, 2);
ld.insert(a);
a.setId(2, 1);
ld.insert(a);
a.setWeight(8);
a.setId(1, 3);
ld.insert(a);
a.setId(3, 1);
ld.insert(a);
a.setWeight(11);
a.setId(2, 3);
ld.insert(a);
a.setId(3, 2);
ld.insert(a);
a.setWeight(8);
a.setId(2, 4);
ld.insert(a);
a.setId(4, 2);
ld.insert(a);
a.setWeight(2);
a.setId(4, 5);
ld.insert(a);
a.setId(5, 4);
ld.insert(a);
a.setWeight(7);
a.setId(3, 5);
ld.insert(a);
a.setId(5, 3);
ld.insert(a);
a.setWeight(1);
a.setId(3, 6);
ld.insert(a);
a.setId(6, 3);
ld.insert(a);
a.setWeight(6);
a.setId(6, 5);
ld.insert(a);
a.setId(5, 6);
ld.insert(a);
a.setWeight(7);
a.setId(4, 8);
ld.insert(a);
a.setId(8, 4);
ld.insert(a);
a.setWeight(4);
a.setId(4, 7);
ld.insert(a);
a.setId(7, 4);
ld.insert(a);
a.setWeight(2);
a.setId(6, 7);
ld.insert(a);
a.setId(7, 6);
ld.insert(a);
a.setWeight(14);
a.setId(8, 7);
ld.insert(a);
a.setId(7, 8);
ld.insert(a);
a.setWeight(9);
a.setId(8, 9);
ld.insert(a);
a.setId(9, 8);
ld.insert(a);
a.setWeight(10);
a.setId(9, 7);
ld.insert(a);
a.setId(7, 9);
ld.insert(a);
int res[10];
ld.Prim(1, res);
cout << "最小生成树: ";
for (int i = 1; i < 10; i++) {
cout << res[i] << " ";
}
return 0;
}
prim 算法
优先级队列实现
由于
linkedDigraph设计的原因, 初始化时要求传入图最大点数, 也就是说有n个点的图, 某些点和边可能不存在, 如果不使用优先队列, 就要先判断哪些点是不存在的(入度和出度都为0)
template<class T>
void linkedDigraph<T>::Prim(int start, int* res) {
// 顶点是否被访问过
bool* vis = new bool[n + 1];
for (int i = 1; i <= n; i++) {
vis[i] = false;
}
// 结果矩阵初始化
for (int i = 1; i <= n; i++) {
res[i] = 0; // 默认 0 是一个不存在的点
}
priority_queue<Edge<T>> q;
Edge<T> a;
a.setId(0, start);
a.setWeight(0);
q.push(a);
int i = 0;
while (!q.empty()) {
Edge<T> cur = q.top(); // 会优先弹出权值最小的边
q.pop();
if (vis[cur.v2()] == true) continue; // 顶点访问过, 就跳过
vis[cur.v2()] = true; // 标记访问过
res[++i] = cur.v2();
// 访问所有与 cur 相邻的顶点
chainNode<Edge<T>>* temp = aList[cur.v2()].root();
while (temp) {
// 如果点 2 已经访问过, 那么其对应的边就不需要再入队了
if (!vis[temp->element.v2()]) q.push(temp->element);
temp = temp->next;
}
}
}
Kruskal算法
并查集检测回路
并查集合并的规则是 A 是 B 的祖先, C 是 D 的祖先, 要合并 B 和 C , 就分别让其祖先进行 PK, 谁赢了谁做祖先
bool union_(int i, int j) {
int f1 = find(i),
f2 = find(j); // 获取各自的祖先
if (f1 != f2) { // 祖先不同, 合并
if (rank[f1] < rank[f2]) father[f1] = f2;
else if (rank[f1] > rank[f2]) father[f2] = f1;
// rank[f1] == rank[f2]
else { father[f2] = f1; rank[f1]++; }
return true;
}
else return false; // 祖先相同, 形成回路
}
一条边的两个顶点若存在相同的祖先, 说明成环
路径压缩
在查询的过程中, 将路径上的节点的父节点都改成其祖先(根), 这样在合并时查找祖先只需要一步就能找到
int find(int x) {
return father[x] == x ? x : (father[x] = find(father[x]));
}
具体实现
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
class Edge {
public:
int v1, v2, weight;
Edge(int v1=-1, int v2=-1, int w=-1) : v1(v1), v2(v2), weight(w) {};
Edge& operator = (const Edge& e) {
v1 = e.v1; v2 = e.v2; weight = e.weight;
return *this;
}
bool operator < (const Edge& e) const {
return weight < e.weight;
}
};
class Kruskal {
vector<int> father;
vector<int> rank; // 秩
vector<Edge> e; // 边
int n; // 点数
public:
Kruskal(int n, const vector<Edge>& edge): n(n) {
for (int i = 0; i < n; i++) father.push_back(i);
for (int i = 0; i < n; i++) rank.push_back(1);
e = edge;
}
// 查询 x 的父节点
int find(int x) {
return father[x] == x ? x : (father[x] = find(father[x]));
}
// 合并
bool union_(int i, int j) {
int f1 = find(i),
f2 = find(j); // 获取各自的祖先
if (f1 != f2) { // 祖先不同, 合并
if (rank[f1] < rank[f2]) father[f1] = f2;
else if (rank[f1] > rank[f2]) father[f2] = f1;
// rank[f1] == rank[f2]
else { father[f2] = f1; rank[f1]++; }
return true;
}
else return false; // 祖先相同, 形成回路
}
int minTree() {
sort(e.begin(), e.end()); // 权重升序排
int m = 0; // 已选边数
int minWeight = 0;
for (int i = 0; i < e.size(); i++) {
if (m == n - 1) break; // 树的边数等于点树减一, 此时已经生成一棵树了
int v1 = e[i].v1,
v2 = e[i].v2;
if (union_(v1, v2)) {
m++;
minWeight += e[i].weight;
}
}
return minWeight;
}
};
int main() {
vector<Edge> e = {
Edge(1,8,3),Edge(1,2,8),Edge(2,8,9),Edge(8,4,2),
Edge(2,3,5),Edge(3,6,7),Edge(4,7,11),Edge(4,5,13),
Edge(5,7,12),Edge(0,5,1),Edge(5,9,4),Edge(0,9,6)
};
Kruskal k(10, e);
cout << k.minTree() << endl;
return 0;
}
关键路径
知识点: 关键路径
最长权重的路径叫做「关键路径」
测试用例
int main() {
linkedDigraph<string> ld(10);
Edge<string> a;
a.setWeight(2);
a.setId(1, 2);
ld.insert(a);
a.setWeight(3);
a.setId(1, 3);
ld.insert(a);
a.setWeight(20);
a.setId(1, 4);
ld.insert(a);
a.setWeight(8);
a.setId(4, 3);
ld.insert(a);
a.setWeight(1);
a.setId(2, 5);
ld.insert(a);
a.setWeight(7);
a.setId(5, 3);
ld.insert(a);
a.setWeight(1);
a.setId(3, 6);
ld.insert(a);
a.setWeight(6);
a.setId(4, 6);
ld.insert(a);
a.setWeight(4);
a.setId(4, 7);
ld.insert(a);
a.setWeight(1);
a.setId(7, 8);
ld.insert(a);
a.setWeight(2);
a.setId(7, 9);
ld.insert(a);
a.setWeight(1);
a.setId(9, 10);
ld.insert(a);
a.setWeight(2);
a.setId(8, 10);
ld.insert(a);
a.setWeight(2);
a.setId(6, 8);
ld.insert(a);
a.setWeight(3);
a.setId(5, 8);
ld.insert(a);
vector<int> res(10);
ld.criticalPath(1, res);
cout << "关键路径: ";
for (int i = 0; i < 10; i++) {
if (res[i] == 0) break;
cout << res[i] << " ";
}
return 0;
}
template<class T>
void linkedDigraph<T>::criticalPath(int start, vector<int>& res) {
// 事件最早时间
vector<int> ev(n + 1, 0);
// 事件最迟时间
vector<int> lv(n + 1, 0x7fffffff);
queue<Edge<T>> q;
stack<Edge<T>> s;
// 计算事件最迟时间
Edge<T> a;
a.setId(0, start);
a.setWeight(0);
q.push(a); // 源点入队
s.push(a); // 源点入栈
while (!q.empty()) {
Edge<T> cur = q.front();
q.pop();
// 取路径最大
if (ev[cur.v1()] + cur.getWeight() > ev[cur.v2()]) {
ev[cur.v2()] = ev[cur.v1()] + cur.getWeight();
}
// 其相邻点入队
const chainNode<Edge<T>>* temp = aList[cur.v2()].root();
while (temp) {
q.push(temp->element);
// 这一步方便计算事件最迟时间, 因为要从后往前遍历
s.push(temp->element);
temp = temp->next;
}
}
cout << "事件最早时间: ";
for (int i = 1; i <= n; i++) {
cout << ev[i] << " ";
}
cout << endl;
// 计算最迟事件时间
lv[n] = ev[n];
a.setId(s.top().v2(), 0);
a.setWeight(0);
s.push(a); // 汇点入栈
while (!s.empty()) {
const Edge<T> cur = s.top();
s.pop();
if (lv[cur.v2()] - cur.getWeight() < lv[cur.v1()]) {
lv[cur.v1()] = lv[cur.v2()] - cur.getWeight();
}
}
cout << "事件最迟时间: ";
for (int i = 1; i <= n; i++) {
cout << lv[i] << " ";
}
cout << endl;
// 存储活动(边)的数组
vector<Edge<T>> edge(e);
// 活动最早时间
vector<int> e_(e);
// 活动最迟时间
vector<int> l_(e);
// 顶点是否访问过
vector<bool> vis(n + 1);
// 获取边
int index = 0;
a.setId(0, start);
a.setWeight(0);
q.push(a); // 源点入队
while (!q.empty()) {
Edge<T> cur = q.front();
q.pop();
if (vis[cur.v2()]) continue;
vis[cur.v2()] = true;
// 其相邻点入队
chainNode<Edge<T>>* temp = aList[cur.v2()].root();
while (temp) {
q.push(temp->element);
edge[index++] = temp->element;
temp = temp->next;
}
}
// 计算活动的最迟和最早时间
index = 0;
for (int i = 0; i < e; i++) {
Edge<T> temp = edge[i];
// 活动(边)的最早时间与其起点事件一致
e_[i] = ev[temp.v1()];
// 活动的最迟等于终点事件最迟减权
l_[i] = lv[temp.v2()] - temp.getWeight();
if (l_[i] - e_[i] == 0) {
res[index] = temp.v1();
res[index + 1] = temp.v2();
index++;
}
}
cout << "活动最早时间: ";
for (int i = 0; i < e; i++) {
cout << e_[i] << " ";
}
cout << endl;
cout << "活动最迟时间: ";
for (int i = 0; i < e; i++) {
cout << l_[i] << " ";
}
cout << endl;
}