保持城市天际线 图解题解
这道题到底在问什么
- 输入
- grid=[[3,0,8,4],[2,4,5,7],[9,2,6,3],[0,3,1,0]]
- 输出
- 35
- 输入
- grid=[[0,0,0],[0,0,0],[0,0,0]]
- 输出
- 0 (全 0,谁也加不了)
最优解:为什么这么做
一句话答案:LeetCode 807 保持城市天际线:每栋楼最高能升到它所在行最大值、列最大值里较小的那个,既不改四个方向的天际线,把每格增量累加就是答案,时间 O(n²)、空间 O(n)。
十六座楼能加多高,还不改四个方向的天际线
给一个 n × n 的高度矩阵 grid,grid[r][c] 是这栋楼的高度。可给任意楼加任意高度,唯一限制是从东、南、西、北四个方向看的天际线都不能变。问所有楼一共最多能加多少。题面例子 grid=[[3,0,8,4],[2,4,5,7],[9,2,6,3],[0,3,1,0]],答案 35;矩阵全 0 时谁也加不了,答案 0。
四个方向的天际线,本质就是行最大和列最大
先把『天际线』说清楚:从东西方向望,看到的是每行的最大值;从南北望,看到的是每列的最大值。要真一栋栋加高再重看四个方向,每加一次都得重算整幅,慢得没边。好在天际线只由行最大、列最大这两组数说了算,它们在原矩阵里就定死了。
每格的天花板,为什么是行最大和列最大里较小的那个
盯住第 (i,j) 格:它一旦超过本行最大值,东西向这行天际线就被顶高;超过本列最大值,南北向这列也变。所以它最高只能到本行最大、本列最大里较小的那个,取较大的会顶破较小方向。这个上限就是它的天花板 min(rowMax[i], colMax[j])。每格各升各的、互不牵扯。这种只顾眼前拿满、不回头的做法就是贪心,这里成立:天花板由原矩阵算出,加高超不过行列最大值,改不了别格。
先量出两条天际线,再逐格填增量
分两遍走。第一遍求 rowMax 和 colMax:扫每行、每列各取最大值。第二遍逐格算:格子 (i,j) 的天花板是 min(rowMax[i], colMax[j]),减掉现高 grid[i][j] 就是这格能加的量,所有格相加即答案。减原高别丢,加的是能往上升的那截,不是新高。
拿题面这张 4 × 4 矩阵,把 35 一格格算出来
先量两条天际线。四行最大值是 8、7、9、3,得 rowMax=[8,7,9,3];四列最大值是 9、4、8、7,得 colMax=[9,4,8,7]。
再逐格取 min 减原高。第 0 行:min(8,9)−3=5、min(8,4)−0=4、min(8,8)−8=0、min(8,7)−4=3,小计 12。往下三行:第 1 行天花板 7、4、7、7,减原高 2、4、5、7,增量 5、0、2、0,小计 7;第 2 行天花板 9、4、8、7,减 9、2、6、3,增量 0、2、2、4,小计 8;第 3 行天花板全是 3,减 0、3、1、0,增量 3、0、2、3,小计 8。合起来 12+7+8+8=35。
天花板取 min 不取 max,这条错了满盘皆输
第一个坑是把天花板取成 max,楼会升过较小方向的最大值、顶破那个方向的天际线;取较小的才能同时压住行、列两个上界。第二个坑是忘了减 grid[i][j],加的成了整座新高度而非增量,结果大出一截。还有人想真把楼加高再模拟看天际线,可 min(rowMax, colMax) 已保证两方向都不超,直接套公式就够。
复杂度上,求行列最大扫一遍、逐格累加再扫一遍,都是 n² 个格,时间 O(n²);只额外存 rowMax、colMax 两条长度 n 的数组,空间 O(n)。边界都指向 0:某格同时是所在行和列的最大值时,天花板等于自身、增量为 0;矩阵全 0 时每格天花板都是 0。
▶ 动画逐步走查(共 28 步)——想跟着动画一帧帧对照就展开
- 3记牢这句:每格的「天花板」是它所在行、列两个最大值里较小的那个。升到天花板既不破坏天际线,又榨干了所有可加空间。下面每一帧都在套它。
- 4每格数字 = 建筑高度这就是 4 × 4 的高度矩阵,每个数字是一座建筑的当前高度。我们的任务:在不改变四个方向天际线的前提下,把这些建筑能加的高度全部加起来。第一步,先把每行最大、每列最大求出来。
- 5橙色 = 本行;高亮 = 本行最大看第 0 行 3、0、8、4,里面最大的是 8(高亮那格)。这就是从东西方向看这一行时,天际线的高度。记进 rowMax,继续下一行。
- 6橙色 = 本行;高亮 = 本行最大看第 1 行 2、4、5、7,里面最大的是 7(高亮那格)。这就是从东西方向看这一行时,天际线的高度。记进 rowMax,继续下一行。
- 7橙色 = 本行;高亮 = 本行最大看第 2 行 9、2、6、3,里面最大的是 9(高亮那格)。这就是从东西方向看这一行时,天际线的高度。记进 rowMax,继续下一行。
- 8橙色 = 本行;高亮 = 本行最大看第 3 行 0、3、1、0,里面最大的是 3(高亮那格)。这就是从东西方向看这一行时,天际线的高度。记进 rowMax,继续下一行。
- 9橙色 = 本列;高亮 = 本列最大换个方向,看第 0 列 3、2、9、0,最大的是 9。这是从南北方向看这一列的天际线高度。记进 colMax,四列扫完就两组数据齐了。
- 10橙色 = 本列;高亮 = 本列最大换个方向,看第 1 列 0、4、2、3,最大的是 4。这是从南北方向看这一列的天际线高度。记进 colMax,四列扫完就两组数据齐了。
- 11橙色 = 本列;高亮 = 本列最大换个方向,看第 2 列 8、5、6、1,最大的是 8。这是从南北方向看这一列的天际线高度。记进 colMax,四列扫完就两组数据齐了。
- 12橙色 = 本列;高亮 = 本列最大换个方向,看第 3 列 4、7、3、0,最大的是 7。这是从南北方向看这一列的天际线高度。记进 colMax,四列扫完就两组数据齐了。
- 13rowMax 与 colMax 都已算好rowMax 是 8、7、9、3,colMax 是 9、4、8、7。接下来对每一格,取它所在行最大和列最大里较小的那个当天花板,减去原高就是这一格能加的量。我们从左上角一格一格扫,边扫边累加。
- 14紫色 = 当前格;橙色 = 行/列最大来源当前格 3:本行最大 8、本列最大 9,较小的是 8(被行卡住),就是它的天花板。能加 5,累计来到 5。
- 15紫色 = 当前格;橙色 = 行/列最大来源当前格 0:本行最大 8、本列最大 4,较小的是 4(被列卡住),就是它的天花板。能加 4,累计来到 9。
- 16紫色 = 当前格;橙色 = 行/列最大来源当前格 8:本行最大 8、本列最大 8,较小的是 8(被行卡住),就是它的天花板。原本就到顶,加不了,累计来到 9。
- 17紫色 = 当前格;橙色 = 行/列最大来源当前格 4:本行最大 8、本列最大 7,较小的是 7(被列卡住),就是它的天花板。能加 3,累计来到 12。
- 18紫色 = 当前格;橙色 = 行/列最大来源当前格 2:本行最大 7、本列最大 9,较小的是 7(被行卡住),就是它的天花板。能加 5,累计来到 17。
- 19紫色 = 当前格;橙色 = 行/列最大来源当前格 4:本行最大 7、本列最大 4,较小的是 4(被列卡住),就是它的天花板。原本就到顶,加不了,累计来到 17。
- 20紫色 = 当前格;橙色 = 行/列最大来源当前格 5:本行最大 7、本列最大 8,较小的是 7(被行卡住),就是它的天花板。能加 2,累计来到 19。
- 21紫色 = 当前格;橙色 = 行/列最大来源当前格 7:本行最大 7、本列最大 7,较小的是 7(被行卡住),就是它的天花板。原本就到顶,加不了,累计来到 19。
- 22紫色 = 当前格;橙色 = 行/列最大来源当前格 9:本行最大 9、本列最大 9,较小的是 9(被行卡住),就是它的天花板。原本就到顶,加不了,累计来到 19。
- 23紫色 = 当前格;橙色 = 行/列最大来源当前格 2:本行最大 9、本列最大 4,较小的是 4(被列卡住),就是它的天花板。能加 2,累计来到 21。
- 24紫色 = 当前格;橙色 = 行/列最大来源当前格 6:本行最大 9、本列最大 8,较小的是 8(被列卡住),就是它的天花板。能加 2,累计来到 23。
- 25紫色 = 当前格;橙色 = 行/列最大来源当前格 3:本行最大 9、本列最大 7,较小的是 7(被列卡住),就是它的天花板。能加 4,累计来到 27。
- 26紫色 = 当前格;橙色 = 行/列最大来源当前格 0:本行最大 3、本列最大 9,较小的是 3(被行卡住),就是它的天花板。能加 3,累计来到 30。
- 27紫色 = 当前格;橙色 = 行/列最大来源当前格 3:本行最大 3、本列最大 4,较小的是 3(被行卡住),就是它的天花板。原本就到顶,加不了,累计来到 30。
- 28紫色 = 当前格;橙色 = 行/列最大来源当前格 1:本行最大 3、本列最大 8,较小的是 3(被行卡住),就是它的天花板。能加 2,累计来到 32。
- 29紫色 = 当前格;橙色 = 行/列最大来源当前格 0:本行最大 3、本列最大 7,较小的是 3(被行卡住),就是它的天花板。能加 3,累计来到 35。
- 30每格已升到自己的天花板十六格全算完,这是抬升后的城市。你可以验一下:每行最大仍是 8、7、9、3,每列最大仍是 9、4、8、7,四个方向的天际线一点没变。所有增量加起来正好 35,就是答案。
⚠️ 容易写错的地方
✗ 错:把天花板取成 max(rowMax, colMax)
✓ 对:必须取 min
升到较大那个会顶破较小方向的天际线,违反约束
✗ 错:忘了减去原高 grid[i][j]
✓ 对:增量 = 天花板 − 原高
加的是「能升的部分」,不是新高度本身
✗ 错:以为要真的去模拟加高再比天际线
✓ 对:直接用 min 公式即可
min(rowMax,colMax) 已保证两方向都不超,无需模拟
完整代码(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 maxIncreaseKeepingSkyline(self, grid: List[List[int]]) -> int:
row_max = [max(row) for row in grid]
col_max = [max(col) for col in zip(*grid)]
return sum(
min(row_max[i], col_max[j]) - x
for i, row in enumerate(grid)
for j, x in enumerate(row)
)C++
#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;
class Solution {
public:
int maxIncreaseKeepingSkyline(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<int> rowMax(m);
vector<int> colMax(n);
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
rowMax[i] = max(rowMax[i], grid[i][j]);
colMax[j] = max(colMax[j], grid[i][j]);
}
}
int ans = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
ans += min(rowMax[i], colMax[j]) - grid[i][j];
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int maxIncreaseKeepingSkyline(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[] rowMax = new int[m];
int[] colMax = new int[n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
rowMax[i] = Math.max(rowMax[i], grid[i][j]);
colMax[j] = Math.max(colMax[j], grid[i][j]);
}
}
int ans = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
ans += Math.min(rowMax[i], colMax[j]) - grid[i][j];
}
}
return ans;
}
}复杂度
时间
O(n²)
两遍遍历矩阵:一遍求行列最大,一遍逐格累加
空间
O(n)
只额外存 rowMax、colMax 两个长度 n 的数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 保持城市天际线 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么每格各升各的、互不干扰,加起来就是全局最大?+
因为每格的天花板 min(rowMax[i], colMax[j]) 只由原矩阵的行最大、列最大决定,而这两组最大值在加高过程里不会变——任何一栋楼都升不过它所在行、列的原最大值,也就动不了 rowMax、colMax。既然各格的上限互相独立、没有『这格多加就逼那格少加』的牵制,每格都取满自己的天花板,合起来就是能加的最大总量。
为什么取行最大和列最大里较小的,不是较大的?+
一栋楼同时受两个方向约束。它不能超过本行最大值,否则东西向天际线变高;也不能超过本列最大值,否则南北向天际线变高。两个上限里,较小的那个会先被撞到:升到较小值时,另一个方向还有余量、不受影响;可一旦越过较小值去够较大值,较小值那个方向就被顶破了。所以能同时满足两个约束的最高点,就是两者里较小的那个。
空间能不能降到 O(1)?+
行列最大值是必须先掌握的全局信息,绕不开。为了讲清楚这里存了 rowMax、colMax 两条数组。实际可以只预存一个方向,比如先算好 colMax,再逐行现算当前行的最大值并累加,省掉 rowMax 那条;但保留的 colMax 仍要存 n 个值,所以量级还是 O(n),降不到 O(1)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 保持城市天际线 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。