算法笔记 [图论章节笔记]

图的存储

邻接矩阵 二维矩阵

邻接表

vector<int> Adj[N]; Adj[1].push_back(3);

同时存储边的终点编号和边权,建立一个结构体 Node

struct Node
{
    int v;  // 边的终点编号
    int w;  // 边权
};

邻接表可以表示为 vector<Node> Adj[N]
往邻接表中添加元素可以使用如下方式:

Node temp;
temp.v = 3;
temp.w = 4;
Adj[1].push_back(temp);
// 或者先定义一个结构体构造函数

struct Node
{
    int v, w;
    Node(int _v, int _w):v(_v), w(_w){} // 构造函数
}

Adj[1].push_back(Node(3, 4)); // 直接加入

图的遍历

DFS

沿着一条路径直到无法继续前进,才退回到路径上里当前顶点最近的还存在未访问分支顶点的岔路口,并前往访问那些未访问分支顶点,知道遍历完整个图。

DFS(u) // 访问定点u
{
    vis[u] = true;
    for(从u出发能到达的所有顶点v)  // 枚举从u出发可以到达的所有顶点v
    {
        if vis[v] = false  // 如果v未被访问
            DFS(v);   // 递归访问v
    }
}

DFSTrave(G) // 遍历图G
{
    for(G的所有顶点)  // 对G的所有顶点u
        if vis[u] == false;  // 如果u未被访问
            DFS(u);  // 访问u所在的连通块
}

邻接矩阵实现

const int MAXV = 1000; // 最大顶点数
const int INF = 1000000000; // 设INF为一个很大的数

int n, G[MAXV][MAXV];  // n为顶点数,MAXV为最大顶点数
bool vis[MAXV] = {false}; //如果顶点i被访问了,则 vis[i] == true

// u 为当前访问的顶点标号
// depth 为深度
void DFS(int u, int depth)
{
    vis[u] = true;  // u 已经被访问过了
    // 对u进行一些操作
    // 对所有的从u出发能达到的分支顶点进行枚举
    for(int v = 0; v < n; v++)
    {
        // 如果v未被访问,并且u可达v
        if(vis[v]==false && G[u][v]!=INF)
            DFS(v, depth+1);  // 递归访问v,深度+1
    }
}

void DFSTrave() // 遍历图 G
{
    for(int u = 0; u < n; u++)  // 对每个顶点u
    {
        if(vis[u] == false)    // 如果u未被访问到
            DFS(u, 1); // 访问u和u的连通块,从第一层开始
    }
}

邻接表实现

vector<int> Adj[MAXV];  // 图G的邻接表
int n;   // 顶点数,MAXV为最大顶点数
bool vis[MAXV] = {false};

void DFS(int u, int depth)
{
    vis[u] = true;
    for(int i=0; i<Adj[u].size(); i++)
    {
        int v = Adj[u][i];
        if(vis[v] == false)
            DFS(v, depth+1);
    }
}

void DFSTrave()
{
    for(int u=0; u<n; u++)
    {
        if(vis[u] == false)
            DFS(u, 1);
    }
}

BFS

建立一个队列,每次将初始顶点加入队列,伺候每次都取出队首顶点进行访问,并把从该顶点出发可以到达的未曾加入过队列的顶点全部加入队列,直到队列为空。

BFS(u) // 遍历u所在的连通块
{
    queue q;  // 定义队列q
    将u入队;
    inq[u] = true;  // 设置u已被加入过队列
    while(q非空)
    {
        取出q的队首元素u进行访问;
        for(从u出发可达的所有顶点v)  // 枚举从u能直接到达的顶点v
        {
            if(inq[v] == false)
                将v入队;
                inq[v] = true; // 设置v已经加入过队列
        }
    }
}

BFSTrave(G)  // 遍历图G
{
    for(G的所有顶点u)  // 枚举G的所有顶点u
    {
        if(inq[u] == false)  // 如果u未曾加入该队列
        {
            BFS(u);  // 遍历u所在的连通块
        }
    }
}

邻接矩阵实现

