图及其应用

195 阅读15分钟

离散数学中关于图的介绍

链表迭代器的实现

邻接链表迭代器

这里不使用指针 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 表示无边, 数字代表

构造函数的时间复杂度:

O(n2)O(n^2)

出度和入度

image.png

由邻接矩阵可以很方便的计算出度和入度

// 顶点的出度
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;
}

增加、删除

增加和删除一条边只需要把矩阵相应位置上的值改成 10 , 因此时间复杂度:

O(1)O(1)
完整代码
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

image.png

某个点的出度为 h[i+1]-h[i]

边总数, 无向图为 h[n+1]/2

image.png

有向图 h[n+1]

image.png

增加和删除一条边

O(n+e)O(n+e)

邻接链表

使用一个 n 维链表数组 表示点的关系

image.png

链表内元素表示与当前顶点相邻的点

构造函数的时间复杂度:

O(n)O(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. 访问 1
  2. 依次访问 4 3 2
  3. 再访问下一层的 7 6 5
  4. 最后访问 9 8

image.png

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)

与前序遍历类似, 使用栈实现 出栈时访问

image.png

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的边), 说明存在环, 该任务相互依赖, 不可能完成

基于深度优先的拓扑排序 使用栈实现

image.png

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;
        }
    }
}

基于广度优先的拓扑排序 使用队列实现

image.png

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;
}
普通实现

顺便完成作业题...

image.png

顶点编号从 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算法

只适合无向图

image.png

测试用例

因为要求无向图, 但是 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算法

并查集检测回路

并查集合并的规则是 AB 的祖先, CD 的祖先, 要合并 BC , 就分别让其祖先进行 PK, 谁赢了谁做祖先

image.png

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; // 祖先相同, 形成回路
}

一条边的两个顶点若存在相同的祖先, 说明成环

image.png

路径压缩

在查询的过程中, 将路径上的节点的父节点都改成其祖先(根), 这样在合并时查找祖先只需要一步就能找到

int find(int x) {
    return father[x] == x ? x : (father[x] = find(father[x]));
}
具体实现

image.png

image.png

#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;
}

image.png

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;
}