题目链接
题目描述
总共有 个营员,编号为
。每个营员
都填写了一份调查表,指明他愿意将资料拷贝给哪些其他营员。
资料的传递是具有传递性的:如果营员 愿意给
,
又愿意给
,那么一旦
获得了资料,
和
最终也都能获得资料。
现在需要确定,至少要刻录多少张光盘(即初始发给多少个营员),才能保证所有 个营员最终都能得到资料。
解题思路
这是一个关于有向图中信息传递和覆盖的问题。我们可以将问题抽象为一个图论模型来求解。
1. 图的构建
- 将
个营员视为图中的
个顶点。
- 如果营员
愿意将资料拷贝给营员
,我们就在图中画一条从
指向
的有向边
。
- 这样,我们就构建了一个代表资料传递关系的有向图。
2. 强连通分量 (SCC)
在有向图中,如果两个顶点可以相互到达,则称它们是强连通的。图中的一个极大强连通子图被称为强连通分量 (Strongly Connected Component, SCC)。
在本题中,一个 SCC 具有重要的性质:只要 SCC 内的任意一个营员获得了资料,那么该 SCC 内的所有其他营员最终也都能获得资料,因为他们之间可以相互传递。
因此,我们可以将每个 SCC “收缩”成一个超级节点。这样,原图就被转换成了一个由这些超级节点构成的有向无环图 (Directed Acyclic Graph, DAG)。
3. 求解DAG的起点
问题现在转化为了:在这个由 SCC 构成的 DAG 中,我们需要给多少个超级节点“源头”(即发放光盘),才能使得信息流能够覆盖所有的超级节点?
答案是:我们需要给所有入度为 0 的超级节点发放光盘。
一个超级节点(SCC)的入度为 0,意味着没有任何来自其他 SCC 的边指向它。因此,这个 SCC 无法从其他群体接收到资料,必须有一个初始的资料来源(光盘)。只要所有入度为 0 的 SCC 都获得了光盘,那么资料就可以沿着 DAG 的边流向所有其他 SCC,最终覆盖所有营员。
所以,题目的答案就是缩点后图中入度为 0 的 SCC 的数量。
4. 算法实现
我们可以使用 Tarjan 算法 来高效地求解 SCC。
- 建图:根据输入构建邻接表表示的有向图。
- Tarjan 算法:对图进行深度优先搜索 (DFS),计算每个节点的
(发现时间戳) 和
(能追溯到的最早祖先的时间戳),并用一个栈来辅助。当
时,就找到了一个 SCC。
- 统计入度:在找到所有 SCC 并为每个节点分配一个 SCC 编号后,我们遍历原图的所有边
。如果节点
和
属于不同的 SCC(即
),则说明在缩点后的 DAG 中,有一条从
指向
的边。此时,我们将
的入度加一。
- 计算结果:最后,统计所有 SCC 中入度为 0 的数量,即为最终答案。
代码
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
vector<vector<int>> adj;
vector<int> dfn, low, scc_id;
vector<bool> on_stack;
stack<int> s;
int timestamp, scc_count;
void tarjan(int u) {
dfn[u] = low[u] = ++timestamp;
s.push(u);
on_stack[u] = true;
for (int v : adj[u]) {
if (dfn[v] == 0) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (on_stack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
scc_count++;
int node;
do {
node = s.top();
s.pop();
on_stack[node] = false;
scc_id[node] = scc_count;
} while (node != u);
}
}
int main() {
int n;
cin >> n;
adj.resize(n + 1);
for (int i = 1; i <= n; ++i) {
int v;
while (cin >> v && v != 0) {
adj[i].push_back(v);
}
}
dfn.assign(n + 1, 0);
low.assign(n + 1, 0);
scc_id.assign(n + 1, 0);
on_stack.assign(n + 1, false);
timestamp = 0;
scc_count = 0;
for (int i = 1; i <= n; ++i) {
if (dfn[i] == 0) {
tarjan(i);
}
}
if (scc_count == 0) {
cout << 0 << endl;
return 0;
}
vector<int> in_degree(scc_count + 1, 0);
for (int u = 1; u <= n; ++u) {
for (int v : adj[u]) {
if (scc_id[u] != scc_id[v]) {
in_degree[scc_id[v]]++;
}
}
}
int zero_in_degree_count = 0;
for (int i = 1; i <= scc_count; ++i) {
if (in_degree[i] == 0) {
zero_in_degree_count++;
}
}
cout << zero_in_degree_count << endl;
return 0;
}
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.Stack;
public class Main {
private static List<List<Integer>> adj;
private static int[] dfn, low, sccId;
private static boolean[] onStack;
private static Stack<Integer> stack;
private static int timestamp, sccCount;
private static void tarjan(int u) {
dfn[u] = low[u] = ++timestamp;
stack.push(u);
onStack[u] = true;
for (int v : adj.get(u)) {
if (dfn[v] == 0) {
tarjan(v);
low[u] = Math.min(low[u], low[v]);
} else if (onStack[v]) {
low[u] = Math.min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
sccCount++;
int node;
do {
node = stack.pop();
onStack[node] = false;
sccId[node] = sccCount;
} while (node != u);
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
adj = new ArrayList<>();
for (int i = 0; i <= n; i++) {
adj.add(new ArrayList<>());
}
for (int i = 1; i <= n; i++) {
int v;
while ((v = sc.nextInt()) != 0) {
adj.get(i).add(v);
}
}
dfn = new int[n + 1];
low = new int[n + 1];
sccId = new int[n + 1];
onStack = new boolean[n + 1];
stack = new Stack<>();
timestamp = 0;
sccCount = 0;
for (int i = 1; i <= n; i++) {
if (dfn[i] == 0) {
tarjan(i);
}
}
if (sccCount == 0) {
System.out.println(0);
return;
}
int[] inDegree = new int[sccCount + 1];
for (int u = 1; u <= n; u++) {
for (int v : adj.get(u)) {
if (sccId[u] != sccId[v]) {
inDegree[sccId[v]]++;
}
}
}
int zeroInDegreeCount = 0;
for (int i = 1; i <= sccCount; i++) {
if (inDegree[i] == 0) {
zeroInDegreeCount++;
}
}
System.out.println(zeroInDegreeCount);
}
}
import sys
# 增加递归深度限制
sys.setrecursionlimit(200005)
# 全局变量,用于Tarjan算法
adj = []
dfn, low, scc_id = [], [], []
on_stack = []
stack = []
timestamp = 0
scc_count = 0
def tarjan(u):
global timestamp, scc_count
timestamp += 1
dfn[u] = low[u] = timestamp
stack.append(u)
on_stack[u] = True
for v in adj[u]:
if dfn[v] == 0:
tarjan(v)
low[u] = min(low[u], low[v])
elif on_stack[v]:
low[u] = min(low[u], dfn[v])
if dfn[u] == low[u]:
scc_count += 1
while True:
node = stack.pop()
on_stack[node] = False
scc_id[node] = scc_count
if node == u:
break
def main():
global adj, dfn, low, scc_id, on_stack, stack, timestamp, scc_count
line = sys.stdin.readline()
if not line.strip():
print(0)
return
n = int(line)
adj = [[] for _ in range(n + 1)]
for i in range(1, n + 1):
parts = list(map(int, sys.stdin.readline().split()))
adj[i] = parts[:-1] # 最后一个元素0是终止符
# 初始化Tarjan算法所需的数组
dfn = [0] * (n + 1)
low = [0] * (n + 1)
scc_id = [0] * (n + 1)
on_stack = [False] * (n + 1)
stack = []
timestamp = 0
scc_count = 0
# 执行Tarjan算法
for i in range(1, n + 1):
if dfn[i] == 0:
tarjan(i)
if n == 0:
print(0)
return
# 计算缩点后每个SCC的入度
in_degree = [0] * (scc_count + 1)
for u in range(1, n + 1):
for v in adj[u]:
if scc_id[u] != scc_id[v]:
in_degree[scc_id[v]] += 1
# 统计入度为0的SCC数量
zero_in_degree_count = 0
for i in range(1, scc_count + 1):
if in_degree[i] == 0:
zero_in_degree_count += 1
print(zero_in_degree_count)
if __name__ == "__main__":
main()
算法及复杂度
- 算法:Tarjan 算法求强连通分量 (SCC) + 拓扑图入度统计
- 时间复杂度:
,其中
是营员数(顶点数),
是所有愿意分享关系的总数(边数)。Tarjan 算法和后续的入度统计都只需要遍历一次所有的顶点和边。
- 空间复杂度:
,用于存储图的邻接表以及 Tarjan 算法所需的辅助数组。