这里写自定义目录标题
更多精彩内容
这里是带你游历编程世界的Dashcoding编程社,我是Dash/北航硕士/ICPC区域赛全国排名30+/给你呈现我们眼中的世界!
256题算法特训课,帮你斩获大厂60W年薪offer
原题
2024考研408真题-唯一拓扑排序
B站动画详解
问题分析
在有向图中,拓扑排序是对图中所有顶点的线性排序,使得对于每一条有向边 (u, v),顶点 u 在顶点 v 之前出现。拓扑排序仅适用于有向无环图(DAG)。拓扑排序的唯一性问题,即是否存在唯一的拓扑排序,实际上是在判断图中是否存在唯一的拓扑序列。在拓扑排序过程中,如果存在多个顶点的入度为0,那么就会存在多种可能的拓扑排序方式,因此我们需要检查图是否能够保证在任何情况下都只有一种合法的拓扑排序方式。
为了确定一个图是否有唯一的拓扑排序,我们需要使用拓扑排序算法,并在过程中检查是否在任何时刻存在多个入度为0的顶点。如果在执行拓扑排序时发现队列中的元素数目大于1,则说明存在多个入度为0的顶点,这会导致拓扑排序的多样性,从而图中存在多种拓扑排序。
思路分析
判断一个图是否存在唯一的拓扑排序,关键在于理解拓扑排序的唯一性条件。对于一个有向图,如果存在某个顶点有多个入度为0的顶点,则可以选择不同的顶点作为拓扑排序的起点,导致存在多种可能的拓扑排序。因此,为了确保拓扑排序的唯一性,在执行拓扑排序的过程中,每一步都应该只有一个顶点的入度为0。如果在任意一步中,有多个顶点的入度为0,那么该图就有多个拓扑排序,唯一性就不成立。
在这道题目中,我们采用基于广度优先搜索(BFS)的拓扑排序算法。在执行拓扑排序时,维护一个队列,每次从队列中取出一个入度为0的顶点,并将其相邻的顶点的入度减1。如果在某个时刻,队列中有超过一个入度为0的顶点,则意味着存在多种拓扑排序方案,此时返回0,表示拓扑排序不唯一;如果拓扑排序能够完整进行,并且在每一步中队列中始终只有一个入度为0的顶点,则返回1,表示拓扑排序唯一。
算法实现
我们采用了拓扑排序的基本思想,但在此基础上增加了判断唯一性的逻辑。具体来说,算法流程如下:
-
初始化:首先统计每个顶点的入度,并将入度为0的顶点加入队列。如果初始时刻队列中的元素不止一个,则直接返回0,因为此时存在多种选择,导致拓扑排序不唯一。
-
执行拓扑排序:进入循环,每次从队列中取出一个入度为0的顶点,并将其相邻顶点的入度减1。如果在此过程中,发现队列中有超过一个入度为0的顶点,则返回0,表示不唯一。
-
判断结束条件:如果在队列为空时,已经处理了所有顶点,则表明拓扑排序唯一,返回1;否则,返回0。
整个过程通过一个简单的BFS遍历实现,利用队列维护当前可以加入拓扑序列的顶点。
代码详解
标准代码程序
C++代码
#include "dashoj_2024_408.h"
int uniquely(MGraph G) {
struct queue {
int q[maxV * 2]; // 队列数组,存储顶点
int front = 1, rear = 0, sz = 0; // 初始化队列指针和大小
// 入队操作
void push(int v) {
q[++rear] = v;
sz++;
}
// 出队并返回队首元素
int getFrontAndPop() {
int v = q[front++];
sz--;
return v;
}
// 返回队列的大小
int size() {
return sz;
}
} Q;
int in[maxV]; // 记录每个顶点的入度
int cnt = 0; // 统计已经加入拓扑序列的顶点数量
// 初始化入度数组
for (int i = 1; i <= G.numberVertices; i++) in[i] = 0;
// 统计每个顶点的入度
for (int i = 1; i <= G.numberVertices; i++)
for (int j = 1; j <= G.numberVertices; j++) {
if (G.edge[i][j] == 1) in[j]++;
}
// 将入度为0的顶点入队
for (int i = 1; i <= G.numberVertices; i++) {
if (in[i] == 0) {
Q.push(i);
cnt++;
}
}
// 如果初始时队列中顶点数多于1,返回0
if (Q.size() != 1) return 0;
// 拓扑排序过程
while (Q.size() > 0) {
int p = Q.getFrontAndPop(); // 获取并移除队首元素
for (int i = 1; i <= G.numberVertices; i++) {
if (G.edge[p][i] == 1) { // 找到所有p的相邻顶点
in[i]--; // 相邻顶点入度减1
if (in[i] == 0) { // 如果入度变为0,入队
Q.push(i);
cnt++;
}
}
}
// 如果队列中有多于1个入度为0的顶点,返回0
if (Q.size() > 1) return 0;
}
// 如果所有顶点都加入了拓扑序列,返回1,否则返回0
if (cnt == G.numberVertices) return 1;
return 0;
}
Java代码
import dashoj_2024_408.*;
public class Solution {
public int uniquely(MGraph G) {
int[] inDegree = new int[G.numberVertices + 1];
Queue<Integer> queue = new LinkedList<>();
int count = 0;
// 计算每个顶点的入度
for (int i = 1; i <= G.numberVertices; i++) {
for (int j = 1; j <= G.numberVertices; j++) {
if (G.edge[i][j] == 1) {
inDegree[j]++;
}
}
}
// 将入度为0的顶点加入队列
for (int i = 1; i <= G.numberVertices; i++) {
if (inDegree[i] == 0) {
queue.add(i);
count++;
}
}
// 如果初始队列中顶点数多于1,直接返回0
if (queue.size() != 1) {
return 0;
}
// 拓扑排序过程
while (!queue.isEmpty()) {
int current = queue.poll();
for (int i = 1; i <= G.numberVertices; i++) {
if (G.edge[current][i] == 1) {
inDegree[i]--;
if (inDegree[i] == 0) {
queue.add(i);
count++;
}
}
}
// 如果队列中有多于1个顶点,返回0
if (queue.size() > 1) {
return 0;
}
}
// 检查是否所有顶点都已处理
return count == G.numberVertices ? 1 : 0;
}
}
Python代码
from dashoj_2024_408 import *
def uniquely(G):
in_degree = [0] * (G.numberVertices + 1)
queue = []
count = 0
# 计算每个顶点的入度
for i in range(1, G.numberVertices + 1):
for j in range(1, G.numberVertices + 1):
if G.edge[i][j] == 1:
in_degree[j] += 1
# 将入度为0的顶点加入队列
for i in range(1, G.numberVertices + 1):
if in_degree[i] == 0:
queue.append(i)
count += 1
# 如果初始队列中顶点数多于1,直接返回0
if len(queue) != 1:
return 0
# 拓扑排序过程
while queue:
current = queue.pop(0)
for i in range(1, G.numberVertices + 1):
if G.edge[current][i] == 1:
in_degree[i] -= 1
if in_degree[i] == 0:
queue.append(i)
count += 1
# 如果队列中有多于1个顶点,返回0
if len(queue) > 1:
return 0
# 检查是否所有顶点都已处理
return 1 if count == G.numberVertices else 0
Javascript代码
import { MGraph } from 'dashoj_2024_408';
function uniquely(G) {
const inDegree = Array(G.numberVertices + 1).fill(0);
const queue = [];
let count = 0;
// 计算每个顶点的入度
for (let i = 1; i <= G.numberVertices; i++) {
for (let j = 1; j <= G.numberVertices; j++) {
if (G.edge[i][j] === 1) {
inDegree[j]++;
}
}
}
// 将入度为0的顶点加入队列
for (let i = 1; i <= G.numberVertices; i++) {
if (inDegree[i] === 0) {
queue.push(i);
count++;
}
}
// 如果初始队列中顶点数多于1,直接返回0
if (queue.length !== 1) {
return 0;
}
// 拓扑排序过程
while (queue.length > 0) {
const current = queue.shift();
for (let i = 1; i <= G.numberVertices; i++) {
if (G.edge[current][i] === 1) {
inDegree[i]--;
if (inDegree[i] === 0) {
queue.push(i);
count++;
}
}
}
// 如果队列中有多于1个顶点,返回0
if (queue.length > 1) {
return 0;
}
}
// 检查是否所有顶点都已处理
return count === G.numberVertices ? 1 : 0;
}
复杂度分析
对于这道题目的时间复杂度和空间复杂度分析如下:
-
时间复杂度:图的初始化和入度统计需要遍历整个邻接矩阵,因此时间复杂度为 O ( V 2 ) O(V^2) O(V2),其中 V V V
是顶点的数量。在最坏情况下,我们需要遍历所有边来处理拓扑排序的每一步,这部分时间复杂度是 O ( V × E ) O(V \times E) O(V×E),其中 E E E
是边的数量。结合起来,算法的时间复杂度为 O ( V 2 + V × E ) O(V^2 + V \times E) O(V2+V×E)。 -
空间复杂度:算法使用了一个队列和一个数组来存储入度信息,以及邻接矩阵存储图,因此空间复杂度为 O ( V 2 ) O(V^2) O(V2)。
考虑到题目的数据范围,算法在时间和空间上的表现是可接受的,能够在合理时间内处理问题。