Loading

数据结构系列5——图的应用

图的应用

最小生成树

概念

  • 最小生成树不唯一,但其对应的边的权值之和唯一并且最小
  • 最小生成树的边数 = 顶点数 - 1
  • 注意,下述两种方法均为避圈法,关于最小生成树还有破圈法

Prim算法

  • 基于贪心,证明如下

20220717115926

  • 时间复杂度\(O(|V|^2)\),不依赖\(|E|\),适合稠密图
  • 实现——邻接矩阵
// 与dijkstra思路相同
// d[]意义不同,dijkstra中表示的是到起点s的距离,prim中表示的是到集合s的距离
// prim可以选取任一顶点作为起点,这里选择0
const int MAXN = 1e3;
const int INF = 0x7fffffff;

int n, G[MAXN][MAXN];
int d[MAXN];
bool vis[MAXN] = {false};

int prim() {
    fill(d, d + MAXN, INF);
    // selete node 0
    d[0] = 0;
    int ans = 0;
    for (int i = 0; i < n; i++) {
        int u = -1, mn = INF;
        for (int j = 0; j < n; j++) {
            if (vis[j] == false && d[j] < mn) {
                u = j;
                mn = d[j];
            }
        }
        if (u == -1) return -1;
        vis[u] = true;
        // 将与集合s距离最小的边加入最小生成树
        ans += d[u];
        for (int v = 0; v < n; v++) {
            // v未访问 && u和v可达 && 以u为中介可以使v离集合s更近
            if (vis[v] == false && G[u][v] != INF && G[u][v] < d[v]) {
                d[v] = G[u][v];
            }
        }
    }
    return ans;
}
  • 实现——邻接表
const int MAXN = 1e3;
const int INF = 0x7fffffff;

struct node {
    int v, dis;
};
vector<node> Adj[MAXN];
int n;
int d[MAXN];
bool vis[MAXN] = {false};

int prim() {
    fill(d, d + MAXN, INF);
    d[0] = 0;
    int ans = 0;
    for (int i = 0; i < n; i++) {
        int u = -1, mn = INF;
        for (int j = 0; j < n; j++) {
            if (vis[j] == false && d[j] < mn) {
                u = j;
                mn = d[j];
            }
        }
        if (u == -1) return -1;
        vis[u] = true;
        // 将与集合s距离最小的边加入最小生成树
        ans += d[u];
        for (int j = 0; j < Adj[u].size(); j++) {
            int v = Adj[u][j].v;
            // v未访问 && u为中转可以使得v离集合s更近
            if (vis[v] == false && Adj[u][j].dis < d[v]) {
                d[v] = Adj[u][j].dis;
            }
        }
    }
    return ans;
}

Kruskal算法

  • 基于贪心
  • 按照边的权值由小到大的顺序,不断选择当权未被选取过且权值最小的边,且该边依附的顶点在T中不同的连通分量上
  • 时间复杂度(主要来源于对边排序):\(O(|E|log|E|)\)
  • 实现
// 每次选择图中最小边权的边,如果边两端的顶点在不同的连通块中,就把这条边加入最小生成树
const int MAXN = 1e3;
const int INF = 0x7fffffff;

struct edge {
    int u, v;
    int cost;
} E[MAXN];

bool cmp(edge a, edge b) {
    return a.cost < b.cost;
}

int father[MAXN];
int findFather(int x) {
    int a = x;
    while (x != father[x]) {
        x = father[x];
    }
    while (a != father[a]) {
        int z = a;
        a = father[a];
        father[z] = x;
    }
    return x;
}

// 注意本例的下标从1开始
int kruskal(int n, int m) {
    // ans为所求边权和,numEdge为当前生成树的边数
    int ans = 0, numEdge = 0;
    for (int i = 0; i < n; i++) {
        father[i] = i;
    }
    sort(E, E + m, cmp);
    // 枚举每一条边
    for (int i = 0; i < m; i++) {
        int faU = findFather(E[i].u);
        int faV = findFather(E[i].v);
        if (faU != faV) {
            father[faU] = faV; // Union
            ans += E[i].cost;
            numEdge++;
            if (numEdge == n - 1) break;
        }
    }
    return numEdge == n - 1 ? ans : -1;
}

