拓扑排序(两种实现方法与一个例题)

博客介绍了拓扑排序,即有向无环图顶点的排序,使边起点在终点前。阐述了用深度优先搜索和点的入度实现拓扑排序的方法。还分析了Leetcode第1632题,可将矩阵转化为图,通过拓扑排序确定元素秩。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

拓扑排序

对于有向无环图G=(V, E),拓扑排序指的是图的顶点V的一个排序,使得对于E中的每一条边(u, v),顶点u在排序中都出现在顶点v之前。


使用深度优先搜索进行拓扑排序

下面是使用深度优先搜索的拓扑排序

def DFS(G, node):
		visited[node] = True
		for v in G.ad_list(node):
			if not visited[v]:
				DFS(G, v)
		sorted.insert(0, node)

def TopologicalSort(G):
	# G是一个代表图的类,G.vertexes顶点列表,G.ad_list(node)是node的邻接列表
	visited = {}
	sorted = []
	for v in G.vertexes:
		visited[v] = False
	for v in G.vertexes:
		if not visited[i]:
			DFS(G, v)
	return sorted

第一个函数使用了递归形式的深度优先搜索。
当其中的for循环结束时,从node出发的边指向的点已经被放入了sorted,因此实现了拓扑排序。
第二个函数的第二个for循环保证了搜索所有的点,即使G不是连通图。


根据点的入度实现拓扑排序

下面是根据点的入度实现的拓扑排序

from collections import Counter, deque
def TopologicalSort(G):
	degree = Counter()
	for e in G.edges:
		dgree[e[1]] += 1 # 记录e[1]的入度
	sorted = []
	q = deque([v for v in G.vertexes if degree[v] == 0])
	# 初始化先进先出队列q,入度为零的顶点应当放到所有顶点的前面
	while q:
		node = q.popleft()
		sorted.append(node) # 指向q中的第一个顶点的点应当已经在sorted中
		for v in G.ad_list(node):
			degree[v] -= 1
			if degree[v] == 0:
				# 只有入度为零的顶点可以进入队列
				q.append(v)
	return sorted

简单说明一下
将入度为零的顶点放入sorted表中,然后删除该顶点以及发出的边,然后又会有新的入度为零的,重复此过程。
显然该过程在所有的点被放入sorted中之前不会停止,若是停止了,说明删去上一批点后没有诞生新的入度为零的点,说明图G有环,这与假设相悖。


Leetcode第1632题

LeetCode第1632题矩阵转换后的秩
内容如下

给你一个 m x n 的矩阵 matrix ,请你返回一个新的矩阵 answer ,其中 answer[row][col] 是 matrix[row][col] 的秩。
每个元素的 秩 是一个整数,表示这个元素相对于其他元素的大小关系,它按照如下规则计算:

  • 秩是从 1 开始的一个整数。
  • 如果两个元素 p 和 q 在 同一行 或者 同一列 ,那么:
    • 如果 p < q ,那么 rank(p) < rank(q)
    • 如果 p == q ,那么 rank(p) == rank(q)
    • 如果 p > q ,那么 rank(p) > rank(q)
  • 秩 需要越 小 越好。
    题目保证按照上面规则 answer 数组是唯一的。

可以将矩阵转化为一个图。
在同一行或者同一列中,将相等的元素合并为一个点,然后将其按矩阵元素大小排序,相邻的元素之间从小的发出边指向大的。
这样,这个图拓扑排序中,在前面的元素小,在后面的元素大,每个元素只与同行同列的元素关联,在初始化秩为1的元素后,同行同列元素的相对位置就是相应的秩。

来自官方的解答代码如下

