徒步旅行中的补给问题 | 豆包MarsCode AI刷题

3 阅读5分钟

问题描述

小R正在计划一次从地点A到地点B的徒步旅行,总路程需要 N 天。为了在旅途中保持充足的能量,小R每天必须消耗1份食物。幸运的是,小R在路途中每天都会经过一个补给站,可以购买食物进行补充。然而,每个补给站的食物每份的价格可能不同,并且小R最多只能同时携带 K 份食物。

现在,小R希望在保证每天都有食物的前提下,以最小的花费完成这次徒步旅行。你能帮助小R计算出最低的花费是多少吗?


测试样例

样例1:

输入:n = 5 ,k = 2 ,data = [1, 2, 3, 3, 2]
输出:9

样例2:

输入:n = 6 ,k = 3 ,data = [4, 1, 5, 2, 1, 3]
输出:9

样例3:

输入:n = 4 ,k = 1 ,data = [3, 2, 4, 1]
输出:10

解题思路:

这个问题可以通过动态规划或贪心算法来解决。在这里,我们可以使用贪心算法,因为小R每天都需要消耗食物,我们可以假设小R总是在价格最低的补给站购买尽可能多的食物,直到他的背包满了或者不再需要购买食物为止。也就是说是要找到一个滑动窗口内元素的最小值的总和。滑动窗口的大小为k,并且窗口在数组data上从左向右滑动。

  1. 初始化:创建一个LinkedList作为栈来存储滑动窗口内的元素,并初始化结果变量res为0。

  2. 遍历数组:对于数组data中的每个元素,执行以下步骤:

    • 如果栈为空或者当前元素大于等于栈顶元素,将当前元素添加到栈顶。
    • 如果当前元素小于栈顶元素,则弹出栈顶元素,直到栈为空或者当前元素大于等于新的栈顶元素,然后将当前元素添加到栈顶。
  3. 维护滑动窗口:当索引i大于等于k-1时,意味着窗口已经填满,需要开始滑动。检查栈的第一个元素是否等于窗口最左边的元素(即data[i-k]),如果是,则从栈中移除该元素。

  4. 累加结果:每次循环结束时,将栈顶元素(即当前窗口的最小值)加到结果res上。

  5. 返回结果:遍历结束后,返回累加的结果res

相关知识点说明:

  • 栈(Stack) :栈是一种后进先出(LIFO)的数据结构。在Java中,LinkedList可以当作栈使用,其提供了addLast()removeLast()方法分别对应栈的pushpop操作。
  • 滑动窗口:这是一种常用的数组/列表处理技巧,通常用于处理固定大小窗口内的元素。窗口在数组上滑动,以包含连续的子数组。
  • 最小值维护:使用栈来维护滑动窗口内的最小值。栈底到栈顶的元素是单调递增的,这样栈顶始终是当前窗口的最小值。

代码实现

以下是代码实现

import java.util.*;

public class Main {
    public static int solution(int n, int k, int[] data) {
        // Edit your code here
        int res = 0;
        LinkedList<Integer> stack = new LinkedList<>();
        int len = data.length;
        for(int i = 0; i<len; i++){
            if(stack.isEmpty() || data[i] >= stack.getLast()){
                stack.addLast(data[i]);
            }else{
                while(!stack.isEmpty() && data[i] < stack.getLast()){
                    stack.removeLast();
                }
                stack.addLast(data[i]);
            }
            if(i>k-1 && stack.getFirst() == data[i-k]){
                stack.removeFirst();
            }
            res+=stack.getFirst();
        }
        return res;
    }
    
    public static void main(String[] args) {
        // Add your test cases here

        System.out.println(solution(5, 2, new int[]{1, 2, 3, 3, 2}) == 9);
        System.out.println(solution(6, 3, new int[]{4, 1, 5, 2, 1, 3}) == 9);
        System.out.println(solution(4, 1, new int[]{3, 2, 4, 1}) == 10);
        
    }
}

这个贪心算法假设了最优解是在每个补给站购买尽可能多的食物,直到背包满或者不需要再购买。这种方法在大多数情况下都是有效的,因为它总是试图在价格最低的时候购买尽可能多的食物。然而,需要注意的是,贪心算法并不总是能得到全局最优解,但在这种情况下,它通常能给出正确的答案。

时间复杂度

solution方法中,主要的操作是遍历数组data,并在遍历过程中对栈stack进行操作。以下是详细的分析:

  1. 遍历数组:代码包含一个循环,该循环遍历数组data的每个元素一次。因此,这部分的时间复杂度是O(n)。

  2. 栈操作

    • 在每次迭代中,可能需要将元素添加到栈中,这是O(1)操作。
    • 如果当前元素小于栈顶元素,那么可能需要弹出多个元素直到找到小于或等于当前元素的元素,最坏情况下可能需要弹出栈中所有元素。然而,由于每个元素最多只会被弹出一次,因此整个算法中的总弹出次数是O(n),即使是在最坏情况下。

由于上述操作是嵌套的,但每个元素最多只会被添加和弹出一次,因此整体的时间复杂度仍然是O(n)。

空间复杂度

solution方法中:

  1. stack:在最坏的情况下,如果数组data中的元素是单调递增的,那么栈可能需要存储所有元素。因此,栈的空间复杂度是O(n)。
  2. 其他变量:如resleni等变量只占用常数空间,因此它们的空间复杂度是O(1)。

综合上述分析,空间复杂度是O(n)。

总结

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

这里假设n是数组data的长度。需要注意的是,尽管代码在每次迭代中都累加了栈顶元素到res,但这并不改变时间复杂度,因为累加操作是常数时间的。