AcWing 蓝桥杯AB组辅导课 05、树状数组与线段树

news2024/10/7 10:17:43

文章目录

  • 前言
  • 一、树状数组
    • 1.1、树状数组知识点
    • 1.2、树状数组代码模板
    • 模板题:AcWing 1264. 动态求连续区间和
    • 例题
      • 例题1、AcWing 1265. 数星星【中等,信息学奥赛一本通】
    • 习题
      • 习题1:1215. 小朋友排队【中等,蓝桥杯】
  • 二、 线段树
    • 知识点
    • 模板题:AcWing 1264. 动态求连续区间和
    • 例题
      • 例题1:1270. 数列区间最大值【简单】
    • 习题
      • 习题1:1228. 油漆面积【困难,蓝桥杯】
  • 三、差分
    • 知识点
    • 一维差分模板题:ACWing 797. 差分
    • 二维差分模板题:AcWing 798. 差分矩阵
    • 习题
      • 习题1:1232. 三体攻击【困难,蓝桥杯,三维差分】
  • 其他知识点习题(数学,找规律)
    • 习题1:1237. 螺旋折线(中等,蓝桥杯)
  • 参考文章

前言

前段时间为了在面试中能够应对一些算法题走上了刷题之路,大多数都是在力扣平台刷,目前是300+,再加上到了新学校之后,了解到学校也有组织蓝桥杯相关的程序竞赛,打算再次尝试一下,就想系统学习一下算法(再此之前是主后端工程为主,算法了解不多刷过一小段时间),前段时间也是第一次访问acwing这个平台,感觉上面课程也是比较系统,平台上题量也很多,就打算跟着acwing的课程来走一段路,大家一起共勉加油!

  • 目前是打算参加Java组,所以所有的题解都是Java。、

所有博客文件目录索引:博客目录索引(持续更新)

本章节树状数组、线段树及找规律的习题一览:包含所有题目的Java题解链接

image-20221011161625200

例题:

  • AcWing 1264. 树状数组-模板题 动态求连续区间和 Java题解、AcWing 1264. 线段树-模板题 动态求连续区间和 Java题解
  • AcWing 1265. 树状数组-例题 数星星 Java题解
  • AcWing 1270. 线段树-习题 数列区间最大值 Java题解

习题:

  • AcWing 1215. 树状数组-习题 小朋友排队 Java题解(树状数组、归并排序两种解法,包含详细思路)
  • AcWing 1228. 线段树-油漆面积 Java题解(详细图示+思路分析,困难,蓝桥杯)
  • AcWing 1232. 差分—习题 三体攻击(三维差分,蓝桥杯) Java题解(详细分析及推导)
  • AcWing 1237. 找规律—习题 螺旋折线 Java题解(含详细分析)

学习三体攻击题目的前置知识点-差分(一维、二维):

  • AcWing 797. 一维差分-模板题 差分 Java题解(详细推导)
  • AcWing 798. 二维差分-模板题 差分矩阵 Java详细题解(包含分析思路)

一、树状数组

前缀和是离线做法,树状数组是在线做法。

1.1、树状数组知识点

学习:树状数组详解

树状数组或二叉索引树(Binary Indexed Tree),又以其发明者命名为 Fenwick 树。其初衷是解决数据压缩里的累积频率的计算问题,现多用于高效计算数列的前缀和、区间和。

复杂度:

  • 时间复杂度:O(logn) 的时间得到任意前缀和,并同时支持在 O(logn) 时间内支持动态单点值的修改。
  • 空间复杂度 O(n)。

重点就是利用二进制的变化,动态地更新树状数组。

支持的功能:单点查询、区间修改(前缀和)

  • 其他问题可以通过转化来进行求解,如:
    • ①进行区间修改、单点查询需要采用一个差分的思想并进行转化才可以。
    • ②进行区间修改、区间查询也是差分。

与前缀和比较:

  • 前缀和:查询O(1);修改O(n)。总和时间为O(n)
  • 树状数组:查询O(logn);修改O(logn)。总和时间为logn

对于没有修改的时候使用前缀和是比较明智的选择,若是有大量的修改操作那么就不太建议使用前缀和了!

需要证明的点:

1、为什么之后一个结点包含它?

2、为什么这个证明就是加上lowbit?

具体介绍

image-20221012165452239

其中对于树状数组中某个下标位置表示的是一段范围的总值,例如:sum[1]表示[1,1]中的值,sum[3]表示[3,3]中的值,sum[6]表示[5,6]中的值,对于其中具体范围是根据一个lowbit()函数来进行计算的。

  • lowbit(i) 表示 (i - lowbit(i), i]。一般的话lowbit(i) 实际上是使用 i & (i - 1)来计算得到这个值。
    • lowbit()函数计算得到的实际上是对应二进制从右到左第一个1的位置(另其为k = index-1),得到的值为2k
    • 如6,其二进制是110,第1个1从右到左是第2个,那么值为21=2,前一个位置元素+当前位置元素就是两个,表示该C[6]=A[5]+A[6]
  • 例如lowbit(1)得到值为1,lowbit(3)得到值为1,lowbit(6)得到值为2。

①计算C[i]的值,实际上就是在对应每个相应范围的父节点上进行加值

//其中4是目标位置,val表示该4位置需要相加的和
//更新一个值,需要更新后面的分段值
for (int i = 4; i <= 9; i += lowbit(i)){
	C[i] += val;
}

//例如更新4位置,值为2
第一轮:i = 4, c[4] += 2lowbit(4)=4
第二轮:i = 8, c[8] += 4lowbit(8)=8
第三轮结束

②计算[0, i]范围前缀和

lowbit只是辅助得到index的合并范围,之后我们想要得到前缀和也是通过该函数来进行合并计算的。时间复杂度为O(logn)

举例:求6的前缀和

//计算和
for (int i = 6; i > 0; i -= lowbit(i)) {
	ret += C[i];
}

第一轮:i = 6, lowbit(6) = 2   ret += C[6]
第二轮:i = 4, lowbit(4) = 4   ret += C[4]
第三轮结束
其中C[6] = sum[5] + sum[6]
   C[4] = C[2] + sum[3] + sum[4],又其中C[2]=sum[1] + sum[2]

1.2、树状数组代码模板

public class Main {

    static final int N = 10010;
    public static int[] C = new int[N];
    //具体数组中的元素值
    public static int n;

    //根据当前的点确定该点的范围[i - lowbit(i), i]
    static int lowbit(int i) {
        return i & -i;//或者是return i-(i&(i-1));表示求数组下标二进制的非0最低位所表示的值
    }

    static void add(int x, int val)//单点更新
    {
        for (int i = x; i <= n; i += lowbit(i)) {
            C[i] += val;//由叶子节点向上更新树状数组C,从左往右更新
        }
    }

    static int sum(int x)//求区间[1,i]内所有元素的和
    {
        int ret = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            ret += C[i];//从右往左累加求和
        }
        return ret;
    }

    public static void main(String[] args) {
        n = 6;
        //原始数组初始化
        int[] arr = new int[7];
        for (int i = 1; i <= n; i++) {
            arr[i] = i;
        }
        //初始化树状数组
        for (int i = 1; i <= n; i++) {
            add(i, arr[i]);
        }
        //实践
        //1、查询前5个值,当前数组为{1, 2, 3, 4, 5, 6}
        System.out.println(sum(3));//预计值为6
        //2、给第二个位置上加上2。此时arr数组为{1, 4, 3, 4, 5, 6}
        add(2, 2);
        //3、计算[1, 4]范围的值。
        System.out.println(sum(4) - sum(1));//预计值为11
    }

}

image-20221012202749968


模板题:AcWing 1264. 动态求连续区间和

题目链接:1264. 动态求连续区间和

分析:

本题实际上就是基本代码模板的一个示例题目,基本与代码模板一致,同样是针对于连续区间进行求和。

题解:

复杂度分析:时间复杂度O(nlogn);空间复杂度O(n)

