第五讲 树状数组和线段树和差分

本文详细介绍了树状数组和线段树两种数据结构,并通过多个编程题目的实例展示了它们在动态求和、区间修改、区间查询等问题中的应用。树状数组适用于快速求前缀和与单点修改,而线段树则适用于区间查询和修改。文章还探讨了这两种数据结构在处理动态数组、求逆序对、计算平方和、求最大值等场景下的策略和算法实现。
摘要由CSDN通过智能技术生成

树状数组要点

树状数组包含于线段树,能用树状数组解决的就用,不可以的就用线段树。
树状数组可以快速的求前缀和
根据以下两点可以判断是否可以使用树状数组
1、给某个位置上的数加上一个数
2、求某一个前缀和
区域查询,单点修改
在这里插入图片描述

第n层:前一层的数加上当前项
… …
第2层:前一层的数加上当前项
第1层:前一层的数加上当前项
第0层:奇数项指向自己

我们可以发现,每一个树状数组的位置保存的都是原数组的一段连续数字的和

如何确定层数呢?
看每一个位置的数的二进制后面有多少个连续的0,就是在第几层。
在这里插入图片描述
然后c[x]里面的值等于[x-2k, x]
如何求2k的值呢,可以用lowbit(x) = 2k(后面有多少个0,就返回多少2n,n为0的个数总和)
所以c[x] = (x - lowbit(x), x] 注意:左边是开区间,不包含x - lowbit(x)在内。(lowbit(x)的原理: lowbit(x) = x & -x = 2k

假设给我们一个x,求其前缀和s[x],
在这里插入图片描述
我们可以已经得到了c[x]后,顺便左端点也求出了,也就是剩余部分的右端点为x-lowbit(x),
所以我们可以递归的求前面剩下的数即:s[x] = c[x] + c[x - lowbit(x)] + …
代码模板

int res = 0;//定义一个和
for (i = x; i > 0; i -= lowbit(i)) res += c[i];  //假设我们求x的前缀和,
return res;

求和是比较简单的
比较难理解的地方的更新的过程,假设我们给A[x] + v 我们要如何更新他的前缀和呢?
在这里插入图片描述
假设修改的是7,我们要修改的数从图中很容易看出来就是,7,8,16
对应的代码是:

for (int i = x; i <= n; i += lowbit(i)) c[x] += v;

求x的父节点就是+ lowbit(x)(x & -x)就可以了

时间复杂度,+lowbit(x) 和 -lowbit(x)都是一样的,也就是logn,每次都是升一层或降一层

线段树要点

也是维护一个序列
性质:层数不超过4logn,节点数不超过4n
用一个一维数组来存(堆的存储方式)
在这里插入图片描述

在这里插入图片描述
操作1:单点修改,这是一个递归+回溯的过程,递归到长度为1时,修改,然后回溯,把父节点的值做相应的修改。这个步骤的时间复杂度是O(logn)

操作2:区间查询,也是一个递归的过程,
例如,我们求一下,2~5区间的和是多少 看图知道为17
在这里插入图片描述
我们再看看区间会用到哪些节点
如果说,查询的区间完全包下面的节点,则返回,否则递归到和我查询区间有交集的节点
凡是用到的节点就用圈表示
在这里插入图片描述
被完全包含的节点有 2 7 8 和为 17

这个步骤的时间复杂度是O(logn) 最多会递归到4logn层

那能不能区间修改区间查询?
可以,但是涉及区间修改会很复杂,会指数级别变难
在这里插入图片描述

线段树有4个核心函数
1、pushup 用子节点信息来更新当前节点信息 (有时候会省略,写到其他函数里面,因为只有一句话)
2、build 在一段区间上初始化线段树
3、modify 修改
4、query 查询

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

给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b] 的连续和。

输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。

第二行包含 n 个整数,表示完整数列。

接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。

数列从 1 开始计数。

输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8
输出样例:
11
30
35

思路

按照题目意思,我们需要求出a~b区间的和,以及第a个位置要加b这两个操作,我们可以先定义一个函数把原数组的值加到我们开辟的树状数组里面,然后在另外定义一个函数来计算a到b区间的和,

代码

import java.io.*;

public class Main {
    static final int N = 100010;
    static int n, m;
    static int[] o = new int[N];
    static int[] st = new int[N];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    
    static int Int(String s) { //把字符串转为整形
        return Integer.parseInt(s);
    }
    
    static int lowbit(int x) { //计算下标偏移量
        return x & -x;
    }
    
    static void add(int x, int v) { //构造树状数组;可以在第x个位置加上v;
        for (int i = x; i <= n; i += lowbit(i)) st[i] += v; /*这一步可以看上面的截图来理解,相当于是树枝是的数都要加某个值,因为属于子节点的一定也属于父节点,这就是前缀和的思想*/
    }
    
