}
//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();
}
}

---
### 例题
#### 例题1、AcWing 1265. 数星星【中等,信息学奥赛一本通】
题目链接:[1265. 数星星]( )
**分析:**
看上去是一个二维的平面图。
细节:由于是给出的坐标点都是从左往右,接着从下往上的,所以实际上我们无需去区分x,y点,只需要计算在x位置的数量即可,因为顺序是先从下往上,那么对于同x位置的,在上面的统计时也会把下方同位置的一起统计。

核心:每颗星星只需要对其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();
}
}

---
### 习题
#### 习题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);
}
}

做法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);
}
}

## 二、 线段树
### 知识点
线段树是一棵二叉树,而树状数组是一个多叉树。
操作1:单点修改。【涉及到递归回溯,修改最底下位置的值,最后来回溯计算当前结点值(左节点+右节点)】
操作2:区间查询。
* 最大查询时间为O(4logn),实际上就是O(logn)
是否支持区间修改,区间查询?
* 肯定是可以的,大部分的区间查询都是涉及到比较麻烦的问题,需要加一个额外的标记【懒标记】。
* 加懒标记的难度会涨很大。4 -> 8,一般在蓝桥杯中是用不上的。
**大部分情况下就是做单点修改与区间查询**。
**y总思路梳理总结**:

* 单点修改:指定左或者右(一条路径)来进行递归下去,实际修改掉值之后,就会进行回溯向上计算最新的区间范围值。【右边红线杉删除的情况,单点修改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();
}
}

---
### 例题
#### 例题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();
}
}

---
### 习题
#### 习题1:1228. 油漆面积【困难,蓝桥杯】
题目链接:[1228. 油漆面积]( ):
**分析:**
题意就是给我们多个矩形,这些矩形的区域可能会重叠,我们需要计算出所有矩形的面积之和(重叠的面积只需要算一份即可)。
根据题目给出的数据范围,n的长度为1万,时间复杂度应当为O(nlogn)。
直接来拿输入案例举例:
3
1 5 10 10
3 1 20 20
2 7 15 17

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

可以看到上图中根据标号,我们总共计算了5个矩形面积并进行相加即可求得总面积。
单个矩形的面积公式为 = 两条边的x坐标值相减绝对值 \* 对应x范围内的y轴的总长度。
其中对于y轴总长度是比较难求得的,因为可能会有不同的矩形在同一个x上,以及矩形可能不是连续的如下:

* 面积为 = (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

下面是每次遍历边时的更新过程:
①update(1, 5, 9, 1)

②update(1, 7, 16, 1)

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

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

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

最后我们来计算下面积:s = 5 + 12 + 133 + 95 + 95 = 340
**思路1:扫描线+线段树**
复杂度分析:时间复杂度O(nlogn);空间复杂度O(n)
import java.util.*;
import java.io.*;
//扫描线+线段树
//边
class Segment implements Comparable{
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);
}
}

## 三、差分
### 知识点
简而言之:针对于**指定范围**中的**每个坐标位置进行同等操作**来进行时间复杂度优化,看模板题题目即可。
效果:将原本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();
}
}


### 二维差分模板题: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]
其中对于步骤二中里的二维差分操作用图示来进行演示:

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

题解:
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();
}
}

### 习题
#### 习题1:1232. 三体攻击【困难,蓝桥杯,三维差分】
[1232. 三体攻击]( ):困难,二分+三维前缀和。二分、前缀和、差分
学习文章:[AcWing 1232. 三体攻击(精简题解)]( )、[AcWing 1232. 三体攻击 (Java)]( )
**分析:**
上来ABC就都是1e6,光是一个读入就是n3,暴力的话肯定是不行的,这里的话思路是使用三维前缀和+二分,每次攻击的复杂度是O(1),题目中说要找到第几轮攻击时有战舰炸毁,可以采用二分,那么时间复杂度就是O(n.logn):
下面一些图示演示的x,y,z的方向为如下:


此时为:`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]`

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

那么我们就需要减去`a[x-1][y][z-1]`、减去`a[x][y-1][z-1]`:
下面的图示仅仅只是说明两个减去的坐标位置的前缀三维数组的起始(这里并没有画出来所有的前缀框,请自行脑补一下):

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

最终我们可以求得三维前缀和:
//步骤一:首先确定三维前缀和公式
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)
其中的步骤二推导:

速记两个公式:

代码:
复杂度分析:时间复杂度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)//即可获取到索引下标

## 其他知识点习题(数学,找规律)
### 习题1:1237. 螺旋折线(中等,蓝桥杯)
题目链接:[1237. 螺旋折线]( )
**分析**:
本题的标签时找规律、数学、推公式,给出的范围是109,也就是10亿。

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

首先先确定其所在的层,怎么确定?max(|x|, |y|) = k,确定好层之后我们就可以找基准点了,这里求得k=1,那么基准点就是(1, 1),该点的长度为4。
接着我们可以来求(-1, 1)与基准点(1, 1)的曼哈顿距离,这里求得2,看下图,很明显是在基准点的左边,此时就一定是需要-2。