import java.util.*;
import java.io.*;

class Main {

    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static final int N = 100010;
    static int n, m;
    //arr:原数组、tr:树状数组
    static int[] arr = new int[N], tr = new int[N];
    
    public static int lowbit(int x) {
        return x & -x;
    }
    
    //add i val:添加到指定位置值
    public static void add(int x, int val) {
        for (int i = x; i <= n; i += lowbit(i)) {
            tr[i] += val;
        }
    }
    
    //sum i:求[1,i]的所有和
    public static int sum(int x) {
        int ret = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            ret += tr[i];
        }
        return ret;
    }
    
    public static void main(String[] args) throws Exception {
        String[] s = cin.readLine().split(" ");
        n = Integer.valueOf(s[0]);
        m = Integer.valueOf(s[1]);
        s = cin.readLine().split(" ");
        //初始化
        for (int i = 1; i <= n; i++) {
            arr[i] = Integer.valueOf(s[i - 1]);
            //添加到树状数组
            add(i, arr[i]);
        }
        //读取m行数据来进行响应技术
        while (m-- != 0) {
            s = cin.readLine().split(" ");
            int k = Integer.valueOf(s[0]);
            int a = Integer.valueOf(s[1]);
            int b = Integer.valueOf(s[2]);
            if (k == 0) {
                //求[a,b]的和
                out.println(sum(b) - sum(a - 1));
            }else {
                //第 a 个数加 b
                add(a, b);
            }
        }
        out.flush();
    }
}

image-20221013194959747


例题

例题1、AcWing 1265. 数星星【中等,信息学奥赛一本通】

题目链接:1265. 数星星

分析:

看上去是一个二维的平面图。

细节:由于是给出的坐标点都是从左往右,接着从下往上的,所以实际上我们无需去区分x,y点,只需要计算在x位置的数量即可,因为顺序是先从下往上,那么对于同x位置的,在上面的统计时也会把下方同位置的一起统计。

image-20221012214753963

核心:每颗星星只需要对其x来进行加1即可,计算sum实际上就是统计[1, x]位置的即可。

本道题用Java解的一些问题:

1、因为限时时间太短,所以输入、输出函数需要使用BufferedReader和PrintWriter,不然就会报超时。

题解:

复杂度分析:时间复杂度O(logn);空间复杂度O(n)

import java.util.*;
import java.io.*;

class Main {
    
    static BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static final int N = 32010;
    static int n;
    //树状数组、索引数组
    static int[] tr = new int[N], index = new int[N];
    
    public static int lowbit(int x) {
        return x & -x;
    }
    
    //在x位置+1
    public static void add(int x) {
        for (int i = x; i < N; i += lowbit(i)) {
            tr[i]++;
        }
    }
    
    //计算前缀和
    public static int sum(int x) {
        int ret = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            ret += tr[i];
        }
        return ret;
    }
    
    
    public static void main (String[] args) throws Exception{
        n = Integer.parseInt(cin.readLine());
        for (int i = 0; i < n; i++) {
            String[] s = cin.readLine().split(" ");
            int x = Integer.parseInt(s[0]);
            x++;
            //统计星级的数量(统计出为0的数量)
            index[sum(x)]++;
            //再进行加1
            add(x);
        }
        for (int i = 0; i < n; i++) {
            //效率由高到低比较:
            //[PrintWriter].println() > [PrintWriter].printf() > System.out.println() > System.out.printf()
            //经过测试:out.println(index[i]); 比 out.printf("%d\n", index[i])效率更高,后者在该题中也会超时。
            out.println(index[i]);
        }
        out.flush();
    }
}

image-20221013110550907


习题

习题1:1215. 小朋友排队【中等,蓝桥杯】

题目链接:1215. 小朋友排队

分析

首先看一下数据范围,10万数据量,那么就是O(n.logn)、O(n)的时间复杂度

树状数组思路

如何求得每个小朋友的交换次数?

  • 每个小朋友之前小朋友>该小朋友的身高的数量 + 每个小朋友之后小朋友<该小朋友的身高的数量。

问题:那么我们如何高效的得到每个小朋友之前与之后的数量呢?

答:暴力的话复杂度为O(n2);使用树状数组的话就是O(nlogn)就能够计算出来了。对于之前数量通过从前往右遍历一遍+添加到树状数组中可获取,之后数量则是从后往前遍历一遍+添加到树状数组。

得到了交换的次数那么怎么与生气值关联起来,举例:

  • 交换1次:1 = 1
  • 交换2次:1+2 = 3
  • 交换3次:1+2+3 = 6

得到式子:生气值 = (交换次数 x (交换次数 + 1))/ 2

归并排序思路

本质上与树状数组思路大体一致,同样是求到每个小朋友需要移动的次数,接着来计算生气值。只不过在这里并没有通过树状数组来求得小于或大于某个小朋友的数量,而是通过归并排序来进行求得某个小朋友需要交换的次数。

举例子:

[5, 7, 4, 6]

归并排序的过程如下:
[5, 7]  [4, 6]
[5] [7] [4] [6]
=== 开始回溯 ===
[5, 7]  [4, 6]
[4, 5, 6, 7]

关键来看[5, 7]  [4, 6] => [4, 5, 6, 7]这个过程中的每个节点需要交换的次数
i表示从[5,7]的第一个位置开始,j表示从[4,6]的第一个位置开始  =>    i = 0, j = 2  mid = 1
第一次比较:5 > 4  temp=[4],此时就需要将左边框中的4移动到第一个位置,很显然需要移动两次,怎么计算呢?mid - i + 1即可求得2,也就是4要移动2次,那么此时cnt[4] += 2。此时j++,j = 3

第二次比较:5 < 6  temp=[4,5],此时就需要将右边框子中的5移动到第二个位置,很显然需要移动一次,怎么计算?主要关键在于要看右边框中数移走了几位,那么同样可通过 j - (mid + 1)求得1,表示5要移动一次,那么此时cnt[5] += 1。此时i++,i = 1

第三次比较:7 > 6  temp[4, 5, 6]  同理 mid - i + 1 = 1,cnt[6] += 1,j++

最后跳转循环(由于i<=mid && j <= r),处理各自剩余框中元素,此时由两种情况,左框有剩余或者右框有剩余
对于左框有剩余,需要计算移动次数j - (mid + 1)得2,cnt[7] += 2
对于右框有剩余,由于temp数组长度与源数组一致,那么对于最右边框中的剩余元素根本就无需进行移动。

最后梳理下:
cnt[4] = 2
cnt[5] = 1
cnt[6] = 1
cnt[7] = 2
生气值为 = 2 * 3 / 2 + 1 * 2 / 2 + 1 * 2 / 2 + 2 * 3 / 2 = 3 + 1 + 1 + 3 = 8
此时就可以得到生气值为8啦

做法1:树状数组

复杂度分析:时间复杂度O(nlogn);空间复杂度O(n)