    static int query(int x) { //计算[1, x]区间的和
        int res = 0;
        for (int i = x; i > 0; i -= lowbit(i)) res += st[i];
        return res;
    }
    
    public static void main(String[] args) throws IOException {
        String[] s = in.readLine().split(" ");
        n = Int(s[0]);
        m = Int(s[1]);
        String[] s1 = in.readLine().split(" ");
        for (int i = 1; i <= n; i ++ ) o[i] = Int(s1[i - 1]);
        for (int i = 1; i <= n; i ++ ) add(i, o[i]);
        
        while(m -- > 0) {
            String[] s2 = in.readLine().split(" ");
            int k = Int(s2[0]);
            int a = Int(s2[1]);
            int b = Int(s2[2]);
            if (k == 0) System.out.println(query(b) - query(a - 1)); //输出[a,b]区间的和
            else add(a, b); //k == 1 则 第a个位置的数 + b的值
        }
    }
}

AcWing 1265. 数星星

天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。

如果一个星星的左下方(包含正左和正下)有 k 颗星星,就说这颗星星是 k 级的。

在这里插入图片描述

例如,上图中星星 5 是 3 级的(1,2,4 在它左下),星星 2,4 是 1 级的。

例图中有 1 个 0 级,2 个 1 级,1 个 2 级,1 个 3 级的星星。

给定星星的位置,输出各级星星的数目。

换句话说,给定 N 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。

输入格式
第一行一个整数 N,表示星星的数目;

接下来 N 行给出每颗星星的坐标,坐标用两个整数 x,y 表示;

不会有星星重叠。星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出。

输出格式
N 行,每行一个整数,分别是 0 级,1 级,2 级,……,N−1 级的星星的数目。

数据范围
1≤N≤15000,
0≤x,y≤32000
输入样例:
5
1 1
5 1
7 1
3 3
5 5
输出样例:
1
2
1
1
0

思路

在这里插入图片描述
按照题目意思:星星是一层一层往上加的,同一层里面,按照x的从小到大出现
也就是说,y在某个时刻是最大的,我们只需要找不大于它横坐标的星星有多少个
所以我们要找有多少个星星在某个星星的左下角区域,
用一个数组表示横坐标下有多少个星星 A[i] x = i, x <= xi 的星星
这里有两个操作
求x <= xi 有多少个星星,也就是求[1 , i]的前缀和
如果说在 x <= xi这个范围里,每合理存在一个星星,就给对于的数组A[i]加1,
其实构造树状数组的步骤就是修改某个值的步骤,而读取前缀和,就是读取每个星星的等级

代码

import java.io.*;

public class Main {
    static final int N = 32010;
    static int[] tr = new int[N];
    static int[] level = new int[N];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    static int lowbit(int x) {
        return x & -x;
    }
    
    static void add(int x) {
        for (int i = x; i <= N; i += lowbit(i)) tr[i] ++;
    }
    
    static int sum(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 IOException {
        int n = Int(in.readLine());
        
        for (int i = 0; i < n; i ++ ) {
            int x;
            String[] s = in.readLine().split(" ");
            x = Int(s[0]);
            x ++;
            level[sum(x)] ++;
            add(x);
        }
        
        for (int i = 0; i < n; i ++) System.out.println(level[i]);
        
    }
}

AcWing 1264. 动态求连续区间和(线段树法)

给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b] 的连续和。

输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。

第二行包含 n 个整数,表示完整数列。

接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。

数列从 1 开始计数。

输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8
输出样例:
11
30
35

思路

代码

import java.io.*;

public class Main {
    static final int N = 100010;
    static int n, m;
    static int[] w = new int[N];
    static Node[] tr = new Node[N * 4];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    
    static class Node {
        public int l, r, sum;
        
        public Node(int sum, int l, int r) {
            this.l = l;
            this.r = r;
            this.sum = sum;
        }
    }
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    static void pushup(int u) {
        tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
    }
    
    static void build(int u, int l, int r) {
        if (l == r) tr[u] = new Node(w[r], l, r);
        else {
            tr[u] = new Node(0, l, r);//报错点2,记得构造函数参数的顺序
            int mid = l + r >> 1;
            build(u << 1, l, mid);//报错点1,要继续拆分
            build(u << 1 | 1, mid + 1, r);
            pushup(u);
        }
    }
    
    static void modify(int u, int x, int v) {
        if (tr[u].l == tr[u].r) tr[u].sum += v;
        else {
            int mid = tr[u].l + tr[u].r >> 1;
            if (x <= mid) modify(u << 1, x, v);
            else modify(u << 1 | 1, x, v);
            pushup(u);
        }
    }
    
