线段树与树状数组总结分析(可能是最终版)(下)

简介: 线段树与树状数组总结分析(可能是最终版)

例题2离散化,下标很大,值很小

我们看这组数据[1,10],[1,3],[6,10],很明显答案是3

但是离散化之后为[1,4],[1,2],[3,4],答案变成了2

为解决这种问题,我们可以在更新线段树的时候将区间从[l,r]变成[l,r-1],就将区间转化成了[1,3],[1,1],[3,3]这样的树

但是当我们遇到这样的数据[1,3],[1,1],[2,2],[3,3],就会导致区间更新时出错,我们可以将初始数据的r都加上1,就排除了li和ri相等的情况,如果没有这种情况,离散化后的区间也都是一样的

其实这道题数据很弱,不管这样的情况也能过(逃

正解https://www.luogu.com.cn/paste/vl2ora2z

卡壳点:

query return时,+号写成,

u<<1|1,写错成u<<1

modify部分

下标用错,用了1,n

modify的值写成了1,应该是有多个值,每张海报的值不一样。

build的时候由于v的下标0的值是无意义的边界,所以build的r要写成v.size()-1;

题解代码

#include <iostream>
#include <vector>
#include <cstdio>
#include <algorithm>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef long long ll;
const int N = 1E5 + 10;
bool vis[N]; //标记是否已经出现过第i张海报
struct node {
   int l, r;
   int id; //id表示lazy标签, 也表示当前节点值. 
}t[N << 2];
vector<int> v; vector<pair<int, int> > area; //v为离散化数组, area存放海报区间
int find(int x) { return lower_bound(v.begin(), v.end(), x) - v.begin(); }
离散化用
void pushdown(node& op, int id) { op.id = id; }
void pushdown(int x) {
   if (!t[x].id) return;
   pushdown(t[x << 1], t[x].id), pushdown(t[x << 1 | 1], t[x].id);
   t[x].id = 0; 
}
//因为线段树内部不维护任何数值, 所以也可以省去pushup这一操作.
void build(int l, int r, int x = 1) {
   t[x].l = l,t[x].r = r,t[x].id = 0 ;//id: 特别的, 如果0也是染色的点, 那么应初始化为-1
   if (l == r) return;
   int mid = l + r >> 1;
   build(l, mid, x << 1), build(mid + 1, r, x << 1 | 1);
}
void modify(int l, int r, int c, int x = 1) {
   if (l <= t[x].l && r >= t[x].r) { pushdown(t[x], c); return; }
   pushdown(x);
   int mid = t[x].l + t[x].r >> 1;
   if (l <= mid) modify(l, r, c, x << 1);
   if (r > mid) modify(l, r, c, x << 1 | 1);
}
int ask(int x = 1) { 
   if (t[x].id) { //当前子树均为同一值, 没必要再递归下去了
   有标记,染过色
       if (vis[t[x].id]) return 0;
       如果出现过返回0
       return vis[t[x].id] = 1;
       没出现过直接返回st赋值语句。
   }
   if (t[x].l == t[x].r) return 0; //到叶子结点一定要结束递归
   叶子节点返回0.
   return ask(x << 1) + ask(x << 1 | 1);
   返回两个节点相加的值。
}
int main()
{
   int T; cin >> T; 
   while (T--) {
       v.clear(); v.push_back(-0x3f3f3f3f); //这里是为了离散化下标从1开始
       area.clear();
       int n; scanf("%d", &n);
       rep(i, n) {
           vis[i] = 0; //顺带初始化vis
           int l, r; scanf("%d %d", &l, &r);
           r++; 
           v.push_back(l), v.push_back(r);
           area.push_back(make_pair(l,r));
       }
       /* 离散化部分 */
       sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end());
       rep(i, n) if (v[i] - v[i - 1] != 1) v.push_back(v[i] - 1); //记为*
       sort(v.begin(), v.end());
       build(1, v.size() - 1); //因为我的v数组有个-INF, 所以实际的建树大小应为v.size()-1
       for (int i = 0; i < n; ++i) {
           int l = area[i].first, r = area[i].second;
           l = find(l), r = find(r);
           modify(l, r-1, i + 1); //个人习惯编号从1开始
       }
       printf("%d\n", ask());
   }
   return 0;
}