//交换规则:每次只能交换位置相邻;每个小朋友交换的不高兴程度是之前的+1
//最终目标:身高从低到高,计算最小的不高兴程度之和

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static final int N = (int)(1e6 + 10);
    //定义身高数组、树状数组
    static int[] h = new int[N], tr = new int[N];
    //统计每个小朋友之前(大于他身高的)+之后(小于他身高的)数量
    static int[] sum = new int[N];
    
    
    public static int lowbit(int x) {
        return x & -x;
    }
    
    public static void add(int x, int v) {
        for (int i = x; i < N; i += lowbit(i)) {
            tr[i] += v; 
        }
    }
    
    public static int query(int x) {
        int res = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            res += tr[i];
        }
        return res;
    }
    
    public static void main(String[] args)throws Exception {
        int n = Integer.parseInt(cin.readLine());
        String[] s = cin.readLine().split(" ");
        for (int i = 0; i < n; i++) {
            h[i] = Integer.parseInt(s[i]);
            h[i]++;//规避身高为0的情况,若是身高为0,若是直接找之前的就是-1,-1不太好作为下标进行索引
        }
        
        //从前往后遍历一遍小朋友身高(确定每个小朋友之前且身高大于该小朋友的数量)
        for (int i = 0; i < n; i++) {
            //身高范围在[h[i] + 1, N - 1]的小朋友数量
            sum[i] = query(N - 1) - query(h[i]);
            //添加到前缀数组中(此时添加)
            add(h[i], 1);
        }
        
        //初始化树状数组
        Arrays.fill(tr, 0);
        
        //从后往前遍历一遍小朋友身高((确定每个小朋友之后且身高小于该小朋友的数量)
        for (int i = n - 1; i >= 0; i--) {
            //身高范围在[0, h[i] - 1]的小朋友数量(注意之前进行了h[i]++,所以只需要h[i]即可)
            sum[i] += query(h[i] - 1);
            //重复添加一遍
            add(h[i], 1);
        }
        
        //最后遍历一遍sum(每个小朋友的左右数量来累加并得到不高兴程度和)
        long res = 0;
        for (int i = 0; i < n; i++) {
            int count = sum[i];
            //需要转为long类型
            res += (long)count * (count + 1) >> 1;
        }
        System.out.println(res);
    }
    
}

image-20221022112007930

做法2:归并排序

复杂度分析:时间复杂度:O(nlogn);空间复杂度O(n)

import java.util.*;
import java.io.*;

class Node {
    public int h;//身高
    public int index;//初始Node节点的编号(用于定位cnt数组中的索引)
    public Node(int h, int index) {
        this.h = h;
        this.index = index;
    }
}

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static final int N = (int)(1e6 + 10);
    //身高数组
    static Node[] childs = new Node[N];
    //定位节点
    static int[] cnt = new int[N];
     
     //归并排序
    public static void mergeSort(int l, int r) {
        if (l >= r) return;
        int mid = (l + r) >> 1;
        mergeSort(l, mid);
        mergeSort(mid + 1, r);
        //开始进行排序
        Node[] temp = new Node[r - l + 1];
        int i = l, j = mid + 1;
        int k = 0;
        while (i <= mid && j <= r) {
            //例子:[5, 7] [4, 6]   temp=[], mid = 1
            //5 > 4  temp=[4]  cnt[4] += 1 - 0 + 1 = 2
            //5 < 6  temp=[4, 5]  注意了这个5相对于原始位置上也移动了一次,这个1次怎么计算?j - (mid + 1)
            //7 > 6  temp=[4, 5, 6]  此时也只移动1次,mid - i + 1 = 1
            if (childs[i].h <= childs[j].h) {
                cnt[childs[i].index] += j - (mid + 1);
                temp[k++] = childs[i++];
            }else {
                //左边的>右边的(无需进行交换)
                cnt[childs[j].index] += mid - i + 1;
                temp[k++] = childs[j++];
            }
        }
        //处理左边剩余的
        while (i <= mid) {
            cnt[childs[i].index] += j - (mid + 1);
            temp[k++] = childs[i++];
        }
        //处理右边剩余的
        while (j <= r) {
            temp[k++] = childs[j++];
        }
        //进行拷贝
        for (i = l, j = 0; i <= r; i++, j++) {
            childs[i] = temp[i - l];
        }
    }
    
    //归并排序解法
    public static void main(String[] args)throws Exception {
        int n = Integer.parseInt(cin.readLine());
        String[] s = cin.readLine().split(" ");
        for (int i = 0; i < n; i++) {
            childs[i] = new Node(Integer.parseInt(s[i]), i);
        }
        
        mergeSort(0, n - 1);
        
        //遍历所有孩子的编号并来进行计算
        long res = 0;
        for (int i = 0; i < n; i++) {
            int count = cnt[i];
            res += (long)count * (count + 1) >> 1;
        }
        System.out.println(res);
    }
}

image-20221022134104227

二、 线段树

知识点

线段树是一棵二叉树,而树状数组是一个多叉树。

操作1:单点修改。【涉及到递归回溯,修改最底下位置的值,最后来回溯计算当前结点值(左节点+右节点)】

操作2:区间查询。

  • 最大查询时间为O(4logn),实际上就是O(logn)

是否支持区间修改,区间查询?

  • 肯定是可以的,大部分的区间查询都是涉及到比较麻烦的问题,需要加一个额外的标记【懒标记】。
  • 加懒标记的难度会涨很大。4 -> 8,一般在蓝桥杯中是用不上的。

大部分情况下就是做单点修改与区间查询

y总思路梳理总结

image-20221013112903214

  • 单点修改:指定左或者右(一条路径)来进行递归下去,实际修改掉值之后,就会进行回溯向上计算最新的区间范围值。【右边红线杉删除的情况,单点修改5位置的值为8】
  • 区间查询:左右子树只要在范围内就都会进行向下递归,直到找到最合适的范围来进行向上递归返回。【查询[2,5]的范围,即可找到位置[2]、[3,4]、[5]】

模板题:AcWing 1264. 动态求连续区间和

题目链接:1264. 动态求连续区间和

分析:

简单调用一波模板函数即可。

题解:

复杂度分析:时间复杂度O(logn);空间复杂度O(n)

import java.util.*;
import java.io.*;

class Main {
    
    static final int N = 100010;
    static BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static int n, m;
    //接收输入的权重值
    static int[] w = new int[N];
    //线段树:需要开4倍空间
    static Node[] tr = new Node[N * 4];
    
    //树状数组节点
    static class Node {
        public int l, r;
        public int sum;
        public Node(int l, int r, int sum) {
            this.l = l;
            this.r = r;
            this.sum = sum;
        }
        
        public Node(int l, int r) {
            this.l = l;
            this.r = r;
        }
    }
    
    //计算当前节点信息的两个儿子节点之和
    public static void push_up(int u) {
        tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
    }
    
    //构建线段树
    //u:当前节点编号;l:左边界;r:右边界
    public static void build(int u, int l, int r) {
        if (l == r) tr[u] = new Node(l, r, w[r]);
        else {
            tr[u] = new Node(l, r);//赋值左右边界的初值,当前并不计算sum值
            int mid = (l + r) >> 1;
            //递归左、右儿子
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
            //更新当前节点信息
            push_up(u);
        }
    }
    
    //查询:从根结点开始往下找对应的一个区间。该结点是左右两边根据具体的范围来进行向下递归,左右通吃
    //u:当前结点编号。l:确定左边范围。r:确定右边范围。
    public static int query(int u, int l, int r) {
        //若是当前区间完全包含了,直接返回它的值就好
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
        //记录中点
        int mid = (tr[u].l + tr[u].r) >> 1;
        int sum = 0;
        //看当前区间的中点与左边有没有交集(符合条件就进行向左下、右下递归)
        if (mid >= l) sum += query(u << 1, l, r);
        if (r >= mid + 1) sum += query(u << 1 | 1, l, r);
        return sum;
    }
    
    //修改函数。【左右确定单个路径向下,最后向上回溯计算节点值】
    //u:当前节点的编号。x:要修改的位置。v:增加的值
    public static void modify (int u, int x, int v) {
        //若是当前到达叶子节点,计算sum值
        if (tr[u].l == tr[u].r) {
            tr[u].sum += v;
        }else {
            //计算当前节点元素的中间值
            int mid = (tr[u].l + tr[u].r) >> 1;
            //确定要找的x是在左边还是右边
            if (x <= mid) {
                modify(u << 1, x, v);//向左递归
            }else {
                modify(u << 1 | 1, x, v);//向右递归
            }
            //递归回溯时重新计算当前的节点值
            push_up(u);
        }
    }
    