    static int query (int u, int l, int r) {
        if (tr[u].l >= 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 (mid < r) sum += query(u << 1 | 1, l, r);
        return sum;
    }
    
    public static void main(String[] args) throws IOException {
        String[] s1 = in.readLine().split(" ");
        n = Int(s1[0]);
        m = Int(s1[1]);
        String[] s2 = in.readLine().split(" ");
        for (int i = 1; i <= n; i ++ ) w[i] = Int(s2[i - 1]);
        build(1, 1, n);
        
        while (m -- > 0) {
            String[] s3 = in.readLine().split(" ");
            int k = Int(s3[0]);
            int a = Int(s3[1]);
            int b = Int(s3[2]);
            if (k == 0) System.out.println(query(1, a, b));
            else modify(1, a, b);
        }
    }
}

AcWing 1270. 数列区间最大值

输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y,要求你说出 X 到 Y 这段区间内的最大数。

输入格式
第一行两个整数 N,M 表示数字的个数和要询问的次数;

接下来一行为 N 个数;

接下来 M 行,每行都有两个整数 X,Y。

输出格式
输出共 M 行,每行输出一个数。

数据范围
1≤N≤105,
1≤M≤106,
1≤X≤Y≤N,
数列中的数字均不超过231−1
输入样例:
10 2
3 2 4 5 6 8 1 2 9 7
1 4
3 8
输出样例:
5
8

思路

维护区间的最大值,把原来的sum操作改为max 也就是把左右两边的子区间取一个MAX(l, r)操作

代码

import java.io.*;

public class Main {
    static final int N = 100010;
    static int n, m;
    static int[] w = new int[N];
    static Node[] tr = new Node[N * 4];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
    
    static class Node {
        public int l, r, max;
        public Node(int max, int l, int r) {
            this.max = max;
            this.l = l;
            this.r = r;
        }
    }
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    static void build(int u, int l, int r) {
        if (l == r) tr[u] = new Node(w[l], l, r);
        else {
            tr[u] = new Node(0, l, r);
            int mid = l + r >> 1;
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
            tr[u].max = Math.max(tr[u << 1].max, tr[u << 1 | 1].max);
        }
    }
    
    static int query(int u, int l, int r) {
        if (tr[u].l >= l && tr[u].r <= r) return tr[u].max;
        int mid = tr[u].l + tr[u].r >> 1;
        int max = -N;
        if (mid >= l) max = query(u << 1, l, r);
        if (mid < r) max = Math.max(query(u << 1 | 1, l, r), max);//注意,右半边是取不到mid的
        return max;
    }
    
    public static void main(String[] args) throws IOException {
        String[] s1 = in.readLine().split(" ");
        n = Int(s1[0]);
        m = Int(s1[1]);
        String[] s2 = in.readLine().split(" ");
        for (int i = 1; i <= n; i ++ ) w[i] = Int(s2[i - 1]);
        
        build(1, 1, n);
        
        int l, r;
        while (m -- > 0) {
            String[] s3 = in.readLine().split(" ");
            l = Int(s3[0]);
            r = Int(s3[1]);
            out.write(query(1, l, r) + "\n");
        }
        out.flush();
        out.close();
    }
}

AcWing 1215. 小朋友排队

n 个小朋友站成一排。

现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。

每个小朋友都有一个不高兴的程度。

开始的时候,所有小朋友的不高兴程度都是 0。

如果某个小朋友第一次被要求交换,则他的不高兴程度增加 1,如果第二次要求他交换,则他的不高兴程度增加 2(即不高兴程度为 3),依次类推。当要求某个小朋友第 k 次交换时,他的不高兴程度增加 k。

请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。

如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。

输入格式
输入的第一行包含一个整数 n,表示小朋友的个数。

第二行包含 n 个整数 H1,H2,…,Hn,分别表示每个小朋友的身高。

输出格式
输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。

数据范围
1≤n≤100000,
0≤Hi≤1000000
输入样例:
3
3 2 1
输出样例:
9
样例解释
首先交换身高为3和2的小朋友,再交换身高为3和1的小朋友,再交换身高为2和1的小朋友,每个小朋友的不高兴程度都是3,总和为9。

思路

用贪心的思想,我们会猜到是冒泡排序
在这里插入图片描述
按照题目意思,我们看得出这是冒泡排序,冒泡排序的交换操作,可以看做是逆序对的交换操作,只有前面的数大于后面的数才要交换,所以说,有两个性质:
1、如果有k个逆序对,至少需要交换k次
2、在冒泡排序中,每次必然交换(ai, ai+1), ai > ai+1 因此必然使逆序对数量减1

贪心策略:最优解里我们猜一下,是不是每个小朋友交换的次数是固定的?
我们来看看有没有这个性质
在这里插入图片描述
对于上面的序列:32541
对于2 我们假设前面有k1个比2大,有k2个比2小,那么2至少需要交换k1+k2次
然后我们可以计算每个小朋友交换的次数,我们会发现交换的总和为2k(2k为逆序对数量)
在这里插入图片描述
所以,全局是可以取到k的最小值的,意味着每一次交换都会干掉一个逆序对,且没有多余的操作,所以每一个小朋友的交换次数都可以取相等,而且可以恰好交换k1+k2
在这里插入图片描述
分析到这,得出问题,对于序列中的每一个数,分别统计k1,k2有多少,
小朋友的不高兴程度为:1 + 2 + 3 + 4 + 。。。+ k1+ k2.

可以使用归并排序,也可以使用树状数组:规定一个ai,求有多少个大于ai的数之和(也就是前缀和)

代码

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

public class Main {
    static final int N = 1000010;
    static int n;
    static int[] tr = new int[N];
    static int[] h = new int[N];
    static int[] sum = new int[N];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    static int lowbit(int x) {
        return x & -x;
    }
    
