冗余连接
题目描述
有一个图,它是一棵树,拥有 n 个节点(节点编号 1 到 n)和 n - 1 条边的连通无环无向图。现在在这棵树的基础上,添加一条边(依然是 n 个节点,但有 n 条边),使这个图变成了有环图。请你找出冗余边,删除后,使该图可以重新变成一棵树。
输入输出格式
输入格式:
第一行包含一个整数 n,表示节点的数量。
接下来的 n 行,每行包含两个整数 s 和 t,表示一条边连接节点 s 和 t。
输出格式:
输出两个整数,表示冗余边的两个节点,按升序排列。
解题思路
-
问题分析:
- 原本是一个树结构,添加一条边后形成环。
- 冗余边就是导致环形成的那条边。
- 我们需要找出这条边并删除它,使图重新变成树。
-
并查集(Union-Find)数据结构:
- 并查集是一种用于处理动态连通性问题的数据结构。
- 它支持两种操作:查找(find)和合并(union)。
- 查找操作用于确定一个节点的根节点,合并操作用于将两个节点所在的集合合并。
-
算法步骤:
- 初始化并查集,每个节点的父节点初始化为自己。
- 遍历每一条边,对于每一条边的两个节点 s 和 t:
- 如果 s 和 t 已经连通(属于同一个集合),则这条边就是冗余边,记录下来。
- 如果 s 和 t 不连通,则将它们合并到同一个集合中。
- 最后输出记录的冗余边。
-
代码实现:
- 使用 Java 实现并查集类(DisJoint)。
- 在主函数中读取输入,遍历每条边,利用并查集判断是否为冗余边。
代码实现
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
DisJoint disjoint = new DisJoint(n + 1);
int[] lastRedundantEdge = new int[2];
for (int i = 0; i < n; i++) {
int s = sc.nextInt();
int t = sc.nextInt();
if (disjoint.isSame(s, t)) {
lastRedundantEdge[0] = s;
lastRedundantEdge[1] = t;
} else {
disjoint.join(s, t);
}
}
System.out.println(lastRedundantEdge[0] + " " + lastRedundantEdge[1]);
}
}
class DisJoint {
private int[] father;
public DisJoint(int N) {
father = new int[N];
for (int i = 0; i < N; ++i) {
father[i] = i;
}
}
public int find(int n) {
return n == father[n] ? n : (father[n] = find(father[n]));
}
public void join(int n, int m) {
n = find(n);
m = find(m);
if (n == m) return;
father[m] = n;
}
public boolean isSame(int n, int m) {
n = find(n);
m = find(m);
return n == m;
}
}
示例
输入:
5
1 2
1 3
2 3
3 4
4 5
输出:
2 3
解释:
- 原本的树结构是 1-2-3-4-5。
- 添加边 2-3 后形成环。
- 冗余边是 2-3,删除后图重新变成树。
冗余连接 II
题目描述
有一种有向树,该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。
现在有一个有向图,该图是在有向树中的两个没有直接链接的节点中间添加一条有向边。该有向图有 n 个节点(节点编号从 1 到 n)和 n 条边。请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一棵有向树。
输入描述:
第一行输入一个整数 N,表示有向图中节点和边的个数。
后续 N 行,每行输入两个整数 s 和 t,代表这是 s 节点连接并指向 t 节点的单向边。
输出描述:
输出一条可以删除的边,若有多条边可以删除,请输出标准输入中最后出现的一条边。
解题思路
-
问题分析:
- 原本是一个有向树,添加一条边后可能形成环或者导致某个节点有两个父节点。
- 需要找出这条冗余边并删除,使图重新变成有向树。
-
两种情况:
- 情况一:存在一个节点有两个父节点。此时需要判断哪一条边是冗余的。
- 情况二:图中存在环。此时需要找出形成环的最后一条边。
-
算法步骤:
- 记录入度:遍历所有边,记录每个节点的入度,找出是否有节点入度为 2。
- 处理情况一:如果存在入度为 2 的节点,找出两条指向该节点的边,分别尝试删除,判断是否能形成树。
- 处理情况二:如果不存在入度为 2 的节点,使用并查集找出形成环的最后一条边。
-
并查集(Union-Find)数据结构:
- 用于处理动态连通性问题,支持查找和合并操作。
- 在检测环时,如果两个节点已经连通,则当前边是冗余边。
-
代码实现:
- 使用 Java 实现并查集类(DisJoint)。
- 在主函数中读取输入,记录边和节点信息。
- 根据是否存在入度为 2 的节点,分别处理两种情况。
代码实现
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
List<Edge> edges = new ArrayList<>();
Node[] nodeMap = new Node[n + 1];
for (int i = 1; i <= n; i++) {
nodeMap[i] = new Node();
}
Integer doubleIn = null;
for (int i = 0; i < n; i++) {
int s = sc.nextInt();
int t = sc.nextInt();
nodeMap[t].in++;
if (nodeMap[t].in > 1) doubleIn = t;
Edge edge = new Edge(s, t);
edges.add(edge);
}
Edge result = null;
if (doubleIn != null) {
List<Edge> doubleInEdges = new ArrayList<>();
for (Edge edge : edges) {
if (edge.t == doubleIn) doubleInEdges.add(edge);
if (doubleInEdges.size() == 2) break;
}
Edge edge = doubleInEdges.get(1);
if (isTreeWithExclude(edges, edge, nodeMap)) {
result = edge;
} else {
result = doubleInEdges.get(0);
}
} else {
result = getRemoveEdge(edges, nodeMap);
}
System.out.println(result.s + " " + result.t);
}
public static boolean isTreeWithExclude(List<Edge> edges, Edge excludeEdge, Node[] nodeMap) {
DisJoint disjoint = new DisJoint(nodeMap.length + 1);
for (Edge edge : edges) {
if (edge == excludeEdge) continue;
if (disjoint.isSame(edge.s, edge.t)) {
return false;
}
disjoint.join(edge.s, edge.t);
}
return true;
}
public static Edge getRemoveEdge(List<Edge> edges, Node[] nodeMap) {
int length = nodeMap.length;
DisJoint disjoint = new DisJoint(length);
for (Edge edge : edges) {
if (disjoint.isSame(edge.s, edge.t)) return edge;
disjoint.join(edge.s, edge.t);
}
return null;
}
static class DisJoint {
private int[] father;
public DisJoint(int N) {
father = new int[N];
for (int i = 0; i < N; ++i) {
father[i] = i;
}
}
public int find(int n) {
return n == father[n] ? n : (father[n] = find(father[n]));
}
public void join(int n, int m) {
n = find(n);
m = find(m);
if (n == m) return;
father[m] = n;
}
public boolean isSame(int n, int m) {
n = find(n);
m = find(m);
return n == m;
}
}
static class Edge {
int s;
int t;
public Edge(int s, int t) {
this.s = s;
this.t = t;
}
}
static class Node {
int id;
int in;
int out;
}
}
示例
输入:
5
1 2
1 3
3 4
4 1
4 5
输出:
4 1
解释:
- 原本的有向树结构被破坏,因为添加了边 4->1,形成了环。
- 删除边 4->1 后,图重新变成有向树。