int n, G[MAXV][MAXV];  // n为顶点数,MAXV为最大顶点数
bool inq[MAXV] = {false};  // 若顶点i曾如果队列,则inq[i]=true,初值为false

void BFS(int u)  // 遍历u所在的连通块
{
    queue<int> q;
    q.push(u);      // 将初始点q入队
    inq[u] = true;  // 设置u已经被加入过队列
    while(!q.empty()) // 只要队列非空
    {
        int u = q.front();  // 取出队首元素
        q.pop();   // 队首元素出队
        for(int v = 0; v < n; v++)
        {
            // 如果u的邻接点i未曾加入过队列
            if(inq[v]==false && G[u][v]==INF)
            {
                q.push(v);   // 将v入队
                inq[v] = true;  // 标记其已经被被加入队列
            }
        }
    }
}

void BFSTrave()  // 遍历图G
{
    for(int u = 0; u < n; u++)  // 枚举所有顶点
    {
        if(inq[u] == false) // 如果u未曾加入过队列
        {
            BFS(u);    // 遍历u所在连通块
        }
    }
}

邻接表实现

vector<int> Adj[MAXV];
int n;  // 顶点数
bool inq[MAXV] = {false};

void BFS(int u)
{
    queue<int> q;
    q.push(u);
    inq[u] = true;
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        for(int v = 0; v < Adj[u].size(); v++)
        {
            int ch = Adj[u][v];
            if(inq[ch] != false)
            {
                q.push(ch);
                inq[ch] = true;
            }
        }
    }
}

void BFSTrave()
{
    for(int u = 0; u < n; u++)
    {
        if(inq[u] == false)
            BFS(u);
    }
}

连通块内其他顶点的层号,需要使用一个结构体来存储:

struct Node
{
    int v;   // 顶点编号
    int layer;  // 顶点层号
};

vector<Node> Adj[MAXV];  // 图

// 层号的传递关系
void BFS(int s)
{
    queue<Node> q;
    Node start;
    start.v = s;
    start.layer = 1;
    q.push(start);
    inq[start.s] = true;
    while(!q.empty())
    {
        Node topNode = q.front();
        q.pop();
        int u = topNode.v;
        for(int i = 0; i < Adj[u].size(); i++)
        {
            Node next = Adj[u][i];
            next.layer = topNode.layer + 1;
            if(inq[next.v] == false)
            {
                q.push(next);
                inq[next.v] = true;
            }
        }
    }
}

最短路径

问题描述: 对于给定的任意图G(V, E),起点S和终点T,如何求出从S到T的最短路径。

Dijkstra

解决单源最短路问题,可以求得起点到其他的所有点的最短距离。基本思想是对图 G(V, E) 设置集合 S ,存放已被访问的顶点,从集合 V-S 中选择与起点s 的最短距离最小的一个顶点 u,访问并加入集合 S ,令顶点 u 为中介点,优化起点 s 与所有从 u 能到达的顶点 v 之间的最短距离。
时间复杂度为 O ( V 2 ) O(V^2)

伪代码

// G为图,一般设为全局变量,数组d为源点到达各点的最短路径长度,s为起点
Dijkstra(G, d[], s)
{
    初始化;
    for(循环n次)
    {
        u = 使 d[u] 最小的还未被访问过的顶点的标号;
        记u已被访问;
        for(从u出发能到达的所有顶点v)
        {
            if(v未被访问 && 以u为中介点使s到顶点v的最短距离d[v]更优)
            {
                优化d[v];
            }
        }
    }
}

邻接矩阵实现

const int MAXV  1000; // 最大顶点数
const int INF = 1000000000;
int n, G[MAXV][MAXV];  // 顶点数和最大顶点数
int d[MAXV];  // 起点到各点的最短路径长度
bool vis[MAXV] = {false};  // 标记数组