    static int query(int x) {
        int res = 0;
        for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];
        return res;
    }
    
    static void add(int u, int v) {
        for (int i = u; i < N; i += lowbit(i)) tr[i] += v;
    }
    
    public static void main(String[] args) throws IOException {
        n = Int(in.readLine());
        String[] s = in.readLine().split(" ");
        for (int i = 0; i < n; i ++ ) {
            h[i] = Int(s[i]);
            h[i] ++;   //这里为什么要++,因为hi可以取到0,在下面计算有多少个比它小的数时,由于不能加本身hi,即求前i - 1的前缀和
        }
        
        for (int i = 0; i < n; i ++) { // 计算有多少个大于当前数的数的数量
            sum[i] = query(N - 1) - query(h[i]); // 求hi + 1 到 N - 1的和也就是比i大的数的数量
            add(h[i], 1);
        }
        
        Arrays.fill(tr,0);
        /*重置树状数组用以保存x前面有多少个比它小的数(上一个是保存比它大的数
        实际上也是先求小的用总数减去小的数的数量得出大的数量)
        */
        
        for (int i = n - 1; i >= 0; i -- ) {
            sum[i] += query(h[i] - 1);  // 这里就是为什么h[i] ++ 的原因
            add(h[i], 1);
        }
        long res = 0;
        for (int i = 0; i < n; i ++ ) res += (long)sum[i] * (sum[i] + 1) / 2;
        
        System.out.print(res);
    }
}

AcWing 1228. 油漆面积

X星球的一批考古机器人正在一片废墟上考古。

该区域的地面坚硬如石、平整如镜。

管理人员为方便,建立了标准的直角坐标系。

每个机器人都各有特长、身怀绝技。

它们感兴趣的内容也不相同。

经过各种测量,每个机器人都会报告一个或多个矩形区域,作为优先考古的区域。

矩形的表示格式为 (x1,y1,x2,y2),代表矩形的两个对角点坐标。

为了醒目,总部要求对所有机器人选中的矩形区域涂黄色油漆。

小明并不需要当油漆工,只是他需要计算一下,一共要耗费多少油漆。

其实这也不难,只要算出所有矩形覆盖的区域一共有多大面积就可以了。

注意,各个矩形间可能重叠。

输入格式
第一行,一个整数 n,表示有多少个矩形。

接下来的 n 行,每行有 4 个整数 x1,y1,x2,y2,空格分开,表示矩形的两个对角顶点坐标。

输出格式
一行一个整数,表示矩形覆盖的总面积。

数据范围
1≤n≤10000,
0≤x1,x2,y2,y2≤10000
数据保证 x1<x2 且 y1<y2。

输入样例1:
3
1 5 10 10
3 1 20 20
2 7 15 17
输出样例1:
340
输入样例2:
3
5 2 10 6
2 7 12 10
8 1 15 15
输出样例2:
128

思路

这类题的做法叫扫描线法 ,类似高数的求积分面积的做法,就是尽可能的分割成很多个小矩形
在这里插入图片描述
划线要平行与y轴,一个长条一个长条来统计

在这里插入图片描述
可以根据每个长条的有效面积来算总面积
在这里插入图片描述
h为长条里面有效面积的宽
扫描线法可以增加难度的两个点:
1、大数据量 一般时间复杂度为 nlogn
2、不规则图形,一般难在公式的推导

这道题就是第一种情况
这类题是非常特殊的线段树
有如下标记
在这里插入图片描述
这些标记有懒标记的特点也就是延迟更新的思想,线段树里比较常用
另外:如果涉及区间修改的操作,一般都需要加一个懒标记
在这里插入图片描述
这道题有个特殊的点,在于它的懒标记不更新,看如下解释
画一个简单的线段树,这里的每一个子节点存储的信息有什么特点呢
线段树里,cnt和 lem 存储不考虑父节点信息的情况下的结果也就是,被修改后父节点的值不更新子节点的值
后面我人听傻了,截个图,后面理解吧
在这里插入图片描述