经过模仿得到的acwing代码

#include <iostream>
#include <vector>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef pair<int,int>PII;
const int N = 1e4 + 10;
bool st[N<<1];
struct node{
  int l,r;
  int lazy;
}tr[N<<3];
vector<int> v; vector<PII > area;
int find(int u) {
  return lower_bound(v.begin(),v.end(),u) - v.begin();
}
void pushdown(node &tr,int lazy){
  tr.lazy = lazy;
}
void pushdown(int u){
  if(tr[u].lazy){
    pushdown(tr[u<<1],tr[u].lazy),pushdown(tr[u<<1|1],tr[u].lazy);
    tr[u].lazy = 0;
  }
}
void build(int u,int l,int r){
  tr[u].l = l,tr[u].r = r,tr[u].lazy = 0;
  if(l == r) return;
  int mid = l + r >> 1;
  build(u<<1,l,mid),build(u<<1|1,mid+1,r);
}
void modify(int u,int l,int r,int v){
  if(tr[u].l >= l && tr[u].r <= r) {
    pushdown(tr[u],v);
    return;
  }
  pushdown(u);
  int mid = tr[u].l + tr[u].r >> 1;
  if(l <= mid) modify(u<<1,l,r,v);
  if(r > mid) modify(u<<1|1,l,r,v);
}
int query(int u){
  if(tr[u].lazy){
    if(st[tr[u].lazy])return 0;
    return st[tr[u].lazy] = 1;
  }
  if(tr[u].l == tr[u].r ) return 0;
  return query(u<<1) + query(u<<1|1);
}
int main(){
  int T;
  cin >> T;
  while(T--){
    v.clear();
    //初始化
    v.push_back(-0x3f3f3f3f);
    vector下标处理, 从1开始,设置-0x3f3f3f3f,可以在离散化二分中当作一个边界。
    area.clear();
    memset(st,0,sizeof st);
    //初始化
    int n;
    scanf("%d",&n);
    for(int i = 1;i <= n;i++){
      int l,r;
      scanf("%d%d",&l,&r);
      r++;
      区间染色离散化处理
      v.push_back(l),v.push_back(r);
      因为后面会排序,所以直接push_back进离散化vector就行。
      area.push_back(make_pair(l,r));
      c++98只能用make_pair,不能{l,r};
    }
    //
    sort(v.begin(),v.end());v.erase(unique(v.begin(),v.end()),v.end());
    离散化去重
    for(int i = 1;i <= n;i++) if(v[i] - v[i-1] != 1) v.push_back(v[i] - 1);
    获取离散化后的最大边界,用于建树:如果不相邻的话就要多加一个与v[i]相邻的元素
    sort(v.begin(),v.end());
    //
    v离散化。
    build(1,1,v.size()-1);
    for(int i = 0;i < n;i++){
      int l = area[i].first,r = area[i].second;
      l = find(l),r = find(r);
      find函数,把原始的l,r数据通过二分查找,映射到离散化后的l,r上。
      modify(1,l,r-1,i+1);
    }
    printf("%d\n",query(1));
  }
  return 0;
}

套路:

区间染色query查询:

前提:

区间染色问题的query

应对
如果是查颜色种类的话:
int query(int u){
  if(tr[u].lazy){
    if(st[tr[u].lazy]) return 0;
    return st[tr[u].lazy] = 1;
  }
  if(tr[u].l == tr[u].r ) return 0;
  return query(u<<1) + query(u<<1|1);
}
如果是查不同颜色和颜色对应的段数:

用cnt数组维护

int cnt[N];
int last = 0;
void query(int u){
  if(last != tr[u].lazy) cnt[tr[u].lazy]++;
  if(tr[u].lazy || tr[u].l == tr[u].r) {
    last = tr[u].lazy;
    return 0;
  }
//  有标记直接进上面的语句了,不需要pushdown 
  query(u<<1),query(u<<1|1);
}

