1. 问题描述:
一些学校连接在一个计算机网络上,学校之间存在软件支援协议,每个学校都有它应支援的学校名单(学校 A 支援学校 B,并不表示学校 B 一定要支援学校 A)。当某校获得一个新软件时,无论是直接获得还是通过网络获得,该校都应立即将这个软件通过网络传送给它应支援的学校。因此,一个新软件若想让所有学校都能使用,只需将其提供给一些学校即可。现在请问最少需要将一个新软件直接提供给多少个学校,才能使软件能够通过网络被传送到所有学校?最少需要添加几条新的支援关系,使得将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件?
输入格式
第 1 行包含整数 N,表示学校数量。第 2..N+1 行,每行包含一个或多个整数,第 i+1 行表示学校 i 应该支援的学校名单,每行最后都有一个 0 表示名单结束(只有一个 0 即表示该学校没有需要支援的学校)。
输出格式
输出两个问题的结果,每个结果占一行。
数据范围
2 ≤ N ≤ 100
输入样例:
5
2 4 3 0
4 5 0
0
0
1 0
输出样例:
1
2
来源:https://www.acwing.com/problem/content/description/369/
2. 思路分析:
对于当前的图直接做的话不好做,如果是有向无环图那么问题会变得简单一点,我们可以使用tarjan算法将原图转换为一个有向无环图(DAG),假设转换为之后的有向无环图总共有p个起点和q个终点,可以给图中的每一个起点发一个信息那么图中的其余点都是可以接收到信息的,而题目中要求解至少给多少个学校发信息,相当于是至少给多少个起点发信息使得网络中的其余点都可以接收到信息,所以至少需要给p个起点发信息,也即第一问的答案是p,对于第二问其实问的是我们需要在原图中添加多少条边使其变成一个强连通分量(图中任意两点可以相互到达),我们将原图转换为有向无环图之后需要添加至少max(p,q)条边使其变为一个强连通分量,这个结论比较重要,但是证明的过程很难,我们记住这个结论就可以了,题目的核心是使用tarjan算法将原图转换为有向无环图,包含两个步骤:① 求解强连通分量,在dfs的过程中求解出每一个强连通分量并且将强连通分量中的所有点标记在对应的强连通分量的编号中;② 缩点,遍历所有节点,然后遍历当前点的所有邻接点,然后统计每一个强连通分量整体的入度和出度,最后遍历一下每一个强连通分量计算入度和出度为0的个数,输出答案即可。
3. 代码如下:
from typing import List
class Solution:
# 定义tarjan算法需要使用到的全局变量
stk, in_stk, idx, timestamp, ssc_cnt, top = None, None, None, None, None, None
def tarjan(self, u: int, dfn: List[int], low: List[int], g: List[List[int]]):
dfn[u] = low[u] = self.timestamp + 1
self.timestamp += 1
self.stk[self.top + 1] = u
self.top += 1
self.in_stk[u] = 1
for next in g[u]:
if dfn[next] == 0:
self.tarjan(next, dfn, low, g)
# 更新low[u]
low[u] = min(low[u], low[next])
elif self.in_stk[next] == 1:
low[u] = min(low[u], dfn[next])
if dfn[u] == low[u]:
# 找出当前强连通分量中的所有点
self.ssc_cnt += 1
while True:
# 弹出栈顶元素
t = self.stk[self.top]
self.top -= 1
self.idx[t] = self.ssc_cnt
self.in_stk[t] = 0
# 找到了强连通分量的最高点
if t == u: break
def process(self):
n = int(input())
g = [list() for i in range(n + 1)]
# 节点的下标是从1开始的
for i in range(1, n + 1):
s = list(map(int, input().split()))
if len(s) > 1:
# 注意最后一个元素是0是不加入边中
for j in range(len(s) - 1):
g[i].append(s[j])
dfn, low = [0] * (n + 10), [0] * (n + 10)
# 因为后面使用到stk的下标是从1开始的, 所以需要在一开始加上一个0
self.stk, self.idx, self.in_stk = [0] * (n + 10), [0] * (n + 10), [0] * (n + 10)
self.ssc_cnt = self.timestamp = self.top = 0
for i in range(1, n + 1):
if dfn[i] == 0:
self.tarjan(i, dfn, low, g)
# 缩点
din, dout = [0] * (self.ssc_cnt + 1), [0] * (self.ssc_cnt + 1)
for i in range(1, n + 1):
for next in g[i]:
a, b = self.idx[i], self.idx[next]
if a != b:
dout[a] += 1
din[b] += 1
a = b = 0
# 计算入度和出度为0的点
for i in range(1, self.ssc_cnt + 1):
if din[i] == 0: a += 1
if dout[i] == 0: b += 1
print(a)
# 只有一个强连通分量的时候需要特判一下
if self.ssc_cnt == 1: print(0)
else: print(max(a, b))
if __name__ == '__main__':
Solution().process()