class Solution:
    def matrixRankTransform(self, matrix: List[List[int]]) -> List[List[int]]:
        m, n = len(matrix), len(matrix[0])
        uf = UnionFind(m, n)

        for i, row in enumerate(matrix):
            # 合并一行中相同的点
            num2indexList = defaultdict(list)
            for j, num in enumerate(row):
                num2indexList[num].append([i, j])
            for indexList in num2indexList.values():
                i1, j1 = indexList[0]
                for k in range(1, len(indexList)):
                    i2, j2 = indexList[k]
                    uf.union(i1, j1, i2, j2)
        for j in range(n):
            # 合并一列中相同的点
            num2indexList = defaultdict(list)
            for i in range(m):
                num2indexList[matrix[i][j]].append([i, j])
            for indexList in num2indexList.values():
                i1, j1 = indexList[0]
                for k in range(1, len(indexList)):
                    i2, j2 = indexList[k]
                    uf.union(i1, j1, i2, j2)

        degree = Counter()
        # 用来计数点的入度
        adj = defaultdict(list)
        # 用相邻列表表示有向图中的边
        for i, row in enumerate(matrix):
            # 构造一行中的边
            num2index = {}
            for j, num in enumerate(row):
                num2index[num] = (i, j)
            sortedArray = sorted(num2index.keys())
            for k in range(1, len(sortedArray)):
                i1, j1 = num2index[sortedArray[k - 1]]
                i2, j2 = num2index[sortedArray[k]]
                ri1, rj1 = uf.find(i1, j1)
                ri2, rj2 = uf.find(i2, j2)
                degree[(ri2, rj2)] += 1
                adj[(ri1, rj1)].append([ri2, rj2])
        for j in range(n):
            # 构造一列中的边
            num2index = {}
            for i in range(m):
                num = matrix[i][j]
                num2index[num] = (i, j)
            sortedArray = sorted(num2index.keys())
            for k in range(1, len(sortedArray)):
                i1, j1 = num2index[sortedArray[k - 1]]
                i2, j2 = num2index[sortedArray[k]]
                ri1, rj1 = uf.find(i1, j1)
                ri2, rj2 = uf.find(i2, j2)
                degree[(ri2, rj2)] += 1
                adj[(ri1, rj1)].append([ri2, rj2])
        
        rootSet = set()
        # 用一个集合来记录合并后的点
        ranks = {}
        for i in range(m):
            for j in range(n):
                ri, rj = uf.find(i, j)
                rootSet.add((ri, rj))
                ranks[(ri, rj)] = 1
        q = deque([[i, j] for i, j in rootSet if degree[(i, j)] == 0])
        # 初始化q用作拓扑排序的队列,添加入度为零的点
        while q:
            i, j = q.popleft()
            for ui, uj in adj[(i, j)]:
                degree[(ui, uj)] -= 1
                if degree[(ui, uj)] == 0:
                    q.append([ui, uj])
                ranks[(ui, uj)] = ranks[(i, j)] + 1
                # 官解给出的是max(ranks[ui, uj], ranks[i, j] + 1),
                # 但显然ranks[ui, uj] <= ranks[i, j] + 1
        res = [[1] * n for _ in range(m)]
        for i in range(m):
            for j in range(n):
            	# 计算秩
                ri, rj = uf.find(i, j)
                res[i][j] = ranks[(ri, rj)]
        return res

class UnionFind:
	'''
	这个类模拟图中的点,可以合并同行同列的相等元素为一个点
	'''
    def __init__(self, m, n):
        self.m = m
        self.n = n
        self.root = [[[i, j] for j in range(n)] for i in range(m)]
        # 初始化矩阵中各个元素所指向的点
        self.size = [[1] * n for _ in range(m)]
        # 初始化各个点(集合)的大小

    def find(self, i, j):
    	# 找到合并之后的结果
        ri, rj = self.root[i][j]
        if [ri, rj] == [i, j]:
            return [i, j]
        self.root[i][j] = self.find(ri, rj)
        return self.root[i][j]

    def union(self, i1, j1, i2, j2):
        ri1, rj1 = self.find(i1, j1)
        ri2, rj2 = self.find(i2, j2)
        if [ri1, rj1] != [ri2, rj2]:
        	# 将小的向大的合并,保证同行同列的相等元素合并后所指向的是同一个点
            if self.size[ri1][rj1] >= self.size[ri2][rj2]:
                self.root[ri2][rj2] = [ri1, rj1]
                self.size[ri1][rj1] += self.size[ri2][rj2]
            else:
                self.root[ri1][rj1] = [ri2, rj2]
                self.size[ri2][rj2] += self.size[ri1][rj1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值