    public static void main(String[] args) throws Exception {
        String[] s = cin.readLine().split(" ");
        n = Integer.parseInt(s[0]);
        m = Integer.parseInt(s[1]);
        s = cin.readLine().split(" ");
        for (int i = 1; i <= n; i++) {
            w[i] = Integer.parseInt(s[i - 1]);
        }
        //初始化
        build(1, 1, n);
        while (m-- != 0) {
            s = cin.readLine().split(" ");
            int k = Integer.parseInt(s[0]);
            int a = Integer.parseInt(s[1]);
            int b = Integer.parseInt(s[2]);
            if (k == 0) {
                //查询[a,b]范围的值
                out.println(query(1, a, b));
            }else {
                //修改指定a位置的值为b
                modify(1, a, b);
            }
        }
        out.flush();
    }
}

image-20221013201315834


例题

例题1:1270. 数列区间最大值【简单】

题目链接:1270. 数列区间最大值

分析:

实际上就是将求线段树模板题中区间和替换为求最大值。

题解:

复杂度分析:时间复杂度O(logn);空间复杂度O(n)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static final int N = 100010;
    static int n, m;
    static int[] w = new int[N];
    static Node[] tr = new Node[N * 4];
    
    static class Node {
        public int l, r;
        public int max;
        public Node(int l, int r, int max) {
            this.l = l;
            this.r = r;
            this.max = max;
        }
        
        public Node(int l, int r) {
            this.l = l;
            this.r = r;
        }
    } 
    
    //更新最新值(取最大值)
    public static void push_up(int u) {
        tr[u].max = Math.max(tr[u << 1].max, tr[u << 1 | 1].max); 
    }
    
    //构建
    public static void build(int u, int l, int r) {
        if (l == r) tr[u] = new Node(l, r, w[l]);
        else {
            //初始化左右节点
            tr[u] = new Node(l, r);
            //计算左右两个值
            int mid = l + r >> 1;
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
            //更新最新值
            push_up(u);
        }
    }
    
    //查询
    public static int query(int u, int l, int r) {
        //在确定范围当中直接返回
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].max;
        int mid = tr[u].l + tr[u].r >> 1;
        //这里就不是求和,而是来进行求最大值
        int max = Integer.MIN_VALUE;
        if (mid >= l) max = Math.max(max, query(u << 1, l, r));
        if (r >= mid + 1) max = Math.max(max, query(u << 1 | 1, l, r));
        return max;
    }
    
    
    //区间范围最大值
    public static void main (String[] args) throws Exception {
        String[] s = cin.readLine().split(" ");
        n = Integer.parseInt(s[0]);
        m = Integer.parseInt(s[1]);
        s = cin.readLine().split(" ");
        for (int i = 1; i <= n; i++) {
            w[i] = Integer.parseInt(s[i - 1]);
        }
        build(1, 1, n);
        while (m-- != 0) {
            s = cin.readLine().split(" ");
            int x = Integer.parseInt(s[0]);
            int y = Integer.parseInt(s[1]);
            out.println(query(1, x, y));
        }
        out.flush();
    }
}

image-20221013212200783


习题

习题1:1228. 油漆面积【困难,蓝桥杯】

题目链接:1228. 油漆面积:

分析:

题意就是给我们多个矩形,这些矩形的区域可能会重叠,我们需要计算出所有矩形的面积之和(重叠的面积只需要算一份即可)。

根据题目给出的数据范围,n的长度为1万,时间复杂度应当为O(nlogn)。

直接来拿输入案例举例:

3
1 5 10 10
3 1 20 20
2 7 15 17

image-20221023122027207

那么对于所有矩形如何进行计算总面积呢?可以采用一种【扫描线的思路】,从左至右来开始进行扫描:

image-20221023122436031

可以看到上图中根据标号,我们总共计算了5个矩形面积并进行相加即可求得总面积。

单个矩形的面积公式为 = 两条边的x坐标值相减绝对值 * 对应x范围内的y轴的总长度。

其中对于y轴总长度是比较难求得的,因为可能会有不同的矩形在同一个x上,以及矩形可能不是连续的如下:

image-20221023123112552

  • 面积为 = (x2 - x1) * (s1 + s2),其中s1=y1-y2,s2=y3-y4。
  • 那么对于高度,实际上就是我们之前所说的x1-x2之间的高度长度范围,我们用一个len来表示,对于该图就是len=(y1-y2) + (y3-y4)=s1+s2,得到len后,即面积=(x2 - x1) * len

那么对于这个len的值总长度就是一个十分大的难点了,梳理下就是在区间范围中矩形的总长度,此时我们就可以采用线段树来进行解决该问题!

针对于上面输入案例来进行梳理下流程:

在线段树中其中pushup()更新结点的操作是根据cnt来确定len的取值:

cnt > 0             =>   len = r - l + 1
cnt = 0 && l == r   =>   len = 0
cnt = 0 && l != r   =>   len = 左儿子.len + 右儿子.len

image-20221023165846727

下面是每次遍历边时的更新过程:

①update(1, 5, 9, 1)

image-20221023160100595

②update(1, 7, 16, 1)

image-20221023162338163

③update(1, 1, 19, -1)

image-20221023164254344

④update(1, 5, 9, -1)

image-20221023164853620

⑤update(1, 7, 16, -1)

image-20221023165511541

最后我们来计算下面积:s = 5 + 12 + 133 + 95 + 95 = 340

思路1:扫描线+线段树

复杂度分析:时间复杂度O(nlogn);空间复杂度O(n)

import java.util.*;
import java.io.*;

//扫描线+线段树

//边
class Segment implements Comparable<Segment>{
    int x1;
    int y1;
    int y2;
    int cnt;//1表示是入边;0表示是出边
    
    public Segment(int x1, int y1, int y2, int cnt) {
        this.x1 = x1;
        this.y1 = y1;
        this.y2 = y2;
        this.cnt = cnt;
    }
    
    @Override
    public int compareTo(Segment s) {
        return this.x1 - s.x1;
    }
}

//线段树结点
class Node {
    //左右范围
    int l, r;
    //覆盖的次数
    int cnt;
    //当前范围包含的长度(由cnt决定,cnt>0表示覆盖,此时就需要计算[l, r]的长度;若是cnt == 0 && l == r,此时len=0;若是cnt == 0 && l != r,len=left.len + right.len)
    int len;
    
    public Node(int l, int r) {
        this.l = l;
        this.r = r;
    }
    
}


class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final int N = 10010;
    //线段树结点设置4倍的N
    static Node[] tr = new Node[4 * N];
    //边的长度
    static Segment[] segments = new Segment[2 * N];
    
    //构建树
    public static void build (int u, int l, int r) {
        if (l == r){
            tr[u] = new Node(l, r);
        }else {
            tr[u] = new Node(l, r);
            int mid = (l + r) >> 1;
            //递归左、右儿子
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
        }
    }
    
    //更新当前的节点值
    public static void pushUp(int u) {
        //若是当前节点的cnt>0表示被覆盖,此时直接计算覆盖的区间范围长度
        if (tr[u].cnt > 0) {
            tr[u].len = tr[u].r - tr[u].l + 1;
        }else if (tr[u].l == tr[u].r) {
            //cnt == 0 && l == r,此时长度即为0
            tr[u].len = 0;
        }else {
            tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
        }
    }
    
    //修改值
    public static void modify(int u, int l, int r, int cnt) {
        //若是当前的节点包含再次范围当中,对线段树结点中的cnt覆盖值进行更新
        if (tr[u].l >= l && tr[u].r <= r) {
            tr[u].cnt += cnt;
        }else {
            //拆分左右区间范围,递归向下去查找
            int mid = (tr[u].l + tr[u].r) >> 1;
            if (l <= mid) modify(u << 1, l, r, cnt);
            if (r > mid) modify(u << 1 | 1, l, r, cnt);
        }
        //根据cnt覆盖的次数更新下当前范围的len
        pushUp(u);
    }
    
    public static void main (String[] args)throws Exception {
        int n = Integer.parseInt(cin.readLine());
        int k = 0;
        for (int i = 0; i < n; i++) {
            String[] s = cin.readLine().split(" ");
            int x1 = Integer.parseInt(s[0]);
            int y1 = Integer.parseInt(s[1]);
            int x2 = Integer.parseInt(s[2]);
            int y2 = Integer.parseInt(s[3]);
            //每一个正方形都包含一个入边与出边
            segments[k++] = new Segment(x1, y1, y2, 1);
            segments[k++] = new Segment(x2, y1, y2, -1);
        }
        
        //排序所有边(根据入边来进行排序)
        Arrays.sort(segments, 0, 2 * n);
        
        //从节点1开始,范围为[0, 10000]
        build(1, 0, 10000);
        
        //遍历所有的边(2 * n个)
        int res = 0;
        for (int i = 0; i < 2 * n; i++) {
            //第二条边开始来进行计算面积
            if (i > 0) {
                //宽:segments[i].x1 - segments[i - 1].x1   长:tr[1].len
                res += (segments[i].x1 - segments[i - 1].x1) * tr[1].len;
            }
            //更新当前边的信息
            modify(1, segments[i].y1, segments[i].y2 - 1, segments[i].cnt);
        }
        System.out.println(res);
    }
}

