2477. 到达首都的最少油耗 : 逐步讲解最低油耗求解思路

题目描述

这是 LeetCode 上的 2477. 到达首都的最少油耗 ,难度为 中等

Tag : 「DFS」

给你一棵 n 个节点的树(一个无向、连通、无环图),每个节点表示一个城市,编号从 0n - 1,且恰好有 n - 1 条路。

0 是首都。给你一个二维整数数组 roads,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> r o a d s [ i ] = [ a i , b i ] roads[i] = [a_{i}, b_{i}] </math>roads[i]=[ai,bi] ,表示城市 <math xmlns="http://www.w3.org/1998/Math/MathML"> a i a_{i} </math>ai 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b i b_{i} </math>bi 之间有一条 双向路 。

每个城市里有一个代表,他们都要去首都参加一个会议。

每座城市里有一辆车。给你一个整数 seats 表示每辆车里面座位的数目。

城市里的代表可以选择乘坐所在城市的车,或者乘坐其他城市的车。相邻城市之间一辆车的油耗是一升汽油。

请你返回到达首都最少需要多少升汽油。

示例 1:

lua 复制代码
输入:roads = [[0,1],[0,2],[0,3]], seats = 5

输出:3

解释:
- 代表 1 直接到达首都,消耗 1 升汽油。
- 代表 2 直接到达首都,消耗 1 升汽油。
- 代表 3 直接到达首都,消耗 1 升汽油。
最少消耗 3 升汽油。

示例 2:

diff 复制代码
输入:roads = [[3,1],[3,2],[1,0],[0,4],[0,5],[4,6]], seats = 2

输出:7

解释:
- 代表 2 到达城市 3 ,消耗 1 升汽油。
- 代表 2 和代表 3 一起到达城市 1 ,消耗 1 升汽油。
- 代表 2 和代表 3 一起到达首都,消耗 1 升汽油。
- 代表 1 直接到达首都,消耗 1 升汽油。
- 代表 5 直接到达首都,消耗 1 升汽油。
- 代表 6 到达城市 4 ,消耗 1 升汽油。
- 代表 4 和代表 6 一起到达首都,消耗 1 升汽油。
最少消耗 7 升汽油。

示例 3:

ini 复制代码
输入:roads = [], seats = 1

输出:0

解释:没有代表需要从别的城市到达首都。

提示:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = n < = 105 1 <= n <= 105 </math>1<=n<=105
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> r o a d s . l e n g t h = n − 1 roads.length = n - 1 </math>roads.length=n−1
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> r o a d s [ i ] . l e n g t h = 2 roads[i].length = 2 </math>roads[i].length=2
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 < = a i , b i < n 0 <= a_{i}, b_{i} < n </math>0<=ai,bi<n
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> a i ! = b i a_{i} != b_{i} </math>ai!=bi
  • roads 表示一棵合法的树。
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = s e a t s < = 1 0 5 1 <= seats <= 10^5 </math>1<=seats<=105

DFS

将双向图看作是以节点 0 为根的有向树,从每个节点出发往 0 前行,可看作是自底向上的移动过程。

seats = 1 时,每个节点前往 0 的过程相互独立,总油耗为每节点到 0 的最短距离之和。

seats 不为 1 时,考虑组成顺风车,此时总的油耗不该超过 seats = 1 的情况。

不难发现,只有「深度大的节点,在前往 0 过程中,搭乘深度小顺风车」可减少油耗 (例如在上图节点 3 在经过节点 1 时搭乘顺风车,可与节点 1 合计使用一份油耗前往到 0),否则如果是深度小的节点先往深度大的节点走,再一同前往 0,会额外多经过某些边,产生不必要的油耗。

考虑组成顺风车时,总油耗该如何计算。

基于上述分析,无论 seats 是否为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1(是否组成顺风车),每个节点前往 0 的路径总是不变,即经过的边固定不变,必然是自底向上。

因此我们可统计每条边会被多少个节点经过,通过 DFS 统计「以每个节点为根时,子树的节点数量」即是经过该节点往上的边。

