题目地址:
https://www.lintcode.com/problem/path-with-maximum-minimum-value/description
给定一个 m × n m\times n m×n的二维矩阵 A A A,找到从 A [ 0 ] [ 0 ] A[0][0] A[0][0]到 A [ m − 1 ] [ n − 1 ] A[m-1][n-1] A[m−1][n−1]的路径,使得路径上的最小值最大。路径每一步可以走四个方向,但同一个格子不能重复走。返回该路径的最小值。
一个比较好理解也比较好写的方法是用并查集来做。思路参考https://blog.csdn.net/qq_46105170/article/details/109713256。思路非常像Kruskal最小生成树算法。代码如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
class UnionFind {
private int[] parent;
public UnionFind(int size) {
parent = new int[size];
Arrays.fill(parent, -1);
}
public void insert(int x) {
parent[x] = x;
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y) {
int px = find(x), py = find(y);
if (px == py) {
return;
}
parent[px] = py;
}
public boolean exists(int x) {
return parent[x] != -1;
}
}
class Pair {
int x, y, val;
public Pair(int x, int y, int val) {
this.x = x;
this.y = y;
this.val = val;
}
}
/**
* @param A: a List[List[int]]
* @return: Return the maximum score of a path
*/
public int maximumMinimumPath(int[][] A) {
// Write your code here
int m = A.length, n = A[0].length;
UnionFind uf = new UnionFind(m * n);
List<Pair> list = new ArrayList<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
list.add(new Pair(i, j, A[i][j]));
}
}
int start = 0, end = m * n - 1;
int[] d = {1, 0, -1, 0, 1};
// 按权值从大到小排序
list.sort((p1, p2) -> -Integer.compare(p1.val, p2.val));
for (int i = 0; i < list.size(); i++) {
Pair pair = list.get(i);
int x = pair.x, y = pair.y;
uf.insert(transfer(x, y, n));
for (int j = 0; j < 4; j++) {
int nextX = x + d[j], nextY = y + d[j + 1];
if (0 <= nextX && nextX < m && 0 <= nextY && nextY < n && uf.exists(transfer(nextX, nextY, n))) {
uf.union(transfer(x, y, n), transfer(nextX, nextY, n));
}
}
if (uf.exists(start) && uf.exists(end) && uf.find(start) == uf.find(end)) {
return pair.val;
}
}
return -1;
}
private int transfer(int x, int y, int n) {
return x * n + y;
}
}
时间复杂度 O ( m n log ( m n ) ) O(mn\log (mn)) O(mnlog(mn)),空间 O ( m n ) O(mn) O(mn)。
上面的做法是逐个添加点,然后慢慢让起点和终点连通,一旦连通了,那么添加的那个点的值即为所求。但这样做似乎并不是Kruskal算法,说服力不够(事实上尽管与Kruskal算法看上去不像,但是也可以像证明Kruskal算法的方法去证明这个方法是对的)。下面介绍逐个添加边的办法。考虑每条边 e e e的权重应该是多少,显然如果要走 e e e,那么路径的最小值必然会小于等于连接 e e e的两个顶点的点权较小值,所以我们规定如果 e = ( x , y ) e=(x,y) e=(x,y)则 e = max { x , y } e=\max\{x,y\} e=max{x,y}。这样就可以将整个矩阵每个entry看成点,相邻entry看成边,整个矩阵看成一张图,然后做Kruskal算法(边权从大到小排序)。代码如下:
import java.util.ArrayList;
import java.util.List;
public class Solution {
class Edge {
int x, y, len;
public Edge(int x, int y, int len) {
this.x = x;
this.y = y;
this.len = len;
}
}
class UnionFind {
int[] parent;
public UnionFind(int size) {
parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y) {
int px = find(x), py = find(y);
if (px != py) {
parent[px] = py;
}
}
}
/**
* @param A: a List[List[int]]
* @return: Return the maximum score of a path
*/
public int maximumMinimumPath(int[][] A) {
// Write your code here
if (A.length == 1 && A[0].length == 1) {
return A[0][0];
}
List<Edge> list = new ArrayList<>();
for (int i = 0; i < A.length; i++) {
for (int j = 0; j < A[0].length; j++) {
if (j + 1 < A[0].length) {
list.add(new Edge(transform(i, j, A), transform(i, j + 1, A), Math.min(A[i][j], A[i][j + 1])));
}
if (i + 1 < A.length) {
list.add(new Edge(transform(i, j, A), transform(i + 1, j, A), Math.min(A[i][j], A[i + 1][j])));
}
}
}
// 按边权从大到小排序
list.sort((e1, e2) -> -Integer.compare(e1.len, e2.len));
UnionFind uf = new UnionFind(A.length * A[0].length);
for (Edge e : list) {
uf.union(e.x, e.y);
if (uf.find(0) == uf.find(transform(A.length - 1, A[0].length - 1, A))) {
return e.len;
}
}
return -1;
}
private int transform(int x, int y, int[][] A) {
return x * A[0].length + y;
}
}
时空复杂度一样。