网格游戏 图解题解
这道题到底在问什么
- 输入
- grid=[[3,1,2,5],[4,2,1,2]]
- 输出
- 6
- 输入
- grid=[[2,5,4],[1,5,1]]
- 输出
- 4
先想最直接的笨办法
回顾整个思路:机器人一的路径就是选一列往下拐,拐完给机器人二留下上排右段和下排左段两块残料,机器人二只能二选一拿较大的。我们逐列枚举拐点,每列算出 max(上排后缀, 下排前缀),再取所有列里最小的那个,就是机器人二在双方都最优时拿到的点数,这张图上是 6。真正写代码时,前缀后缀都能一边扫一边增减维护,不用每列重新求和。(动画第 24 步)
最优解:一步一步想明白
- 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++ 跟着练。
面试官可能追问
这题的核心观察是什么?+
任何从 (0,0) 到 (1,n-1) 的路径,形状都是上排走一段、某列下拐、再下排走到底,所以机器人一只需决定在哪一列下拐。它一走过就清零,机器人二只剩上排拐点右段和下排拐点左段两块,且因为路径单调只能二选一拿较大的。于是枚举拐点列,取 max(上排后缀, 下排前缀) 的最小值。
怎么把它做到线性?+
用前缀后缀和滚动维护。先求出上排整行的和 s1,下排前缀 s2 从 0 开始。枚举列 j 时先让 s1 减去上排第 j 个,s1 就成了上排的后缀和;此刻 s2 正好是下排前缀和;用 max(s1, s2) 更新答案后,再把 s2 加上下排第 j 个。每列常数操作,总体 O(n) 时间、O(1) 空间。
有哪些容易出错的细节?+
一是机器人二取的是 max 不是两块之和,因为路径不能回头;二是点数总和可达五十亿,C 加加 要用 long long、Java 要用 long 防溢出;三是拐点那一列上下两格都归机器人一,后缀从下一列起、前缀到上一列止,别把拐点列算进任何一侧。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 网格游戏 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。