网格游戏 图解题解
这道题到底在问什么
- 输入
- grid=[[3,1,2,5],[4,2,1,2]]
- 输出
- 6
- 输入
- grid=[[2,5,4],[1,5,1]]
- 输出
- 4
先想最直接的笨办法
回顾整个思路:机器人一的路径就是选一列往下拐,拐完给机器人二留下上排右段和下排左段两块残料,机器人二只能二选一拿较大的。我们逐列枚举拐点,每列算出 max(上排后缀, 下排前缀),再取所有列里最小的那个,就是机器人二在双方都最优时拿到的点数,这张图上是 6。真正写代码时,前缀后缀都能一边扫一边增减维护,不用每列重新求和。(动画第 24 步)
最优解:为什么这么做
一句话答案:LeetCode 2017 网格游戏:2 行网格里机器人一先走清零、机器人二捡剩下,路径形状就是选一列下拐,枚举拐点用前缀和后缀和算 max(上排后缀, 下排前缀) 取最小,时间 O(n)、空间 O(1)。
两行网格里的机器人博弈,到底在求什么
grid 是 2 行 n 列,每格写着点数。两个机器人先后从左上角 (0,0) 走到右下角 (1,n-1),每步只能向右或向下。机器人一先走,收走沿途点数、把格子清零;机器人二再走,收走剩下的。机器人一想让机器人二拿得尽量少,机器人二想让自己拿得尽量多,两边都最优,问机器人二拿多少。题面上排是 3、1、2、5,下排是 4、2、1、2,答案 6。
两边都要最优,是不是得把走法两两配对推演
两个机器人各自求最优,乍看像要把机器人一走法和机器人二应对两两摆到一起推演,组合一多就压不住。路径其实没几种:只能向右或向下,任何一条从 (0,0) 到 (1,n-1) 的路都长一个样——上排往右走到某一列,在那列往下拐进下排,再沿下排走到底。机器人一真正能决定的,只有在哪一列下拐,一共就 n 种选择。
拐点一定下来,机器人二只够得着两块残料
机器人一沿某列下拐、清零整条路后,没被碰过的只剩两块:上排拐点右边的一段,和下排拐点左边的一段。机器人二同样只能向右向下走,一旦下拐就回不到上排,这两块没法都要,只能挑点数大的一块。
于是机器人二在这一列能拿到的,就是 max(上排拐点右段之和, 下排拐点左段之和)。前者是从拐点右边加到头的后缀和,后者是从最左加到拐点左边的前缀和——前缀和、后缀和,就是从头、从尾累加起来的跑动总和。机器人一要做的,是在所有拐点里让较大者尽量小,逐列取最小。
用一前一后两个滚动和,一趟就扫完
不必每列都把两段重新加一遍。参考代码里 s1 先存上排整行的和,s2 从 0 起。从左往右枚举拐点列时,先让 s1 减掉上排当前这个数,s1 就变成上排后缀和;此刻 s2 恰好是下排前缀和;拿 max(s1, s2) 更新答案后,再把 s2 加上下排当前这个数,留给下一列。一列只做减、比、加各一次,走完就是 O(n)。
拿题面的 3、1、2、5 和 4、2、1、2 逐列走一遍
上排总和 11 先当作 s1,s2 从 0 起。拐第 0 列:s1 减 3 变 8,s2 还是 0,较大者 8,答案记 8;s2 加 4 变 4。拐第 1 列:s1 减 1 变 7,s2 是 4,较大者 7,答案更新成 7;s2 加 2 变 6。
拐第 2 列:s1 减 2 变 5,s2 是 6,较大者取 6,答案更新成 6;s2 加 1 变 7。拐第 3 列:s1 减 5 变 0,s2 是 7,较大者 7,不比 6 小,答案仍是 6。四列下来,机器人一拐第 2 列把对手压到 6,即最终答案。
别把拐点列算进残料,也别用 int 接总和
复杂度先交代:求一次上排和是 O(n),枚举 n 个拐点每列常数步,时间 O(n),只用几个滚动变量,空间 O(1)。剩下几处提前避开。拐点那一列上下两格都被机器人一收走,上排后缀从它右边一列起、下排前缀到它左边一列止,归错边就多算一格。点数总和能到五十亿、越过 int 上限,C 加加 用 long long、Java 用 long,int 会溢出成负数。n 为 1 时上下都没有残料,机器人二只能拿 0。最容易想歪的,是把机器人二所得当成两块之和——路径不能回头,较大者才是它真拿到的。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这一句:机器人一挑一列往下拐,机器人二就只能在上排右段和下排左段里挑较大的一块拿走。机器人一要做的,是让这个较大值尽量小。下面我们从头把这张图看明白,再逐列试拐点。
- 4看清网格这是我们的舞台,一张 2 行 4 列的网格。上排四个数是 3、1、2、5,下排是 4、2、1、2。两个机器人都从左上角 (0,0) 出发,一路只能向右或向下,最后都要走到右下角 (1,3)。先记住这些点数的位置,尤其上排最右边那个 5 和下排的 4,后面它们是胜负手。
- 5路径 = 上排一段加下排一段先看机器人一能走成什么样。因为只能向右或向下,它的路一定是:上排从头往右走到某一列,在那一列往下拐一格,再沿下排走到底。这里拿拐点在第 1 列举个样子,紫色两格就是往下拐的地方,灰色就是它走过并清零的整条路。换个拐点,灰色路径就跟着变。机器人一要挑的,正是这个拐点列。
- 6两块残料取其大机器人一走完清零后,轮到机器人二。它眼前只剩两块没被清掉的:绿色是上排拐点右边那一段,橙色是下排拐点左边那一段。关键在于,机器人二也只能向右向下走,一旦下拐就回不到上排,所以这两块它没法都拿,只能挑一块。它当然挑点数大的那块。机器人一要做的,就是选个拐点让这较大的一块尽量小。
- 7拐点第 0 列现在试机器人一在第 0 列往下拐。紫色是它下拐的两格,灰色是它走过并清零的整条路:上排从第 0 列到第 0 列,再下排从第 0 列到第 3 列。这些格子机器人二再来时都是 0 了。接下来看它给机器人二留下了哪两块。
- 8上排后缀 = 8先看绿色这块,上排拐点右边、从第 1 列起的一段:1 加 2 加 5,一共 8。机器人二要想拿它,就得在上排一直走到底再下拐。这个数记成 topSuffix,等于 8。
- 9下排前缀 = 0拐点就在第 0 列,下排左边没有格子留下,这块是空的,bottomPrefix 等于 0。机器人二在下排这一侧什么也捡不到。
- 10本列对方拿 8机器人二在两块里挑大的:上排后缀 8 和下排前缀 0,较大的是 8,红色标出的就是它实际拿走的那块。也就是说,机器人一要是拐第 0 列,机器人二会拿到 8。把它和之前的最优比一比,目前机器人一能把对手压到最少 8。
- 11拐点第 1 列现在试机器人一在第 1 列往下拐。紫色是它下拐的两格,灰色是它走过并清零的整条路:上排从第 0 列到第 1 列,再下排从第 1 列到第 3 列。这些格子机器人二再来时都是 0 了。接下来看它给机器人二留下了哪两块。
- 12上排后缀 = 7先看绿色这块,上排拐点右边、从第 2 列起的一段:2 加 5,一共 7。机器人二要想拿它,就得在上排一直走到底再下拐。这个数记成 topSuffix,等于 7。
- 13下排前缀 = 4再看橙色这块,下排拐点左边、前 1 列的一段:4,一共 4。机器人二要想拿它,就得一开始就下拐、沿下排走。这个数记成 bottomPrefix,等于 4。
- 14本列对方拿 7机器人二在两块里挑大的:上排后缀 7 和下排前缀 4,较大的是 7,红色标出的就是它实际拿走的那块。也就是说,机器人一要是拐第 1 列,机器人二会拿到 7。把它和之前的最优比一比,目前机器人一能把对手压到最少 7。
- 15拐点第 2 列现在试机器人一在第 2 列往下拐。紫色是它下拐的两格,灰色是它走过并清零的整条路:上排从第 0 列到第 2 列,再下排从第 2 列到第 3 列。这些格子机器人二再来时都是 0 了。接下来看它给机器人二留下了哪两块。
- 16上排后缀 = 5先看绿色这块,上排拐点右边、从第 3 列起的一段:5,一共 5。机器人二要想拿它,就得在上排一直走到底再下拐。这个数记成 topSuffix,等于 5。
- 17下排前缀 = 6再看橙色这块,下排拐点左边、前 2 列的一段:4 加 2,一共 6。机器人二要想拿它,就得一开始就下拐、沿下排走。这个数记成 bottomPrefix,等于 6。
- 18本列对方拿 6机器人二在两块里挑大的:上排后缀 5 和下排前缀 6,较大的是 6,红色标出的就是它实际拿走的那块。也就是说,机器人一要是拐第 2 列,机器人二会拿到 6。把它和之前的最优比一比,目前机器人一能把对手压到最少 6。
- 19拐点第 3 列现在试机器人一在第 3 列往下拐。紫色是它下拐的两格,灰色是它走过并清零的整条路:上排从第 0 列到第 3 列,再下排从第 3 列到第 3 列。这些格子机器人二再来时都是 0 了。接下来看它给机器人二留下了哪两块。
- 20上排后缀 = 0拐点已经是最后一列了,上排右边没有格子留下,这块是空的,topSuffix 等于 0。机器人二在上排这一侧什么也捡不到。
- 21下排前缀 = 7再看橙色这块,下排拐点左边、前 3 列的一段:4 加 2 加 1,一共 7。机器人二要想拿它,就得一开始就下拐、沿下排走。这个数记成 bottomPrefix,等于 7。
- 22本列对方拿 7机器人二在两块里挑大的:上排后缀 0 和下排前缀 7,较大的是 7,红色标出的就是它实际拿走的那块。也就是说,机器人一要是拐第 3 列,机器人二会拿到 7。把它和之前的最优比一比,目前机器人一能把对手压到最少 6。
- 23答案 = 6四种拐法全试完了:拐第 0 列对方拿 8,第 1 列拿 7,第 2 列拿 6,第 3 列拿 7。机器人一当然选让对手最少的那个,也就是拐第 2 列,把机器人二死死压在 6。你看这一拐很妙,机器人一把上排右段压到只剩一个 5,而下排前面 4 加 2 等于 6 更大,所以机器人二只能拿这 6。
- 24答案 = 6回顾整个思路:机器人一的路径就是选一列往下拐,拐完给机器人二留下上排右段和下排左段两块残料,机器人二只能二选一拿较大的。我们逐列枚举拐点,每列算出 max(上排后缀, 下排前缀),再取所有列里最小的那个,就是机器人二在双方都最优时拿到的点数,这张图上是 6。真正写代码时,前缀后缀都能一边扫一边增减维护,不用每列重新求和。
⚠️ 容易写错的地方
✗ 错:以为机器人二能把两块残料都拿走,写成上排后缀加下排前缀
✓ 对:路径单调不回头,机器人二只能二选一,取 max(上排后缀, 下排前缀)
一旦向下拐就回不到上排,两块残料之间无法兼得,加起来会把答案算大
✗ 错:C 加加 或 Java 里把 s1、s2、答案声明成 int
✓ 对:C 加加 用 long long、Java 用 long,返回类型也随之
点数总和最大到五十亿,远超 int 上限,用 int 会溢出得到错误负数或乱值
✗ 错:把拐点那一列也算进某一侧的残料
✓ 对:拐点两格都被机器人一收走,上排后缀从第 j 加 1 列起、下排前缀到第 j 减 1 列止
拐点列上下两格都在机器人一路径上、已清零,归错边会多算一格
✗ 错:误以为机器人一要最大化自己的收益
✓ 对:机器人一的目标是让机器人二拿到的最小,自己拿多少无所谓
本题只问机器人二的得分,机器人一是压制方,枚举拐点求对手得分的最小值
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def gridGame(self, grid: List[List[int]]) -> int:
ans = inf
s1, s2 = sum(grid[0]), 0
for j, v in enumerate(grid[0]):
s1 -= v
ans = min(ans, max(s1, s2))
s2 += grid[1][j]
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
using ll = long long;
class Solution {
public:
long long gridGame(vector<vector<int>>& grid) {
ll ans = LLONG_MAX;
int n = grid[0].size();
ll s1 = 0, s2 = 0;
for (int& v : grid[0]) s1 += v;
for (int j = 0; j < n; ++j) {
s1 -= grid[0][j];
ans = min(ans, max(s1, s2));
s2 += grid[1][j];
}
return ans;
}
};Java
import java.util.*;
class Solution {
public long gridGame(int[][] grid) {
long ans = Long.MAX_VALUE;
long s1 = 0, s2 = 0;
for (int v : grid[0]) {
s1 += v;
}
int n = grid[0].length;
for (int j = 0; j < n; ++j) {
s1 -= grid[0][j];
ans = Math.min(ans, Math.max(s1, s2));
s2 += grid[1][j];
}
return ans;
}
}复杂度
时间
O(n)
先求一次上排和是 O(n),再从左到右枚举 n 个拐点列,每列只做减一个数、比一次大小、加一个数这几步常数操作,总共随列数 n 线性增长
空间
O(1)
只用了 s1、s2、答案这几个变量滚动维护前缀和与后缀和,不额外开数组,占用是常数,不随 n 变化
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 网格游戏 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
机器人二明明留着两块残料,为什么只能拿其中一块?+
因为机器人二和机器人一一样,每步只能向右或向下,路径单调不回头。上排拐点右段要走到,得一直待在上排;下排拐点左段要走到,得一开始就下拐沿下排走。这两件事没法同时做——一旦下拐进了下排,就再也回不到上排。所以两块之间只能二选一,机器人二当然挑点数大的那块,能拿到的就是 max(上排后缀, 下排前缀)。把它写成两块之和,会把答案算大。
为什么滚动时 s1 减掉当前列就变成了后缀和?+
s1 起初是上排整行的和。枚举到某一列、进循环第一步就把上排这一列的数从 s1 里减掉,减完 s1 里剩的正好是这一列右边所有数的和,也就是上排后缀和。与此同时 s2 是这一列左边下排数的前缀和,两者一凑就是当前拐点下机器人二能拿的两块。更新完答案后再把下排这一列加进 s2,s2 就为下一列的前缀和准备好了,一前一后各自滚动,不用每列重算。
机器人一要让对手最少,那它自己拿多少要不要一起算?+
不用。本题只问机器人二的得分,机器人一是压制方,它的唯一目标是让机器人二拿到的点数最小,自己收了多少无所谓。所以整套解法只围着机器人二的得分转:每个拐点算出对手能拿的 max(上排后缀, 下排前缀),再在所有拐点里取最小。若误以为机器人一要最大化自己收益,枚举的目标就反了,答案也会错。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 网格游戏 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。