区间染色的l,r离散化

前提:l,r的值特别大以至于没法开出对应的树
应对:
vector<int> v;vector<PII > area;
开两个vector,一个离散化处理,一个存原始
int find(int u){return lower_bound(v.begin(),v.end(),u) - v.begin();}
二分查找函数,找到第一个大于等于u的元素,原始l,r可以通过这个find函数映射为离散l,r
v.clear(),area.clear();
v.push_back(-0x3f3f3f3f);
让v下标改为1至n
for(int i = 1;i <= n;i++){
  int l,r;
  scanf("%d%d",&l,&r);
  r++;
  v.push_back(l),v.push_back(r);
  area.push_back(make_pair(l,r));
}
读入信息,线段树区间染色离散化的r要先++,操作时再-1抵消影响
sort(v.begin(),v.end());v.erase(unique(v.begin(),v.end()),v.end());
for(int i = 1;i <= n;i++) if(v[i]  - v[i-1] != 1) v.push_back(v[i]-1);
sort(v.begin(),v.end());
离散化操作
for(int i = 0;i < n;i++){
  int l = find(area[i].first),r = find(area[i].second);
  modify(1,l,r-1,i+1);
}
操作时通过下标0到n-1拿出存原始l,r的vector里的元素
然后经过find映射成离散化l,r
modify操作时r-1

区别

区间等值修改里,

lazy存的是子树中所有节点要修改的值或要修改成的值,根据题意决定初始化的值,如例题1中批量+v操作,初始化为0,例题2批量替换,初始化为v的定义域之外的值。

sum存的是子树所有节点sum的总和 ,一般初始化为0.

是一个修改时给节点打上lazy,遇到lazy时解开lazy的过程。


区间自适应修改里,

lazy作为一个bool变量,存的是当前节点是否能代表子树接受修改,根据题意决定初始化的值

sum只有在lazy为1时有实际意义,

存的是这颗所有节点的值都相同的子树里的这个相同的值

修改值时,直接修改节点的值

树状数组

lowbit证明

树状数组详解 - Xenny - 博客园 (cnblogs.com)

单点修改

例题1求区间内操作数量

题链

22ACM集训队-树状数组与线段树基础 - Virtual Judge (vjudge.net)

卡壳点:

看了半天,原来不是求区间前缀和,而是求区间内进行了多少次操作……

怪不得题解看着很怪

套路:

前提:

取区间内操作数量

情景:

学校决定在某个时刻在某一段种上一种树,保证任一时刻不会出现两段相同种类的树,现有两种操作:

  • K=1,读入 l,r 表示在 l 到 r 之间种上一种树,每次操作种的树的种类都不同;
  • K=2,读入 l,r 表示询问 l 到 r 之间有多少种树。
应对:

用一个括号序列的思路来求解

比线段树更优,更简单,所以不用线段树。

![[Pasted image 20230413173234.png]]

