青训营刷题-1

257 阅读8分钟

1 青海湖租车之旅

油价飞升的今天,我们尽量减少花费。我们出门旅游,有时候租车去旅游也是一种不错的方式。这次我们这次旅游是从「青海湖」到「景点 X」,景点 X 可以是「敦煌」、「月牙泉」等,线路的路径是唯一的,假设我们每走 1 km 消耗 1 L 的油,车油箱容量 400L。比如:如果「景点 X」是敦煌,我们在青海湖租车前油箱是 200L 的,在「景点 X」(敦煌)还车的时候也是 200L 的,路上有很多加油站,加油站在青海湖和「景点 X」的连线上。

输入格式

第 1 行表示「青海湖」到「景点 X」的距离,距离最远不超过 10000 km。 第 2 行表示接下来 N 行表示 N 个加油站(N 为正整数)。 接下来 N(1 <= N <= 100)行表示,每一个加油站情况。每一个加油站包括距离「景点 X」的距离 a km(0 <= a <= 10000),以及每升汽油的价格 b 元(0 <= b <= 2000),a 和 b 均为正整数。

输出格式

如果不能到达目的地「景点 X」,输出 Impossible。 如果能到达目的地「景点 X」,输出最小花费多少元。

输入样例: 500 4 100 1 200 30 400 40 300 20

输出样例: 4300

解题思路

要解决这个问题,我们需要找到从「青海湖」到「景点 X」的最小花费路径,考虑油箱容量、初始油量、最终油量要求以及沿途加油站的油价。以下是详细的解题思路:

1. 问题分析

  • 总距离(D) :从「青海湖」到「景点 X」的总距离,单位为公里(km)。
  • 油箱容量:400 升。
  • 初始油量:200 升。
  • 最终油量:到达「景点 X」时,油箱中至少需要保留 200 升油。
  • 油耗:每公里消耗 1 升油。
  • 加油站信息:每个加油站提供距离「景点 X」的距离(a km)和油价(b 元/升)。

2. 转换加油站位置

为了便于计算,我们将所有加油站的位置转换为相对于起点(「青海湖」)的距离:

distanceFromStart = D - a

这样,我们可以按从起点到终点的顺序处理加油站。

3. 排序加油站

将所有加油站按距离起点的距离升序排序。然后,添加一个虚拟的终点加油站,位置为 D km,油价为 0 元/升,以方便算法处理终点的情况。

4. 贪心算法策略

采用贪心算法,通过在油价较低的加油站尽可能多地加油,以减少总花费。具体策略如下:

  1. 初始化参数

    • 当前油量(currentFuel)为初始油量 200 升。
    • 总花费(totalCost)为 0 元。
    • 上一个加油站的位置(prevDistance)为 0 km。
  2. 遍历每个加油站

    • 计算从上一个加油站到当前加油站的距离(distanceToStation)。

    • 更新当前油量:

      currentFuel -= distanceToStation
      
    • 如果当前油量小于 0,则无法到达当前加油站,输出 Impossible

    • 寻找下一个油价更低的加油站

      • 从当前加油站开始,寻找下一个油价低于当前加油站的加油站,且在油箱容量(400 km)可达范围内。

      • 如果找到这样的加油站:

        • 计算到达该加油站所需的油量(distanceToCheaper)。

        • 特殊情况:如果找到的是终点加油站,确保到达终点时油量至少为 200 升。

        • 计算需要加油的量:

          requiredFuel = distanceToCheaper + (终点需要保留的油量,如果是终点)
          fuelToBuy = requiredFuel - currentFuel
          fuelToBuy = min(fuelToBuy, tankCapacity - currentFuel)
          
        • 更新总花费和当前油量:

          totalCost += fuelToBuy * currentStation.price
          currentFuel += fuelToBuy
          
      • 如果没有找到更便宜的加油站:

        • 尽可能多地加油,以满足到达下一个加油站或终点的需求,同时考虑最终需要保留的油量。

        • 计算需要加油的量:

          neededFuel = (distanceToEnd + finalFuel) - currentFuel
          fuelToBuy = min(neededFuel, tankCapacity - currentFuel)
          
        • 更新总花费和当前油量:

          totalCost += fuelToBuy * currentStation.price
          currentFuel += fuelToBuy
          
    • 处理油价为 0 的加油站

      • 如果当前加油站的油价为 0(且不是终点),应尽可能多地加油,因为加油成本为 0 元。

      • 加油量为:

        fuelToBuy = min(tankCapacity - currentFuel, distanceToEnd + finalFuel - currentFuel)
        
      • 更新当前油量(无需更新总花费,因为油价为 0)。

    • 更新上一个加油站的位置

      prevDistance = currentStation.distance
      
  3. 最终检查

    • 遍历完所有加油站后,检查到达终点时油量是否至少为 200 升。
    • 如果满足条件,输出 totalCost
    • 否则,输出 Impossible