image-20221022175937215

三、差分

知识点

简而言之:针对于指定范围中的每个坐标位置进行同等操作来进行时间复杂度优化,看模板题题目即可。

效果:将原本O(n)、O(n2)…复杂度通过差分优化为O(1)的时间复杂度。


一维差分模板题:ACWing 797. 差分

来源博客:ACWing 797. 差分(C++)、797. 差分

题目描述
输入一个长度为 n n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中[l,r] 之间的每个数加上 c。
请你输出进行完所有操作后的序列。

输入格式
第一行包含两个整数 n 和 m
第二行包含 n 个整数,表示整数序列。

接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。
输出格式
共一行,包含 n nn 个整数,表示最终序列。

数据范围
1 ≤ n , m ≤ 100000 , 1≤n,m≤100000,1≤n,m≤100000,
1 ≤ l ≤ r ≤ n , 1≤l≤r≤n,1≤l≤r≤n,
− 1000 ≤ c ≤ 1000 , −1000≤c≤1000,−1000≤c≤1000,
− 1000 ≤ 整数序列中元素的值 ≤ 1000 −1000≤整数序列中元素的值≤1000−1000≤整数序列中元素的值≤1000

输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1

输出样例:
3 4 5 3 4 2

分析:

若是n组数据,每次都对[l, r]区间来进行+c,那么时间复杂度为O(n2),给我们的数据量为10万,直接爆了。

对于在区间中的元素都做同样的操作,我们就可以使用差分操作,能够将时间复杂度优化为O(n),需要牺牲一些空间复杂度而已。

还是拿输入样例来举例子:

6个数:1 2 2 1 2 1,放入到a数组中,分别表示为a[1] = 1、a[2] = 2、a[3] = 2、a[4] = 1、a[5] = 2、a[6] = 1
我们将a数组的元素看做是b数组的前缀和数组(b数组我们暂时不讨论其元素是什么)
此时a数组是b数组的前缀和可列为如下:
a[1] = b[1]                                               
a[2] = b[1] + b[2]
a[3] = b[1] + b[2] + b[3]
a[4] = b[1] + b[2] + b[3] + b[4]
a[5] = b[1] + b[2] + b[3] + b[4] + b[5]
a[6] = b[1] + b[2] + b[3] + b[4] + b[5] + b[6]

//第一步骤:在输入a数组的各个元素时,求得b数组的值
//反推过来求b数组的元素值:
b[1] = a[1]
b[2] = a[2] - b[1] = a[2] - a[1]
b[3] = a[3] - (b[1] + b[2]) = a[3] - a[2]       //其中根据上面公式a[2] = b[1] + b[2]
b[4] = a[4] - (b[1] + b[2] + b[3]) = a[4] - a[3]
此时可推出:b[i] = a[i] - a[i - 1]

此时设想若是我们在b[l] += c,此时a[l] ... a[6] 的值都会+c,因为前面公式可以看出效果
但是我们若是只想要在[l, r]区间的每个元素+c,那么实际在[r+1, 6](后半部分)的每个元素无需+c,此时就需要在b[r + 1] -= c,那么相当于后面多加的值就抵消了!
    
 //第二步骤:求得了b数组后,那么我们去求最终的a数组元素值情况时,就按照如下方式来求得最终的a数组元素
a[1] = b[1]                                      =>      a[1] = b[1]                       
a[2] = b[1] + b[2]                               =>      a[2] = a[1] + b[1]
a[3] = b[1] + b[2] + b[3]                        =>      a[3] = a[2] + b[3]
a[4] = b[1] + b[2] + b[3] + b[4]                 =>      a[4] = a[3] + b[4]
a[5] = b[1] + b[2] + b[3] + b[4] + b[5]          =>      a[5] = a[4] + b[5]
a[6] = b[1] + b[2] + b[3] + b[4] + b[5] + b[6]   =>      a[6] = a[5] + b[6]
此时可推出:a[i] = a[i - 1] + b[i]

实际上我感觉就是将状态转移到了b中,最后再由b推导最终的a数组结果,这个中间过程的时间就巧妙的化解为了时间复杂度为O(1)。

代码:

一维差分数组:

复杂度分析:时间复杂度O(n);空间复杂度O(n)

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static final int N = (int)(1e6 + 10);
    static int n, m;
    static int[] a = new int[N], b = new int[N];
    
    public static void main(String[] args) throws Exception{
        String[] s = cin.readLine().split(" ");
        int n = Integer.parseInt(s[0]);
        int m = Integer.parseInt(s[1]);
        s = cin.readLine().split(" ");
        for (int i = 1; i <= n; i++) {
            a[i] = Integer.parseInt(s[i - 1]);
            //计算b数组的值
            b[i] = a[i] - a[i - 1];
        }
        
        //接收[l, r]范围进行+c
        for (int i = 0; i < m; i++) {
            s = cin.readLine().split(" ");
            int l = Integer.parseInt(s[0]);
            int r = Integer.parseInt(s[1]);
            int c = Integer.parseInt(s[2]);
            //对b数组来进行操作
            b[l] += c;
            b[r + 1] -= c;
        }
        
        //求得最终的a数组序列
        for (int i = 1; i <= n; i++) {
            a[i] = a[i - 1] + b[i];
            out.printf("%d ", a[i]);
        }
        out.flush();
    }
    
}

image-20221023212646003

image-20221024134310661

二维差分模板题:AcWing 798. 差分矩阵

原题:AcWing 798. 差分矩阵

分析

矩阵边长为1000,遍历一遍矩阵则是O(n2),而进行范围操作则是10万次,若是暴力在每个格子进行+c的话每一次操作的次数最大为O(n2)。最大复杂度即为:1000x1000x10万 = 1000亿,绝对超时。

通过使用二维差分,我们每次对范围进行操作的复杂度为O(1),时间复杂度最大仅为读取的最大时间复杂度O(n2)也就是100万次,对于10万次范围操作直接被容纳其中。效率大大提升!

学习二维差分前需要学习二维前缀和:

下面是推演差分操作: AcWing 796. 前缀和-模板题(二维) 子矩阵的和 Java题解

接下来就是二维差分的一整个过程:

//步骤一:令a数组为b数组的二维前缀和
a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i]][j]
反推出:b[i][j] = a[i][j] + a[i - 1][j - 1] - a[i - 1][j] - a[i][j - 1]
   
//尝试推演,验证a数组是否为二维前缀和
a[1][1] = a[0][1] + a[1][0] - a[0][0] + b[1][1]
    a[1]	[1] - a[0][1] = a[1][0] - a[0][0] + b[1][1] = b[1][1]  => a[1][1] = a[0][1] + b[1][1] = b[1][1]
a[1][2] = a[0][2] + a[1][1] - a[0][1] + b[1][2]
     a[1][2] = b[1][1] + b[1][2]
