C语言之切蛋糕(运用前缀和算法和单调队列算法)

题目描述

今天是小 Z 的生日,同学们为他带来了一块蛋糕。这块蛋糕是一个长方体,被用不同色彩分成了 n 个相同的小块,每小块都有对应的幸运值。

小 Z 作为寿星,自然希望吃到的蛋糕的幸运值总和最大,但小 Z 最多又只能吃 m(m≤n) 小块的蛋糕。

请你帮他从这 n 小块中找出连续的 k(1≤k≤m) 块蛋糕,使得其上的总幸运值最大。

形式化地,在数列 {pn​} 中,找出一个子段 [l,r](r−l+1≤m),最大化 i=∑​pi​。

输入格式

第一行两个整数 n,m。分别代表共有 n 小块蛋糕,小 Z 最多只能吃 m 小块。

第二行 n 个整数,第 i 个整数 pi​ 代表第 i 小块蛋糕的幸运值。

输出格式

仅一行一个整数,即小 Z 能够得到的最大幸运值。

cs 复制代码
输入
5 2
1 2 3 4 5
输出
9
输入
6 3
1 -2 3 -4 5 -6
输出
5
说明/提示
数据规模与约定
对于 20% 的数据,有 1≤n≤100。
对于 100% 的数据,有 1≤n≤5×10^5,|pi|<=500.
保证答案的绝对值在 [0,2^31−1] 之内。
cs 复制代码
#include <stdio.h>
#include <limits.h>

#define MAXN 10000010

int sum[MAXN];      
int q[MAXN];       
int front = 0, rear = -1;  

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    
    sum[0] = 0;
    for (int i = 1; i <= n; i++) {
        int a;
        scanf("%d", &a);
        sum[i] = sum[i - 1] + a;
    }
    
    int ans = INT_MIN;
    
    q[++rear] = 0;
    
    for (int i = 1; i <= n; i++) {
        while (front <= rear && q[front] < i - m) {
            front++;
        }
        
        if (front <= rear) {
            int current = sum[i] - sum[q[front]];
            if (current > ans) {
                ans = current;
            }
        }
        
        while (front <= rear && sum[q[rear]] >= sum[i]) {
            rear--;
        }
        
        q[++rear] = i;
    }
    
    printf("%d\n", ans);
    
    return 0;
}

解析上述代码:

整体思路是:

因为q[]数组表示的是sum[]数组的下标,但是i是从1开始的,所以再循环开始之前需要将0插入到q[]数组中。之后开始进行循环。循环主要由几部分组成,包括:1.将之前前缀和sum[]数组的下标依次通过循环加入到q[]数组中;2.窗口滑动的条件;3.进行前缀和作差运算,达到q[]数组中有两个元素就可以作差;最后输出最大值。接下来就是考虑这几部分的先后顺序,最终达到目的。

先后顺序怎么考虑呢?

首先将0放到q[]数组中,此时队列是[0];

开始进行循环,先进行作差还是先进行窗口滑动的判断呢?

暂时还看不出来,所以先进行后面循环观察一下才可以进行判断。

当进行第一次循环时,i=1,肯定要把i=1加入到q[]数组中,现在队列变成了[0,1],front=1这个时候感觉可以进行作差了,用sum[front]-?,这个?是在不断变化的,当把它滑出窗口的时候,减去的就不是此时的?了,所以肯定不好判断是什么;那就有可能sum[front]是被减的,那什么来减呢?此时的下标是1,刚好和i=1吻合,所以就是sum[i]-sum[front],刚好无论窗口是否滑动,front一直是窗口的第一个元素下标,所以刚好满足窗口内元素的前缀和。

进行第二次循环时,把i=2加入到q[]数组中,队列变[0,1,2],sum[2]-sum[0]刚好是第二个元素和第三个元素的和。

第三次循环,i=3加入之后,队列变成了[0,1,2,3]front此时还是0,但计算的时候应该是front=1,所以在作差之前就应该进行窗口滑动的检查,因此,窗口滑动检查放在作前缀和差之前。

那元素添加到队列中应该放在哪里呢?

如果放在滑动窗口检查之前,第三次循环直接使队列变成[0,1,2,3],经检验是可行的,但是后面维持单调性时需要稍作修改,特别麻烦。所以我们将元素添加到队列中的操作放在后面(而且是在维护单调性的后面),同样也可以实现,而且维持单调性时的条件更好写。(这种排列单调队列的写法是该算法的原创顺序)

因为要算最大的值,所以必不可少的维持单调性的步骤,因为要求最大值,所以是单调递增,所以如果前面有比当前值还大的,就需要把前面的删去。

先计算作差再进行单调性维护,因为经过一次次循环,已经放到队列中的元素sum[]的值肯定是最小的,也就是最优的,所以先计算才可能是得到历史最大sum,而如果先维护再进行作差,可能将历史最小sum全部删除,最后只留下新添加的元素,这肯定是不对的。所以应该先计算作差再进行单调性维护,最后再添加符合题意的新元素到队列中。

1.#include <limits.h>该头文件,定义各种整数类型的极限值常量

int ans = INT_MIN; // ← 这里用到了limits.h中定义的INT_MIN

limits.h包含的重要常量:

整数类型的最小值:

  • INT_MIN:int类型的最小值(通常是 -2147483648)

  • LONG_MIN:long类型的最小值

  • CHAR_MIN:char类型的最小值