最短路

Dijkstra算法(单源最短路)

  • 基于贪心
  • 时间复杂度\(O(|V|^2)\),不适用带负权的图
  • 求解每对顶点之间的最短路,也可以考虑对每个顶点分别使用Dijkstra(共n次),此时时间复杂度为\(O(|V|^2) \cdot |V| = O(|V|^3)\)
  • 步骤(不考虑pre[]时,考虑的版本见代码)
    1. 初始化集合\(S\)\(dist[]\)
    2. 从集合\(V - S\)中选出\(V_u\),满足\(dist[u] = min\{dist[j] | V_j\in V - S\}\)(选择当前\(V_u\)作为中转),令\(S = S \cup \{ u \}\)
    3. 修改从\(V_0\)出发到集合\(V - S\)上任一顶点\(V_v\)可达的最短路径长度,若\(dist[u] + G[u][v] < dist[u]\),更新\(dist[v] = dist[u] + G[u][v]\)
    4. 重复2~3共\(n - 1\)次直到所有顶点都在\(S\)
  • 实现——邻接矩阵版
const int MAXN = 1e3;
const int INF = 0x7fffffff;

int n, G[MAXN][MAXN];
// 表示到当前起点s的最短路径,根据集合中选中的当前顶点u进行优化,每次关注新选择的u能否缩短到s的最短路,注意与prim的d区分
int d[MAXN];
int pre[MAXN];
bool vis[MAXN] = {false};

void dijkstra(int s) {
    fill(d, d + MAXN, INF);
    d[s] = 0;
    for (int i = 0; i < n; i++) {
        int u = -1, mn = INF;
        for (int j = 0; j < n; j++) {
            if (vis[j] == false && d[j] < mn) {
                u = j;
                mn = d[j];
            } 
        }
        if (u == -1) return;
        vis[u] = true;
        // 若出现更优情况,一定是以u当中转,若以其他顶点在之前的迭代中就已经优化
        for (int v = 0; v < n; v++) {
            if (vis[v] == false && G[u][v] != INF && d[v] + G[u][v] < d[v]) {
                d[v] = d[u] + G[u][v];
                pre[v] = u;
            }
        }
    }
}
  • 实现——邻接表版本
const int MAXN = 1e3;
const int INF = 0x7fffffff;

struct node {
    int v, dis;
};
vector<node> Adj[MAXN];
int n;
int d[MAXN];
int pre[MAXN];
bool vis[MAXN] = {false};

void dijkstra(int s) {
    fill(d, d + MAXN, INF);
    d[s] = 0;
    for (int i = 0; i < n; i++) {
        int u =  -1, mn = INF;
        for (int j = 0; j < n; j++) {
            if (vis[j] == false && d[j] < mn) {
                u = j;
                mn = d[j];
            }
        }
        if (u == -1) return;
        vis[u] = true;
        for (int j = 0; j < Adj[u].size(); j++) {
            int v = Adj[u][j].v;
            if (vis[v] == false && d[u] + Adj[u][j].dis < d[v]) {
                d[v] = d[u] + Adj[u][j].dis;
                pre[v] = u;
            }
        }
    }
}
  • pre数组的访问
void dfs(int s, int v) {
    if (v == s) {
        cout << s << endl;
        return;
    }
    dfs(s, pre[v]);
    cout << v << endl;
}

  • 关于两个衡量尺度的问题