代码

import java.io.*;
import java.util.Arrays;
public class Main {
    static class Segment implements Comparable<Segment> {
        int x, y1, y2;
        int k;// 标记是否正在被扫描,是为1,扫描完后为-1;
        Segment(int x, int y1, int y2, int k) {
            this.x = x;
            this.y1 = y1;
            this.y2 = y2;
            this.k = k;
        }
        
        public int compareTo(Segment s) {
            if (x == s.x) return 0;
            return x > s.x ? 1 : -1;
        }
    }
    
    static class Node {
        int l, r; // 维护y轴
        int cnt; //覆盖的次数
        int len; //至少被覆盖一次的长度
        Node(int l, int r, int cnt, int len) {
            this.l = l;
            this.r = r;
            this.cnt = cnt;
            this.len = len;
        }
    }
    
    static final int N = 10010;
    static Segment[] seg = new Segment[N * 2];
    static Node[] tree = new Node[N * 4];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    static void pushup(int u) {
        if (tree[u].cnt > 0) tree[u].len = tree[u].r - tree[u].l + 1;
        else if (tree[u].r == tree[u].l) tree[u].len = 0;
        else tree[u].len = tree[u << 1].len + tree[u << 1 | 1].len;
    }
    
    static void build(int u, int l, int r) { //初始化线段树
        tree[u] = new Node(l, r, 0, 0);
        if (l == r) return;
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
    }
    
    static void modify (int u, int l, int r, int k) {
        if (l <= tree[u].l && r >= tree[u].r) {
            tree[u].cnt += k;
            pushup(u);
        }else {
            int mid = tree[u].l + tree[u].r >> 1;
            if (mid >= l) modify(u << 1, l, r, k);
            if (mid < r) modify(u << 1 | 1, l, r, k);
            pushup(u);
        }
    }
    
    public static void main(String[] args) throws IOException {
        int n = Int(in.readLine());
        int m = 0;
        while (n -- > 0) {
            String[] s = in.readLine().split(" ");
            int x1 = Int(s[0]);
            int y1 = Int(s[1]);
            int x2 = Int(s[2]);
            int y2 = Int(s[3]);
            seg[m ++] = new Segment(x1, y1, y2, 1);
            seg[m ++] = new Segment(x2, y1, y2, -1);
        }
        
        Arrays.sort(seg, 0, m);
        
        build(1, 0, 10000);
        
        int res = 0;
        for (int i = 0; i < m; i ++) {
            if (i > 0) res += tree[1].len * (seg[i].x - seg[i - 1].x);//为什么tr[1]里面是1?因为下面的修改都反映到第一层了。
            modify(1, seg[i].y1, seg[i].y2 - 1,seg[i].k);//相当于给线段树赋值。
        }
        System.out.print(res);
    }
    
}

AcWing 1232. 三体攻击

三体人将对地球发起攻击。

为了抵御攻击,地球人派出了 A×B×C 艘战舰,在太空中排成一个 A 层 B 行 C 列的立方体。

其中,第 i 层第 j 行第 k 列的战舰(记为战舰 (i,j,k))的生命值为 d(i,j,k)。

三体人将会对地球发起 m 轮“立方体攻击”,每次攻击会对一个小立方体中的所有战舰都造成相同的伤害。

具体地,第 t 轮攻击用 7 个参数 lat,rat,lbt,rbt,lct,rct,ht 描述;

所有满足 i∈[lat,rat],j∈[lbt,rbt],k∈[lct,rct] 的战舰 (i,j,k) 会受到 ht 的伤害。

如果一个战舰累计受到的总伤害超过其防御力,那么这个战舰会爆炸。

地球指挥官希望你能告诉他,第一艘爆炸的战舰是在哪一轮攻击后爆炸的。

输入格式
第一行包括 4 个正整数 A,B,C,m;

第二行包含 A×B×C 个整数,其中第 ((i−1)×B+(j−1))×C+(k−1)+1 个数为 d(i, j, k);

第 3 到第 m+2 行中,第 (t − 2) 行包含 7 个正整数 lat, rat, lbt, rbt, lct, rct, ht。

输出格式
输出第一个爆炸的战舰是在哪一轮攻击后爆炸的。

保证一定存在这样的战舰。

数据范围
1≤A×B×C≤106,
1≤m≤106,
0≤d(i, j, k), ht≤109,
1≤lat≤rat≤A,
1≤lbt≤rbt≤B,
1≤lct≤rct≤C
层、行、列的编号都从 1 开始。