5. 处理特殊情况

  • 没有加油站

    • 如果没有任何加油站,且初始油量不足以直接到达终点(D ≤ initialFuel - finalFuel),则输出 Impossible
  • 加油站信息不完整

    • 如果某个加油站的输入信息不完整(例如只有距离,没有油价),则视为无效,加油站不可用,可能导致无法完成旅程。
  • 油价为 0 的普通加油站

    • 除终点外,油价为 0 的加油站应作为普通加油站处理,可以免费加油,但仍需遵循油箱容量和最终油量的限制。

算法实现

import java.util.*;

public class Main {
    // 定义加油站类
    static class GasStation implements Comparable<GasStation> {
        int distance; // 距离起点的距离
        int price; // 每升价格

        GasStation(int distance, int price) {
            this.distance = distance;
            this.price = price;
        }

        // 按距离升序排序
        public int compareTo(GasStation other) {
            return this.distance - other.distance;
        }
    }

    public static String solution(int distance, int n, List<List<Integer>> gasStations) {
        // Please write your code here
        // 初始化加油站列表
        List<GasStation> stations = new ArrayList<>();

        // 读取并转换加油站信息
        for (int i = 0; i < n; i++) {
            List<Integer> station = gasStations.get(i);
            if (station.size() != 2) {
                // 加油站信息不完整,无法继续
                return "Impossible";
            }
            int a = station.get(0); // 距离终点的距离
            int b = station.get(1); // 油价
            int distanceFromStart = distance - a;
            if (distanceFromStart < 0 || distanceFromStart > distance) {
                // 无效的加油站位置,忽略
                continue;
            }
            stations.add(new GasStation(distanceFromStart, b));
        }

        // 添加终点作为一个虚拟加油站,价格为 0
        stations.add(new GasStation(distance, 0));

        // 按距离升序排序
        Collections.sort(stations);

        // 初始化参数
        int tankCapacity = 400; // 油箱容量
        int initialFuel = 200;  // 初始油量
        int finalFuel = 200;    // 终点需要保留的油量
        int currentFuel = initialFuel;
        long totalCost = 0;     // 使用 long 防止溢出
        int prevDistance = 0;

        // 遍历每个加油站
        for (int i = 0; i < stations.size(); i++) {
            GasStation currentStation = stations.get(i);
            int distanceToStation = currentStation.distance - prevDistance;
            currentFuel -= distanceToStation;

            if (currentFuel < 0) {
                // 油量不足以到达当前加油站
                return "Impossible";
            }

            // 如果是最后一个加油站(终点),不需要加油
            if (i == stations.size() - 1) {
                // 检查是否有足够的油到达终点并保留 finalFuel
                if (currentFuel < finalFuel) {
                    // 需要额外加油,但终点不可加油
                    return "Impossible";
                }
                // 已到达终点
                break;
            }

            // 寻找下一个油价更低的加油站
            int minPrice = currentStation.price;
            int minPriceIndex = -1;
            for (int j = i + 1; j < stations.size(); j++) {
                GasStation nextStation = stations.get(j);
                int distanceToNext = nextStation.distance - currentStation.distance;
                if (distanceToNext > tankCapacity) {
                    // 超出油箱容量,无法到达
                    break;
                }
                if (nextStation.price < minPrice) {
                    minPrice = nextStation.price;
                    minPriceIndex = j;
                    break;
                }
            }

            if (minPriceIndex != -1) {
                // 找到了下一个油价更低的加油站
                GasStation cheaperStation = stations.get(minPriceIndex);
                int distanceToCheaper = cheaperStation.distance - currentStation.distance;

                // 判断是否是虚拟终点加油站
                boolean isVirtualEndpoint = (cheaperStation.distance == distance && cheaperStation.price == 0);

                // 计算所需油量
                int requiredFuel = distanceToCheaper;
                if (isVirtualEndpoint) {
                    // 如果是终点,确保到达终点后有 finalFuel
                    requiredFuel += finalFuel;
                }

                if (requiredFuel > tankCapacity) {
                    // 无法到达更便宜的加油站(包括终点需要的 finalFuel)
                    return "Impossible";
                }

                if (requiredFuel > currentFuel) {
                    // 需要加油
                    int fuelToBuy = requiredFuel - currentFuel;
                    // 确保不会超过油箱容量
                    fuelToBuy = Math.min(fuelToBuy, tankCapacity - currentFuel);
                    totalCost += (long) fuelToBuy * currentStation.price;
                    currentFuel += fuelToBuy;
                    // 调试输出
                    // System.out.println("加油站位置: " + currentStation.distance + " km, 加油量: " + fuelToBuy + " L, 总花费: " + totalCost);
                }

                // 特殊处理:如果当前加油站油价为 0,尽可能多加油
                if (currentStation.price == 0 && !isVirtualEndpoint) {
                    int maxFuelCanBuy = tankCapacity - currentFuel;
                    // 需要加的油量是 tankCapacity - currentFuel
                    int fuelToBuy = Math.min(maxFuelCanBuy, distance - currentStation.distance - finalFuel);
                    fuelToBuy = Math.max(fuelToBuy, 0); // 不能为负
                    totalCost += (long) fuelToBuy * currentStation.price; // 这里 fuelToBuy * 0 = 0
                    currentFuel += fuelToBuy;
                    // 虽然油价为0,但需要确保不超过所需的油量
                }

            } else {
                // 没有找到更便宜的加油站,尽可能加满
                int distanceToEnd = distance - currentStation.distance;
                // 需要的油量是到终点所需的油量加上最终需留的油量
                int neededFuel = distanceToEnd + finalFuel;
                // 能买的最大油量
                int maxFuelCanBuy = tankCapacity - currentFuel;
                // 需要加的油量
                int fuelToBuy = Math.min(neededFuel - currentFuel, maxFuelCanBuy);
                fuelToBuy = Math.max(fuelToBuy, 0); // 不能为负
                totalCost += (long) fuelToBuy * currentStation.price;
                currentFuel += fuelToBuy;

                // 特殊处理:如果当前加油站油价为 0,尽可能多加油
                if (currentStation.price == 0) {
                    int additionalFuel = Math.min(tankCapacity - currentFuel, distanceToEnd + finalFuel - currentFuel);
                    additionalFuel = Math.max(additionalFuel, 0);
                    totalCost += (long) additionalFuel * currentStation.price; // 0元,不影响总花费
                    currentFuel += additionalFuel;
                }
            }

            // 更新前一个加油站的位置
            prevDistance = currentStation.distance;
        }

        // 检查是否在终点有足够的油量
        if (currentFuel < finalFuel) {
            return "Impossible";
        }

        return String.valueOf(totalCost);
    }

