算法(二)——一维差分、等差数列差分

文章目录

一维差分、等差数列差分


一维差分

差分解决的是 区间修改(更新)问题,特别是多次区间修改问题,能将每次修改的复杂度控制到O(1),解题的一般步骤:

  • 初始化差分数组

    对于一个给定的数组 a,其差分数组 d 的定义如下:

    1. d[0] = a[0]
    2. 对于 i 从 1 到 n−1,d[i]=a[i]−a[i−1]

    这样,差分数组 d 的每个元素 d[i] 表示原数组 a 中第 i 个元素与第 i−1 个元素的差值。

  • 确定修改区间并根据修改要求修改差分数组

    假设我们要修改的区间是lr,其中lr分别是区间的起始位置和结束位置,要修改的操作是:对[l, r]区间内的所有元素加一个数num,修改的操作为:差分数组d[l] += numd[r + 1] -= num(前提:r + 1 不会越界,不过通常我们可以申请一个较大数组避免越界即可)

  • 差分数组求前缀和得到修改后的数组

    之前区间修改操作都在差分数组中体现,差分数组其实就代表了最终的数组,求解最终数组只需要计算差分数组的前缀和即可,具体:

    1. 初始化前缀和数组 s 的第一个元素 s[0] 为差分数组 d 的第一个元素 d[0] ,即 s[0] = d[0]
    2. 对于 i 从 1 到 n−1 ,计算 s[i] = s[i − 1] + d[i]

举一个例子,假如有一个数组a: [3, 5, 2, 6, 4],修改操作:

  • [1, 3]的元素加5
  • [2, 4]的元素减2
  1. 初始化差分数组d: [3, 2, -3, 4, -2]
  2. 处理第一次修改得:d: [3, 7, -3, 4, -7]
  3. 处理第二次修改得:d: [3, 7, -5, 4, -7]
  4. 得到最终数组:s: [3, 10, 5, 9, 2]

例题:航班预订统计

原题链接:1109. 航班预订统计 - 力扣(LeetCode)


这是一道典型的一维差分问题,只是初始数组是一个全零数组,意味着差分数组也是全零的,我们只需要每次O(1)地修改差分数组,最后求前缀和返回即可。

java 复制代码
class Solution {
    public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] ret = new int[n];
        for(int i = 0; i < bookings.length; i++) {
            ret[bookings[i][0] - 1] += bookings[i][2];
            if(bookings[i][1] - 1 < n - 1) {
                ret[bookings[i][1]] -= bookings[i][2];
            }
        }
        for(int i = 1; i < n; i++) {
            ret[i] += ret[i - 1];
        }
        return ret;
    }
}
  • 这里只创建了一个数组,通过下标映射将其正确地更新为最终数组

等差数列差分

等差数列差分的问题描述:

原数组全为0,接下来会有多个区间修改操作,每次操作:数组的 l~r 范围上的元素依次加上首项s、尾项e、公差d的等差数列,最终返回多次更新后的结果。

等差数列差分当然也是区间修改问题,题目会给出五个参数(有时没有给出5个参数,但是缺少的参数可以由其他参数得到,例如公差):

  • l:修改区间的起始位置
  • r:修改区间的结束位置
  • s:等差数列的首项
  • e:等差数列的尾项
  • d:等差数列的公差

等差数列差分的做题步骤:

  • 由于原数组全0,因此差分数组实际上也是全0,可以将原数组作为差分数组使用
  • 对于差分数组d[],执行如下修改:d[l] += sd[l + 1] += d - sd[r + 1] -= d + ed[r + 2] += e
  • 最终对差分数组求 两次前缀和 得到最终数组

举个例子:现对一个全0数组[0, 0, 0, 0, 0, 0]进行如下修改操作:(结果应该是:[0, 3, 7, 16, -1, -7])

  • [1, 3]区间的元素从左往右加上首项为3,尾项为11的等差序列的每一项
  • [3, 5]区间的元素从左往右加上首项为5,尾项为-7的等差数列的每一项
  1. 第一次修改:d[1] += 3d[1 + 1] += (4 - 3) d[3 + 1] -= (4 + 11)d[3 + 2] += 11 ,得到d: [0, 3, 1, 0, -15, 11]
  2. 第二次修改:d[3] += 5d[3 + 1] += (-6 - 5) d[5 + 1] -= (-6 + (-7))(越界,但是我们可以一开始就选择创建一个较大的数组),d[5 + 2] += (-7) ,得到d: [0, 3, 1, 5, -26, 11]
  3. 求两次前缀和:[0, 3, 4, 9, -17, -6][0, 3, 7, 16, -1, -7],第二次前缀和的结果就是最终数组。

简单反向验证一下4步修改(d[l] += sd[l + 1] += d - sd[r + 1] -= d + ed[r + 2] += e )的公式:

假设原数组:[0, 0, 0, 0, 0, 0, 0, 0],已知l、r、s、e、d

  • 第二次前缀和的结果就是最终结果,反推第一次前缀和的结果如图,再次反推四次修改后的结果,可以发现,l位置 += sl+1位置 += d-sr+1位置 += (-d-e) <=> r+1位置 -= d+er+2位置 += e

例题:三步必杀

原题链接:P4231 三步必杀 - 洛谷 | 计算机科学教育新生态


很典型的等差数列差分问题,基本没有绕弯子,可以直接用技巧解决,关键是看出能得出5个参数,也很简单不再赘述。

另外,洛谷是ACM模式的,注意上一篇介绍的输入输出的处理。

java 复制代码
import java.io.*;