整数类型的最大值:

  • INT_MAX:int类型的最大值(通常是 2147483647)(其他类似上述)

2.为什么要使用int ans = INT_MIN?

求最大值的通用模式:

初始值 = 可能的最小值
for 每个元素:
if (当前值 > 当前最大值):
更新最大值

为什么不能初始化为0?就是因为如果当所有的数字都是负数时,会出现下面的情况

int ans = 0;
int arr[] = {-5, -3, -8}; // 实际最大值是-3

for (int i = 0; i < 3; i++) {
if (arr[i] > ans) { // -5 > 0? 不成立
ans = arr[i]; // 永远不会更新!
}
}
// 最终ans = 0,但正确答案应该是-3 ❌

这时就要正确初始化,即利用上面int ans = INT_MIN;

3.为什么用q[front]<i-m作为判断条件,而不是q[front]<i-m+1呢?

while (front <= rear && q[front] < i - m) {

front++;

}

首先明确上述代码while循环是用来处理由于窗口右移而滑出的元素的。

常规情况(i-m+1)

当我们说子段 a[l]...a[r] 时:

  • 长度 = r - l + 1

  • 限制:r - l + 1 ≤ m

  • 所以:l ≥ r - m + 1

但对于该题目队列存储的是前缀和数组sum[]的下标,子段和 = sum[i] - sum[j]。

  • sum[i] = a[1] + a[2] + ... + a[i]

  • sum[j] = a[1] + a[2] + ... + a[j]

所以: sum[i] - sum[j] = a[j+1] + a[j+2] + ... + a[i]

这意味着:

  • 子段从 j+1 开始

  • 子段到 i 结束

  • 子段长度 = i - j(不是 i-j+1!)

限制条件:i - j ≤ m -> j ≥ i - m。

因为q[]数组的值是sum[]数组的下标,通过q[]数组进行窗口滑动,来保证sum[i]-sum[j]是不超过m块的,且是这几块的和。由上述sum[i]-sum[j]可以得到j是滑动窗口的初始位置,即j=q[front],替换上去就是q[front]<i-m时才能保证该元素是在该窗口里边。这个和之前窗口滑动的区别就是这个题目不需要删除不符合题意的元素,而只删除被滑动窗口滑出的元素。

每次都更新最大值,但是只有在最后才输出最终的最大值。

假设数组:[3, -1, 2, 4], m=2

前缀和: sum[0]=0, sum[1]=3, sum[2]=2, sum[3]=4, sum[4]=8

初始化: q = [0] (存储j=0)

循环开始:
i=1:
`

  • 队列当前: q = [0] (j=0)//还没开始循环的时候,先将sum[]数组的最初下标作为第一个值放到q[]数组中。
  • 操作后: q = [0, 1] (添加了当前的i=1)//进行第一次循环之后,把当前的i=1添加到q[]数组中

    i=2:
  • 队列当前: q = [0, 1] (j=0, j=1)`

//过期检查不成立,所以下一步是sum[2]-sum[0]是下标为0,1,2的元素的和
`

  • 操作后: q = [0, 1, 2] (添加了当前的i=2)//进行第二次循环后,把i=2添加到q[]数组中

    i=3:
  • 队列当前: q = [0, 1, 2] (j=0, j=1, j=2)
  • 过期检查: m=2, i=3, i-m=1
    检查q[front]=0 < 1? 成立,移除0//已知窗口的大小和之前判断好的元素滑出条件,进行该操作,元素删除之后刚好再添加上一个新的元素。而前缀和的减法运算是在滑出该元素之后,算的是sum[3]-sum[1]的和。在每一次计算差之前都会进行过期检查,刚好可以在q[]数组中留下两个值,用来进行作差,这正好符合窗口大小。
    队列变为: [1, 2]
  • 操作后: q = [1, 2, 3] (添加了当前的i=3)

    i=4:
  • 队列当前: q = [1, 2, 3] (j=1, j=2, j=3)
  • 过期检查: i=4, i-m=2
    检查q[front]=1 < 2? 成立,移除1
    队列变为: [2, 3]
  • 操作后: q = [2, 3, 4] (添加了当前的i=4)`
相关推荐
量子炒饭大师2 小时前
【C++入门】Cyber骇客构造器的核心六元组 —— 【类的默认成员函数】明明没写构造函数也能跑?保姆级带你掌握六大类的默认成员函数(上:函数篇)
开发语言·c++·dubbo·默认成员函数
漫漫求2 小时前
Go的panic、defer、recover的关系
开发语言·后端·golang
Tony Bai2 小时前
2025 Go 官方调查解读:91% 满意度背后的隐忧与 AI 时代的“双刃剑”
开发语言·后端·golang
沐知全栈开发2 小时前
R 绘图 - 饼图
开发语言
进击的小头2 小时前
连续系统离散化方法(嵌入式信号处理实战指南)
c语言·算法·信号处理
charlie1145141912 小时前
嵌入式C++开发——RAII 在驱动 / 外设管理中的应用
开发语言·c++·笔记·嵌入式开发·工程实践
Fcy6482 小时前
C++11 新增特性(中)
开发语言·c++·c++11·可变参数模版·c++11 类的新增功能·c++11slt新增特性
小码过河.2 小时前
17装饰器模式
开发语言·python·装饰器模式
嫂子开门我是_我哥2 小时前
第八节:条件判断与循环:解锁Python的逻辑控制能力
开发语言·python