Java 代码:

Java 复制代码
class Solution {
    int N = 100010, M = 2 * N, idx = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    long ans = 0;
    public long minimumFuelCost(int[][] roads, int seats) {
        int n = roads.length + 1;
        Arrays.fill(he, -1);
        for (int[] r : roads) {
            int a = r[0], b = r[1];
            add(a, b); add(b, a);
        }
        dfs(0, -1, seats);
        return ans;
    }
    int dfs(int u, int fa, int t) {
        int cnt = 1;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            cnt += dfs(j, u, t);
        }
        if (u != 0) ans += Math.ceil(cnt * 1.0 / t);
        return cnt;
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    int N = 100010, M = 2 * N, idx = 0;
    int he[100010], e[200020], ne[200020];
    long long ans = 0;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    long long minimumFuelCost(vector<vector<int>>& roads, int seats) {
        int n = roads.size() + 1;
        memset(he, -1, sizeof(he));
        for (auto& r : roads) {
            int a = r[0], b = r[1];
            add(a, b); add(b, a);
        }
        dfs(0, -1, seats);
        return ans;
    }
    int dfs(int u, int fa, int t) {
        int cnt = 1;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            cnt += dfs(j, u, t);
        }
        if (u != 0) ans += ceil(cnt * 1.0 / t);
        return cnt;
    }
};

Python 代码:

Python 复制代码
class Solution:
    def minimumFuelCost(self, roads: List[List[int]], seats: int) -> int:
        n, m, ans = len(roads) + 1, len(roads) * 2 + 1, 0
        he, e, ne, idx = [-1] * n, [0] * m, [0] * m, 0

        def add(a, b):
            nonlocal idx
            e[idx] = b
            ne[idx] = he[a]
            he[a] = idx
            idx += 1

        def dfs(u, fa, t):
            nonlocal ans
            cnt = 1
            i = he[u]
            while i != -1:
                j, i = e[i], ne[i]
                if j == fa: continue
                cnt += dfs(j, u, t)
            if u != 0:
                ans += math.ceil(cnt * 1.0 / t)
            return cnt

        for a, b in roads:
            add(a, b)
            add(b, a)
        dfs(0, -1, seats)
        return ans

TypeScript 代码:

TypeScript 复制代码
function minimumFuelCost(roads: number[][], seats: number): number {
    const n = roads.length + 1, m = n * 2;
    const he = Array(n).fill(-1), e = Array(m).fill(0), ne = Array(m).fill(0);
    let ans = 0, idx = 0;
    const add = function(a: number, b: number) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    const dfs = function(u: number, fa: number, t: number): number {
        let cnt = 1;
        for (let i = he[u]; i != -1; i = ne[i]) {
            const j = e[i];
            if (j == fa) continue;
            cnt += dfs(j, u, t);
        }
        if (u != 0) ans += Math.ceil(cnt * 1.0 / t);
        return cnt;
    }
    for (const [a, b] of roads) {
        add(a, b); add(b, a);
    }
    dfs(0, -1, seats);
    return ans;
};
  • 时间复杂度:建图复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n);递归统计每个子树节点数量并计算油耗复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n);整体复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.2477 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
hsling松子4 小时前
使用PaddleHub智能生成,献上浓情国庆福
人工智能·算法·机器学习·语言模型·paddlepaddle
dengqingrui1235 小时前
【树形DP】AT_dp_p Independent Set 题解
c++·学习·算法·深度优先·图论·dp
C++忠实粉丝5 小时前
前缀和(8)_矩阵区域和
数据结构·c++·线性代数·算法·矩阵
ZZZ_O^O6 小时前
二分查找算法——寻找旋转排序数组中的最小值&点名
数据结构·c++·学习·算法·二叉树
CV-King6 小时前
opencv实战项目(三十):使用傅里叶变换进行图像边缘检测
人工智能·opencv·算法·计算机视觉
2401_857622666 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php