a[2][1] = a[1][1] + a[2][0] - a[1][0] + b[2][1]
	 a[2][1] = a[1][1] + b[2][1] = b[1][1] + b[2][1]
a[2][2] = a[2][1] + a[1][2] - a[1][1] + b[2][2]
     a[2][2] = b[1][1] + b[2][1] + b[1][2] + b[2][2]

实际上我们对b[i][j]去加上c,那么就会对后面从i,j位置开始的都会+c

//步骤二:进行二维差分
//范围操作即为(x1,y1, x2,y2):来对b数组进行二维差分  
b[x1][y1] += c
b[x2 + 1][y1] -= c
b[x1][y2 + 1] -= c
b[x2 + 1][y2 + 1] += c
    
//步骤三:最后计算每个最新的a数组的范围值即为(二维前缀和求范围公式)
a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j]

其中对于步骤二中里的二维差分操作用图示来进行演示:

image-20221024165737552

经过差分操作,我们最终可以明显的看到最后的会只是对目标范围进行的+c。

  • 红色范围表示是进行+c,蓝色则是-c。

image-20221024165753808

题解:

import java.util.*;
import java.io.*;

class Main {
    
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    static final int N = 1010;
    static int[][] a = new int[N][N], b = new int[N][N];
    static int n, m, q;
    
    //差分
    public static void insert(int x1, int y1, int x2, int y2, int c) {
        b[x1][y1] += c;
        b[x2 + 1][y1] -= c;
        b[x1][y2 + 1] -= c;
        b[x2 + 1][y2 + 1] += c;
    }
    
    public static void main(String[] args)throws Exception {
        String[] s = cin.readLine().split(" ");
        n = Integer.parseInt(s[0]);
        m = Integer.parseInt(s[1]);
        q = Integer.parseInt(s[2]);
        for (int i = 1; i <= n; i++) {
            s = cin.readLine().split(" ");
            for (int j = 1; j <= m; j++) {
                a[i][j] = Integer.parseInt(s[j - 1]);
                //初始化b数组
                b[i][j] = a[i][j] + a[i - 1][j - 1] - a[i - 1][j] - a[i][j - 1];
            }
        }
        
        //对范围来进行操作
        while (q -- != 0) {
            s = cin.readLine().split(" ");
            int x1 = Integer.parseInt(s[0]);
            int y1 = Integer.parseInt(s[1]);
            int x2 = Integer.parseInt(s[2]);
            int y2 = Integer.parseInt(s[3]);
            int c = Integer.parseInt(s[4]);
            insert(x1, y1, x2, y2, c);
        }
        
        //进行最后一步合并计算
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
                out.printf("%d ", a[i][j]);
            }
            out.println();
        }
        out.flush();
    }
    
}

image-20221024170522440

习题

习题1:1232. 三体攻击【困难,蓝桥杯,三维差分】

1232. 三体攻击:困难,二分+三维前缀和。二分、前缀和、差分

学习文章:AcWing 1232. 三体攻击(精简题解)、AcWing 1232. 三体攻击 (Java)

分析:

上来ABC就都是1e6,光是一个读入就是n3,暴力的话肯定是不行的,这里的话思路是使用三维前缀和+二分,每次攻击的复杂度是O(1),题目中说要找到第几轮攻击时有战舰炸毁,可以采用二分,那么时间复杂度就是O(n.logn):

下面一些图示演示的x,y,z的方向为如下:

image-20221026103553480

image-20221024202151013

此时为:b(x, y, z) + a(x - 1, y, z) + a(x, y - 1, z) - a(x -1 , y - 1)

接着我们最终的剩余的就是将右边第一层的格子计算出来a(x, y, z - 1) - a(x - 1, y, z- 1) - a (x, y - 1, z - 1) + a(x - 1, y - 1, z - 1)

首先是+a[x][y][z-1]

image-20221024202912724

你可以注意到上面一层、左边一大列重复覆盖了,也就是除了紫色的部分有绿色的都是重复的。

image-20221024203417058

那么我们就需要减去a[x-1][y][z-1]、减去a[x][y-1][z-1]

下面的图示仅仅只是说明两个减去的坐标位置的前缀三维数组的起始(这里并没有画出来所有的前缀框,请自行脑补一下):

image-20221024204208787

而减去的两个前缀,你会发现a[x-1][y-1][z-1]部分多减了一份,那么此时就需要+a[x-1][y-1][z-1]

image-20221024204351204

最终我们可以求得三维前缀和:

//步骤一:首先确定三维前缀和公式
a(x, y, z) = b(x, y, z) + a(x - 1, y, z) + a(x, y - 1, z) - a(x - 1, y - 1, z)
           + a(x, y, z - 1) - a(x - 1, y, z - 1) - a(x, y - 1, z - 1) + a(x - 1, y - 1, z - 1) 

//三维差分倒推求b数组
b(x, y, z) = a(x, y, z) - a(x - 1, y, z) - a(x, y - 1, z) + a(x - 1, y - 1, z)
           - a(x, y, z - 1) + a(x - 1, y, z - 1) + a(x, y - 1, z - 1) - a(x - 1, y - 1, z - 1)

//步骤二:根据范围来进行差分计算(对b三维数组进行操作)
//二维正面(以z1为)
b[x1    ][y1    ][z1] += val
b[x1    ][y2 + 1][z1] -= val
b[x2 + 1][y1    ][z1] -= val
b[x2 + 1][y2 + 1][z1] += val
//转为z2+1,且符号改变
b[x1    ][y1    ][z2 + 1] -= val
b[x1    ][y2 + 1][z2 + 1] += val
b[x2 + 1][y1    ][z2 + 1] += val
b[x2 + 1][y2 + 1][z2 + 1] -= val
    

//步骤三:最后反推求出a数组推导来进行计算
a(x, y, z) = b(x, y, z) + a(x - 1, y, z) + a(x, y - 1, z) - a(x - 1, y - 1, z)
           + a(x, y, z - 1) - a(x - 1, y, z - 1) - a(x, y - 1, z - 1) + a(x - 1, y - 1, z - 1) 

其中的步骤二推导:

image-20221025194327034

速记两个公式:

image-20221025204827876

代码:

复杂度分析:时间复杂度O(n3.logn);空间复杂度O(n3)

import java.util.*;
import java.io.*;

class Main {
    
    //10亿i
    static final BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
    static final PrintWriter out = new PrintWriter(new BufferedOutputStream(System.out));
    