//洛谷的主类必须是Main
public class Main {
    public static int MAX_N = 10_000_005;
    public static long[] arr = new long[MAX_N];
    public static int m, n;
    public static void main(String[] args) throws IOException {
        try(BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out))) {
            StreamTokenizer in = new StreamTokenizer(br);
            while(in.nextToken() != StreamTokenizer.TT_EOF) {
                n = (int) in.nval;
                in.nextToken();
                m = (int) in.nval;
                for(int i = 0, l, r, s, e; i < m; i++) {
                    in.nextToken();
                    l = (int) in.nval;
                    in.nextToken();
                    r = (int) in.nval;
                    in.nextToken();
                    s = (int) in.nval;
                    in.nextToken();
                    e = (int) in.nval;
                    set(l, r, s, e, (e - s) / (r - l));
                }
                build();
                long max = arr[1];
                long ret = arr[1];
                for(int i = 2; i <= n; i++) {
                    if(arr[i] > max) {
                        max = arr[i];
                    }
                    ret ^= arr[i];
                }
                out.println(ret + " " + max);
                out.flush();
            }
        }
    }

    //两次前缀和
    private static void build() {
        for(int i = 2; i <= n; i++) {
            arr[i] += arr[i - 1];
        }
        for(int i = 2; i <= n; i++) {
            arr[i] += arr[i - 1];
        }
    }
    
    //四次修改
    private static void set(int l, int r, int s, int e, int d) {
        arr[l] += s;
        arr[l + 1] += d - s;
        arr[r + 1] -= d + e;
        arr[r + 2] += e;
    }
}

例题:Lycanthropy

原题链接:P5026 Lycanthropy - 洛谷 | 计算机科学教育新生态


粗略地读题目,可以捕捉到有等差数列以及修改的关键字,并且可以确定全零原数组,因此可以判定这是一道等差数列差分问题。

想象一下,一个物体落水了,落水点周围的水位是不是下降了,再往外水就会溅起来,落水范围由落水点以及方块体积决定,大体状况如下图:

我们能够观察到四个单调区间(题目描述了6个,但是可以合成为4个),因此每个小方块落水,会对数组进行四个区间修改,处理好边界得出5个参数即可。

java 复制代码
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.io.IOException;
import java.io.BufferedWriter;

public class Main {
    public static int MAX_L = 1_000_001;
    public static int OFFSET = 30001;
    public static int[] arr = new int[OFFSET + MAX_L + OFFSET];
    public static int n, m;

    public static void main(String[] args) throws IOException {
        try(BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)))) {
            StreamTokenizer in = new StreamTokenizer(br);
            while(in.nextToken() != StreamTokenizer.TT_EOF) {
                n = (int) in.nval;
                in.nextToken();
                m = (int) in.nval;
                for(int i = 0, v, x; i < n; i++) {
                    in.nextToken();
                    v = (int) in.nval;
                    in.nextToken();
                    x = (int) in.nval;
                    set(v, x);
                }
                build();
                int start = OFFSET + 1;
                out.print(arr[start++]);
                for(int i = 2; i <= m; i++) {
                    out.print(" " + arr[start++]);
                }
                out.flush();
            }
        }
    }

    public static void set(int v, int i) {
        _set(i - 3 * v + 1, i - 2 * v, 1, v, 1);
        _set(i + 2 * v + 1, i + 3 * v - 1, v - 1, 1, -1);
        _set(i - 2 * v + 1, i, v - 1, -v, -1);
        _set(i + 1, i + 2 * v, -v + 1, v, 1);
    }
    public static void _set(int l, int r, int s, int e, int d) {
        arr[l + OFFSET] += s;
        arr[l + 1 + OFFSET] += d - s;
        arr[r + 1 + OFFSET] -= d + e;
        arr[r + 2 + OFFSET] += e;
    }

    public static void build() {
        for(int i = 1; i <= m + OFFSET; i++) {
            arr[i] += arr[i - 1];
        }
        for(int i = 1; i <= m + OFFSET; i++) {
            arr[i] += arr[i - 1];
        }
    }
}
  • 关键就是确定每次落水的四个区间的5个参数,不要有交集
  • 这道题目有一个数组越界的注意点,即:如果落水位置十分靠左,并且体积较大时(意味着会波及到负的下标位置,此时数组越界),因此我们可以创建一个较大的数组,有效区域在中间,左边是一倍偏移量,右边同理,这样就避免了各种复杂边界逻辑判断。

相关推荐
不二青衣15 分钟前
利用平面进行位姿约束优化
算法·平面
Wang's Blog17 分钟前
数据结构与算法之二叉树: LeetCode 654. 最大二叉树 (Ts版)
算法·leetcode
不二青衣20 分钟前
使用gtsam添加OrientedPlane3Factor平面约束因子
人工智能·算法·平面
985小水博一枚呀30 分钟前
【大厂面试AI算法题中的知识点】方向涉及:ML/DL/CV/NLP/大数据...本篇介绍自动驾驶检测模型如何针对corner case 优化?
人工智能·深度学习·神经网络·算法·面试·cnn
飞哥不鸽1 小时前
《leetcode-runner》如何手搓一个debug调试器——引言
算法·leetcode·开源·调试器·插件开发·项目架构
深图智能1 小时前
OpenCV实现彩色图像的直方图均衡化
图像处理·opencv·算法·计算机视觉
siy23332 小时前
[c语言日寄]c语言也有“回”字的多种写法——整数交换的三种方式
c语言·开发语言·笔记·学习·算法
van叶~2 小时前
算法妙妙屋-------2..回溯的奇妙律动
c++·算法
闲人编程4 小时前
PID控制器 (Proportional-Integral-Derivative Controller) 算法详解及案例分析
python·算法·pid·路径规划·微分控制·积分控制·比例控制
诚丞成4 小时前
字符串算法篇——字里乾坤,算法织梦,解构字符串的艺术(下)
c++·算法