void Dijkstra(int s)  // 起点
{
    fill(d, d + max, INF);
    d[s] = 0; // 起点到达自身距离为0

    // 主算法
    for(int i = 0; i < n; i++)
    {
        // 1.找距离最近的点
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++)
        {
            if(vis[j]==false && d[j] < min)
            {
                u = j;
                MIN = d[j];
            }
        }

        // 2.标记已访问
        if(u == -1)  return; // 没有找到,不连通
        vis[u] = true;  // 标记访问

        // 3.更新距离
        for(int i = 0; i < n; i++)
        {
            if(vis[v] == false && g[u][v] != INF && d[u] + g[u][v] < d[v])
            {
                // v未被访问并且u可达v并且更优
                d[v] = d[u] + g[u][v];
            }
        }
    }
}

邻接表实现

struct Node
{
    int v, dis;  // v为边的目标结点,dis为边权
}
vector<Node> Adj[MAXV];  // 邻接表存储图
int n; // 顶点数
int d[MAXV];
bool vis[MAXV] = {false};

void Dijkstra(int s)
{
    fill(d, d + MAXV, INF);
    d[s] = 0;
    for(int i = 0; i < n; i++)
    {
        // 找距离最近的
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++)
        {
            if(vis[j] == false && d[j] < MIN)
            {
                MIN = d[j];
                u = j;
            }
        }
    }
    if(u == -1) return;
    vis[u] = true;

    // 更新
    for(int j = 0; j < Adj[u].size(); j++)
    {
        int v = Adj[u][j].v; // U可达顶点
        if(vis[v] == false && d[u] + Adj[u][j].dis < d[v])
            d[v] = d[u] + Adj[u][j].dis;
    }
}

以上两种实现方式的时间复杂度均为 O ( V 2 ) O(V^2) ,可以使用优先队列进而实现堆优化降低时间复杂度为 O ( V l o g V + E ) O(VlogV+E)

#include <iostream>
using namespace std;
const int MAXV = 1000;
const int INF = 0x3fffffff;
int n, m, s, G[MAXV][MAXV];
int d[MAXV];
bool vis[MAXV];

void Dijkstra(int s)
{
    fill(d, d + MAXV, INF);
    d[s] = 0;
    for (int i = 0; i < n; i++)
    {
        int u = -1, MIN = INF;
        for (int j = 0; j < n; j++)
        {
            if (!vis[j] && d[j] < MIN)
            {
                MIN = d[j];
                u = j;
            }
        }
        if (u == -1)
            return;
        vis[u] = true;
        for (int j = 0; j < n; j++)
        {
            if (!vis[j] && G[u][j] != INF && d[u] + G[u][j] < d[j])
                d[j] = d[u] + G[u][j];
        }
    }
}

int main()
{
    scanf("%d%d%d", &n, &m, &s);
    fill(G[0], G[0] + MAXV * MAXV, INF);
    for (int i = 0; i < m; i++)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        G[u][v] = w;
    }
    Dijkstra(s);
    for (int i = 0; i < n; i++)
        printf("%d ", d[i]);
    return 0;
}

以上是最短路径距离的求法,下面介绍最短路径求法:
设置一个数组 pre[] 记录从起点 s 到顶点 v 的最短路径上的前一个结点(前驱结点编号)

if(v未被访问 && 以u为中介点使s到顶点v的最短距离d[v]更优)
{
    优化d[v];
    令v的前驱为u;
}

具体实现如下:

const int MAXV  1000; // 最大顶点数
const int INF = 1000000000;
int n, G[MAXV][MAXV];  // 顶点数和最大顶点数
int d[MAXV];  // 起点到各点的最短路径长度
bool vis[MAXV] = {false};  // 标记数组
int pre[MAXV];  // 表示从起点到顶点v上的最短路径上的前一个结点

void Dijkstra(int s)  // 起点
{
    fill(d, d + max, INF);
    for(int i = 0; i < n; i++)
    {
        pre[i] = i;  // 初始状态是每一个点的前驱为自身
    }
    d[s] = 0; // 起点到达自身距离为0

    // 主算法
    for(int i = 0; i < n; i++)
    {
        // 1.找距离最近的点
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++)
        {
            if(vis[j]==false && d[j] < min)
            {
                u = j;
                MIN = d[j];
            }
        }

        // 2.标记已访问
        if(u == -1)  return; // 没有找到,不连通
        vis[u] = true;  // 标记访问

        // 3.更新距离
        for(int i = 0; i < n; i++)
        {
            if(vis[v] == false && g[u][v] != INF && d[u] + g[u][v] < d[v])
            {
                // v未被访问并且u可达v并且更优
                d[v] = d[u] + g[u][v];
                pre[v] = u;
            }
        }
    }
}