输入样例:
2 2 2 3
1 1 1 1 1 1 1 1
1 2 1 2 1 1 1
1 1 1 2 1 2 1
1 1 1 1 1 1 2
输出样例:
2
样例解释
在第 2 轮攻击后,战舰 (1,1,1) 总共受到了 2 点伤害,超出其防御力导致爆炸。

思路

先从一维开始分析:
在这里插入图片描述
在某一个区间进行操作,每次操作让区间减去一个数,问第几次操作出现负数
算法框架是二分,一共有n次操作,每次操作让区间内的数减去一个h,求出现负数的位置
构造一个差分数组
在前缀和数组的一个区域里加一个数c等于在差分数组的一个区域里的两头一个加c一个减c
在这里插入图片描述
在考虑一下二维
在这里插入图片描述
如果只是左上角加c就会包括所有的右下角的点,而我们只需要中间的矩形,所以处理右下角加c外,另外另个角要减c以抵消其他点(加c减c等于没加)

三维:
在这里插入图片描述
s(x, y, z) 的公式,从截图可以看出,当s()内的参数有奇数个减1就是正的,有偶数个减1就是负的
所以b(x, y ,z) += c
b(x, y, z + 1) -= c

对于本题,由于是造成的是伤害,所以要加的c是一个负数,所以
b(x, y, z) -= h;
b(x, y, z + 1)

可以用3为的二进制数来表示这8个点
数组空间范围N = 2000010;
如何确定N的范围呢?比如,当数据是1维时,会多一个点,当数据是二维时会多两条线,所以说,当数据是三维的时候,会多3个面,所以开2倍的106就够了,
在这里插入图片描述
但这道题使用一维数组来存数据的,所以需要映射一下,
在这里插入图片描述
总结:核心思想利用求前缀和的公式,转换一下,变成求差分的公式,

在二分时,为什么要备份呢,因为每一次二分操作都会执行m次的立方体攻击,然后判断是否出现了爆炸,出现的话,缩小一半的范围,恢复现场,然后在攻击m次,直到找到那个第一个爆炸的战舰

代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.io.PrintWriter;

public class Main {
    static final int N = 2000010;
    static int A, B, C, m;
    static long[] s = new long[N];
    static long[] b = new long[N];
    static long[] bp = new long[N];
    static int[][] op = new int[N >> 1][7];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static PrintWriter out = new PrintWriter(System.out);
    static int[][] d = new int[][]{
            {0, 0, 0, 1},
            {0, 0, 1, -1},
            {0, 1, 0, -1},
            {0, 1, 1, 1},
            {1, 0, 0, -1},
            {1, 0, 1, 1},
            {1, 1, 0, 1},
            {1, 1, 1, -1},
    };
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    static int get(int i, int j, int k) {
        return (i * B + j) * C + k;
    }
    
    static boolean check(int mid) {
        b = Arrays.copyOf(bp, bp.length);
        for (int i = 1; i <= mid; i ++ ) {
            int x1 = op[i][0];
            int x2 = op[i][1];
            int y1 = op[i][2];
            int y2 = op[i][3];
            int z1 = op[i][4];
            int z2 = op[i][5];
            int t = op[i][6];
            
            b[get(x1,     y1,     z1)]     -= t;
            b[get(x1,     y1,     z2 + 1)] += t;
            b[get(x1,     y2 + 1, z1)]     += t;
            b[get(x1,     y2 + 1, z2 + 1)] -= t;
            b[get(x2 + 1, y1,     z1)]     += t;
            b[get(x2 + 1, y1,     z2 + 1)] -= t;
            b[get(x2 + 1, y2 + 1, z1)]     -= t;
            b[get(x2 + 1, y2 + 1, z2 + 1)] += t;
        }
        
        Arrays.fill(s, 0);
        
        for (int i = 1; i <= A; i ++ )
            for (int j = 1; j <= B; j ++ )
                for (int k = 1; k <= C; k ++ ) {
                    s[get(i, j, k)] = b[get(i, j, k)];
                    for (int u = 1; u < 8; u ++ ) { //注意这里u从1开始,因为第0个指向的是本身
                        int x = i - d[u][0];
                        int y = j - d[u][1];
                        int z = k - d[u][2];
                        int t = d[u][3];
                        s[get(i, j, k)] -= s[get(x, y, z)] * t; 
                    }
                    if (s[get(i, j, k)] < 0) return true;
                }
        return false;
    }
    
