1970. 你能穿过矩阵的最后一天

1970. 你能穿过矩阵的最后一天

1970. 你能穿过矩阵的最后一天 (Hard)

给你一个下标从 1 开始的二进制矩阵,其中 0 表示陆地, 1 表示水域。同时给你 rowcol 分别表示矩阵中行和列的数目。

一开始在第 0 天, 整个 矩阵都是 陆地 。但每一天都会有一块新陆地被 淹没变成水域。给你一个下标从 1 开始的二维数组 cells ,其中 cells[i] = [rᵢ, cᵢ] 表示在第 i 天,第 rᵢcᵢ 列(下标都是从 1 开始)的陆地会变成 水域 (也就是 0 变成 1 )。

你想知道从矩阵最 上面 一行走到最 下面 一行,且只经过陆地格子的 最后一天 是哪一天。你可以从最上面一行的 任意 格子出发,到达最下面一行的 任意 格子。你只能沿着 四个 基本方向移动(也就是上下左右)。

请返回只经过陆地格子能从最 上面 一行走到最 下面 一行的 最后一天

示例 1:

输入:row = 2, col = 2, cells = [[1,1],[2,1],[1,2],[2,2]]
输出:2
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 2 天。

示例 2:

输入:row = 2, col = 2, cells = [[1,1],[1,2],[2,1],[2,2]]
输出:1
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 1 天。

示例 3:

输入:row = 3, col = 3, cells = [[1,2],[2,1],[3,3],[2,2],[1,1],[1,3],[2,3],[3,2],[3,1]]
输出:3
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 3 天。

提示:

  • 2 <= row, col <= 2 * 10⁴
  • 4 <= row * col <= 2 * 10⁴
  • cells.length == row * col
  • 1 <= rᵢ <= row
  • 1 <= cᵢ <= col
  • cells 中的所有格子坐标都是 唯一 的。

分析

没有较好的贪心策略,必须用一个 row*col 的数组来模拟。

二分

最朴素的做法:遍历 cells,每次将对应的位置置为 1,然后用 BFS 或 DFS 来检查第一行和最后一行是否联通。

有单调性,可以改进成二分 + BFS/DFS,这样时间复杂度为 O(SlogS),其中 S == row*col 即格子总数, 空间复杂度相同。

代码略。

并查集

遍历 cells,只需要判断当前情况下第一列和最后一列是否联通,这可以用并查集, 注意添加两个虚拟节点。

并查集各种操作的复杂度可以看做常数级,总体复杂度为 O(S),空间复杂度同样为 O(S)。

func latestDayToCross(row int, col int, cells [][]int) int {
	grid := make([][]int, row)
	for i := range grid {
		grid[i] = make([]int, col)
	}
	total := row * col
	initUF(total + 2)
	left := total
	right := total + 1
	getID := func(r, c int) int {
		return r*col + c
	}
	for r := 0; r < row; r++ {
		id := getID(r, 0)
		union(id, left)
		id = getID(r, col-1)
		union(id, right)
	}
	for i, cell := range cells {
		r, c := cell[0]-1, cell[1]-1
		grid[r][c] = 1
		// 联通8个方向上的相邻1
		for dr := -1; dr <= 1; dr++ {
			for dc := -1; dc <= 1; dc++ {
				if dr == 0 && dc == 0 {
					continue
				}
				nr, nc := r+dr, c+dc
				if nr < 0 || nr >= row || nc < 0 || nc >= col || grid[nr][nc] == 0 {
					continue
				}
				union(getID(r, c), getID(nr, nc))
				if find(left) == find(right) {
					return i
				}
			}
		}
	}
	return len(cells)
}

var uf []int

func initUF(n int) {
	uf = make([]int, n)
	for i := range uf {
		uf[i] = i
	}
}
func find(x int) int {
	for x != uf[x] {
		x, uf[x] = uf[x], uf[uf[x]]
	}
	return x
}
func union(x, y int) {
	x, y = find(x), find(y)
	uf[x] = y
}

测试用例:


func Test_latestDayToCross(t *testing.T) {
	type args struct {
		row   int
		col   int
		cells [][]int
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		{args: args{6, 2, [][]int{{2, 1}, {4, 2}, {6, 2}, {4, 1}, {1, 2}}}, want: 3},
	}
	for _, tt := range tests {
		if got := latestDayToCross(tt.args.row, tt.args.col, tt.args.cells); got != tt.want {
			t.Errorf("%q. latestDayToCross() = %v, want %v", tt.name, got, tt.want)
		}
	}
}

逆向并查集

如果不考虑最左最右两列的连通性,而是考虑最上最下两行的连通性,需要逆向思考,参考:打砖块,代码略。