2.2 算法之 前缀和

本文将介绍一维前缀和这一算法技巧,其常用于优化其他算法,并帮助读者巩固相关知识点,以期提升对前缀和思想的理解。前置知识包括语言基础,目标是深入掌握一维前缀和的应用。

学习笔记参考:https://leetcode.cn/leetbook/read/qian-zhui-he/nk1f5i/

一维前缀和

前缀和是算法题中一个重要的技巧,经常作为一种优化方式出现在其他算法的一些子环节中。本课程就来带着同学们整理与巩固前缀和相关知识点,在学习完本课程之后,希望同学们对前缀和的思想有一个更加深入的理解。

本小节所需前置知识:语言基础

本小节所讲述的知识:一维前缀和

TBD

3147. 从魔法师身上吸取的最大能量

在神秘的地牢中,n 个魔法师站成一排。每个魔法师都拥有一个属性,这个属性可以给你提供能量。有些魔法师可能会给你负能量,即从你身上吸取能量。

你被施加了一种诅咒,当你从魔法师 i 处吸收能量后,你将被立即传送到魔法师 (i + k) 处。这一过程将重复进行,直到你到达一个不存在 (i + k) 的魔法师为止。

换句话说,你将选择一个起点,然后以 k 为间隔跳跃,直到到达魔法师序列的末端,在过程中吸收所有的能量。

给定一个数组 energy 和一个整数k,返回你能获得的 最大 能量。

示例 1:
输入: energy = [5,2,-10,-5,1], k = 3
输出: 3
解释:可以从魔法师 1 开始,吸收能量 2 + 1 = 3。
示例 2:
输入: energy = [-2,-3,-1], k = 2
输出: -1
解释:可以从魔法师 2 开始,吸收能量 -1。

提示:
1 <= energy.length <= 10^5
-1000 <= energy[i] <= 1000
1 <= k <= energy.length - 1

朴素解法1(超时)

int maximumEnergy(int *energy, int energySize, int k)
{
    int *res = (int*)malloc(sizeof(int) * energySize);
    memset(res, 0, sizeof(int) * energySize);
    int j = 0;
    for (int i = 0; i < energySize; i++) {
        // res[i]  = energy[i];
        int cnt = (energySize + k - 1) / k;
        j = 0;
        while ((i + j * k < energySize) && (cnt > 0)) {
            res[i] = res[i] + energy[i + j * k];
            j++;
            cnt--;
        }
    }
    for (j = 0; j < (energySize); j++) {
        printf("%d ", res[j]);
    }
    int result = -1001;
    for (int i = 0; i < energySize; i++) {
        result = fmax(result, res[i]);
    }
    return result;
}
int main()
{
    int energy[3] = {-2, -3, -1};
    int energySize = 3;
    int k = 2;
    int ret = maximumEnergy(energy, energySize, k);
    return 0;
}

在这里插入图片描述
k = 1
从特殊到一般,先想一想,k=1 怎么做?

此时只能一步一步地向右走。无论起点在哪,终点都是 n−1。

如果选择 i 为起点,我们计算的是子数组 [i,n−1] 的元素和,即后缀和。

后缀和怎么算?我们可以倒着遍历 energy,同时累加元素和,即为后缀和。

答案等于所有后缀和的最大值。

k = 2
再想一想,k=2 怎么做?

此时我们有两个终点:n−2 和 n−1。

对于终点 n−1:
如果选择 n−3 为起点,那么我们累加的是下标为 n−3,n−1 的元素和。
如果选择 n−5 为起点,那么我们累加的是下标为 n−5,n−3,n−1 的元素和。
如果选择 n−7 为起点,那么我们累加的是下标为 n−7,n−5,n−3,n−1 的元素和。
一般地,从 n−1 开始倒着遍历,步长为 −k=−2,累加元素和,计算元素和的最大值。

对于终点 n−2:
如果选择 n−4 为起点,那么我们累加的是下标为 n−4,n−2 的元素和。
如果选择 n−6 为起点,那么我们累加的是下标为 n−6,n−4,n−2 的元素和。
如果选择 n−8 为起点,那么我们累加的是下标为 n−8,n−6,n−4,n−2 的元素和。
一般地,从 n−2 开始倒着遍历,步长为 −k=−2,累加元素和,计算元素和的最大值。

