到达首都的最少油耗 图解题解
这道题到底在问什么
- 输入
- roads=[[0,1],[0,2],[0,3]], seats=5
- 输出
- 3
- 输入
- roads=[], seats=1
- 输出
- 0
最优解:为什么这么做
一句话答案:LeetCode 2477 到达首都的最少油耗:把 roads 建成无向图,从首都 0 后序 DFS 汇总子树人数,每条边油耗按人数除以座位向上取整累加,时间 O(n)。
把所有代表送到首都,最少烧多少油
n 座城连成一棵树,城市 0 是首都,roads 里每项是一条双向路。每座城住一位代表要赶去首都,一辆车坐 seats 人,相邻两城之间开一趟耗 1 升油,路上能拼车。求送全部代表到首都最少烧多少油。题面例子 roads=[[0,1],[0,2],[0,3]]、seats=5,答案 3;另一组 roads=[]、seats=1,只有首都、没人出行,答案 0。
让每人各开一辆直奔首都,亏在哪
题面准了拼车,能省多少?要排一张全局拼车表——谁半路捎上谁、在哪汇合——十万位代表的组合根本枚举不完。让每人各开一辆直达倒好算,可相邻本能合坐的代表却各烧各的油,越靠近首都越亏。
每条路都非过不可,于是各算各的
树上每座城通往首都的路是唯一的。盯住某一条边,它下面那棵子树里的所有代表不管怎么拼车,最后都得挤过这唯一一条边往首都走。过边人数固定、又非走不可,把车尽量坐满就是最优:车数 = 子树人数 ÷ 座位数 再向上取整。每条边彼此不牵扯,各自算完加起来就是全局最少油耗。
怎么把子树人数数清?从首都 0 做后序遍历,也就是先把每个孩子子树数清,再算上自己这一位。roads 是双向路,记成来回都能走,一趟 DFS 既往下钻、又把人数捎回来。
DFS 每到一座城,返回什么、累加什么
递归写成 dfs(a, fa),a 是当前城、fa 是父亲。先给 sz 记 1,算上 a 自己那位代表。再遍历 a 的邻居 b:碰到 fa 就跳过,免得走回父亲;碰到孩子 b,先递归 t = dfs(b, a) 拿到子树人数,给 a 到 b 这条边结账,ans 加上 t ÷ seats 向上取整的车数,再把 t 并进 sz。走完返回 sz。
返回值 sz 只往父亲传子树人数,累加器 ans 一路收每条边的油耗。首都 0 头顶没有上行边,dfs(0, -1) 的返回值只是收尾,不给根算过路油。
拿题面 [[0,1],[0,2],[0,3]] 顺着递归跑一趟
roads=[[0,1],[0,2],[0,3]]、seats=5。建图后首都 0 连着末端城 1、城 2、城 3。dfs(0, -1) 依次钻进三个孩子:dfs(1, 0) 里城 1 除父亲外没邻居,sz=1 返回;回到 0 给边 0-1 结账,1 ÷ 5 向上取整 1 辆,ans=1。城 2、城 3 同样各返回 1、各加 1 辆,ans 走到 3。三人虽一辆五座车坐得下,可分处三条路各必开一辆,所以 3 升不是 1 升。
边界那组 roads=[]、seats=1:一条边都没有,0 没邻居直接返回,ans 没加,答案 0。
座位为 1 的深链,会把 int 撑爆
复杂度上,建邻接表遍历 n−1 条边、后序 DFS 每座城每条边只看常数次,时间 O(n);邻接表加上递归栈最坏在链形树深达 n 层,空间也是 O(n)。
几处最容易栽:车数一律向上取整,坐不满也得再开一辆,向下取整会漏人;ans 必须用 long,座位为 1、树深成链时靠近根的边人数极大,车数逐段累加能到十亿量级、int 会溢出;遍历邻居忘跳过 fa 就会折回父亲、无限递归;给根补上行边的车则多算了油。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:每条路的车数,等于这条路下面那棵子树的人数,除以座位数再向上取整。为什么可以一条边一条边地独立算,因为子树里的人不管怎么拼,最终都得挤过这唯一一条通向首都的路。下面把首都放在最上面,一层一层往下把人数数清。
- 4目标:总油耗最少先把整棵树摆出来。城市 0 是首都,画在最上面,另外 7 座城顺着道路挂在它下面,一共 8 座城,每座城住着一位代表。每辆车坐 2 人,也就是 seats 等于 2。首都自己的代表已经在会场,不烧油;其余 7 位都要往上开。我们从最底下的城开始,一层层把子树里的人数数清,再折算每条路要几辆车。
- 5根 = 首都 0从首都 0 出发做后序搜索。后序的意思是先把孩子那边全部数完,再回过头结算自己。首都下面挂着城市 1 和城市 2 两大分支,先钻进城市 1 那一支,一路往下探到最底端。
- 6访问城市 1顺着道路往下走,来到城市 1。它底下还挂着更下级的城,别急着给它算账,先继续深入,把它子树里到底有多少位代表数清楚,再回来处理它头顶这条路。
- 7城市 3 子树 = 1城市 3 是这条线的末端,底下没有别的城了。它这棵子树里就它自己一位代表,人数记 1。把它标成绿色,表示这一块已经数清,可以把这个 1 交还给它的父亲了。
- 81 人过路, 开 1 车城市 3 这一支数好了,子树里一共 1 位代表。这些人都要挤过 城市 3 到城市 1 这一条路开向首都。每辆车坐 2 人:1 位代表还坐不满一车, 但也得开 1 辆车。这条路就耗 1 升油,累计油耗到 1 升。同时把这 1 位代表并进城市 1 的子树一起往上带。
- 9访问城市 4顺着道路往下走,来到城市 4。它底下还挂着更下级的城,别急着给它算账,先继续深入,把它子树里到底有多少位代表数清楚,再回来处理它头顶这条路。
- 10城市 6 子树 = 1城市 6 是这条线的末端,底下没有别的城了。它这棵子树里就它自己一位代表,人数记 1。把它标成绿色,表示这一块已经数清,可以把这个 1 交还给它的父亲了。
- 111 人过路, 开 1 车城市 6 这一支数好了,子树里一共 1 位代表。这些人都要挤过 城市 6 到城市 4 这一条路开向首都。每辆车坐 2 人:1 位代表还坐不满一车, 但也得开 1 辆车。这条路就耗 1 升油,累计油耗到 2 升。同时把这 1 位代表并进城市 4 的子树一起往上带。
- 12城市 4 子树 = 2城市 4 的下级都收拢完了,把它自己那位代表也算上,这棵子树一共 2 位代表。城市 4 标绿,表示它这一块彻底数清。接下来就轮到它头顶那条通往上级的路,用这 2 个人去折算要开几辆车。
- 132 人过路, 开 1 车城市 4 这一支数好了,子树里一共 2 位代表。这些人都要挤过 城市 4 到城市 1 这一条路开向首都。每辆车坐 2 人:2 位代表正好坐满 1 辆车。这条路就耗 1 升油,累计油耗到 3 升。同时把这 2 位代表并进城市 1 的子树一起往上带。
- 14城市 1 子树 = 4城市 1 的下级都收拢完了,把它自己那位代表也算上,这棵子树一共 4 位代表。城市 1 标绿,表示它这一块彻底数清。接下来就轮到它头顶那条通往上级的路,用这 4 个人去折算要开几辆车。
- 154 人过路, 开 2 车城市 1 这一支数好了,子树里一共 4 位代表。这些人都要挤过 城市 1 到城市 0 这一条路开向首都。每辆车坐 2 人:4 位代表正好装满 2 辆车, 一个空座都不剩。这条路就耗 2 升油,累计油耗到 5 升。同时把这 4 位代表并进城市 0 的子树一起往上带。
- 16访问城市 2顺着道路往下走,来到城市 2。它底下还挂着更下级的城,别急着给它算账,先继续深入,把它子树里到底有多少位代表数清楚,再回来处理它头顶这条路。
- 17访问城市 5顺着道路往下走,来到城市 5。它底下还挂着更下级的城,别急着给它算账,先继续深入,把它子树里到底有多少位代表数清楚,再回来处理它头顶这条路。
- 18城市 7 子树 = 1城市 7 是这条线的末端,底下没有别的城了。它这棵子树里就它自己一位代表,人数记 1。把它标成绿色,表示这一块已经数清,可以把这个 1 交还给它的父亲了。
- 191 人过路, 开 1 车城市 7 这一支数好了,子树里一共 1 位代表。这些人都要挤过 城市 7 到城市 5 这一条路开向首都。每辆车坐 2 人:1 位代表还坐不满一车, 但也得开 1 辆车。这条路就耗 1 升油,累计油耗到 6 升。同时把这 1 位代表并进城市 5 的子树一起往上带。
- 20城市 5 子树 = 2城市 5 的下级都收拢完了,把它自己那位代表也算上,这棵子树一共 2 位代表。城市 5 标绿,表示它这一块彻底数清。接下来就轮到它头顶那条通往上级的路,用这 2 个人去折算要开几辆车。
- 212 人过路, 开 1 车城市 5 这一支数好了,子树里一共 2 位代表。这些人都要挤过 城市 5 到城市 2 这一条路开向首都。每辆车坐 2 人:2 位代表正好坐满 1 辆车。这条路就耗 1 升油,累计油耗到 7 升。同时把这 2 位代表并进城市 2 的子树一起往上带。
- 22城市 2 子树 = 3城市 2 的下级都收拢完了,把它自己那位代表也算上,这棵子树一共 3 位代表。城市 2 标绿,表示它这一块彻底数清。接下来就轮到它头顶那条通往上级的路,用这 3 个人去折算要开几辆车。
- 233 人过路, 开 2 车城市 2 这一支数好了,子树里一共 3 位代表。这些人都要挤过 城市 2 到城市 0 这一条路开向首都。每辆车坐 2 人:3 位代表, 先装满 1 辆车, 还剩 1 人得再开 1 辆, 一共 2 辆车。这条路就耗 2 升油,累计油耗到 9 升。同时把这 3 位代表并进城市 0 的子树一起往上带。
- 24答案 = 9回到首都 0。它的两大分支都汇总完了:城市 1 那支的 4 人账已折成 2 升油,城市 2 那支的 3 人账折成 2 升油。连同下面各条小路,所有边的油耗加起来一共 9 升。首都自己的代表本就在会场,不用开车,不产生油耗。这 9 升就是最终答案。
- 25总油耗 = 9 升回头看一眼七条路各烧了多少油。靠近末端的五条路,人数都不超过 2,各开 1 辆车、各 1 升;剩下两条汇进首都的大路:城市 1 到首都 4 个人正好坐满 2 车,是 2 升;城市 2 到首都 3 个人得开 2 车,末车坐不满也算,还是 2 升。五个 1 加两个 2,正好 9 升。核心始终一句:每条路的车数,是它下面子树人数除以座位再向上取整。
⚠️ 容易写错的地方
✗ 错:累加答案 ans 用 int
✓ 对:ans 必须用 long 或 long long
城市数可达十万、座位为 1 时车数会累加到十亿量级,int 装不下会溢出成负数或错值
✗ 错:每条边车数写成 子树人数 ÷ seats 向下取整
✓ 对:要向上取整
哪怕只剩 1 个人坐不满一车,也得再开一辆;向下取整会把这批人漏掉,油耗算少
✗ 错:遍历邻居时不跳过父节点 fa
✓ 对:遇到 fa 要 continue
道路是无向边,不跳过父亲就会沿原路走回去,造成无限递归或把人数重复计入
✗ 错:给首都 0 头顶也加一条边的车
✓ 对:根没有上行边,不产生油耗
首都的代表本就在会场,dfs(0) 的返回值只用来收尾,别再给根算过路油耗
完整代码(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 TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def minimumFuelCost(self, roads: List[List[int]], seats: int) -> int:
def dfs(a: int, fa: int) -> int:
nonlocal ans
sz = 1
for b in g[a]:
if b != fa:
t = dfs(b, a)
ans += ceil(t / seats)
sz += t
return sz
g = defaultdict(list)
for a, b in roads:
g[a].append(b)
g[b].append(a)
ans = 0
dfs(0, -1)
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 <optional>
#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;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
long long minimumFuelCost(vector<vector<int>>& roads, int seats) {
int n = roads.size() + 1;
vector<int> g[n];
for (auto& e : roads) {
int a = e[0], b = e[1];
g[a].emplace_back(b);
g[b].emplace_back(a);
}
long long ans = 0;
function<int(int, int)> dfs = [&](int a, int fa) {
int sz = 1;
for (int b : g[a]) {
if (b != fa) {
int t = dfs(b, a);
ans += (t + seats - 1) / seats;
sz += t;
}
}
return sz;
};
dfs(0, -1);
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private List<Integer>[] g;
private int seats;
private long ans;
public long minimumFuelCost(int[][] roads, int seats) {
int n = roads.length + 1;
g = new List[n];
Arrays.setAll(g, k -> new ArrayList<>());
this.seats = seats;
for (int[] e : roads) {
int a = e[0], b = e[1];
g[a].add(b);
g[b].add(a);
}
dfs(0, -1);
return ans;
}
private int dfs(int a, int fa) {
int sz = 1;
for (int b : g[a]) {
if (b != fa) {
int t = dfs(b, a);
ans += (t + seats - 1) / seats;
sz += t;
}
}
return sz;
}
}复杂度
时间
O(n)
建无向邻接表遍历 roads 共 n 减 1 条边,是 O(n);后序 DFS 每座城恰好进出一次,每条边只被看常数次,合起来随城市数线性增长
空间
O(n)
按峰值算。邻接表存全部边是 O(n);递归栈深度等于树高,最坏是一条链时栈里排到接近 n 层。两者都是 O(n),不随规模平方增长
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 到达首都的最少油耗 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么能一条边一条边地独立累加车数,而不用统筹全局怎么拼车?+
因为树上每座城通往首都的路是唯一的。某条边下面子树里的所有代表,不管中途怎么拼车、在哪汇合,最终都必须挤过这一条边才能往首都走。既然过边人数是固定的、这条边又非走不可,把车尽量坐满就是这条边的最优,也就是子树人数 ÷ 座位数向上取整那么多辆。每条边彼此不牵扯,各自的最优加起来就是全局最优,不需要排全局拼车表。
子树人数 sz 用 int 就够,为什么答案 ans 偏要用 long?+
单棵子树的人数最多也就是节点总数 n,十万级别,int 装得下。但 ans 是每条边车数的累加:当座位为 1、树又深时,靠近根的边要过的人极多,车数等于人数,一层层累加下来总量能到十亿量级,超过 int 上限,会溢出成负数或错值。所以 sz 用 int、ans 用 long 或 long long,是按各自能到多大分别定的。
遍历邻居时那个跳过父亲 fa 的判断,去掉会怎样?+
roads 建的是无向邻接表,每条边在两个端点的邻居列表里都记了一份。DFS 从 a 走到孩子 b 后,b 的邻居里也有 a;要是不判断 b != fa(或遇到 fa 就 continue),就会立刻从 b 又走回 a,两点之间来回横跳,造成无限递归栈溢出,人数也被重复计入。记住父亲、遇到它就跳过,才能保证只往子树深处走、不折返。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 到达首都的最少油耗 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。