统计10之前有多少个左括号,再统计3之前以后多少个右括号,3之前的右括号肯定可以和10里面的某些左括号(匹配,且在3之前,说明这对括号对应的操作没有覆盖3到10

所以覆盖了3到10的括号就是10之前有多少个左括号减去3之前的右括号


用两个树状数组,一个维护左括号,一个维护右括号,单点修改就行了。

代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 5e4 + 10;
int tr1[N],tr2[N];
int lowbit(int x){
  return x & -x;
}
void add(int tr[],int x){
  for(int i = x;i <= N;i+=lowbit(i)) tr[i] ++;
}
int query(int tr[],int x){
  int res = 0;
  for(int i= x;i;i-=lowbit(i)) res += tr[i];
  return res;
}
int main(){
  int n,m;
  cin >> n >> m;
  for(int i = 1;i <= m;i++){
    int op,l,r;
    scanf("%d%d%d",&op,&l,&r);
    if(op == 1)add(tr1,l),add(tr2,r);
    else printf("%d\n",query(tr1,r)- query(tr2,l-1));//左括号-右括号 
  }
  return 0;
}

例题2树状数组模拟哈希

题链

22ACM集训队-树状数组与线段树基础 - Virtual Judge (vjudge.net)

卡壳点:

树状数组套路不熟悉,query里res没有初始化为0

没有看出来是树状数组模拟哈希,套路不熟悉。

没有注意到没有给出操作数量的范围,没开long long导致wa。

同类题目

( 树状数组哈希前缀和)小朋友排队

AcWing 1215. 小朋友排队 - AcWing

[AcWing 1215. 小朋友排队 - AcWing](https://www.acwing.com/activity/content/problem/content/1722/)
#### 我的思路(疯狂踩陷阱)
// 1e5,1e6
// ,表示小朋友的不高兴程度和的最小值。
// 开始的时候,所有小朋友的不高兴程度都是  0
//  。
// 如果某个小朋友第一次被要求交换,则他的不高兴程度增加  1
//  ,如果第二次要求他交换,则他的不高兴程度增加  2
//  (即不高兴程度为  3
//  ),依次类推。当要求某个小朋友第  k
//   次交换时,他的不高兴程度增加  k
//  。
//  n = (1 + cnt)*cnt / 2;
**//  相邻交换的最优解是冒泡排序,要求排序次数**
//  每次交换,一个数前面比他大的数量-1,或在后面比他小的数-1,所以交换次数就是在前面且> x,在后面且< x的数字数量,**哈希后求前缀和**可以实现这个操作,**动态求前缀和适合用树状数组,用树状数组模拟hash求前缀和后缀和**,然后等查数列求和求每一项
***
#### 题解思路
// 先前总结过一个 求数组中有多少个数小于等于的套路
// 变化一下,
// 求数组中前面有多少个数小于等于x,只要遍历数组,边哈希边前缀和(l-1,x)
// 求数组中后面有多少个数小于等于x,只要逆序遍历数组,边哈希边前缀和(l-1,x)
// 求数组中前面有多少个数大于等于x,只要遍历数组,边哈希边前缀和(x-1,r)
// 求数组中后面有多少个数大于等于x,只要逆序遍历数组,边哈希边前缀和(x-1,r)
// 单纯小于或大于x,则x变为x-1或x+1,
// 检查数据范围时要把计算式全部看一遍,看看会不会爆范围。
***// 检查数据范围时看到数据包含0或负数,取模时要打起十二分精神,看看有没有踩到陷阱***
***
### 套路:
哈希前缀和
前提条件
  序列,元素关系。序列中大于x的数量,小于x的数量,或是前面大于x的数量,后面大于x的元素数量
应对
  要一个哈希数组,一个保存结果的数组,可能要一个原数组
  求数组中前面有多少个数小于等于x,只要遍历数组,边哈希边前缀和(l-1,x)
  求数组中后面有多少个数小于等于x,只要逆序遍历数组,边哈希边前缀和(l-1,x)
  求数组中前面有多少个数大于等于x,只要遍历数组,边哈希边前缀和(x-1,r)
  求数组中后面有多少个数大于等于x,只要逆序遍历数组,边哈希边前缀和(x-1,r)
  严格小于或大于x,则x变为x-1或x+1,
  发现要求前面时,顺序遍历数组,后面时,倒序遍历数组
   检查数据范围时要把计算式全部看一遍,看看会不会爆数据。
   如果序列元素会改变,需要动态维护序列,要用树状数组或线段树来模拟哈希
用树状数组模拟哈希的重点是
重点是要满足边哈希,边求哈希前缀和
贪心策略
前提条件
  相邻交换序列元素:
 应对
  最优策略是冒泡排序,每个元素会被交换的次数等于
  在前面且> x,在后面且< x的数字数量之和
(树状数组,线段树)(数组模拟哈希)(解题步骤)acwing数星星

https://www.acwing.com/problem/content/1267/

### 题链
https://www.acwing.com/problem/content/1267/
~~没买课的点不开,耗子尾汁~~ 
文末放图片
### 解决问题先看本质,找数据范围与输出
>// 输出格式
// N行,每行一个整数,分别是 0级,1级,2级,……,N−1级的星星的数目。
// 数据范围
// 1≤N≤15000
// ,
// 0≤x,y≤32000
**范围不是很特殊,没有太多信息**
### // 带着疑惑去看题干,轻松抓重点
==// 级是什么,数量如何统计:==
**在题干中发现:**
>// 如果一个星星的左下方(包含正左和正下)有 k颗星星,就说这颗星星是 k级的。
**//  因为涉及到坐标以及求和,到这里可以想到大概是用一个二维前缀和,但是二维前缀和要开的数组太大了,用不了,再看有没有其他条件,
 //  结果发现还真有:**
>//  **不会有星星重叠。星星按 y坐标增序给出,y坐标相同的按 x坐标增序给出。**
//  给出的点的y是不严格递增的,且y相同时,x是递增的这意味着,前面的点永远在后面的点的下方,
由此题目就变成按顺序看星星的话可以只看x坐标来判断前面的星星是否要计入后面的点的数量。
数量的计算就可以变成计算哈希统计x坐标各种情况的出现次数,然后计算1~x的前缀和。
//  因为涉及到了数组单点修改以及求和操作,所以可以用一个树状数组或线段树来模拟哈希维护数据
//观察样例发现星星计数时是不计自身的,所以先求树状数组里统计的前缀和,再修改树状数组里的值
// 统计数量级又要统计各种数量的出现情况,又要一层哈希
// 第一颗星星的左下方不可能有星星,且计数时不计自身,必然有0级的星星最后遍历哈希0到n-1
### 套路:
###### 树状数组模拟哈希
前提条件
  需要动态维护序列元素大小以及前缀和。
应对:
  求[l,r] 前缀和,用query(r) - query(l - 1)
###### 哈希前缀和
前提条件
  序列,元素关系。序列中大于x的数量,小于x的数量,或是前面大于x的数量,后面大于x的元素数量
应对
  求数组中前面有多少个数小于等于x,只要遍历数组,边哈希边前缀和(l-1,x)
  求数组中后面有多少个数小于等于x,只要逆序遍历数组,边哈希边前缀和(l-1,x)
  求数组中前面有多少个数大于等于x,只要遍历数组,边哈希边前缀和(x-1,r)
  求数组中后面有多少个数大于等于x,只要逆序遍历数组,边哈希边前缀和(x-1,r)
  严格小于或大于x,则x变为x-1或x+1,
   检查数据范围时要把计算式全部看一遍,看看会不会爆数据。
   如果序列元素会改变,需要动态维护序列,要用树状数组或线段树来模拟哈希
---
### 代码:
```cpp
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 4e4;
using namespace std;
int tr[N],w[N],f[N];
int lowbit(int x){
    return x & -x;
}
void add(int x,int v){
    for(int i = x;i <= N;i+=lowbit(i)) tr[i] += v;
}
int query(int x){
    int res = 0;
    for(int i = x;i ;i -=lowbit(i)) res += tr[i];
    return res;
}
int main(){
    int n;
    int x,y;
    cin >> n;
    for(int i = 0;i < n;i++){
        scanf("%d%d",&x,&y);
        x++;
        f[query(x)] ++;
        add(x,1);
    }
    for(int i  = 0;i < n;i++){
        printf("%d\n",f[i]);
    }
    return 0;
}

题3树状数组区间修改,用不到,暂时不管

`则A[1]+A[2]+…+A[n]


``= (D[1]) + (D[1]+D[2]) + … + (D[1]+D[2]+…+D[n])


``= n*D[1] + (n-1)*D[2] +… +D[n]


``= n * (D[1]+D[2]+…+D[n]) - (0D[1]+1D[2]+…+(n-1)*D[n])


`1到n的前缀和等于= n * (D[1]+D[2]+…+D[n]) - (0D[1]+1D[2]+…+(n-1)*D[n])


`所以上式可以变为∑ni = 1A[i] = n∑ni = 1D[i] - ∑ni = 1( D[i](i-1) );


`所以是需要维护tr1[i] = iD[i],tr2[i] = D[i](i-1);


1 int n,m;
 2 int a[50005] = {0},c[50005]; //对应原数组和树状数组
 3 
 4 int lowbit(int x){
 5     return x&(-x);
 6 }
 7 
 8 void updata(int i,int k){    //在i位置加上k
 9     while(i <= n){
10         c[i] += k;
11         i += lowbit(i);
12     }
13 }
14 
15 int getsum(int i){        //求D[1 - i]的和,即A[i]值
16     int res = 0;
17     while(i > 0){
18         res += c[i];
19         i -= lowbit(i);
20     }
21     return res;
22 }
23 
24 int main(){
25     cin>>n;27     for(int i = 1; i <= n; i++){
28         cin>>a[i];
29         updata(i,a[i] - a[i-1]);   //输入初值的时候,也相当于更新了值
31     }
32     
33     //[x,y]区间内加上k
34     updata(x,k);    //A[x] - A[x-1]增加k
35     updata(y+1,-k);        //A[y+1] - A[y]减少k
36     
37     //查询i位置的值
38     int sum = getsum(i);
39 
40     return 0;
41 }

模仿

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e4 + 10;
int f[N],tr1[N],tr2[N];
int lowbit(int x){
  return x & -x;
}
void add(int x,int v){
  for(int i = x;i <= N;i+=lowbit(i)){
    tr1[i] += v;
    tr2[i] += v * (x - 1);
  }
}
int query(int x){
  int res = 0;
  for(int i = x;i;i-=lowbit(i)) res += x * tr1[i] - tr2[i];
  return res;
}
int main(){
  int n,m;
  cin >> n >> m;
  for(int i = 1;i <= m;i++){
    int op,l,r;
    scanf("%d%d%d",&op,&l,&r);
    if(op == 1) add(l,1),add(r + 1,-1);
    else printf("%d\n",query(r) - query(l-1));
  }
  return 0;
}
目录
相关文章
|
算法
代码随想录 Day26 贪心 01 全集 LeetCode455 分发饼干 LeetCodeT346摆动序列 LeetCdoe T53 最大子数组和
代码随想录 Day26 贪心 01 全集 LeetCode455 分发饼干 LeetCodeT346摆动序列 LeetCdoe T53 最大子数组和
46 0
|
7月前
|
测试技术
【一刷《剑指Offer》】面试题 9:斐波那契数列(扩展:青蛙跳台阶、矩阵覆盖)
【一刷《剑指Offer》】面试题 9:斐波那契数列(扩展:青蛙跳台阶、矩阵覆盖)
|
7月前
|
人工智能
牛客xiao白月赛39 D(线段树维护区间)
牛客xiao白月赛39 D(线段树维护区间)
38 0
|
搜索推荐 算法 C语言
手撕排序算法3:优化版冒泡排序和快速排序的三种方法(包括三数取中,小区间优化,递归版本)(上)
手撕排序算法3:优化版冒泡排序和快速排序的三种方法(包括三数取中,小区间优化,递归版本)(上)
手撕排序算法3:优化版冒泡排序和快速排序的三种方法(包括三数取中,小区间优化,递归版本)(上)
|
搜索推荐 算法
手撕排序算法3:优化版冒泡排序和快速排序的三种方法(包括三数取中,小区间优化,递归版本)(下)
手撕排序算法3:优化版冒泡排序和快速排序的三种方法(包括三数取中,小区间优化,递归版本)(下)
|
机器学习/深度学习 存储 算法
【数据结构与算法篇】手撕八大排序算法之快排的非递归实现及递归版本优化(三路划分)
【数据结构与算法篇】手撕八大排序算法之快排的非递归实现及递归版本优化(三路划分)
231 0
线段树与树状数组总结分析(可能是最终版)(中)
线段树与树状数组总结分析(可能是最终版)
49 0