打印路径:

void DFS(int s, int v)
{
    if(s == v)
    {
        printf("%d\n", s);
        return;
    }
    DFS(s, pre[v]);
    printf("%d\n", v); // 输出每一层顶点号
}

如果最短距离不是只有一条,需要考虑其他的因素(第二标尺),在距离最短的基础上选择第二标尺最优的路径,常见形式如下:

  1. 每条边增加一个边权,在最短路径有多条时要求路径上的花费之和最小。
  2. 每个点增加一个点权,在最短路径有多条时要求路径上的点权之和最小。
  3. 直接问有多少条最短路径

对于这三种题目,需要增加一个数组存放新增的边权或点权,详细说明如下:

新增边权
cost[u][v] 表示 u->v的花费,新增数组 c[u] 表示从起点到结点 u的最小花费为 c[u],更新规则如下:

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

新增点权
用数组 weights 表示每个城市中拥有的物资数量, w[] 表示从起点到这个结点所能够获得的最大的物资数量

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[v];
        }
        else if(d[u] + G[u][v] == d[v] && w[u] + weights[u] > w[v]) // 距离相同,物资更多,需要进行更新
        {
            w[v] = w[u] + weights[u][v];
        }
    }
}

最短路径条数
增加一个数组 nums[] 记录从起点到当前节点的最短路径条数

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];
            nums[v] = nums[u];
        }
        else if(d[u] + G[u][v] == d[v])
        {
            nums[v] += nums[u];
        }
    }
}

Dijkstra + DFS
现在 Dijkstra 算法中记录下所有最短路径(只考虑距离),然后从这些最短路径中选出一条第二标尺最优的路径。
1.记录所有的最短路径
使用一个向量 vector<int> pre[MAXV], 里面存放的是结点 v 能产生最短路径的前驱结点
如果 d[u] + G[u][v] < d[v]u 为中介点可以使得 d[v] 更优,令 v 的前驱结点为 u,将原来的 pre[v] 清空(更优),将 u 加入即可。

if(d[u] + G[u][v] < d[v])
{
    d[v] = d[u] + G[u][v];
    pre[v].clear();
    pre[v].push_back(u);
}

如果 d[u] + G[u][v] == d[v]u 为中介点可以使得 d[v] 同样优,令 v 的前驱结点为 u,直接将 u 加入向量即可。

if(d[u] + G[u][v] == d[v])
{
    pre[v].push_back(u);
}
vector<int> pre[MAXV];

