最小体力消耗路径
题目表述
你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。
示例 1

输入:heights = [[1,2,2],[3,8,2],[5,3,5]]
输出:2
解释:路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。
这条路径比路径 [1,2,2,2,5] 更优,因为另一条路径差值最大值为 3 。
示例 2

输入:heights = [[1,2,3],[3,8,4],[5,3,5]]
输出:1
解释:路径 [1,2,3,4,5] 的相邻格子差值绝对值最大为 1 ,比路径 [1,3,5,3,5] 更优。
示例 3

输入:heights = [[1,2,1,1,1],[1,2,1,2,1],[1,2,1,2,1],[1,2,1,2,1],[1,1,1,2,1]]
输出:0
解释:上图所示路径不需要消耗任何体力。
提示
rows == heights.length
columns == heights[i].length
1 <= rows, columns <= 100
1 <= heights[i][j] <= 106
解法一:并查集
可以把二维表看成是一张无环图,边的节点差值的绝对值是无环图边的权重。
将无环图的边按权重排序,依次加入并查集直到左上角跟右下角连通即可。
时间复杂度:O(mnlog(mn)) 空间复杂度:O(mn)
代码实现
from typing import List
class UnionFind:
    """并查集"""
    def __init__(self, n:int):
        self.parent = list(range(n))
        self.size = [1] * n
    
    def findSet(self, x: int)->int:
        """查找元素的根元素,同时找到后将元素连接到根上"""
        if self.parent[x] == x:
            return x
        self.parent[x] = self.findSet(self.parent[x])
        return self.parent[x]
    
    def unite(self, x, y)->bool:
        """将两个集合合并"""
        # 已经在同一个集合中,无需合并
        if self.parent[x] == self.parent[y]:
            return False
        # 查找根元素
        x = self.findSet(x)
        y = self.findSet(y)
        # 将较小的树加入较大者(保证查找效率)
        if self.size[x] < self.size[y]:
            x, y = y, x
        self.parent[y] = x
        self.size[x] += self.size[y]
        return True
    def connected(self, x, y)->bool:
        """两个节点是否连通"""
        return self.findSet(x) == self.findSet(y)
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        m, n = len(heights), len(heights[0])
        edges = []
        for i in range(m):
            for j in range(n):
                idx = i * n + j
                if i > 0: # 垂直边
                    edges.append([idx-n, idx, abs(heights[i][j]-heights[i-1][j])])
                if j > 0: # 水平边
                    edges.append([idx-1, idx, abs(heights[i][j]-heights[i][j-1])])
        # 将边按照权重排序
        edges.sort(key=lambda x:x[2])
        # 并查集搜索,只需要找到连通的路径即可
        uf = UnionFind(m*n)
        res = 0
        for x, y, v in edges:
            uf.unite(x, y)
            if uf.connected(0, m*n-1):
                res = v
                break
        return res
package leetcode
import "sort"
type UnionFind struct {
	Parent []int
	Size   []int
}
// findSet 查找集合根节点
func (uf *UnionFind) findSet(x int) int {
	if x == uf.Parent[x] {
		return x
	}
	uf.Parent[x] = uf.findSet(uf.Parent[x])
	return uf.Parent[x]
}
// unite 合并集合
func (uf *UnionFind) unite(x, y int) bool {
	if uf.Parent[x] == uf.Parent[y] {
		return false
	}
	x = uf.findSet(x)
	y = uf.findSet(y)
	if uf.Size[x] < uf.Size[y] {
		x, y = y, x
	}
	uf.Parent[y] = x
	uf.Size[x] += uf.Size[y]
	return true
}
// connected x, y 是否连通
func (uf *UnionFind) connected(x, y int) bool {
	return uf.findSet(x) == uf.findSet(y)
}
type Edge struct {
	x int
	y int
	v int
}
func makeRange(n int) []int {
	arr := make([]int, n)
	for i := 0; i < n; i++ {
		arr[i] = i
	}
	return arr
}
func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}
func minimumEffortPath(heights [][]int) int {
	m, n := len(heights), len(heights[0])
	edges := []Edge{}
	for i := 0; i < m; i++ {
		for j := 0; j < n; j++ {
			idx := i*n + j
			if i > 0 {
				edges = append(edges, Edge{idx - n, idx, abs(heights[i][j] - heights[i-1][j])})
			}
			if j > 0 {
				edges = append(edges, Edge{idx - 1, idx, abs(heights[i][j] - heights[i][j-1])})
			}
		}
	}
	sort.Slice(edges, func(i, j int) bool { return edges[i].v < edges[j].v })
	uf := UnionFind{makeRange(m * n), makeRange(m * n)}
	for _, edge := range edges {
		uf.unite(edge.x, edge.y)
		if uf.connected(0, m*n-1) {
			return edge.v
		}
	}
	return 0
}