// 在路径的基础上增加花费(越小越好)
for (int v = 0; v < n; v++) {
    if (vis[v] == false && G[u][v] != INF) {
        if (d[u] + G[u][v] < d[v]) {
            d[v] = d[u] + G[u][v];
            c[v] = c[u] + cost[u][v];
        } else if (d[u] = G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) {
            c[v] = c[u] + cost[u][v];
        }
    }
}
// 在路径的基础上增加资源(越多越好)
for (int v = 0; v < n; v++) {
    if (vis[v] == false && G[u][v] != INF) {
        if (d[u] + G[u][v] < d[v]) {
            d[v] = d[u] + G[u][v];
            w[v] = w[u] + weight[u][v];
        } else if (d[u] = G[u][v] == d[v] && w[u] + weight[u][v] > w[v]) {
            w[v] = w[u] + weight[u][v]; 
        }
    }
}
// 统计最短路条数
// init num[s] = 1,其他为0
int num[MAXN] = {0};
num[s] = 1;

for (int v = 0; v < n; v++) {
    if (vis[v] == false && G[u][v] != INF) {
        if (d[u] + G[u][v] < d[v]) {
            d[v] = d[u] + G[u][v];
            num[v] = num[u];
        } else if (d[u] + G[u][v] == d[v]) {
            num[v] += num[u];
        }
    }
}
  • 复杂情况,记录路径最后dfs依次判断
vector<int> pre[MAXN];

// 记录路径
for (int v = 0; v < n; v++) {
    if (!vis[v] && G[u][v] != INF) {
        if (d[u] + G[u][v] < d[v]) {
            d[v] = d[u] + G[u][v];
            pre[v].clear();
            pre[v].push_back(u);
        } else if (d[u] + G[u][v] == d[v]) {
            pre[v].push_back(u);
        }
    }
}
int optValue = INF, totalDist = INF;
vector<int> path, tempPath;

// dfs
void dfs(int u) {
    if (u == st) {
        tempPath.push_back(u);
        int value = 0, dist = 0;
        for (int i = tempPath.size() - 1; i > 0; i--) {
            int id = tempPath[i], idnext = tempPath[i - 1];
            value += C[id][idnext];
            dist += G[id][idnext];
        }
        if (value < optValue) {
            path = tempPath;
            totalDist = dist;
            optValue = value;
        }
        tempPath.pop_back();
        return;
    }

    tempPath.push_back(u);
    for (int i = 0; i < pre[u].size(); i++) {
        dfs(pre[u][i]);
    }
    tempPath.pop_back();
}

Floyd算法(每对顶点间的最短路)

  • 基于动态规划
  • 时间复杂度\(O(|V|^3)\),适用带负权的图,但不允许包含带有负权值的边组成的回路
  • 步骤
    1. 定义一个\(n\)阶方阵序列\(A^{(-1)},A^{(0)},\cdots,A^{(n - 1)}\),其中

    \[A^{(-1)}[i][j] = G[i][j] \\ A^{(k)}[i][j] = min\{ A^{(k - 1)}[i][j], A^{(k - 1)}[i][k] + A^{(k - 1)}[k][j]\}(以k作为中转顶点) \]

    1. 经过\(n\)次迭代后,所得到的\(A^{(n - 1)}[i][j]\)就是\(v_i\)\(v_j\)的最短路径长度,即方阵\(A^{(n - 1)}[i][j]\)中保存了任意一对顶点之间的最短路径长度
  • 实现
int n, m; // 顶点数,边数
int dis[MAXN][MAXN]; // 初始化时令其等于G

void floyd() {
    for (int k = 0; k < n; k++) { // 中转顶点
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (dis[i][j] != INF && dis[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j]) {
                    dis[i][j] = dis[i][k] + dis[k][j];
                }
            }
        }
    }
}

有向无环图描述表达式

  • 有向无环图(DAG):一个有向图中不存在环
  • 可以共享重复存储的部分,节省存储空间

20220717115945