void Dijkstra(int s)
{
    fill(d, d + MAXV, INF);
    d[s] = 0;
    for(int i = 0; i < n; i++)
    {
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++)
        {
            if(!vis[j] && d[j] < MIN)
            {
                MIN = d[j];
                u = j;
            }
        }
        if(u == -1) return;
        vis[u] = true;
        // 更新距离
        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;  // 第二标尺最优值
vector<int> pre[MAXV]; // 存放结点的前驱结点
vector<int> path, tempPath; // 最优路径,临时路径
void dfs(int v) // 当前访问结点
{
    // 递归边界
    if(v == st) // 到达了叶子结点(路径起点)
    {
        tempPath.push_back(v);
        int value; // 记录临时路径第二标尺的值
        // 计算路径上的值
        if(value 优于 optvalue)
        {
            optValue = value;
            path = tempPath;
        }
        tempPath.pop();
        return;
    }
    tempPath.push_back(v);
    dfs(pre[v][i]);  // 深度优先搜索前驱结点
    tempPath.pop();
}

计算第二标尺:

// 边权之和
int value = 0;
for(int i = tempPath.size() - 1; i > 0; i--)
{
    // 当前节点为id,下一个结点为 nextId;
    int id = tempPath[i], nextId = tempPath[i-1];
    value += V[id][nextId];  // 增加边权
}

// 点权
int value = 0;
for(int i = tempPath.size() - 1; i >= 0; i--)
{
    int id = tempPath[i];
    value += W[id];  // 增加点权
}

最小生成树(MST)

在一个给定的无向图 G(V,E) 中求一棵树 T,使得这棵树拥有图 G 中的所有顶点,且所有的边都是来自于图 G 中的边,并且满足整棵树的边权之和最小。

最小生成树的三个性质:

  1. 最小生成树是树,其边数等于顶点数减1,且树内一定不会有环。
  2. 对给定的图 G(V, E), 其最小生成树可以不唯一,但是其边权之和一定是唯一的。
  3. 由于最小生成树是在无向图上生成的,因此其根结点可以是这棵树上的任意一个结点。(题目中一般会给出根结点,以给出的根结点来求解最小生成树即可)

常用算法为 primkruskal 均采用贪心的策略。

prim

基本思想: 对图 G(V, E) 设置集合 S,存放已被访问过的顶点,然后每次从集合 V-S 中选择与集合 S最短距离最小的一个顶点(记为 u),访问并加入集合 S, 之后,令顶点 u 为中介点,优化所有从 u 能到达的顶点 v 与集合 S 之间的最短距离。(这种操作反复执行 n 次,直到集合 S 中包含所有的元素)

连通,边数等于顶点数减1,使得这棵树的边权之和最小。

步骤:

  1. 每次从集合 V-S 中选择与集合 S 最近的一个顶点,访问并将其加入集合 S, 同时将这条离集合最近的边加入到最小生成树中。
  2. 令顶点 u 作为集合 S 与集合 V-S 连接的接口,优化从 u 能够到达的未访问顶点 v与集合 S 的最短路径。

需要设置一个 bool 型数组 vis[] 表示顶点是否被访问过,使用一个 int 型数组 d[] 表示顶点与集合的最短距离,初始时只有起点 sd[s]赋值为 0,其余均为无穷大 INF,表示不可达。

// G 为图,一般设为全局变量,数组 d 为顶点与集合 S 的最短距离
Prim(G, d[])
{
    初始化
    for(循环n次)
    {
        u = 使d[u]最小的还未被访问过的顶点的标号
        记 u 已被访问;
        for(从u出发能够到达的所有顶点v)
            if(v未被访问 && 以u为中介点使得v与集合S的最短距离d[v]更优)
                将G[u][v]赋值给v与集合S的最短距离G[v];
    }
}

邻接矩阵实现

const int MAXV = 1000;  // 最大顶点数
const int INF = 1000000000;  // 设 INF 为一个很大的数

int n, G[MAXV][MAXV];  // n为顶点数
int d[MAXV];  // 顶点与集合 S 的最短距离
bool vis[MAXV] = {false}; // 标记数组,vis[i]==true表示已经被访问过

int prim()  // 默认0号为初始点,函数返回最小生成树的边权之和
{
    fill(d, d + MAXV, INF); // 将整个数组赋值为INF
    d[0] = 0; // 只有0号顶点到集合S的距离为0
    int ans = 0;  // 存放最小生成树的边权之和
    for(int i = 0; i < n; i++) // 循环 n 次
    {
        int u = -1, MIN = INF; // u使d[u]最小,MIN存放最小的d[u]
        for(int j = 0; j < n; j++) // 找到未访问结点中d[]最小的
        {
            if(vis[j] == false && d[j] < MIN)
            {
                u = j;
                MIN = d[j];
            }
        }
        // 找不到小于INF的d[u],则剩下的顶点与集合S不连通
        if(u == -1) return -1;
        vis[u] = true; // 标记u为已访问
        ans += d[u];  // 将与集合S距离最小的边加入最小生成树
        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 // 返回最小生成树的边权之和
}

邻接表实现

struct Node
{
    int v, dis; // v 为边的目标顶点, dis为边权
};

vector<Node> Adj[MAXV];  // 图G,Adj[u]存放从顶点u出发可以到达的所有顶点
int n; // n为顶点数,图 G 使用邻接表实现,MAXV为最大顶点数
int d[MAXV];  // 顶点与集合 S 的最短距离
bool vis[MAXV] = {false}; // 标记数组,vis[i] == true 表示已访问,初值均为false

int prime()  // 默认0号为初始结点,函数返回最小生成树的权值之和
{
    fill(d, d + MAX, INF);
    d[0] = 0;
    int ans = 0;  // 只有0号到集合S的距离为0
    for(int i = 0; i < n; i++) // 循环 n 次
    {
        int u = -1, MIN = INF;  // u使得 d[u] 最小,MIN 存放最小的 d[u]
        for(int j = 0; j < n; j++) // 找到未访问顶点中u最小的
        {
            if(vis[j] == false && d[j] < MIN)
            {
                u = j;
                MIN = d[j];
            }
        }
        // 找不到小于 INF 的 d[u],则剩下的顶点和集合 S 不连通
        if(u == -1) return -1;
        vis[u] = true; // 标记u已访问
        ans += d[u];  // 将与集合S距离最小的边加入最小生成树
        for(int j = 0; j < Adj[u].size(); j++)
        {
            int v = Adj[u][j].v;
            if(vis[v] == false && Adj[u][j].dis < d[v>)
            {
                d[v] = Adj[u][j].dis;
            }
        }
    }
    return ans;  // 返回最小生成树的边权之和
}
#include <iostream>
using namespace std;

const int MAXV = 1000;      // 最大顶点数
const int INF = 1000000000; // 设INF是一个很大的数

int n, m, G[MAXV][MAXV];  // n 为顶点数,MAXV 为最大顶点数
int d[MAXV];              // 顶点与集合S的最短距离
bool vis[MAXV] = {false}; // 标记数组, vis[i]==true表示已经访问过,初值为false

int prim()
{
    fill(d, d + MAXV, INF);
    d[0] = 0;
    int ans = 0;
    for (int i = 0; i < n; i++)
    {
        int u = -1, MIN = INF;
        for (int j = 0; j < n; j++)
        {
            if (vis[j] == false && d[j] < MIN)
            {
                MIN = d[j];
                u = j;
            }
        }
        if (u == -1)
            return -1;
        vis[u] = true;
        ans += d[u];
        for (int v = 0; v < n; v++)
        {
            if (vis[v] == false && G[u][v] != INF && G[u][v] < d[v])
                d[v] = G[u][v];
        }
    }
    return ans;
}

int main()
{
    int u, v, w;
    scanf("%d%d", &n, &m);               // 顶点数,边数
    fill(G[0], G[0] + MAXV * MAXV, INF); // 初始化图
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d%d", &u, &v, &w); // 输入u,v以及边权
        G[u][v] = G[v][u] = w;       // 无向图
    }
    int ans = prim();
    printf("%d\n", ans);
    return 0;
}

kruskal

使用了边贪心的思想。
步骤如下:

  1. 对所有的边按照边权从小到大进行排序
  2. 按边权从小到大测试所有边,如果当前测试边多连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则将边舍弃
  3. 执行步骤2,直到最小生成树中的边数等于总顶点数减1,或是测试完所有边时结束。当结束时如果最小生成树的边数小于总顶点数减1,说明该图不连通。

每次选择图中最小边权的边,如果边两端的顶点在不同的连通块中,把这条边加入最小生成树中。

// 边的定义
struct edge
{
    int u, v;  // 边的两个端点编号
    int cost;  // 边权
};

// 按照边权进行排序
bool cmp(edge a, edge b)
{
    return a.cost < b.cost;
}

// 伪码表示
int kruskal()
{
    令最小生成树的边权之和为ans,最小生成树的当前边数为Num_Edge;
    将所有边按从小到大排序
    for(从小到大枚举所有边)
    {
        if(当前测试边的两个端点在不同的连通块中)
        {
            将该测试边加入最小生成树中;
            ans += 测试边的边权
            最小生成树的当前边数Num_Edge +1
            当边数Num_Edge等于顶点数减1时结束循环;
        }
    }
    return ans;
}

算法中存在的两个问题:

  1. 如何判断测试边中两个端点是否在不同的连通块中
  2. 如何将测试边加入最小生成树中

并查集可以通过查询两个结点所在集合的根结点是否相同来判断它们是否在同一个集合,合并功能只要把测试边的两个端点所在集合进行合并,就能达到将边加入最小生成树的效果。

int father[N];  // 并查集数组
int findFather(int x) // 并查集查询函数
{
    if(father[x] != x) father[x] = findFather(x);
    return father[x];
}

// kruskal 函数返回最小生成树的边权之和,参数 n 为顶点个数,m 为图的边数
int kruskal(int n, int m)
{
    // ans 为所求边权之和,Num_Edge 为当前生成树的边数
    int ans = 0, Num_Edge = 0;
    for(int i = 1; i <= n; i++)  // 顶点范围 [1-n]
    {
        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;  // 合并集合(将当前测试边加入最小生成树中)
            ans += E[i].cost;  // 边权之和增加测试边的边权
            Num_Edge++;  // 当前生成树的边数加1
            if(Num_Edge == n-1) break; // 边数等于顶点数减1时结束算法
        }
    }
    if(Num_Edge != -1)  return -1;  // 无法连通时返回-1
    else return ans;  // 返回最小生成树的边权之和
}

稠密图(边多)则使用 prim 算法,稀疏图,边少使用 kruskal 算法

#include <iostream>
#include <algorithm>
using namespace std;

const int MAXV = 110; // 最大顶点数
const int MAXE = 10010;
// 边集定义
struct edge
{
    int u, v; // 边的两个端点编号
    int cost; // 边权
} E[MAXE];

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

// 并查集部分
int father[MAXV];
int findFather(int x)
{
    if (x != father[x])
        father[x] = findFather(father[x]);
    return father[x];
}

int kruskal(int n, int m)
{
    int ans = 0, Num_Edge = 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;
            ans += E[i].cost;
            Num_Edge++;
            if (Num_Edge == n - 1)
                break;
        }
    }
    if (Num_Edge != n - 1)
        return -1; // 不连通
    else
        return ans;
}

