时间复杂度:O(n²)

原始讲义 · 非增强校订版 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照

## 时间复杂度:O(n²)

Prim 算法和 Dijsktra 算法的比较:

• Prim 每次从未选顶点中选择距离已选集合最近的点(cost 最小),而 Dijsktra 算法每次从未选顶点中选择距离源点最近的顶点(dist最小)。

• Prim 的 cost 记录的是边的长度,Dijsktra 的 dist 记录的是路径的长度。

• Prim 中 cost[k] 的更新条件是 k 距离本次选择的顶点j 比距离已选集合中的其他顶点更近: cost[j] > w(k,j)。Dijsktra 中 dist[v] 的更新条件是借道 u 之后源点离 v 的距离比借道 u 之前更小:dist[u] + w(u, v) < dist[v]。

• Prim 和 Dijsktra都是贪心算法每次选择 xx 最小的,邻接表的写法都可以使用小根堆来优化选择的过程。

## Kruskal算法

核心思想:从边的角度出发,初始时已选边集合不包含任何边。每次将一条最小的、加入集合后不成环的边纳入已选边集合,重复 n-1 次。

## 步骤:

1. 题目给出从小到大排序的边数组或给出优先队列(堆)。

2. 初始化并查集,初始时所有顶点属于不同集合。

3. 循环,直到选出 n-1 条不成环的边:

a.对于还未处理的最小边,使用并查集判断该边的两个端点分别属于哪个集合。

b. 如果不属于同一个集合则选择该边、合并这两个集合。

## 邻接表

伪代码如下(忽略快速排序):