拓扑排序

  • AOV网:用DAG图表示一个工程,其顶点表示活动,用有向边\(<V_i, V_j>表示活动\)V_i\(必须先于活动\)V_j$,则称为顶点表示活动的网络
  • 拓扑排序:一个有向无环图中,每个顶点只出现一次,顶点A在序列中排在顶点B前面,则在图中不存在顶点B到顶点A的路径
  • 时间复杂度:
    • 邻接表:\(O(n + e)\)
    • 邻接矩阵:\(O(n^2)\)
  • 邻接矩阵为三角矩阵则存在拓扑排序,反之则不一定,但是可以通过调整使得邻接矩阵变成三角矩阵
  • 步骤:
    1. 从AOV网中选择一个没有前驱的顶点并输出
    2. 从网中删除顶点和所有以它为起点的有向边
    3. 重复1~2,直到AOV网为空或不存在无前驱的顶点,后者表示图中必然存在环

20220717115955

  • 逆拓扑排序,找没有后继的顶点并输出,其余类似拓扑排序
  • 用dfs遍历一个无环有向图,并在dfs算法退栈返回时打印相应的顶点,则输出顶点的逆拓扑排序,可以将该序列翻转得到拓扑排序,注意,直接dfs不是拓扑排序,必须在出栈前访问才能达到逆拓扑排序,然后将其翻转得到拓扑排序

图中有顶点\(v_i\),它有后继顶点\(v_j\),即存在边\(<v_i, v_j>\)\(v_i\)入栈后,必先遍历完其后继顶点后\(v_i\)才会出栈,也就是说\(v_i\)会在\(v_j\)之后出栈,由于&v_i\(和\)v_j$具有任意性,可以得到输出的顶点时逆拓扑排序

  • 实现
const int MAXN = 1e3;

vector<int> Adj[MAXN];
int n, m; // 顶点数,边数
int inDegree[MAXN]; // 入度

bool topologicalSort() {
    int num = 0;
    queue<int> q;
    for (int i = 0; i < n; i++) {
        if (inDegree[i] == 0) q.push(i);
    }
    while (!q.empty()) {
        int u = q.front();
        cout << u << endl;
        q.pop();
        for (int i = 0; i < Adj[u].size(); i++) {
            int v = Adj[u][i];
            inDegree[v]--; // 后继顶点入度减1,不可能见到负数,每一条边对应一个真实的结点
            if (inDegree[v] == 0) q.push(v);
        }
        Adj[u].clear(); // 清空u的所有出边
        num++;
    }
    return num == n; // 还有顶点,则存在环,排序失败
}

关键路径

  • AOE网:带权有向图中,顶点表示事件,边上的权值表示完成活动的开销(如完成活动所需要的时间),则称为用表示活动的网络

AOV与AOE

  • 都是无环图
  • 它们边和顶点的含义不同,AOV网中的边无权值,仅表示顶点之间的前后关系,AOE网中的边有权值
  • AOE网只有一个入度为0的顶点和一个出度为0的顶点
  • AOE网的特点:
    • 只有进入顶点的各有向边所代表的活动都已经结束,该顶点所代表的事件才能发生
    • 只有某个顶点代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
    • AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),仅有一个出度为0的顶点,称为结束顶点(汇点)
  • 关键路劲
    • 具有最大路径长度的路径,长度代表完成整个工程的最短,关键路径上的活动(注意,边表示活动的时间)称为关键活动
    • 关键路径上的所有活动都是关键活动,决定整个工程的关键,但是也不能任意缩短关键活动,因为可能缩短到一定程度就不再是关键活动
    • 关键路径可能不止一条
  • 事件\(v_k\)的最早发生时间\(ve(k)\)
    • 从源点\(v_1\)到顶点\(v_k\)的最长路径长度,表示从\(v_k\)开始的所有活动最早的开工时间
    • 从前往后求
    • 注意,如果仅仅要求关键路径的长度,ve[汇点]即为关键路径的长度
  • 步骤:
    1. 按拓扑排序的顺序计算
    2. 初始时,令\(ve[1 \cdots n] = 0\)
    3. 输出一个入度为0的顶点\(v_j\)时,计算它所有直接后继顶点\(v_k\)的最早发生时间,\(ve[k] = max\{ ve[j] + weight(v_j, v_k), ve[k] \}\),直到输出全部顶点
  • 事件\(v_k\)的最迟发生时间\(vl(k)\)
    • 在不推迟整个工程完成的前提下,保证它的后继顶点\(v_j\)在其最迟发生时间\(vl(j)\)能够发生时,该事件最迟必须发生的时间
    • 从后往前求
  • 步骤:
    1. 增设一个栈记录拓扑排序,出栈顺序为逆拓扑排序,按逆拓扑排序的顺序计算
    2. 初始时,令\(vl[1 \cdots n] = ve[n]\)(至少要保证汇点的时间不受影响)
    3. 栈顶点\(v_j\)出栈,计算其所有直接前驱顶点\(v_k\)的最迟发生时间,\(vl[k] = min\{ vl[j] - weight(v_k, v_j), vl[k] \}\),直到输出全部栈中顶点
  • 活动\(a_i\)的最早开始时间\(e(i)\)
    • 活动弧的起点表示的事件的最早发生时间
  • 活动\(a_i\)的最迟开始时间\(l(i)\)
    • 活动弧的终点所表示的事件的最迟发生时间与该活动所需时间之差

