1.导论
- 并查集
//并查集(管理一系列不想交的集合),具有查询和合并的功能
class DisjointSetUnion {
int[] f;//存储每个元素的根节点
int[] nodeNum;//记录当前结点为根节点的结点数量
int[] rank;//记录每个根节点对应树的深度
int n;//元素个数
int num;//记录连通量的个数
public DisjointSetUnion(int n) {
this.n = n;
this.num = n;
this.rank = new int[n];//记录每个根节点对应树的深度
this.f = new int[n];//存储每个元素的父节点
this.nodeNum = new int[n];
for (int i = 0; i < n; i++) {
this.rank[i] = 1;//初始值设为1
this.f[i] = i;//一开始将父节点设为自己
this.nodeNum[i] = 1;
}
}
//压缩路径查询方法,递归实现
//一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
//按秩合并方法
public boolean unionSet(int x, int y) {
int fx = find(x), fy = find(y);//找到两个结点的根节点
if (fx == fy) {//两个结点的根节点相同不用合并
return false;
}
if (rank[fx] < rank[fy]) {//以fx为根节点的树的深度小于以fy为根节点的树的深度
f[fx] = fy;
nodeNum[fy] += nodeNum[fx];
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
nodeNum[fx] += nodeNum[fy];
} else {
f[fx] = fy;
rank[fy] += 1;
nodeNum[fy] += nodeNum[fx];
}
num--;
return true;
}
}
- 解题步骤
1.编写并查集类
2.使用并查集对元素进行维护
3.具体问题具体分析解答
2.编程题
2.1 547. 省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
1 <= n <= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j] 为 1 或 0
isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
class Solution {
public int findCircleNum(int[][] isConnected) {
int m = isConnected.length;
int n = isConnected[0].length;
DisjointUnionSet dis = new DisjointUnionSet(m);
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (isConnected[i][j] == 1) {// 将应该连通的城市进行连通
dis.unionSet(i,j);
}
}
}
int ans = 0;
for (int i = 0; i < m; ++i) {
if (dis.f[i] == i) {// 计算连通分量的总数
++ans;
}
}
return ans;
}
}
//并查集
class DisjointUnionSet{
int[] f;
int[] rank;
int n;
public DisjointUnionSet(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if(fx == fy) return false;
if (rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.2 684. 冗余连接
树可以看成是一个连通且 无环 的 无向 图。
给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。
n == edges.length
3 <= n <= 1000
edges[i].length == 2
1 <= ai < bi <= edges.length
ai != bi
edges 中无重复元素
给定的图是连通的
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int m = edges.length;
DisjointSetUnion dis = new DisjointSetUnion(m);
for (int i = 0; i < m; ++i) {
int[] edge = edges[i];
int node1 = edge[0];
int node2 = edge[1];
/**
如果两个顶点属于不同的连通分量,则说明在遍历到当前的边之前,
这两个顶点之间不连通,因此当前的边不会导致环出现,
合并这两个顶点的连通分量。
如果两个顶点属于相同的连通分量,则说明在遍历到当前的边之前,
这两个顶点之间已经连通,因此当前的边导致环出现,为附加的边,
将当前的边作为答案返回。
*/
if (dis.find(node1-1) != dis.find(node2-1)) {
dis.unionSet(node1-1,node2-1);
} else {
return edge;
}
}
return new int[0];
}
}
//并查集
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.3 685. 冗余连接 II
在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。
返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
n == edges.length
3 <= n <= 1000
edges[i].length == 2
1 <= ui, vi <= n
class Solution {
public int[] findRedundantDirectedConnection(int[][] edges) {
int n = edges.length;
DisjointSetUnion dis = new DisjointSetUnion(n);
int conflict = -1;
int cycle = -1;
//记录每个结点的父节点
int[] parent = new int[n + 1];
for (int i = 1; i <= n; ++i) {
parent[i] = i;
}
/**
存在两种情况:
一种是所有的结点都有父节点
一种是有一个结点有两个父节点
*/
for (int i = 0; i < n; ++i) {
int[] edge = edges[i];
int node1 = edge[0], node2 = edge[1];
if (parent[node2] != node2) {
conflict = i;//记录冲突
} else {
parent[node2] = node1;
if (dis.find(node1-1) == dis.find(node2-1)) {//两个结点的根节点相同,说明出现环
cycle = i;//记录环
} else {
dis.unionSet(node1-1, node2-1);
}
}
}
if (conflict < 0) {//说明只有环
int[] redundant = {edges[cycle][0], edges[cycle][1]};//获取成环的边
return redundant;
} else {//既有冲突又有环
int[] conflictEdge = edges[conflict];//获取冲突的边
if (cycle >= 0) {//既有冲突又有环
int[] redundant = {parent[conflictEdge[1]], conflictEdge[1]};
return redundant;
} else {//只有冲突
int[] redundant = {conflictEdge[0], conflictEdge[1]};
return redundant;
}
}
}
}
//并查集
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fy] = fx;
rank[fx] += 1;
}
return true;
}
}
2.4 721. 账户合并
给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其余元素是 emails 表示该账户的邮箱地址。
现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。
合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是 按字符 ASCII 顺序排列 的邮箱地址。账户本身可以以 任意顺序 返回。
1 <= accounts.length <= 1000
2 <= accounts[i].length <= 10
1 <= accounts[i][j].length <= 30
accounts[i][0] 由英文字母组成
accounts[i][j] (for j > 0) 是有效的邮箱地址
class Solution {
public List<List<String>> accountsMerge(List<List<String>> accounts) {
Map<String,Integer> emailToIndex = new HashMap<>();//记录每个邮箱的索引
Map<String,String> emailToName = new HashMap<>();//记录每个邮箱对应的账户
int emailCount = 0;//记录账户的个数
//记录每个邮箱对应的编号和每个邮箱对应的名称
for (List<String> account : accounts) {
String name = account.get(0);
int size = account.size();
for (int i = 1; i< size; ++i) {
String email = account.get(i);
if (!emailToIndex.containsKey(email)) {
emailToIndex.put(email, emailCount++);
emailToName.put(email, name);
}
}
}
//使用并查集进行合并
DisjointSetUnion dis = new DisjointSetUnion(emailCount);
for (List<String> account : accounts) {
String firstEmail = account.get(1);
int firstIndex = emailToIndex.get(firstEmail);
int size = account.size();
for (int i = 2; i < size; ++i) {
String nextEmail = account.get(i);
int nextIndex = emailToIndex.get(nextEmail);
dis.unionSet(firstIndex, nextIndex);
}
}
//记录每个合并后的账户包含哪些邮箱地址
Map<Integer,List<String>> indexToEmail = new HashMap<>();
for (String email : emailToIndex.keySet()) {
int index = dis.find(emailToIndex.get(email));
List<String> account = indexToEmail.getOrDefault(index, new ArrayList<String>());
account.add(email);
indexToEmail.put(index, account);
}
//对每个合并后的账户,整理出题目要求返回账户的格式
List<List<String>> merge = new ArrayList<>();
for (List<String> emails : indexToEmail.values()) {
Collections.sort(emails);
String name = emailToName.get(emails.get(0));
List<String> account = new ArrayList<>();
account.add(name);
account.addAll(emails);
merge.add(account);
}
return merge;
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.5 765. 情侣牵手
N 对情侣坐在连续排列的 2N 个座位上,想要牵到对方的手。 计算最少交换座位的次数,以便每对情侣可以并肩坐在一起。 一次交换可选择任意两人,让他们站起来交换座位。
人和座位用 0 到 2N-1 的整数表示,情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2N-2, 2N-1)。
这些情侣的初始座位 row[i] 是由最初始坐在第 i 个座位上的人决定的。
len(row) 是偶数且数值在 [4, 60]范围内。
可以保证row 是序列 0…len(row)-1 的一个全排列。
class Solution {
public int minSwapsCouples(int[] row) {
int n = row.length;
int N = n/2;
DisjointSetUnion dis = new DisjointSetUnion(N);
int joinCount = 0;
for (int i = 0; i < n; i += 2) {
if (dis.unionSet(row[i] / 2, row[i+1] / 2)) {
++joinCount;
}
}
return joinCount;
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.6 947. 移除最多的同行或同列石头
n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。
如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头。
给你一个长度为 n 的数组 stones ,其中 stones[i] = [xi, yi] 表示第 i 块石头的位置,返回 可以移除的石子 的最大数量。
class Solution {
public int removeStones(int[][] stones) {
int n = stones.length;
DisjointSetUnion dis = new DisjointSetUnion(n);
int joinCount = 0;
for (int i = 0; i < n-1; ++i) {
for (int j = i + 1; j < n; ++j) {
if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1]) {
//如果两个石头在同一行或则同一列,则进行连接
if (dis.unionSet(i,j)) { // 如果可以连接,说明可以移除,即连接次数和移除次数相同
++joinCount;
}
}
}
}
return joinCount;
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.7 959. 由斜杠划分区域
在由 1 x 1 方格组成的 N x N 网格 grid 中,每个 1 x 1 方块由 /、\ 或空格构成。这些字符会将方块划分为一些共边的区域。
(请注意,反斜杠字符是转义的,因此 \ 用 “\” 表示。)。
返回区域的数目。
1 <= grid.length == grid[0].length <= 30
grid[i][j] 是 ‘/’、’’、或 ’ '。
class Solution {
public int regionsBySlashes(String[] grid) {
int N = grid.length;
int size = 4*N*N;
int jointCount = 0;
DisjointSetUnion dis = new DisjointSetUnion(size);
for (int i = 0; i < N; i++) {
char[] row = grid[i].toCharArray();
for (int j = 0; j < N; j++) {
// 二维网格转换为一维表格,index 表示将单元格拆分成 4 个小三角形以后,编号为 0 的小三角形的在并查集中的下标
int index = 4 * (i * N + j);
char c = row[j];
// 单元格内合并
if (c == '/') {
// 合并 0、3,合并 1、2
if (dis.unionSet(index, index + 3)) ++jointCount;
if (dis.unionSet(index + 1, index + 2)) ++jointCount;
} else if (c == '\\') {
// 合并 0、1,合并 2、3
if (dis.unionSet(index, index + 1)) ++jointCount;
if (dis.unionSet(index + 2, index + 3)) ++jointCount;
} else {
if (dis.unionSet(index, index + 1)) ++jointCount;
if (dis.unionSet(index + 1, index + 2)) ++jointCount;
if (dis.unionSet(index + 2, index + 3)) ++jointCount;
}
// 单元格间合并
// 向右合并:1(当前)、3(右一列)
if (j + 1 < N) {
if (dis.unionSet(index + 1, 4 * (i * N + j + 1) + 3)) ++jointCount;
}
// 向下合并:2(当前)、0(下一行)
if (i + 1 < N) {
if (dis.unionSet(index + 2, 4 * ((i + 1) * N + j))) ++jointCount;
}
}
}
return size - jointCount;
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.8 990. 等式方程的可满足性
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:“a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
1 <= equations.length <= 500
equations[i].length == 4
equations[i][0] 和 equations[i][3] 是小写字母
equations[i][1] 要么是 ‘=’,要么是 ‘!’
equations[i][2] 是 ‘=’
class Solution {
public boolean equationsPossible(String[] equations) {
DisjointSetUnion dis = new DisjointSetUnion(26);
for (String str : equations) {
if (str.charAt(1) == '=') {
int index1 = str.charAt(0) - 'a';
int index2 = str.charAt(3) - 'a';
dis.unionSet(index1,index2);
}
}
for (String str : equations) {
if (str.charAt(1) == '!') {
int index1 = str.charAt(0) - 'a';
int index2 = str.charAt(3) - 'a';
if (dis.find(index1) == dis.find(index2)){
return false;
}
}
}
return true;
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.9 1202. 交换字符串中的元素
给你一个字符串 s,以及该字符串中的一些「索引对」数组 pairs,其中 pairs[i] = [a, b] 表示字符串中的两个索引(编号从 0 开始)。
你可以 任意多次交换 在 pairs 中任意一对索引处的字符。
返回在经过若干次交换后,s 可以变成的按字典序最小的字符串。
1 <= s.length <= 10^5
0 <= pairs.length <= 10^5
0 <= pairs[i][0], pairs[i][1] < s.length
s 中只含有小写英文字母
class Solution {
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
if (pairs.size() == 0) {
return s;
}
//使用并查集建图
int n = s.length();
DisjointSetUnion dis = new DisjointSetUnion(n);
for (List<Integer> pair : pairs) {
int index1 = pair.get(0);
int index2 = pair.get(1);
dis.unionSet(index1,index2);
}
//构建映射函数
char[] c = s.toCharArray();
//key:连通分量的代表元,value:同一个连通分量的字符集合(保存在一个优先队列中)
Map<Integer,PriorityQueue<Character>> map = new HashMap<>(n);
for (int i = 0; i < n; ++i) {
int root = dis.find(i);
PriorityQueue<Character> pq = map.getOrDefault(root,new PriorityQueue<Character>());
pq.offer(c[i]);
map.put(root,pq);
}
//重组字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; ++i) {
int root = dis.find(i);
sb.append(map.get(root).poll());
}
return sb.toString();
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.10 1319. 连通网络的操作次数
用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用 connections 表示,其中 connections[i] = [a, b] 连接了计算机 a 和 b。
网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。
给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。
1 <= n <= 10^5
1 <= connections.length <= min(n*(n-1)/2, 10^5)
connections[i].length == 2
0 <= connections[i][0], connections[i][1] < n
connections[i][0] != connections[i][1]
没有重复的连接。
两台计算机不会通过多条线缆连接。
class Solution {
public int makeConnected(int n, int[][] connections) {
if (connections.length < n-1) {// 要连接n台电脑至少需要n-1条线
return -1;
}
DisjointSetUnion dis = new DisjointSetUnion(n);
int jointCount = 0;//记录初始布线时能进行连通的连通次数
for (int[] connection : connections) {
int index1 = connection[0];
int index2 = connection[1];
if (dis.unionSet(index1, index2)) {
++jointCount;
}
}
return n - 1 - jointCount;
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.11 1579. 保证图可完全遍历
Alice 和 Bob 共有一个无向图,其中包含 n 个节点和 3 种类型的边:
类型 1:只能由 Alice 遍历。
类型 2:只能由 Bob 遍历。
类型 3:Alice 和 Bob 都可以遍历。
给你一个数组 edges ,其中 edges[i] = [typei, ui, vi] 表示节点 ui 和 vi 之间存在类型为 typei 的双向边。请你在保证图仍能够被 Alice和 Bob 完全遍历的前提下,找出可以删除的最大边数。如果从任何节点开始,Alice 和 Bob 都可以到达所有其他节点,则认为图是可以完全遍历的。
返回可以删除的最大边数,如果 Alice 和 Bob 无法完全遍历图,则返回 -1 。
1 <= n <= 10^5
1 <= edges.length <= min(10^5, 3 * n * (n-1) / 2)
edges[i].length == 3
1 <= edges[i][0] <= 3
1 <= edges[i][1] < edges[i][2] <= n
所有元组 (typei, ui, vi) 互不相同
class Solution {
public int maxNumEdgesToRemove(int n, int[][] edges) {
DisjointSetUnion disA = new DisjointSetUnion(n);
DisjointSetUnion disB = new DisjointSetUnion(n);
int joinCount = 0;// 记录Alice和Bob都能完全遍历图的最少连通次数
int joinCountA = 0;// 记录Alice的连通次数
int joinCountB = 0;// 记录Bob的连通次数
// 结点编号改为从0开始
for (int[] edge : edges) {
--edge[1];
--edge[2];
}
//先使用公共边再使用独占边进行连通,能保证连通边最少
// 公共边
for (int[] edge : edges) {
if (edge[0] == 3) {
if (disA.unionSet(edge[1], edge[2])) {
++joinCount;
++joinCountA;
}
if (disB.unionSet(edge[1], edge[2])) {
++joinCountB;
}
}
}
// 独占边
for (int[] edge : edges) {
if (edge[0] == 1) {
if (disA.unionSet(edge[1], edge[2])) {
++joinCount;
++joinCountA;
}
} else if (edge[0] == 2) {
if (disB.unionSet(edge[1], edge[2])) {
++joinCount;
++joinCountB;
}
}
}
// 说明Alice和Bob都不能完全遍历图
if (joinCountA < n-1 || joinCountB < n-1) return -1;
// 原来边的总数 - Alice和Bob都能完全遍历图的最少连通次数 = 可以删除的最大边数
return edges.length - joinCount;
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.12 839. 相似字符串组
如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等,那么称 X 和 Y 两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。
例如,“tars” 和 “rats” 是相似的 (交换 0 与 2 的位置); “rats” 和 “arts” 也是相似的,但是 “star” 不与 “tars”,“rats”,或 “arts” 相似。
总之,它们通过相似性形成了两个关联组:{“tars”, “rats”, “arts”} 和 {“star”}。注意,“tars” 和 “arts” 是在同一组中,即使它们并不相似。形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。
给你一个字符串列表 strs。列表中的每个字符串都是 strs 中其它所有字符串的一个字母异位词。请问 strs 中有多少个相似字符串组?
1 <= strs.length <= 300
1 <= strs[i].length <= 300
strs[i] 只包含小写字母。
strs 中的所有单词都具有相同的长度,且是彼此的字母异位词。
class Solution {
public int numSimilarGroups(String[] strs) {
if (strs.length == 1) return 1;
int n = strs.length;
int m = strs[0].length();
DisjointSetUnion dis = new DisjointSetUnion(n);
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (check(strs[i],strs[j],m)) {// 如果字符串相似进行连通
dis.unionSet(i,j);
}
}
}
//连通分量的个数就是相似字符串组的个数
int ans = 0;
for (int i = 0; i < n; ++i) {
if (dis.f[i] == i) {
++ans;
}
}
return ans;
}
//检查是否具有相似性
private boolean check(String a, String b, int len) {
int count = 0;
for (int i = 0; i < len; ++i) {
if (a.charAt(i) != b.charAt(i)) {
++count;
if (count > 2) return false;
}
}
return true;
}
}
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.f = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return false;
if(rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}