    static int A, B, C, m;
    //a原数组,b差分数组
    //bp则是b拷贝的一份数组
    static int[][][] a,b, bp;
    //操作数组
    static int[][] oper;
    //差分数组与原数组转换。以该公式为准b(x, y, z) = a(x, y, z) - a(x - 1, y, z) - a(x, y - 1, z) + a(x - 1, y - 1, z)
    //      - a(x, y, z - 1) + a(x - 1, y, z - 1) + a(x, y - 1, z - 1) - a(x - 1, y - 1, z - 1)
    static int d[][] = {
        {0, 0, 0, 1},
        {-1, 0, 0, -1},
        {0, -1, 0, -1},
        {-1, -1, 0, 1},
        {0, 0, -1, -1},
        {-1, 0, -1, 1},
        {0, -1, -1, 1},
        {-1, -1, -1, -1}
    };
    
    
    //检测当前攻击是否会爆炸
    public static boolean check(int mid) {
        //拷贝一份差分数组
        bp = new int[A + 2][B + 2][C + 3];
        for (int i = 1; i <= A; i++) {
            for (int j = 1; j <= B; j++) {
                for (int k = 1; k <= C; k++) {
                    bp[i][j][k] = b[i][j][k];
                }
            }
        }
        //攻击[1, mid]轮
        for (int i = 1; i <= mid; i++) {
            int x1 = oper[i][0], x2 = oper[i][1];
            int y1 = oper[i][2], y2 = oper[i][3];
            int z1 = oper[i][4], z2 = oper[i][5];
            int h = oper[i][6];
            //步骤二:对b数组来进行范围攻击操作
            add(x1, x2, y1, y2, z1, z2, h);
        }
        
        //初始化原数组(不初始化也可,因为后面也是进行重新计算操作。注意了若是下面a[i][j][k] += b[i][j][k],这里必须初始化)
        //a = new int[A + 2][B + 2][C + 2];
        for (int i = 1; i <= A; i++) {
            for (int j = 1; j <= B; j++) {
                for (int k = 1; k <= C; k++) {
                    //步骤三:换元原数组
                    //方式一:直接使用公式来进行计算推导出a原数组
                    // a[i][j][k] = b[i][j][k] + a[i-1][j][k] + a[i][j-1][k] - a[i-1][j-1][k]
                    // + a[i][j][k-1] - a[i-1][j][k-1] - a[i][j-1][k-1] + a[i-1][j-1][k-1];
                    //方式二:根据定义偏移量来进行统一计算操作
                    a[i][j][k] = b[i][j][k];
                    for (int u = 1; u  < 8; u ++) {
                        int x = i + d[u][0], y = j + d[u][1], z = k + d[u][2], t = d[u][3];
                        a[i][j][k] -= a[x][y][z] * t;
                    }

                    //判断a[i][j][k]是否血量<0,若是成立则表示当前轮有爆炸
                    if (a[i][j][k] < 0) {
                        b = bp;
                        return true;
                    }
                }
            }
        }
        b = bp;
        return false;
    }
    
    private static void add(int x1, int x2, int y1, int y2, int z1, int z2, int val) {
        b[x1][y1][z1] += val;
        b[x1][y2+1][z1] -= val;
        b[x2+1][y1][z1] -= val;
        b[x2+1][y2+1][z1] += val;
        b[x1][y1][z2+1] -= val;
        b[x1][y2+1][z2+1] += val;
        b[x2+1][y1][z2+1] += val;
        b[x2+1][y2+1][z2+1] -= val;
    }
    
    public static void main(String[] args)throws Exception {
        String[] s = cin.readLine().split(" ");
        A = Integer.parseInt(s[0]);
        B = Integer.parseInt(s[1]);
        C = Integer.parseInt(s[2]);
        m = Integer.parseInt(s[3]);
        //初始化数组(根据读入的ABC来进行创建三维数组,避免内存溢出)
        a = new int[A + 2][B + 2][C + 3];
        b = new int[A + 2][B + 2][C + 3];
        oper = new int[m + 1][7];
        //读入生命值
        int index = 0;
        s = cin.readLine().split(" ");
        for (int i = 1; i <= A; i++) {
            for (int j = 1; j <= B; j++) {
                for (int k = 1; k <= C; k++) {
                    a[i][j][k] = Integer.parseInt(s[index++]);
                    //属于步骤一:
                    //写法1:表示对自己本身范围进行初始化伤害
                    //add(i, i, j, j, k, k, a[i][j][k]);
                    //写法2:通过前缀和公式来进行计算b数组血量
                    // b[i][j][k] = a[i][j][k] - a[i - 1][j][k] - a[i][j - 1][k] + a[i - 1][j - 1][k]
                    // - a[i][j][k - 1] + a[i - 1][j][k - 1] + a[i][j - 1][k - 1] - a[i - 1][j - 1][k - 1];
                    //写法3:通过定义x,y,z,h(h是血量)的偏移量来进行统一操作
                    for (int u = 0; u < 8; u++) {
                        int x = i + d[u][0], y = j + d[u][1], z = k + d[u][2], t = d[u][3];
                        b[i][j][k] += a[x][y][z] * t;
                    }
                }
            }
        }
        //读入攻击操作
        for (int i = 1; i <= m; i++){
            s = cin.readLine().split(" ");
            int x1 = Integer.parseInt(s[0]);
            int x2 = Integer.parseInt(s[1]);
            int y1 = Integer.parseInt(s[2]);
            int y2 = Integer.parseInt(s[3]);
            int z1 = Integer.parseInt(s[4]);
            int z2 = Integer.parseInt(s[5]);
            int h = Integer.parseInt(s[6]);
            //最后表示扣除的血量
            oper[i] = new int[]{x1, x2, y1, y2, z1, z2, -h};
        }
        
        //二分
        int l = 1, r = m;
        while (l < r) {
            int mid = (l + r) >> 1;
            //若是当前爆炸,范围往前进行查找
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        
        System.out.println(r);
        
    }
    
}

额外说明:在上面的代码题解里对于这类不确定的三维数组,我们是根据每次读入到的ABC来进行开辟数组,无法直接上来就开1e6空间三次方会直接爆掉!

还有一种思路就是设置一维数组,对应的索引下标我们手动去根据i,j,k来进行计算:

public int get(int i, int j, int k) {
	return (i * B + j) * C + k;
}

get(i, j, k)//即可获取到索引下标

image-20221026091038730

其他知识点习题(数学,找规律)

习题1:1237. 螺旋折线(中等,蓝桥杯)

题目链接:1237. 螺旋折线

分析

本题的标签时找规律、数学、推公式,给出的范围是109,也就是10亿。

image-20221027143719396

所有的点可以依据图的右上角斜线橙色点作为基准点来求得其距离,我们先看下每个基准点所表示的线长度,(1,1)=>4、(2,2)=>(16)、(3,3)=>36,每个点的x与y都是一致的,我们称这个一致的值为k,此时即可求得公式:4 * k * k

现在我们来开始举例:

例1:我们现在要求(-1, 1)位置表示的长度。

image-20221027144646252

首先先确定其所在的层,怎么确定?max(|x|, |y|) = k,确定好层之后我们就可以找基准点了,这里求得k=1,那么基准点就是(1, 1),该点的长度为4。

接着我们可以来求(-1, 1)与基准点(1, 1)的曼哈顿距离,这里求得2,看下图,很明显是在基准点的左边,此时就一定是需要-2。

  • 什么是曼哈顿距离?某个点到另一个点只能进行平移或者上下的最短距离,不能斜线走。公式为:|x1 - x2| + |y1 - y2|。

依据什么我们才能够知道求得曼哈顿距离是要-还是+?根据x与y的下标即可,x < y表示左半部分需要-,x > y表示右半部分需要+。

所以最后的结果就是:4 - 2 = 2。

例2:要求(2, -2)表示的长度。

image-20221027145018868

接下来就来快速求一下,首先确定k也就是层数,k = max(|2|, |-2|) = 2,确定基准点为(2, 2),此时计算出基准点表示的长度为16。

求出(2, -2)与(2, 2)之间的曼哈顿距离, len = |2 - 2| + |2 - -2| = 4

由于x > y所以再右半部分,所以此时结果就是:16 + 4 = 20。

代码

复杂度分析:时间复杂度O(n);空间复杂度O(1)

import java.io.*;
import java.util.*;

class Main {
    
    static final Scanner cin = new Scanner(System.in);
    //注意:x与y要使用long类型,否则ac不过
    static int x, y;
    
