到达首都的最少油耗 图解题解
这道题到底在问什么
- 输入
- roads=[[0,1],[0,2],[0,3]], seats=5
- 输出
- 3
- 输入
- roads=[], seats=1
- 输出
- 0
最优解:一步一步想明白
- 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 上限,所以 ans 必须用 long 或 long long,否则会溢出。
时间和空间复杂度是多少?+
一次后序 DFS 每座城进出一次、每条边看常数次,建图也是遍历一遍边,时间 O(n)。空间按峰值算,邻接表存全部边 O(n),递归栈最坏在树退化成一条链时深达 n 层,所以空间也是 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 到达首都的最少油耗 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。