    public static void main(String[] args) {
        List<List<Integer>> gasStations1 = new ArrayList<>();
        gasStations1.add(List.of(100, 1));
        gasStations1.add(List.of(200, 30));
        gasStations1.add(List.of(400, 40));
        gasStations1.add(List.of(300, 20));

        List<List<Integer>> gasStations2 = new ArrayList<>();
        gasStations2.add(List.of(100, 999));
        gasStations2.add(List.of(150, 888));
        gasStations2.add(List.of(200, 777));
        gasStations2.add(List.of(300, 999));
        gasStations2.add(List.of(400, 1009));
        gasStations2.add(List.of(450, 1019));
        gasStations2.add(List.of(500, 1399));

        List<List<Integer>> gasStations3 = new ArrayList<>();
        gasStations3.add(List.of(101));
        gasStations3.add(List.of(100, 100));
        gasStations3.add(List.of(102, 1));

        List<List<Integer>> gasStations4 = new ArrayList<>();
        gasStations4.add(List.of(34, 1));
        gasStations4.add(List.of(105, 9));
        gasStations4.add(List.of(9, 10));
        gasStations4.add(List.of(134, 66));
        gasStations4.add(List.of(215, 90));
        gasStations4.add(List.of(999, 1999));
        gasStations4.add(List.of(49, 0));
        gasStations4.add(List.of(10, 1999));
        gasStations4.add(List.of(200, 2));
        gasStations4.add(List.of(300, 500));
        gasStations4.add(List.of(12, 34));
        gasStations4.add(List.of(1, 23));
        gasStations4.add(List.of(46, 20));
        gasStations4.add(List.of(80, 12));
        gasStations4.add(List.of(1, 1999));
        gasStations4.add(List.of(90, 33));
        gasStations4.add(List.of(101, 23));
        gasStations4.add(List.of(34, 88));
        gasStations4.add(List.of(103, 0));
        gasStations4.add(List.of(1, 1));

        // System.out.println(solution(500, 4, gasStations1) == "4300");
        // System.out.println(solution(500, 7, gasStations2) == "410700");
        // System.out.println(solution(500, 3, gasStations3) == "Impossible");
        // System.out.println(solution(100, 20, gasStations4) == "0");
        // System.out.println(solution(100, 0, new ArrayList<>()) == "Impossible");
        System.out.println(solution(500, 4, gasStations1).equals("4300"));
        System.out.println(solution(500, 7, gasStations2).equals("410700"));
        System.out.println(solution(500, 3, gasStations3).equals("Impossible"));
        System.out.println(solution(100, 20, gasStations4).equals("0"));
        System.out.println(solution(100, 0, new ArrayList<>()).equals("Impossible"));
    }
}