    public static void main(String[] args) throws IOException {
        String[] ss = in.readLine().split(" ");
        A = Int(ss[0]);
        B = Int(ss[1]);
        C = Int(ss[2]);
        m = Int(ss[3]);
        int q = 0;
        ss = in.readLine().split(" ");
        for (int i = 1; i <= A; i ++ )
            for (int j = 1; j <= B; j ++ )
                for (int k = 1; k <= C; k ++ ) 
                    s[get(i, j, k)] = Long.parseLong(ss[q ++]);
                    
        for (int i = 1; i <= A; i ++ )
            for (int j = 1; j <= B; j ++ )
                for (int k = 1; k <= C; k ++ ) 
                    for (int u = 0; u < 8; u ++) {
                        int x = i - d[u][0];
                        int y = j - d[u][1];
                        int z = k - d[u][2];
                        int t = d[u][3];
                        bp[get(i, j, k)] += s[get(x, y, z)] * t;//bp[]数组是不需要进行操作的,易错为bp[x, y, z]
                    }
        
        for (int i = 1; i <= m; i ++ ){
            ss = in.readLine().split(" "); //记得要循环输入
            for (int j = 0; j < 7; j ++ )
                op[i][j] = Int(ss[j]);
        }
        
        int l = 1, r = m;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        
        out.print(r);
        out.flush();
        out.close();
                
    }
}

AcWing 1237. 螺旋折线

如下图所示的螺旋折线经过平面上所有整点恰好一次。

在这里插入图片描述

对于整点 (X,Y),我们定义它到原点的距离 dis(X,Y) 是从原点到 (X,Y) 的螺旋折线段的长度。

例如 dis(0,1)=3,dis(−2,−1)=9

给出整点坐标 (X,Y),你能计算出 dis(X,Y) 吗?

输入格式
包含两个整数 X,Y。

输出格式
输出一个整数,表示 dis(X,Y)。

数据范围
−109≤X,Y≤109
输入样例:
0 1
输出样例:
3

思路

1、模拟法:类似的题目有 756 蛇形矩阵 (超时) 每次走一个点,和点数成正比
2、每次走一条边,与边数成正比
3、找规律O(1)
右上角和左下角的规律
在这里插入图片描述
左上角和右下角:
在这里插入图片描述
然后查看每条边有什么规律:
上边:|x| <= y
右边:|y| <= x
下边:|x| <= |y| + 1
左边:|y| <= |x|

在这里插入图片描述
给出一个点:1、先判断在哪个边上
2、找该边的特殊点(起点)
3、用公式算偏移量,起点+ 偏移量就可以得出答案了

代码

import java.util.*;

public class Main {
    static Scanner in = new Scanner(System.in);
    static long res;
    public static void main(String[] args) {
        int x = in.nextInt();
        int y = in.nextInt();
        if (Math.abs(x) <= y){  //在上方
            int n = y;
            res = (long)(2 * n - 1) * (2 * n) + x - (-n);
        }else if (Math.abs(y) <= x) {
            int n = x;
            res = (long)(2 * n) * (2 * n) + n - y;
        }else if (Math.abs(x) <= Math.abs(y) + 1 && y < 0) {
            int n = Math.abs(y);
            res = (long)(2 * n) * (2 * n + 1) + n - x;
        }else {
            int n = Math.abs(x);
            res = (long)(2 * n - 1) * (2 * n - 1) + y - (- n + 1);
        }
        System.out.print(res);
    }
}

这道题还有其他规律,有机会重新做一遍

AcWing 797. 差分

输入一个长度为 n 的整数序列。

接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。

请你输出进行完所有操作后的序列。

输入格式
第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数序列。

接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。

输出格式
共一行,包含 n 个整数,表示最终序列。

数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤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

思路

给定a[1],a[2],…,a[n]
构造差分数组b[N],使得a[i] = b[1] + b[2] + … +b[i]
差分的核心操作:
将a[L , R] 全部加上c,等价于:
b[L] += c, b[R + 1] -= c
在这里插入图片描述
在a[L, R]范围内全部加上c,从a[L]分析,当a[L]加上c时,由于a[L]后面的数据都包含a[L]在内,所以说,在该范围内全部加上c,只需要a[L] 加 c就可以了,直到a[R + 1],在减去c等于没有加,对后面的数据没有影响
转换为差分数组b[L],实际上就只有b[L] 加了c,b[L]之后的到b[R]都没有加c,到a[R + 1]在减去c即可,一一对应上面前缀和数组的操作

对 差分数组的初始化:
假定a数组全是0,构造的差分数组也全是0,但实际a数组是有值的,我们可以看做是在全部是0的基础上,进行了n步的插入操作:
1、a[1,1] 内加上了a[1, 1].
2、a[2,2] 内加上了a[2, 2]

a[n,n] 内加上了a[n, n]