    //10000 0000 0  10亿, int即可
    public static void main(String[] args) {
        x = cin.nextInt();
        y = cin.nextInt();
        //根据x,y来确定k
        long k = Math.max(Math.abs(x), Math.abs(y));
        long res = 4 * k * k;
        if (x > y) {
            //右半部分
            res += Math.abs(k - x) + Math.abs(k - y);
        }else{
            //左部分
            res -= Math.abs(k - x) + Math.abs(k - y);
        }
        System.out.println(res);
    }
    
}

image-20221027145051762

参考文章

[1]. ACWing 797. 差分(C++)、bilibili—一维差分(算法)

[2]. AcWing 1237. 螺旋折线

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1347.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

27.5 Java集合之Set学习(基本概念,存储原理,性能测试)

文章目录1.Set接口1.1 Set的特性是什么&#xff1f;2.具体实现2.1 HashSet2.1.1 存储原理2.1.2 性能测试2.2 TreeSet2.2.1 存储原理2.2.2 性能测试2.3 EnumSet&#xff08;了解即可&#xff09;2.3.1 存储原理2.4 LinkedHashSet2.4.1 存储原理2.4.2 性能测试2.4.3 代码地址1.Se…

【Gitee】上传本地项目到 Gitee 仓库(入门篇)

本文主要介绍上传本地项目到 Gitee 仓库的过程&#xff0c;可以说是一个比较傻瓜的教材吧&#xff0c;从0开始&#xff0c;祝大家都能一次成功~~~ 一、前期准备 1. 配置 Gitte 创建 Gitte 账号&#xff0c;绑定好邮箱&#xff0c;并创建一个空仓库 。创建账号绑定邮箱过程这部…

【信号检测】基于小波变换的信号趋势检测和分离研究附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

双十一好物推荐:2022年好用的数码好物分享

一年一度的双十一尽在眼前&#xff0c;因为双十一的优惠力度是一年中最大的一次&#xff0c;所以许多人都想着直接一年屯一次&#xff0c;一次屯一年的理念&#xff0c;那么作为资深剁手党的我来说&#xff0c;对比于选购双十一好物来说我还是比较有心得的&#xff0c;下面让我…

机器视觉之工业摄像机知识点(一)

本文主要记录一些基础的工业摄像机的一些简要知识点。我也是根据我觉得比较重要的来记录。作为一位算法工程师&#xff0c;其实是有两条路来走&#xff0c;即技术专家以及技术经理。这两个实际是不同的职业方向。如果你不擅于与外部沟通交流&#xff0c;并且具备非常强的科研和…

基于OpenHarmony的ArkUI框架进阶对于高性能容器类和持久化和原子化的运用

文章目录高性能容器类Badge原子化服务代码简析表达式持久化高性能容器类 顾名思义&#xff0c;容器类是一个存储类&#xff0c;用于存储各种数据类型的元素&#xff0c;并提供一系列处理数据元素的方法。ArkUI开发框架提供了两种类型的容器类&#xff0c;线性和非线性。这些容…

【机器学习】求矩阵的-1/2次方的方法

目录 一、背景描述 二、D^(-1/2)的理论基础 三、代码实现 四、总结 一、背景描述 今天在看如下论文的时候&#xff1a; 态势感知图卷积网络在电力系统连锁故障中的应用-机器学习文档类资源-CSDN文库https://download.csdn.net/download/mzy20010420/86745616?spm1001.20…

Rust之常用集合(一):向量(vector)

开发环境 Windows 10Rust 1.64.0VS Code 1.72.2 项目工程 这里继续沿用上次工程rust-demo 常用集合 Rust的标准库包括许多非常有用的数据结构&#xff0c;称为集合。大多数其他数据类型表示一个特定的值&#xff0c;但是集合可以包含多个值。与内置数组和元组类型不同&…

2022年数维杯数学建模A题银行效率评价与破产成因分析求解全过程文档及程序

2022年数维杯数学建模 A题 银行效率评价与破产成因分析 原题再现&#xff1a; 银行在国家经济社会发展过程中扮演者重要的决策&#xff0c;银行的破产会对企业和个人造成众多不利的影响。相比国内的银行&#xff0c;国际银行的倒闭频次更高&#xff0c;因此国际银行倒闭原因的…

一小时教你轻松学会使用Java 整合 Easy Excel 操作 Excel 文件

文章目录一、Apache POI简介二、POI操作Excel构建maven项目导入依赖使用POI实现基本写操作使用POI实现大数据量写操作使用POI实现基本读操作使用POI读取不同类型的数据三、Easy Excel简介构建maven项目导入依赖实现写操作实现读操作目前市面上比较流行的操作Excel 文件工具大致…

【前端】vue阶段案例:vue-router使用流程

文章目录目标步骤1.配置映射关系2.导入路由并注册3.完成首页App.vue可能出现的问题&#xff1a;Component name "About" should always be multi-word参考目标 点击首页&#xff0c;则url变为/home&#xff0c;且下面显示的组件是Home组件点击关于&#xff0c;则url变…

更易用的OceanBase|生态工具征文大赛正式开启!

OceanBase 一直在思考&#xff0c;什么样的数据库对用户而言更易用&#xff1f; 更易用&#xff0c;除了功能完善、性能优秀、运行稳定的数据库系统&#xff0c;丰富多样的生态工具也必不可少。 作为一款完全自主研发的原生分布式数据库&#xff0c;OceanBase 的生态工具经历…

Java图片或视频生成GIF动图,发送微信

目录前言GIF简介代码生成图片合成GIF自定义GIF动图视频生成GIF发送微信小结前言 别人的博客文章中有动态显示这是怎么做到的呢&#xff1f;别人的微信发送的表情动态为什么是自己鬼畜视频&#xff1f;这些都是别人做到的&#xff0c;本文就是让自己也可以做到以上的事情&#…

Java基于springboot+vue的图书馆网上图书借阅系统 nodejs前后端分离

在Internet高速发展的今天&#xff0c;我们生活的各个领域都涉及到计算机的应用&#xff0c;其中包括网上图书借阅系统的网络应用&#xff0c;在外国网上图书借阅系统已经是很普遍的方式&#xff0c;不过国内的管理网站可能还处于起步阶段。网上图书借阅系统具有网上图书信息管…

HTML小游戏3—— 机器人总动员(附完整源码)

&#x1f482; 网站推荐:【神级源码资源网】【摸鱼小游戏】&#x1f91f; 风趣幽默的前端学习课程&#xff1a;&#x1f449;28个案例趣学前端&#x1f485; 想寻找共同学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】&#x1f4ac; 免费且实用的计算机相关知…

北京化工大学数据结构2022/10/27作业 题解

目录 问题 A: 二叉树的性质 问题 B: 二叉树的节点 问题 C: 满二叉树 问题 D: 完全二叉树的节点序号 -----------------------------------分割线------------------------------------------ 问题 E: 二叉树的深度 问题 F: 数据结构作业04 -- 二叉树的输入 递归版 迭代版…

【第一阶段:java基础】第4章:java控制结构

本系列博客是韩顺平老师java基础课的课程笔记 韩顺平P103-P1551. 顺序2. 分支3. 循环4. 跳转5. 编程思想1. 顺序 程序从上至下逐行执行&#xff0c;中间没有任何判断和跳转 2. 分支 单分支if双分支if-else多分支if - else if … - else嵌套分支&#xff1a;建议嵌套最好不要…

【水果派不吃灰】Raspberry Pi树莓派Linux系统下替换国内apt软件更新源

目录1. 前言2. 备份原始配置文件3. 修改原始文件3.1 软件更新源3.2 系统更新源4. 更新4.1 sudo apt-get update 更新软件源列表4.1 sudo apt-get upgrade 更新软件版本(时间会久点)4.1 sudo apt-get dist-upgrade4.1 sudo rpi-update 内核版本&#xff08;尽量不更新&#xff0…

Web前端 | JavaScript(BOM编程和JSON)

✅作者简介&#xff1a;一位材料转码农的选手&#xff0c;希望一起努力&#xff0c;一起进步&#xff01; &#x1f4c3;个人主页&#xff1a;每天都要敲代码的个人主页 &#x1f525;系列专栏&#xff1a;Web前端 &#x1f4ac;推荐一款模拟面试、刷题神器&#xff0c;从基础到…

Matplotlib | 世界足球俱乐部排名可视化

文章目录&#x1f3f3;️‍&#x1f308; 1. 导入模块&#x1f3f3;️‍&#x1f308; 2. 示例数据&#x1f3f3;️‍&#x1f308; 3. 画布设置&#x1f3f3;️‍&#x1f308; 4. 画布区域主题分配&#x1f3f3;️‍&#x1f308; 5. 添加数据散点&#x1f3f3;️‍&#x1f…