<!-- verified-by-hand -->
> [!success] 已结合原文第 151-152 页及 Markdown 上下文人工校对
> Find 被跨页截断;Kruskal 缺 edges/m 输入,根变量大小写错误并缺闭括号。
int parent[MAXV];
typedef struct { int u, v, weight; } Edge;
int Find(int x)
{
    if (parent[x] != x) parent[x] = Find(parent[x]);
    return parent[x];
}
void Union(int x, int y)
{
    parent[Find(y)] = Find(x);
}
void Kruskal(ALGraph G, Edge edges[], int m)
{
    int count = 0;
    Qsort(edges, 0, m - 1);
    for (int i = 0; i &lt; G.numVertices; i++) parent[i] = i;
    for (int i = 0; i &lt; m &amp;&amp; count &lt; G.numVertices - 1; i++)
    {
        int rootU = Find(edges[i].u);
        int rootV = Find(edges[i].v);
        if (rootU != rootV)
        {
            Union(rootU, rootV);
            // 输出或加入 edges[i]
            count++;
        }
    }
}
<!-- page 152 -->
return parent[x];
}
// 并查集:合并集合
void Union(int x, int y)
{
    parent[Find(y)] = Find(x);
}
void Kruskal(ALGraph G)
{
    Edge edges[MAXE]; //边集数组
    int count = 0; //统计已纳入生成树中的边数
    //1.题目给出从小到大排序的边数组或给出优先队列(堆)
    不妨假设所有的边已经在数组 edges 中,边的数量为 m
    void Qsort(edges[], 0, G.numEdges-1);
    //2.初始化并查集,初始时所有顶点属于不同集合。
    for (int i = 0; i &lt; G.numvertices; i++)
    parent[i] = i;
    // 3.循环,直到选出 n-1 条不成环的边
    for (int i = 0; i &lt; m; i++)
    {
        //3-a. 对于还未处理的最小边,使用并查集判断该边的两个端点分别属于哪个集合
        int rootU = Find(edges[i].u);
        int rootV = Find(edges[i].v);
        //3-b.如果不属于同一个集合则选择该边、合并这两个集合
        if (rootu != rootv)
        { Union(rootU, rootV);
            将该边加入到生成树中
            count++;
            if (count == G.numvertices-1) break; //已选择 n-1 条边
        }
        输出最小生成树
    }
第一步在考试中不太可能考察,主要考察第二、三步。 <!-- page 153 --> ## 5.3.5 拓扑排序
原资料配图
原资料配图 · 转写内容请回源核验

## 核心目的:

• 解决有向无环图(DAG)中顶点的先后顺序问题,得到拓扑序列。

• 如果顶点a到顶点b 存在有向边,则在拓扑序列中,顶点a 会在顶点b前面。

• 如果该有向图无环,则拓扑序列存在。如果该有向图有环,则拓扑序列不存在。

<table><tr><td rowspan=1 colspan=1>拓扑序列</td><td rowspan=1 colspan=1>条件</td></tr><tr><td rowspan=1 colspan=1>存在且唯一</td><td rowspan=1 colspan=1>每次选择结点时都只有一个结点可选</td></tr><tr><td rowspan=1 colspan=1>存在且不唯一</td><td rowspan=1 colspan=1>选择结点时不止一个结点可选</td></tr><tr><td rowspan=1 colspan=1>不存在</td><td rowspan=1 colspan=1>图中有环,环中的顶点未被访问  0 m</td></tr></table>

## 步骤:

1. 统计每个顶点的入度。

2. 将入度为0的顶点入队。

3. 队列出队一个顶点,访问它,并将它的出边所指向顶点的入度减1。

4. 若减到0,再次入队。

5. 队列为空,排序完成;若有未访问顶点,存在环。

## 邻接表

时间复杂度: O(n+e)

代码如下:

<!-- verified-by-hand -->
> [!success] 已结合原文第 153 页及 Markdown 上下文人工校对
> while 丢失花括号,环判断落在函数外;应统计实际处理的顶点数。
bool TopoSort(ALGraph G)
{
    int indegree[MAXV] = {0}, queue[MAXV];
    int front = 0, rear = 0, count = 0;
    for (int i = 0; i &lt; G.numVertices; i++)
        for (ArcNode *p = G.VerticesList[i].first; p != NULL; p = p-&gt;next)
            indegree[p-&gt;adjvex]++;
    for (int i = 0; i &lt; G.numVertices; i++)
        if (indegree[i] == 0) queue[rear++] = i;
    while (front &lt; rear)
    {
        int u = queue[front++];
        count++;
        // 输出顶点 u
        for (ArcNode *p = G.VerticesList[u].first; p != NULL; p = p-&gt;next)
            if (--indegree[p-&gt;adjvex] == 0) queue[rear++] = p-&gt;adjvex;
    }
    return count == G.numVertices;
}
<!-- page 154 --> ## \*邻接矩阵 <!-- verified-by-hand --> > [!success] 已结合原文第 154 页及 Markdown 上下文人工校对 > 计算入度缺少 j 循环;删除出边时未减入度便入队,环判断位置也错误。
bool TopoSort(MGraph G)
{
    int indegree[MAXV] = {0}, queue[MAXV];
    int front = 0, rear = 0, count = 0;
    for (int i = 0; i &lt; G.numVertices; i++)
        for (int j = 0; j &lt; G.numVertices; j++)
            if (G.Edge[i][j] != 0) indegree[j]++;
    for (int i = 0; i &lt; G.numVertices; i++)
        if (indegree[i] == 0) queue[rear++] = i;
    while (front &lt; rear)
    {
        int u = queue[front++];
        count++;
        // 输出顶点 u
        for (int v = 0; v &lt; G.numVertices; v++)
            if (G.Edge[u][v] != 0 &amp;&amp; --indegree[v] == 0) queue[rear++] = v;
    }
    return count == G.numVertices;
}
<!-- page 155 --> ## 5.3.6 \*关键路径
原资料配图
原资料配图 · 转写内容请回源核验

以边表示活动的网叫AOE网,它是带权无环图,顶点表示事件,弧表示活动,权表示活动持续的时间。通常,AOE-网可用来估算工程的完成时间。

在正常的情况(无环)下,网中只有一个入度为0的点,称作源点,也只有一个出度为0的点,称作汇点。在AOE-网中,一条路径各弧上的权值之和称为该路径的带权路径长度(后面简称路径长度)。要估算整项工程完成的最短时间,就是要找一条从源点到汇点的带权路径长度最长的路径,称为关键路径。

关键路径上的活动叫作关键活动,这些活动是影响工程进度的关键,它们的提前或延期将使整个工程提前或延期。

目的:关键路径用于工程调度、项目管理等任务中,找出一条耗时最长的必经路径,控制整个项目进度。步骤:

1. 先进行拓扑排序,得到拓扑序列;

2. 根据拓扑序列,正向计算每个顶点的最早发生时间 ve

<!-- page 156 -->

3. 根据拓扑序列反过来逆向计算每个顶点的最迟发生时间 v1;

4. 依次判断每条边 u→v:若某活动的最早时间 ve(u)等于最迟时间 vl(v)-w(u,v),则为关键活动。

仅供参考,重在理解过程,代码考察概率不大,万一考察也只是考察部分步骤(如已知拓扑序列求关键路径)。

<!-- verified-by-hand -->
<!-- code-audit: false_positive -->
> [!info] 已人工审读:中文位于合法的 C 块注释 /* ... */ 中;checker 的 zh_nocmt 规则未识别行内块注释,代码本身无需修改。
void CriticalPath(ALGraph G)
{
    int indegree[MAXV] = {0}, ve[MAXV] = {0}, vl[MAXV];
    int topo[MAXV], queue[MAXV], front = 0, rear = 0, count = 0;
    for (int u = 0; u &lt; G.numVertices; u++)
        for (ArcNode *p = G.VerticesList[u].first; p; p = p-&gt;next) indegree[p-&gt;adjvex]++;
    for (int i = 0; i &lt; G.numVertices; i++) if (indegree[i] == 0) queue[rear++] = i;
    while (front &lt; rear)
    {
        int u = queue[front++];
        topo[count++] = u;
        for (ArcNode *p = G.VerticesList[u].first; p; p = p-&gt;next)
        {
            int v = p-&gt;adjvex;
            if (ve[u] + p-&gt;weight &gt; ve[v]) ve[v] = ve[u] + p-&gt;weight;
            if (--indegree[v] == 0) queue[rear++] = v;
        }
    }
    if (count &lt; G.numVertices) return;
    int projectTime = 0;
    for (int i = 0; i &lt; G.numVertices; i++) if (ve[i] &gt; projectTime) projectTime = ve[i];
    for (int i = 0; i &lt; G.numVertices; i++) vl[i] = projectTime;
    for (int i = count - 1; i &gt;= 0; i--)
    {
        int u = topo[i];
        for (ArcNode *p = G.VerticesList[u].first; p; p = p-&gt;next)
        {
            int v = p-&gt;adjvex;
            if (vl[v] - p-&gt;weight &lt; vl[u]) vl[u] = vl[v] - p-&gt;weight;
        }
    }
    for (int u = 0; u &lt; G.numVertices; u++)
        for (ArcNode *p = G.VerticesList[u].first; p; p = p-&gt;next)
        {
            int v = p-&gt;adjvex, e = ve[u], l = vl[v] - p-&gt;weight;
            if (e == l) { /* 输出关键活动 &lt;u,v&gt; */ }
        }
}
<!-- page 157 --> <!-- code-audit: false_positive --> > [!info] 已人工审读:这是复杂度结论正文,不是 C 代码。
时间复杂度:O(n+e)
<!-- page 158 --> ## 5.4 章节练习 ## 5.4.1 习题 ## 【习题5-1】 给定一个用邻接表保存的有向图G,设计算法int OddInDegree(ALGraph G),返回出度为奇数的顶点个数。邻接表定义如下: <!-- verified-by-hand --> > [!success] 已结合原文第 158 页及 Markdown 上下文人工校对 > ArcNode、VNode、ALGraph 的左花括号和 nextarc 成员声明丢失。
typedef struct ArcNode
{
    int adjvex;
    struct ArcNode *nextarc;
} ArcNode;
typedef struct
{
    int data;
    ArcNode *firstarc;
} VNode;
typedef struct
{
    VNode VerticesList[MAXV];
    int numVertices, arcnum;
} ALGraph;
(1)给出算法的基本设计思想。 (2)采用C或C++语言描述算法,关键之处给出注释。 (3)说明所设计算法的时间复杂度。 ## 【习题5-2】 给定一个用邻接表保存的无向图G,设计算法int IsConnected(ALGraph G)判断该图是否连通,若连通则返回1,否则返回0。邻接表定义如下:C <!-- verified-by-hand --> > [!success] 已结合原文第 158 页及 Markdown 上下文人工校对 > 三个结构体左花括号丢失并混入水印。
typedef struct ArcNode
{
    int adjvex;
    struct ArcNode *nextarc;
} ArcNode;
typedef struct
{
    int data;
    ArcNode *firstarc;
} VNode;
typedef struct
{
    VNode VerticesList[MAXV];
    int numVertices, arcnum;
} ALGraph;
(1) 给出算法的基本设计思想。 (2)采用C或C++语言描述算法,关键之处给出注释。 (3)说明所设计算法的时间复杂度。 <!-- page 159 --> ## 【习题5-3】 给定一个用邻接表保存的无向图G,设计算法int HasCycle(ALGraph \*G)判断该图是否含有环,若含有环则返回1,否则返回0。邻接表定义如下: <!-- verified-by-hand --> > [!success] 已结合原文第 159 页及 Markdown 上下文人工校对 > ArcNode 与 ALGraph 左花括号及 nextarc 成员声明丢失。
typedef struct ArcNode
{
    int adjvex;
    struct ArcNode *nextarc;
} ArcNode;
typedef struct
{
    ArcNode *VerticesList[MAXV];
    int numVertices, arcnum;
} ALGraph;
(1)给出算法的基本设计思想。 (2)采用C或C++语言描述算法,关键之处给出注释。 (3) 说明所设计算法的时间复杂度。 ## 【习题5-4】 给定一个用邻接表保存的带权无向图G,你可以直接调用该函数ArcNode\*FindEdge()得到一个指针,指向当前未处理过的边中权值最小的那条边(内部实现细节不需要考虑)。设计算法int MSTWeight(ALGraph G)判断返回最小生成树的权值之和。邻接表定义如下: <!-- verified-by-hand --> > [!success] 已结合原文第 159 页及 Markdown 上下文人工校对 > ArcNode、VNode、ALGraph 均缺少左花括号。
typedef struct ArcNode
{
    int ivex, jvex;
    struct ArcNode *nextarc;
    int weight;
} ArcNode;
typedef struct
{
    int data;
    ArcNode *firstarc;
} VNode;
typedef struct
{
    VNode VerticesList[MAXV];
    int numVertices, arcnum;
} ALGraph;
(1)FindEdge()函数每次会从某个数据结构中取出最小边,应该使用什么数据结构?为什么? (2) 给出算法的基本设计思想。 (3)采用C或C++语言描述算法,关键之处给出注释。 <!-- page 160 --> ```c typedef struct EBox int mark; // 用于遍历标记 int ivex, jvex; // 边依附的两个顶点位置 ```