題目地址:
https://leetcode.com/problems/redundant-connection/
給定一個無向圖,以二維數組,每個位置存一條邊的形式給出。問刪去哪條邊,這個圖會變成一棵樹。題目保證答案存在。並且返回那個下標最大的滿足條件的邊。
法1:DFS。題目的本質是找圈,所以可以用DFS來做。先從一個頂點開始遍歷,找到圈後返回路徑,然後解析出圈,接着再找到下標最大的邊即可。代碼如下:
import java.util.*;
public class Solution {
// 開一個邊的類,以便於最後存入哈希表方便查找滿足條件的”最後一個“邊
class Edge {
int from, to;
Edge() {}
Edge(int from, int to) {
this.from = from;
this.to = to;
}
@Override
public boolean equals(Object another) {
Edge edge = (Edge) another;
return from == edge.from && to == edge.to;
}
@Override
public int hashCode() {
return Objects.hash(from, to);
}
}
public int[] findRedundantConnection(int[][] edges) {
Map<Integer, Set<Integer>> graph = new HashMap<>();
Map<Edge, Integer> edgeIdx = new HashMap<>();
// 用鄰接表建圖,並把每個邊的下標存入edgeIdx這個哈希表
for (int i = 0; i < edges.length; i++) {
graph.putIfAbsent(edges[i][0], new HashSet<>());
graph.get(edges[i][0]).add(edges[i][1]);
graph.putIfAbsent(edges[i][1], new HashSet<>());
graph.get(edges[i][1]).add(edges[i][0]);
edgeIdx.put(new Edge(edges[i][0], edges[i][1]), i);
}
List<Integer> path = new ArrayList<>();
boolean[] visited = new boolean[edges.length + 1];
// 由於環一定存在,所以就直接從任意一個點開始DFS即可
dfs(1, 1, graph, visited, path);
// 開始找環,發現第一個重複數字後就知道了環的位置了
Map<Integer, Integer> map = new HashMap<>();
int start = 0, end = 0;
for (int i = 0; i < path.size(); i++) {
if (map.containsKey(path.get(i))) {
start = map.get(path.get(i));
end = i;
}
map.put(path.get(i), i);
}
// 分離出環後查看哪條邊的下標最大,最後返回
int[] res = null;
int idx = -1;
for (int i = start; i < end; i++) {
Edge edge = new Edge();
edge.from = Math.min(path.get(i), path.get(i + 1));
edge.to = Math.max(path.get(i), path.get(i + 1));
if (idx < edgeIdx.get(edge)) {
idx = edgeIdx.get(edge);
res = edges[idx];
}
}
return res;
}
// 從v開始做DFS,並且v是由w走到的;返回是否找到環
private boolean dfs(int v, int w, Map<Integer, Set<Integer>> graph, boolean[] visited, List<Integer> path) {
path.add(v);
visited[v] = true;
if (graph.containsKey(v)) {
for (int next : graph.get(v)) {
// 不走回頭路
if (next == w) {
continue;
}
// 找到環了,返回true
if (visited[next]) {
path.add(next);
return true;
}
// 否則繼續尋找環
if (dfs(next, v, graph, visited, path)) {
return true;
}
}
}
// 走到了死衚衕,回溯前要恢復現場,把死路走到的點刪掉,並返回false
path.remove(path.size() - 1);
return false;
}
}
時空複雜度。
法2:並查集(disjoint set)。每次遇到一條邊,就將邊的兩個端點union起來,如果遇到了某一條邊,發現其兩個端點已經屬於同一個集合了,就直接返回。代碼如下:
import java.util.HashSet;
import java.util.Set;
public class Solution {
class UnionFind {
int[] parent;
int[] rank;
UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 1;
}
}
public int find(int p) {
if (p != parent[p]) {
parent[p] = find(parent[p]);
}
return parent[p];
}
// 如果p和q不屬於同一個集合,則將它們的所在集合合併起來,並返回true;
// 否則若p和q屬於同一個集合,則返回false
public boolean union(int p, int q) {
int pid = find(p), qid = find(q);
if (pid == qid) {
return false;
}
if (rank[pid] < rank[qid]) {
parent[pid] = qid;
} else if (rank[pid] > rank[qid]) {
parent[qid] = pid;
} else {
parent[pid] = qid;
rank[qid]++;
}
return true;
}
}
public int[] findRedundantConnection(int[][] edges) {
// 開一個set數一下這個圖一共有多少個頂點
Set<Integer> set = new HashSet<>();
for (int i = 0; i < edges.length; i++) {
set.add(edges[i][0]);
set.add(edges[i][1]);
}
UnionFind uf = new UnionFind(set.size());
for (int i = 0; i < edges.length; i++) {
int p = edges[i][0], q = edges[i][1];
// 發現了p - 1和q - 1屬於同一個集合,則將這條邊返回
if (!uf.union(p - 1, q - 1)) {
return edges[i];
}
}
return null;
}
}
時間複雜度(指的是iterative logarithm),空間。
算法正確性證明:
首先題目已經保證圖連通,並且只需要去掉一條邊,就會成爲樹。那麼當第一次發現兩個點和處於同一個集合的時候,就已經出現了環(因爲在沒有這條邊的時候,就已經存在一條通路了,然後再加上這條邊就成爲了環),由於題目保證只需要去掉一條邊就成爲樹,所以這一條邊就是最終答案。