friend-circles

题目链接:friend-circles

题目描述

班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

题目分析

  1. 要求返回的是朋友圈总数,即不相连的朋友圈数量,典型深搜和并查集题目。

算法

深度优先遍历(Depth-first-Search)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int findCircleNum(int[][] M) {
int result = 0;
for (int row = 0; row < M.length; row++) {
// 遍历每个学生
result += dfs(M, row);
}
return result;
}

public int dfs(int[][] M, int row) {
if (M[row][row] == 0) return 0; // 该学生已经被深搜遍历过
for (int col = 0; col < M.length; col++) {
if (M[row][col] == 1) {
// row, col表示两个人,他们是朋友,从row遍历到col
M[row][col] = 0;
M[col][row] = 0;
dfs(M, col);
}
}
return 1;
}

}

并查集(Union-Find)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
public int findCircleNum(int[][] M) {
UnionFind uf = new UnionFind(M.length);
for (int row = 0; row < M.length; row++) {
for (int col = 0; col < M.length; col++) {
if (M[row][col] == 1) uf.union(row, col); // 两人是朋友,合并朋友圈
}
}
return uf.count;
}

class UnionFind {
int[] parents;
int count; // 统计parents[id] == id的数量

UnionFind(int n) {
parents = new int[n];
count = n;
for (int i = 0; i < n; i++) {
parents[i] = i;
}
}

public int find(int p) {
while (parents[p] != p) {
parents[p] = parents[parents[p]];
p = parents[p];
}
return p;
}

public void union(int p, int q) {
if (find(p) != find(q)) {
// 两者合并,parents[id] == id的数量减1
count--;
parents[find(p)] = find(q);
}
}
}

}

移除最多的同行或同列石头

题目链接:most-stones-removed-with-same-row-or-column

题目描述

在二维平面上,我们将石头放置在一些整数坐标点上。每个坐标点上最多只能有一块石头。现在,move 操作将会移除与网格上的某一块石头共享一列或一行的一块石头。我们最多能执行多少次 move 操作?

题目分析

  1. 刚开始看到这道题目的时候,一脸蒙蔽,不是很明白最多move操作的意思,然后把示例画成二维图,试了一下,终于明白,这道题目的意思是,让最终每行每列上最多只能有一块石头,最少剩下多少块石头,然后返回原石头数量-剩余石头数量。
  2. 很显然,这道题目可以使用深度优先搜索和并查集的方法。我们可以将每行每列定义为一个component,将有石头相连的component合并成一个(并查集的方法),最后在统计有多少个独立的component,即剩下的石头数量;也可以对每块石头所在的component执行一个深度优先遍历,最后统计有从入口执行了多少次,即剩下的石头数量。

算法

深度优先遍历(Depth-first-Search,time: O(N2),memory:O(N)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.util.*;

class Solution {

public int removeStones(int[][] stones) {
int N = stones.length;

// 这里无所谓行和列的概念,我们将行和列都当做一个component,
// graph[i][0]记录了第i块石头的邻居数量。
int[][] graph = new int[N][N];
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
// 两块石头在同一行或同一列,将他们记录为邻居
if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1]) {
graph[i][++graph[i][0]] = j;
graph[j][++graph[j][0]] = i;
}
}
}


boolean[] seen = new boolean[N]; // 深搜遍历过的标志,避免死循环
int ans = 0;
for (int i = 0; i < N; i++) {
if (!seen[i]) {
seen[i] = true;
Stack<Integer> stack = new Stack<>();
ans--;
stack.push(i);
while (!stack.isEmpty()) {
int cur = stack.pop();
ans++;
// 将该块石头的所有邻居推入栈
for (int j = 1; j <= graph[cur][0]; j++) {
if (!seen[graph[cur][j]]) {
seen[graph[cur][j]] = true;
stack.push(graph[cur][j]);
}
}
}
}
}
return ans;
}

}