注意,最迟开始时间与活动弧的起点的最迟发生时间没有关系,该时间可能与该条边表示的活动无关

  • 一个活动最迟开始时间与最早开始时间的差额\(d(i) = l(i) - e(i)\)
    • 表示活动完成的时间余量,若\(d(i) = 0\),则表示活动必须要如期完成,否则会影响整个工程进度,该活动为关键活动
  • 关键路径求解步骤
    1. 从源点出发,令ve(源点) = 0,按拓扑有序求其余顶点的最早发生时间ve()
    2. 从汇点出发,令vl(汇点) = ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间vl()
    3. 根据各顶点的ve()值求所有弧的最早开始时间e()
    4. 根据各顶点的vl()值求所有弧的最迟开始时间l()
    5. 求AOE网中的所有活动的差额d(),找出所有d() = 0的活动构成关键路径

注意按照拓扑排序求,这样可以使得求解井井有条

  • 实现
const int MAXN = 1e3;
const int INF = 0x7fffffff;

struct node {
    int v, w;
};
vector<node> Adj[MAXN];
int n, m;
int inDegree[MAXN];
int ve[MAXN], vl[MAXN];

stack<int> topOrder; // 出栈即为逆拓扑排序

bool toplogicalSort() {
    queue<int> q;
    for (int i = 0; i < n; i++) {
        if (inDegree[i] == 0) q.push(i);
    }
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        topOrder.push(u);
        for (int i = 0; i < Adj[u].size(); i++) {
            int v = G[u][i].v;
            inDegree[v]--;
            if (inDegree[v] == 0) q.push(v);
            if (ve[u] + Adj[u][i].w > ve[v]) ve[v] = ve[u] + G[u][i].w;
        }
    }
    return topOrder.size() == n;
}

// 求关键路径
// 确定汇点时,若汇点不确定,关键路径的长度即数组ve的最大值
int critialPath() {
    fill(ve, ve + MAXN, 0);
    if (toplogicalSort() == false) return -1; // 不是无环图
    fill(vl, vl + MAXN, 0);
    // 利用逆拓扑排序求vl
    while (!topOrder.empty()) {
        int u = topOrder.top();
        topOrder.pop();
        for (int i = 0; i < Adj[u].size(); i++) {
            int v = G[u][i].v;
            if (vl[v] - G[u][i].w < vl[u]) vl[u] = vl[v] - G[u][i].w;
        }
    }

    for (int u = 0; u < n; u++) {
        for (int i = 0; i < G[u].size(); i++) {
            // 这里没有存储v和e,如果需要存储,使用结构体,存边的值
            int v = G[u][i].v, w = G[u][i].w;
            int e = ve[u], l = vl[v] - w; // 活动的最早开始时间和最晚开始时间
            if (e == l) cout << u << ", " << v << endl; // 该条边上的活动关键活动
        }
    }
    return ve[n - 1]; // 关键路径长度
}
posted @ 2022-07-17 12:01  Patrickhao  阅读(38)  评论(0)    收藏  举报