You are given an integer n
. There is an undirected graph with n
nodes, numbered from 0
to n - 1
. You are given a 2D integer array edges
where edges[i] = [ai, bi]
denotes that there exists an undirected edge connecting nodes ai
and bi
.
Return the number of pairs of different nodes that are unreachable from each other.
Example 1:
Input: n = 3, edges = [[0,1],[0,2],[1,2]] Output: 0 Explanation: There are no pairs of nodes that are unreachable from each other. Therefore, we return 0.
Example 2:
Input: n = 7, edges = [[0,2],[0,5],[2,4],[1,6],[5,4]] Output: 14 Explanation: There are 14 pairs of nodes that are unreachable from each other: [[0,1],[0,3],[0,6],[1,2],[1,3],[1,4],[1,5],[2,3],[2,6],[3,4],[3,5],[3,6],[4,6],[5,6]]. Therefore, we return 14.
Constraints:
1 <= n <= 105
0 <= edges.length <= 2 * 105
edges[i].length == 2
0 <= ai, bi < n
ai != bi
This problem asks to find the number of pairs of nodes in an undirected graph that are not connected. The most efficient approach leverages Depth-First Search (DFS) or Breadth-First Search (BFS) combined with a Union-Find data structure (although DFS is shown here).
Algorithm:
Graph Representation: Represent the graph using an adjacency list. This allows for efficient traversal during the DFS.
Depth-First Search (DFS): Perform a DFS to identify connected components within the graph. Each DFS traversal starting from an unvisited node will explore all nodes reachable from that starting node, thus defining a connected component.
Counting Unreachable Pairs: During the DFS, keep track of the size of each connected component encountered. For each connected component of size t
, the number of unreachable pairs involving nodes within that component is 0. However, nodes in this component are unreachable from nodes in all previously visited components. Therefore we must count unreachable pairs between nodes in this component and nodes in all previous components. If s
is the total number of nodes in all previously visited components, the number of additional unreachable pairs is s * t
.
Accumulate Results: Sum the number of unreachable pairs found for each connected component. The final sum represents the total number of unreachable node pairs in the graph.
Time Complexity: O(V + E), where V is the number of vertices (nodes) and E is the number of edges in the graph. This is because the DFS visits each vertex and edge once.
Space Complexity: O(V + E) due to the adjacency list representation of the graph and the recursive call stack (in the DFS implementation).
Code Examples (Python, Java, C++, Go, Typescript, Rust):
The code examples below demonstrate the DFS approach in several languages. The core logic remains consistent across all implementations. Note that slight variations might exist depending on the chosen language's standard library functions and data structures.
Python:
class Solution:
def countPairs(self, n: int, edges: List[List[int]]) -> int:
def dfs(i: int) -> int:
if vis[i]:
return 0
vis[i] = True
return 1 + sum(dfs(j) for j in g[i])
g = [[] for _ in range(n)]
for a, b in edges:
g[a].append(b)
g[b].append(a)
vis = [False] * n
ans = s = 0
for i in range(n):
t = dfs(i)
ans += s * t
s += t
return ans
Java:
class Solution {
private List<Integer>[] g;
private boolean[] vis;
public long countPairs(int n, int[][] edges) {
g = new List[n];
vis = new boolean[n];
Arrays.setAll(g, i -> new ArrayList<>());
for (var e : edges) {
int a = e[0], b = e[1];
g[a].add(b);
g[b].add(a);
}
long ans = 0, s = 0;
for (int i = 0; i < n; ++i) {
int t = dfs(i);
ans += s * t;
s += t;
}
return ans;
}
private int dfs(int i) {
if (vis[i]) {
return 0;
}
vis[i] = true;
int cnt = 1;
for (int j : g[i]) {
cnt += dfs(j);
}
return cnt;
}
}
C++:
class Solution {
public:
long long countPairs(int n, vector<vector<int>>& edges) {
vector<int> g[n];
for (auto& e : edges) {
int a = e[0], b = e[1];
g[a].push_back(b);
g[b].push_back(a);
}
bool vis[n];
memset(vis, 0, sizeof(vis));
function<int(int)> dfs = [&](int i) {
if (vis[i]) {
return 0;
}
vis[i] = true;
int cnt = 1;
for (int j : g[i]) {
cnt += dfs(j);
}
return cnt;
};
long long ans = 0, s = 0;
for (int i = 0; i < n; ++i) {
int t = dfs(i);
ans += s * t;
s += t;
}
return ans;
}
};
Go:
func countPairs(n int, edges [][]int) (ans int64) {
g := make([][]int, n)
for _, e := range edges {
a, b := e[0], e[1]
g[a] = append(g[a], b)
g[b] = append(g[b], a)
}
vis := make([]bool, n)
var dfs func(int) int
dfs = func(i int) int {
if vis[i] {
return 0
}
vis[i] = true
cnt := 1
for _, j := range g[i] {
cnt += dfs(j)
}
return cnt
}
var s int64
for i := 0; i < n; i++ {
t := int64(dfs(i))
ans += s * t
s += t
}
return
}
TypeScript:
function countPairs(n: number, edges: number[][]): number {
const g: number[][] = Array.from({ length: n }, () => []);
for (const [a, b] of edges) {
g[a].push(b);
g[b].push(a);
}
const vis: boolean[] = Array(n).fill(false);
const dfs = (i: number): number => {
if (vis[i]) {
return 0;
}
vis[i] = true;
let cnt = 1;
for (const j of g[i]) {
cnt += dfs(j);
}
return cnt;
};
let [ans, s] = [0, 0];
for (let i = 0; i < n; ++i) {
const t = dfs(i);
ans += s * t;
s += t;
}
return ans;
}
Rust:
impl Solution {
pub fn count_pairs(n: i32, edges: Vec<Vec<i32>>) -> i64 {
let n = n as usize;
let mut g = vec![vec![]; n];
let mut vis = vec![false; n];
for e in edges {
let u = e[0] as usize;
let v = e[1] as usize;
g[u].push(v);
g[v].push(u);
}
fn dfs(g: &Vec<Vec<usize>>, vis: &mut Vec<bool>, u: usize) -> i64 {
if vis[u] {
return 0;
}
vis[u] = true;
let mut cnt = 1;
for &v in &g[u] {
cnt += dfs(g, vis, v);
}
cnt
}
let mut ans = 0;
let mut s = 0;
for u in 0..n {
let t = dfs(&g, &mut vis, u);
ans += t * s;
s += t;
}
ans
}
}
These code examples all implement the same algorithm, differing only in syntax and specific library functions used. They all achieve a time complexity of O(V + E) and a space complexity of O(V + E).