并查集(Union-Find,time: O(NlogN),memory:O(N)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.*;

class Solution {

public int removeStones(int[][] stones) {
int N = stones.length;
DSU dsu = new DSU(20000);
// 将这块石头所在行和列合并成一个component
// 最终,能合并在一起的石头会拥有同样的parent
for (int i = 0; i < N; i++) dsu.union(stones[i][0], stones[i][1] + 10000);

HashSet<Integer> seen = new HashSet<>();
// 遍历所有的石头,找出它的父节点,即最后剩余的最少石头数量。
for (int i = 0; i < N; i++) seen.add(dsu.find(stones[i][0]));
return N - seen.size();
}

class DSU {
int[] parent;

public DSU(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
}

public int find(int p) {
if (parent[p] != p) parent[p] = find(parent[p]);
return parent[p];
}

public void union(int p, int q) {
parent[find(p)] = find(q);
}
}

}

由斜杠划分区域

题目链接:regions-cut-by-slashes

题目描述

在由 1 x 1 方格组成的 N x N 网格 grid 中,每个 1 x 1 方块由 /、\ 或空格构成。这些字符会将方块划分为一些共边的区域。(请注意,反斜杠字符是转义的,因此 \ 用 “\“ 表示。)。返回区域的数目。

题目分析

  1. 该题目用用图来表示的话,一目了然。可是表示成了字符串之后,就感觉有点不知从何处下手。
  2. 想办法将图形表示转化成直观的数组表示法。

算法

并查集(Union Find)

由于每个小区域只填充’/‘或者’',因此,小区域划分最多有三种情况(编号如上图所示):

  1. 编号0和编号1在同一个块,编号2和编号3在同一个块。
  2. 编号0和编号2在同一个块,编号1和编号3在同一个块。
  3. 编号0, 1, 2, 3都在同一个块。

相邻区域间,他们的编号小区域存在一定会在同一个块的情况。其中:

  1. 编号0和上一个区域编号3一定在同一个块。
  2. 编号1和左边区域的编号2一定在同一个块。
  3. 编号2和右边区域的编号1一定在同一个块。
  4. 编号3和下边区域的编号0一定在同一个块。

我们开始给每个小区域编一个唯一的块号,通过遍历区域,达到数出全部唯一块的数目。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class Solution {
private int[] grids; // 用数组来表示块编号
private int num; // 统计唯一块的数目

public int regionsBySlashes(String[] grid) {
int N = grid.length;
buildUnionFind(N);

// 遍历合并所有的区域
for (int r = 0; r < grid.length; r++)
for (int c = 0; c < grid[r].length(); c++)
process(r, c, N, grid[r].charAt(c));
return num;
}

// 构建并查集,初始每个块有一个唯一编号
private void buildUnionFind(int N) {
grids = new int[N * N * 4];
num = N * N * 4;
for (int i = 0; i < grids.length; i++) grids[i] = i;
}

// 根据行和列的序号,得到块在数组中的位置
private int getSubSquare(int r, int c, int N, int ith) {
return r * N * 4 + c * 4 + ith;
}

// 寻找块的根编号
private int find(int p) {
while (grids[p] != p) {
grids[p] = grids[grids[p]];
p = grids[p];
}
return p;
}

// 合并两个块
private void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);

if (rootP != rootQ) num--; // 如果两个块拥有不同的编号,合并后,唯一块的数目减1
grids[rootP] = rootQ;
}


private void process(int r, int c, int N, char square) {
// 获取区域中四个小区域的块编号
int s0 = getSubSquare(r, c, N, 0);
int s1 = getSubSquare(r, c, N, 1);
int s2 = getSubSquare(r, c, N, 2);
int s3 = getSubSquare(r, c, N, 3);

// 同一个区域的小区域合并
switch(square) {
case '\\':
union(s0, s2);
union(s1, s3);
break;
case '/':
union(s0, s1);
union(s2, s3);
break;
default:
union(s0, s1);
union(s1, s2);
union(s2, s3);
}

// 相邻区域的合并
if (r > 0) {
union(s0, getSubSquare(r - 1, c, N, 3));
}
if (r < N - 1) {
union(s3, getSubSquare(r + 1, c, N, 0));
}
if (c > 0) {
union(s1, getSubSquare(r, c - 1, N, 2));
}
if (c < N - 1) {
union(s2, getSubSquare(r, c + 1, N, 1));
}
}
}

深度优先搜索(Depth first Search)

我们很难从原图中,将该图转化成一个用数字表示的数组。但是,当我们将原图表示成一个规模*2的数组时,他将可以表示如下:

1
2
3
4
                        0 1 0 1
["//", "/ "] => 1 0 1 0
0 1 0 0
1 0 0 0

我们将数字1当做一道墙,数字0当做一个空白块,则没有墙相隔的块,他们属于同一个唯一块。从上数字中,我们可以直观的看出,一共存在3个唯一块。但是这时,属于同一个唯一空白块的小空白块他们可能还不相邻(即在斜对角线上)。我们再把原图表示成一个规模*3数组,表示如下:

1
2
3
4
5
6
                        0 0 1 0 0 1
0 1 0 0 1 0
["//", "/ "] => 1 0 0 1 0 0
0 0 1 0 0 0
0 1 0 0 0 0
1 0 0 0 0 0

这时,可以清楚的看出,一共存在3个唯一空白块,且在同一个唯一空白块的所有小空白块,都是相邻的了。这个时候,我们就可以采用深度优先搜索,从数组的边界开始,遍历存在多少个不相邻的0,即可得到答案。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Solution {
int[][] grids; // 表示原图的数组

public int regionsBySlashes(String[] grid) {
buildGrids(grid);
int count = 0;
// 深搜遍历
for (int r = 0; r < grids.length; r++) {
for (int c = 0; c < grids[r].length; c++) {
count += dfs(r, c);
}
}
return count;
}

// 将原图表示成规模*3的数组
private void buildGrids(String[] grid) {
int N = grid.length;
grids = new int[N * 3][N * 3];
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length(); j++) {
if (grid[i].charAt(j) == '/') {
grids[i * 3 + 2][j * 3] = 1;
grids[i * 3 + 1][j * 3 + 1] = 1;
grids[i * 3][j * 3 + 2] = 1;
} else if (grid[i].charAt(j) == '\\') {
grids[i * 3][j * 3] = 1;
grids[i * 3 + 1][j * 3 + 1] = 1;
grids[i * 3 + 2][j * 3 + 2] = 1;
}
}
}
}

private int dfs(int r, int c) {
// 越界返回0
if (r < 0 || r >= grids.length || c < 0 || c >= grids[r].length) return 0;
// 遇到墙(数字1)返回0
if (grids[r][c] == 1) return 0;

// 将空白块改为1,并向他四周遍历,结果返回1
grids[r][c] = 1;
dfs(r - 1, c);
dfs(r + 1, c);
dfs(r, c - 1);
dfs(r, c + 1);
return 1;
}
}
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×