是否可以从 n−3 开始倒着遍历?
不行,因为 n−3 还可以向右跳到 n−1,所以 n−3 不是终点,不能作为倒着遍历的起点。

一般情况
枚举终点 n−k,n−k+1,…,n−1,倒着遍历,步长为 −k。
遍历的同时累加元素和 sufSum,计算 sufSum 的最大值,即为答案。

#define MAX(a, b) ((b) > (a) ? (b) : (a))
int maximumEnergy(int* energy, int energySize, int k){
    int ans = INT_MIN;
    for (int i = energySize - k; i < energySize; i++) { // 枚举终点 i,k个
        int suf_sum = 0;
        for (int j = i; j >= 0; j -= k) {
            suf_sum += energy[j]; // 计算后缀和
            ans = MAX(ans, suf_sum);
        }
    }
    return ans;
}
### 前缀和与差分算法的基本概念及实现原理 #### 一、前缀和 ##### 1.1 一维前缀和 一维前缀和的核心思想是对数组的部分和进行预处理,从而快速查询任意区间的和。对于长度为 $n$ 的数组 $a[]$,定义其前缀和数组 $s[]$ 如下: $$ s[i] = \sum_{j=1}^{i} a[j] $$ 即 $s[i]$ 表示从数组的第一个元素到第 $i$ 个元素的累加和[^1]。 在实际应用中,可以通过一次遍历完成前缀和数组的构建,时间复杂度为 $O(n)$。之后,任何区间 $[l, r]$ 的和都可以通过如下公式快速计算得出: $$ sum(l, r) = s[r] - s[l-1] $$ 其中需要注意的是边界条件,当 $l=1$ 时,应特别处理以避免访问非法索引[^3]。 以下是基于 C++ 实现的一维前缀和代码示例: ```cpp #include <iostream> using namespace std; const int N = 100010; int n, m; int a[N], s[N]; int main() { cin >> n >> m; for(int i = 1; i <= n; i++) cin >> a[i]; // 构建前缀和数组 for(int i = 1; i <= n; i++) s[i] = s[i-1] + a[i]; while(m--){ int l, r; cin >> l >> r; cout << s[r] - s[l-1] << endl; // 计算区间 [l,r] 的和 } } ``` ##### 1.2 二维前缀和 二维前缀和扩展了一维的概念至矩阵上。设有一个大小为 $n\times m$ 的矩阵 $mat[][]$ ,则可以定义一个对应的前缀和矩阵 $prefixSum[][]$ 。具体而言, $$ prefixSum[x][y] = mat[x][y] + prefixSum[x-1][y] + prefixSum[x][y-1] - prefixSum[x-1][y-1] $$ 这里的关键在于理解重叠区域的影响并加以修正。 #### 二、差分 ##### 2.1 一维差分 差分的思想类似于积分与微分的关系。给定一个数组 $b[]$ ,如果存在另一个数组 $c[]$ 满足以下关系,则称 $c[]$ 是 $b[]$ 的差分序列: $$ b[i] = c[1]+c[2]+\ldots+c[i]=\sum _{k=1}^{i}c[k]. $$ 因此,在某些情况下,通过对原始数据做差分变换后再施加修改操作最后还原回去的方式能够极大地简化批量更新过程[^2]。 例如要对某个范围内的数值统一增加或者减少固定量值的时候,仅需调整该范围内首尾两端对应位置上的差分数即可达到目的而无需逐一改变每一个受影响的位置的数据项。 下面给出一段简单的C++程序来展示如何利用差分技术高效解决此类问题: ```cpp #include <bits/stdc++.h> using namespace std; void apply_diff(vector<int>& diff,int start,int end,int val){ diff[start]+=val; if(end+1<diff.size()) diff[end+1]-=val; } vector<int> get_result(const vector<int>& diff){ vector<int> res(diff.size()); res[0]=diff[0]; for(size_t i=1;i<res.size();i++) res[i]=res[i-1]+diff[i]; return res; } // Example usage omitted here. ``` ##### 2.2 二维差分 同样地,也可以推广到更高维度的情况下去考虑更复杂的场景下的优化策略。 总结来说,无论是哪种形式的前缀和还是差分方法论都旨在提升特定类型运算效率的同时保持逻辑清晰简洁易于维护的特点。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值