以上是y总课上讲的,
以下参考了其同学的解释,理解得更加透彻
当我们要在某一个原数组a[n]里,在[L, R]范围内每个数都加上C,我们可以把这个原数组看做是某个差分数组的前缀和,那么首先我们需要按照 b[i] = a[i] - a[i - 1] 的差分规则,构造出一个符合原数组的差分数组b[n],然后利用差分的核心操作:b[L] += c, b[R + 1] -= c,就可以让a[L, R]内的每个数都加上C了
在这里插入图片描述
在这里插入图片描述

代码

import java.io.*;

public class Main {
    static final int N = 100010;
    static int n, m;
    static int[] a = new int[N];
    static int[] b = new int[N];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    public static void main(String[] args) throws IOException {
        String[] ss = in.readLine().split(" ");
        n = Int(ss[0]);
        m = Int(ss[1]);
        ss = in.readLine().split(" ");
        for (int i = 1; i <= n; i ++ ) a[i] = Int(ss[i - 1]);
        for (int i = n; i > 0; i -- ) a[i] -= a[i - 1];
        
        while (m -- > 0) {
            ss = in.readLine().split(" ");
            int l = Int(ss[0]);
            int r = Int(ss[1]);
            int c = Int(ss[2]);
            a[l] += c;
            a[r + 1] -=c;
        }
        
        for (int i = 1; i <= n; i ++ ){
            a[i] += a[i - 1];
            System.out.print(a[i] + " ");
        } 
        
    }
    
}

代码2

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

public class Main {
    static final int N = 100010;
    static int n, m;
    static int[] a = new int[N];
    static int[] b = new int[N];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    static void insert(int l, int r, int c) { // 构造差分数组 && 区间加c
        b[l] += c;
        b[r + 1] -= c;  //在构造差分数组时,这一步相当于a[i] -= a[i - 1]
    }
    
    public static void main(String[] args) throws IOException {
        String[] ss = in.readLine().split(" ");
        n = Int(ss[0]);
        m = Int(ss[1]);
        ss = in.readLine().split(" ");
        for (int i = 1; i <= n; i ++ ) a[i] = Int(ss[i - 1]);
        for (int i = 1; i <= n; i ++ ) insert(i, i, a[i]); // 等价于a[i] -= a[i - 1]
        
        //System.out.print(Arrays.toString(b));
        
        while (m -- > 0) {
            ss = in.readLine().split(" ");
            int l = Int(ss[0]);
            int r = Int(ss[1]);
            int c = Int(ss[2]);
            insert(l, r, c);
        }
        
        for (int i = 1; i <= n; i ++ ){
            b[i] += b[i - 1];
            System.out.print(b[i] + " ");
        } 
        
    }
    
}

798. 差分矩阵

输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。

每个操作都要将选中的子矩阵中的每个元素的值加上 c。

请你将进行完所有操作后的矩阵输出。

输入格式
第一行包含整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q 行,每行包含 5 个整数 x1,y1,x2,y2,c,表示一个操作。

输出格式
共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。

数据范围
1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例:
2 3 4 1
4 3 4 1
2 2 2 2

思路

给定原矩阵a[i, j],构造差分矩阵b[i, j],使得a[][]是b[][]的二维前缀和
在这里插入图片描述
对边界的点做处理,和一维前缀和思想一样,后面的前缀和会因为前面的前缀和的改变而改变

代码

import java.io.*;

public class Main {
    static final int N = 1010;
    static int n, m, q;
    static int[][] a = new int[N][N];
    static int[][] b = new int[N][N];
    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
    
    static int Int(String s) {
        return Integer.parseInt(s);
    }
    
    static void insert(int x1, int y1, int x2, int y2, int c) {
        b[x1][y1] += c;
        b[x1][y2 + 1] -= c;
        b[x2 + 1][y1] -= c;
        b[x2 + 1][y2 + 1] += c;
    }
    
    public static void main(String[] args) throws IOException {
        String[] ss = in.readLine().split(" ");
        n = Int(ss[0]);
        m = Int(ss[1]);
        q = Int(ss[2]);
        for (int i = 1; i <= n; i ++ ) {
            ss = in.readLine().split(" ");
            for (int j = 1; j <= m; j ++ ) a[i][j] = Int(ss[j - 1]);
        }
        
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                insert(i, j, i, j,a[i][j]);
        while(q -- > 0) {
            ss = in.readLine().split(" ");
            int x1 = Int(ss[0]);
            int y1 = Int(ss[1]);
            int x2 = Int(ss[2]);
            int y2 = Int(ss[3]);
            int c = Int(ss[4]);
            insert(x1, y1, x2, y2, c);
        }
        
        for (int i = 1; i <= n; i ++ ){
            for (int j = 1; j <= m; j ++ ) {
                b[i][j] += b[i - 1][j] + b[i][j - 1] -b[i - 1][j - 1];
                out.write(b[i][j] + " ");
            }
            out.write("\n");
        }
        out.flush();
        out.close();
    }
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值