int main()
{
    int n, m;
    scanf("%d%d", &n, &m); // 顶点数,边数
    for (int i = 0; i < m; i++)
        scanf("%d%d%d", &E[i].u, &E[i].v, &E[i].cost);
    int ans = kruskal(n, m);
    printf("%d\n", ans);
    return 0;
}

拓扑排序

步骤:

  1. 定义一个队列 Q,把所有入度为0的结点加入队列。
  2. 取出队首结点,输出,删去所有从它出发的边,令这些边到达的顶点的入度减1,如果某个点的入度减为0,将其加入队列。
  3. 反复进行2操作直到队列为空。如果队列为空时入过队的结点数目恰好为N,说明拓扑排序成功,图G为有向无环图,否则拓扑排序失败,图中含有环

使用数组 inDegree[MAXV] 表示数组的入度

vector<int> G[MAXV];  // 邻接表
int n, m, inDegree[MAXV];  // 顶点数,入度
// 拓扑排序
bool topologicalSort()
{
    int num = 0; // 记录加入拓扑排序列的顶点数
    queue<int> q;
    for(int i=0; i<n; i++)
    {
        if(inDegree[i] == 0)
            q.push(i);  // 将所有入度为0的结点入队
    }
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        for(int i=0; i<G[u].size(); i++)
        {
            int v = G[u][i];  // u的后继节点v
            inDegree[v]--;  // 顶点的入度减1
            if(inDegree[v] == 0)  // 顶点的入度减为0则入队
                q.push(v);
        }
        G[u].clear();  // 清除所有出边
        num++;  // 加入拓扑序列的顶点数加1
    }
    if(num == n)  return true;  // 加入拓扑序列的顶点数为n
    else return false;        // 加入拓扑序列的顶点数小于n
}

若拓扑排序成功,说明是一个有向无环图,否则有环。

发布了166 篇原创文章 · 获赞 27 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/HdUIprince/article/details/105464430
今日推荐