原文链接: https://leetcode-cn.com/problems/cut-off-trees-for-golf-event
英文原文
You are asked to cut off all the trees in a forest for a golf event. The forest is represented as an m x n
matrix. In this matrix:
0
means the cell cannot be walked through.1
represents an empty cell that can be walked through.- A number greater than
1
represents a tree in a cell that can be walked through, and this number is the tree's height.
In one step, you can walk in any of the four directions: north, east, south, and west. If you are standing in a cell with a tree, you can choose whether to cut it off.
You must cut off the trees in order from shortest to tallest. When you cut off a tree, the value at its cell becomes 1
(an empty cell).
Starting from the point (0, 0)
, return the minimum steps you need to walk to cut off all the trees. If you cannot cut off all the trees, return -1
.
You are guaranteed that no two trees have the same height, and there is at least one tree needs to be cut off.
Example 1:
Input: forest = [[1,2,3],[0,0,4],[7,6,5]] Output: 6 Explanation: Following the path above allows you to cut off the trees from shortest to tallest in 6 steps.
Example 2:
Input: forest = [[1,2,3],[0,0,0],[7,6,5]] Output: -1 Explanation: The trees in the bottom row cannot be accessed as the middle row is blocked.
Example 3:
Input: forest = [[2,3,4],[0,0,5],[8,7,6]] Output: 6 Explanation: You can follow the same path as Example 1 to cut off all the trees. Note that you can cut off the first tree at (0, 0) before making any steps.
Constraints:
m == forest.length
n == forest[i].length
1 <= m, n <= 50
0 <= forest[i][j] <= 109
中文题目
你被请来给一个要举办高尔夫比赛的树林砍树。树林由一个 m x n
的矩阵表示, 在这个矩阵中:
0
表示障碍,无法触碰1
表示地面,可以行走比 1 大的数
表示有树的单元格,可以行走,数值表示树的高度
每一步,你都可以向上、下、左、右四个方向之一移动一个单位,如果你站的地方有一棵树,那么你可以决定是否要砍倒它。
你需要按照树的高度从低向高砍掉所有的树,每砍过一颗树,该单元格的值变为 1
(即变为地面)。
你将从 (0, 0)
点开始工作,返回你砍完所有树需要走的最小步数。 如果你无法砍完所有的树,返回 -1
。
可以保证的是,没有两棵树的高度是相同的,并且你至少需要砍倒一棵树。
示例 1:
输入:forest = [[1,2,3],[0,0,4],[7,6,5]] 输出:6 解释:沿着上面的路径,你可以用 6 步,按从最矮到最高的顺序砍掉这些树。
示例 2:
输入:forest = [[1,2,3],[0,0,0],[7,6,5]] 输出:-1 解释:由于中间一行被障碍阻塞,无法访问最下面一行中的树。
示例 3:
输入:forest = [[2,3,4],[0,0,5],[8,7,6]] 输出:6 解释:可以按与示例 1 相同的路径来砍掉所有的树。 (0,0) 位置的树,可以直接砍去,不用算步数。
提示:
m == forest.length
n == forest[i].length
1 <= m, n <= 50
0 <= forest[i][j] <= 109
通过代码
官方题解
解决方法:
框架:
- 从
(0, 0)
开始,对于每棵树,按照高度顺序,我们将计算我们到下一棵树(并移动到那里)的距离,并将该距离添加到答案中。 - 我们定义距离函数
dist(forest, sr, sc, tr, tc)
,该函数计算从源(sr, sc)
到目标(tr, tc)
通过障碍物dist[i][j]==0
的路径距离。(如果路径不可能,此距离函数将返回-1
)。 - 接下来是代码和复杂性分析,这三种方法都很常见。之后,我们的方法中给出的算法将只关注提供
dist
函数。
class Solution(object):
def cutOffTree(self, forest):
trees = sorted((v, r, c) for r, row in enumerate(forest)
for c, v in enumerate(row) if v > 1)
sr = sc = ans = 0
for _, tr, tc in trees:
d = dist(forest, sr, sc, tr, tc)
if d < 0: return -1
ans += d
sr, sc = tr, tc
return ans
class Solution {
int[] dr = {-1, 1, 0, 0};
int[] dc = {0, 0, -1, 1};
public int cutOffTree(List<List<Integer>> forest) {
List<int[]> trees = new ArrayList();
for (int r = 0; r < forest.size(); ++r) {
for (int c = 0; c < forest.get(0).size(); ++c) {
int v = forest.get(r).get(c);
if (v > 1) trees.add(new int[]{v, r, c});
}
}
Collections.sort(trees, (a, b) -> Integer.compare(a[0], b[0]));
int ans = 0, sr = 0, sc = 0;
for (int[] tree: trees) {
int d = dist(forest, sr, sc, tree[1], tree[2]);
if (d < 0) return -1;
ans += d;
sr = tree[1]; sc = tree[2];
}
return ans;
}
}
复杂度分析
这三种算法都具有相似的最坏情况复杂度.
- 时间复杂度:$O((RC)^2)$,在给定的
forest
中有 $R$ 行和 $C$ 列。我们步行到 $RC$ 树,每一次步行都可以花费 $O(RC)$ 时间搜索树。 - 空间复杂度:$O(R*C)$,所用数据结构的最大大小。
方法一:宽度优先搜索(BFS)
算法:
- 我们执行宽度优先搜索,处理队列中的节点(网格位置)。
seen
跟踪在某个时间点已经添加到队列中的节点,这些节点已被处理或在等待处理的队列中。 - 对于下一个要处理的每个节点,我们查看它的周围。如果他们在森林(网格)中,他们没有排队,而且他们不是障碍,我们将把那个相邻节点排队。
- 我们还对每个节点的行驶距离进行计数。如果我们正在处理的节点是我们的 “目标”
(tr, tc)
,我们将返回答案。
def bfs(forest, sr, sc, tr, tc):
R, C = len(forest), len(forest[0])
queue = collections.deque([(sr, sc, 0)])
seen = {(sr, sc)}
while queue:
r, c, d = queue.popleft()
if r == tr and c == tc:
return d
for nr, nc in ((r-1, c), (r+1, c), (r, c-1), (r, c+1)):
if (0 <= nr < R and 0 <= nc < C and
(nr, nc) not in seen and forest[nr][nc]):
seen.add((nr, nc))
queue.append((nr, nc, d+1))
return -1
public int bfs(List<List<Integer>> forest, int sr, int sc, int tr, int tc) {
int R = forest.size(), C = forest.get(0).size();
Queue<int[]> queue = new LinkedList();
queue.add(new int[]{sr, sc, 0});
boolean[][] seen = new boolean[R][C];
seen[sr][sc] = true;
while (!queue.isEmpty()) {
int[] cur = queue.poll();
if (cur[0] == tr && cur[1] == tc) return cur[2];
for (int di = 0; di < 4; ++di) {
int r = cur[0] + dr[di];
int c = cur[1] + dc[di];
if (0 <= r && r < R && 0 <= c && c < C &&
!seen[r][c] && forest.get(r).get(c) > 0) {
seen[r][c] = true;
queue.add(new int[]{r, c, cur[2]+1});
}
}
}
return -1;
}
方法二:A*搜索
算法:
- A*算法是另一种路径查找算法。对于位置
(r, c)
的每个节点,我们使node.f = node.g + node.h
,其中node.g
是从(sr, sc)
到(r, c)
的实际距离,node.h
是从(r, c)
到(tr, tc)
的距离的启发式(猜测)。在这种情况下,我们的猜测将是node.h = abs(r-tr) + abs(c-tc)
。 - 我们保留一个优先队列来决定下一个搜索(扩展)的节点。我们可以证明,如果我们找到目标节点,我们一定走了尽可能短的距离
node.g
。例如,通过考虑最后一次两条反向路径是相同的,在不失去一般性的情况下,我们可以假设两条路径的倒数第二个方格是不同的,然后在这种情况下,没有node.f = node.g + 1
,根据需要首先扩展显示实际行驶距离较小的路径。 - 对于熟悉
Dijkstra
算法的人来说,知道一个 A*搜索是Dijkstra
的一个特例,且node.h
总是 0。
def astar(forest, sr, sc, tr, tc):
R, C = len(forest), len(forest[0])
heap = [(0, 0, sr, sc)]
cost = {(sr, sc): 0}
while heap:
f, g, r, c = heapq.heappop(heap)
if r == tr and c == tc: return g
for nr, nc in ((r-1,c), (r+1,c), (r,c-1), (r,c+1)):
if 0 <= nr < R and 0 <= nc < C and forest[nr][nc]:
ncost = g + 1 + abs(nr - tr) + abs(nc - tc)
if ncost < cost.get((nr, nc), 9999):
cost[nr, nc] = ncost
heapq.heappush(heap, (ncost, g+1, nr, nc))
return -1
public int cutOffTree(List<List<Integer>> forest, int sr, int sc, int tr, int tc) {
int R = forest.size(), C = forest.get(0).size();
PriorityQueue<int[]> heap = new PriorityQueue<int[]>(
(a, b) -> Integer.compare(a[0], b[0]));
heap.offer(new int[]{0, 0, sr, sc});
HashMap<Integer, Integer> cost = new HashMap();
cost.put(sr * C + sc, 0);
while (!heap.isEmpty()) {
int[] cur = heap.poll();
int g = cur[1], r = cur[2], c = cur[3];
if (r == tr && c == tc) return g;
for (int di = 0; di < 4; ++di) {
int nr = r + dr[di], nc = c + dc[di];
if (0 <= nr && nr < R && 0 <= nc && nc < C && forest.get(nr).get(nc) > 0) {
int ncost = g + 1 + Math.abs(nr-tr) + Math.abs(nc-tr);
if (ncost < cost.getOrDefault(nr * C + nc, 9999)) {
cost.put(nr * C + nc, ncost);
heap.offer(new int[]{ncost, g+1, nr, nc});
}
}
}
}
return -1;
}
方法三:Hadlock 算法
没有任何障碍物,从 source = (sr, sc)
到 target = (tr, tc)
的距离就是 taxi(source, target) = abs(sr-tr) + abs(sc-tc)
。这表示必须行驶的最小距离。每当我们离开目标时,我们都会将这个最小值增加2,因为我们一步一个移动,再加上 taxicab 离我们新位置的距离增加了 1。
我们称 detour
为一次绕开目标的移动,可以证明,从源到目标的距离仅为 taxi(source, target) + 2 * detours
,其中,从 source
到 target
的任何路径中,detours
的数量最小。
算法:
- 对于一个
source
和target
,称一个方格的迂回数为从source
到该方格的任何路径中可能出现的最小迂回数。(这里,迂回道是针对target
定义的——距离目标的步数。) - 我们将按照
detour
编号的顺序执行优先级优先搜索。如果找到目标,则找到最低的detour
,因此相应距离最低。同时使用processed
跟踪节点,防止节点被访问两次。 - 由于每个相邻节点只能有相同的
detour
编号或更高的detour
编号,因此一次最多只能考虑两个优先级。因此,我们可以使用deque
(双端队列)来执行此实现。我们将首先放置具有相同要展开的detour
编号的节点,在完成所有具有当前编号的节点之后,将展开具有更高迂回道编号的节点。
def hadlocks(forest, sr, sc, tr, tc):
R, C = len(forest), len(forest[0])
processed = set()
deque = collections.deque([(0, sr, sc)])
while deque:
detours, r, c = deque.popleft()
if (r, c) not in processed:
processed.add((r, c))
if r == tr and c == tc:
return abs(sr-tr) + abs(sc-tc) + 2*detours
for nr, nc, closer in ((r-1, c, r > tr), (r+1, c, r < tr),
(r, c-1, c > tc), (r, c+1, c < tc)):
if 0 <= nr < R and 0 <= nc < C and forest[nr][nc]:
if closer:
deque.appendleft((detours, nr, nc))
else:
deque.append((detours+1, nr, nc))
return -1
public int hadlocks(List<List<Integer>> forest, int sr, int sc, int tr, int tc) {
int R = forest.size(), C = forest.get(0).size();
Set<Integer> processed = new HashSet();
Deque<int[]> deque = new ArrayDeque();
deque.offerFirst(new int[]{0, sr, sc});
while (!deque.isEmpty()) {
int[] cur = deque.pollFirst();
int detours = cur[0], r = cur[1], c = cur[2];
if (!processed.contains(r*C + c)) {
processed.add(r*C + c);
if (r == tr && c == tc) {
return Math.abs(sr-tr) + Math.abs(sc-tc) + 2 * detours;
}
for (int di = 0; di < 4; ++di) {
int nr = r + dr[di];
int nc = c + dc[di];
boolean closer;
if (di <= 1) closer = di == 0 ? r > tr : r < tr;
else closer = di == 2 ? c > tc : c < tc;
if (0 <= nr && nr < R && 0 <= nc && nc < C && forest.get(nr).get(nc) > 0) {
if (closer) deque.offerFirst(new int[]{detours, nr, nc});
else deque.offerLast(new int[]{detours+1, nr, nc});
}
}
}
}
return -1;
}
统计信息
通过次数 | 提交次数 | AC比率 |
---|---|---|
3055 | 7619 | 40.1% |
提交历史
提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
---|