线段树----区间问题的神

《标准线段树》

 

 普通的线段树,其区间一般都是一个序列的数的个数,但还是要根据不同题目来判断

注意:tr[]空间是N*4,N为最大范围

《单点修改,区间查询》

原题:https://www.acwing.com/problem/content/1277/

 

 

 

 

 像这道题,N最大为2*1e5,我们可以事先建立一颗最大范围1~N的线段树,然后不断分裂这个范围,产生子节点,

每一个节点维护的信息是,这个节点代表的区间的最大值,这里范围代表数的个数;

 1 #include <iostream>
 2 #include <algorithm>
 3 #include <cstring>
 4 using namespace std;
 5 typedef long long LL;
 6 const int N = 200010;
 7 struct tree
 8 {
 9     int l, r;
10     //区间[l,r]中的最大值
11     int v;
12 } tr[N * 4];
13 //由子节点维护的信息计算(或更新)父节点维护的信息
14 void pushup(int u)
15 {
16     //解释一下 u<<1|1这个操作:
17     //对于2的倍数,其二进制下的第一位总是0,根据这个特性|1,相当于+1;
18     tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
19 }
20 //将tr[u]这个位置初始化为树
21 void build(int u, int l, int r)
22 {
23     tr[u].l = l, tr[u].r = r;
24     if (l == r)
25         return;
26     //维护信息根据题目的不同,在代码中的写法也不同
27     //这里因为是以逐渐加点(在modify中)来产生信息,
28     //在最开始的建树过程是无维护信息的,所以这里不管tr[u].v;
29     int mid = l + r >> 1;
30     build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
31 }
32 //单点修改,将tr[u]包含区间中的位置为x的值修改为v;
33 void modify(int u, int x, int v)
34 {
35     if (tr[u].l == x && tr[u].r == x)
36         tr[u].v = v;
37     else
38     {
39         int mid = tr[u].l + tr[u].r >> 1;
40         if (x <= mid)
41             modify(u << 1, x, v);
42         else
43             modify(u << 1 | 1, x, v);
44         //注意这个pushup()操作一定要写到这里,而不是写到外面
45         // pushup操作是更新父节点u的维护信息,其是写在子节点u<<1,u<<1|1被更新时写的
46         //如果写在外面,当 if (tr[u].l == x && tr[u].r == x)tr[u].v = v; 生效时
47         //这个时候u是无子节点的,那就会问:这个u点更新了,那如何更新其父节点呢?
48         //很简单,因为在modify时,我们会传入的u是1,即根节点,此后都是递归去更新
49         pushup(u);
50     }
51 }
52 //查询在点u包含的范围内中[l,r]的维护的信息
53 int query(int u, int l, int r)
54 {
55     if (tr[u].l >= l && tr[u].r <= r)
56         return tr[u].v;
57     //注意这里v是取题中数的最小值
58     int v = 0;
59     int mid = tr[u].l + tr[u].r >> 1;
60     if (l <= mid)
61         v = query(u << 1, l, r);
62     //注意:这里就不是else了
63     if (r > mid)
64         v = max(v, query(u << 1 | 1, l, r));
65     return v;
66 }
67 int main()
68 {
69     int m, p, a = 0, x, n = 0;
70     cin >> m >> p;
71     char op[2];
72     build(1, 1, m);
73     while (m--)
74     {
75         scanf("%s%d", op, &x);
76         if (op[0] == 'A')
77         {
78             //注意这里可能爆int;
79             modify(1, n + 1, ((LL)a + x) % p);
80             n++;
81         }
82         else
83         {
84             a = query(1, n - x + 1, n);
85             printf("%d\n", a);
86         }
87     }
88     return 0;
89 }

《时间复杂度》

《区间修改,区间查询》

对于区间修改,tree结构体中要多一个变量lazy,用来记录整个区间的修改,是缓存下放到修改子区间的值

注意:不是缓存修改本区间,本区间的维护的信息是要随时修改的

lazy标记的作用:

 

 

 

  1 #include <iostream>
  2 #include <algorithm>
  3 #include <cstring>
  4 using namespace std;
  5 const int N = 100010;
  6 typedef long long LL;
  7 int w[N];
  8 struct tree
  9 {
 10     // sum是区间[l,r]要维护的信息
 11     // lazy是在多次修改时,将修改的数先累加起来,即先不下放修改给子区间
 12     //到这个区间的lazy不平衡时才下放(如[1,10],先整个区间+10,再[5,10]区间+5,这个时候区间lazy不能平衡,要下放)
 13     //当前的sum是可以即时修改的
 14     int l, r, lazy;
 15     LL sum;
 16 } tr[N * 4];
 17 //用子节点更新父节点u的操作
 18 void pushup(int u)
 19 {
 20     tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
 21 }
 22 //将父节点u的lazy标记下放的操作
 23 void pushdown(int u)
 24 {
 25     //如果积累下来的lazy有修改:
 26     if (tr[u].lazy)
 27     {
 28         int sl = u << 1, sr = u << 1 | 1;
 29         //注意lazy这里是+=
 30         tr[sl].lazy += tr[u].lazy;
 31         //注意可能会爆int
 32         tr[sl].sum += (LL)(tr[sl].r - tr[sl].l + 1) * tr[u].lazy;
 33 
 34         tr[sr].lazy += tr[u].lazy;
 35         tr[sr].sum += (LL)(tr[sr].r - tr[sr].l + 1) * tr[u].lazy;
 36         //全部下放成功,清空:
 37         tr[u].lazy = 0;
 38     }
 39 }
 40 void build(int u, int l, int r)
 41 {
 42     tr[u].l = l, tr[u].r = r;
 43 
 44     if (l == r)
 45     {
 46         tr[u].lazy = 0;
 47         tr[u].sum = w[l];
 48         return;
 49     }
 50     int mid = l + r >> 1;
 51     build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
 52     //子节点变化,pushup
 53     pushup(u);
 54 }
 55 //区间修改
 56 void modify(int u, int l, int r, int d)
 57 {
 58     //注意这里与单点修改有着极不一样的地方
 59     if (tr[u].l >= l && tr[u].r <= r)
 60     {
 61         //注意这里tr[u].sum是+=
 62         tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;
 63         tr[u].lazy += d;
 64         return;
 65     }
 66     // u管辖的区间太大,接下来区间分裂,直到出现上面的情况
 67     //区间分裂说明,在u管辖的区间只会有一部分区间加上修改,lazy不平衡,要pushdown();
 68     pushdown(u);
 69     int mid = tr[u].l + tr[u].r >> 1;
 70     if (l <= mid)
 71         modify(u << 1, l, r, d);
 72     if (r > mid)
 73         modify(u << 1 | 1, l, r, d);
 74     //上面u的子节点发生的变化,要pushup
 75     pushup(u);
 76 }
 77 //区间询问
 78 LL query(int u, int l, int r)
 79 {
 80     if (tr[u].l >= l && tr[u].r <= r)
 81         return tr[u].sum;
 82     //当要分裂询问时
 83     //当然要懒标记下放,要不然父节点一直拿到修改值而不去修改也没用
 84     pushdown(u);
 85     int mid = tr[u].l + tr[u].r >> 1;
 86     LL res = 0;
 87     if (l <= mid)
 88         res += query(u << 1, l, r);
 89     if (r > mid)
 90         res += query(u << 1 | 1, l, r);
 91     return res;
 92 }
 93 int main()
 94 {
 95     int n, m;
 96     cin >> n >> m;
 97     for (int i = 1; i <= n; i++)
 98         scanf("%d", &w[i]);
 99     build(1, 1, n);
100     while (m--)
101     {
102         char op[2];
103         int l, r, d;
104         scanf("%s%d%d", op, &l, &r);
105         if (op[0] == 'Q')
106             printf("%lld\n", query(1, l, r));
107         else
108         {
109             scanf("%d", &d);
110             modify(1, l, r, d);
111         }
112     }
113     return 0;
114 }
115 //关于为啥modify哪里要pushdown,再pushup的原因
116 /* 由于modify直接修改了一些区间上面的add与sum,导致递归路径上的父节点全部都需要重新算一遍sum,所以递归结尾需要加一个pushup,
117 
118 同时递归路径上如果存在带有懒标记的区间,则区间结尾的pushup会用两个子区间的返回值直接覆盖该区间的sum,但是该区间的懒标记add值还没有加到子区间上面去,也就是说懒标记的附加值就会被直接覆盖掉。
119 这种pushup会直接消灭懒标记,所以modify递归路径上必须全部消灭懒标记,即每一层递归都要先pushdown。
120 
121 这里可以看到pushup和pushdown是一个组合,递归中每一层pushup保证了递归树的叶节点的改变对父节点的影响能够及时修正,每一层的pushdown都能保证本次的递归路径上一定不存在懒标记,保证本次递归路径上的sum赋值全部为正确值。
122 
123 所以尽管查询操作并没有新增懒标记且保证递归路径上没有懒标记导致我们不需要pushup,但当我们在query的时候在每一层返回sum之前加一次pushup上述的一套组合依然能保证查询结果一定是正确的,
124 
125 所以可以从这一套组合的性质出发直接 背下来,在简单运用线段树模板时候直接modify和query都用这一套,就省事很多了。
126  */
127 //lazy不是对本区间的维护信息延迟,而是对本区间的子区间延迟
128 //本区间维护的信息是要时刻保持最新

《时间复杂度》

《小总结》

 

 区间询问这里是真的要根据题目的不同而变化的,要考虑如何才能正确的维护信息

《权值线段树和动态开点》

权值线段树的区间一般表示的是值域的范围,当值域过大时,如值域为【1,1e9】我们根本不可能开这么大的tr[]数组保存这些节点

(光叶节点最多也有1e9个),所以我们需要动态开点(或者可以用离散化):

好博客:https://blog.csdn.net/qq_41673789/article/details/102773885

《可持久化线段树----主席树》

好博客:https://modestcoder.blog.csdn.net/article/details/90107874

 

 

 对于值域|A[I]|<=1e9,是我们的区间范围,但是这里,区间范围太大了,但是序列的个数却很少,

所以我们可以进行离散化,

即将一系列数,其中数的个数很小,但是数的范围很大,达到我们不可以用数组来操作

比如:我要求一个数值序列中某个数出现的次数,如果数的范围很小,我们可以用数组cnt[]来记录,每出现一个数i,就cnt[i]++;

但是如果数的范围很大如:1e9,这个时候我们可以离散化,将原本的数值映射到另一个数值上,让这个新数值来表示原来数值

如这道题:

 

 

 

  1 #include <iostream>
  2 #include <algorithm>
  3 #include <cstring>
  4 using namespace std;
  5 const int N = 1e5 + 10;
  6 struct Node
  7 {
  8     int pos, num;
  9 } a[N];
 10 int cnt = 1;
 11 //前缀和
 12 int pre[N];
 13 //找到不小于x的下标
 14 int findl(int x)
 15 {
 16     int l = 1, r = cnt;
 17     int res;
 18     while (l <= r)
 19     {
 20         int mid = l + r >> 1;
 21         if (a[mid].pos >= x)
 22         {
 23             r = mid - 1;
 24             res = mid;
 25         }
 26         else
 27             l = mid + 1;
 28     }
 29     return res;
 30 }
 31 //找到不大于x的下标
 32 int findr(int x)
 33 {
 34     int l = 1, r = cnt;
 35     int res;
 36     while (l <= r)
 37     {
 38         int mid = l + r >> 1;
 39         if (a[mid].pos <= x)
 40         {
 41             l = mid + 1;
 42             res = mid;
 43         }
 44         else
 45             r = mid - 1;
 46     }
 47     return res;
 48 }
 49 int main()
 50 {
 51     int n, m;
 52     cin >> n >> m;
 53     for (int i = 1; i <= n; i++)
 54     {
 55         int x, c;
 56         cin >> x >> c;
 57         a[cnt++] = {x, c};
 58     }
 59     //排序
 60     sort(a + 1, a + cnt, [](struct Node a, struct Node b)
 61          { return a.pos < b.pos; });
 62     //去重,合并,下标i为去重后的下标
 63     int i, j;
 64     for (i = 1, j = 2; j < cnt; j++)
 65     {
 66         if (a[i].pos == a[j].pos)
 67             a[i].num += a[j].num;
 68         else
 69         {
 70             i++;
 71             a[i] = a[j];
 72         }
 73     }
 74     cnt = i;
 75     /*for (int i = 1; i <= cnt; i++)
 76     {
 77         cout << "!!!" << a[i].pos << " " << a[i].num << endl;
 78     }*/
 79     //求前缀和
 80     for (int i = 1; i <= cnt; i++)
 81         pre[i] = pre[i - 1] + a[i].num;
 82     /*for (int i = 1; i <= cnt; i++)
 83     {
 84         cout << pre[i] << " ";
 85     }
 86     cout << endl;*/
 87     while (m--)
 88     {
 89         int l, r;
 90         cin >> l >> r;
 91         if (l > a[cnt].pos || r < a[1].pos)
 92             cout << 0 << endl;
 93         else
 94         {
 95             int resl = findl(l), resr = findr(r);
 96             cout << pre[resr] - pre[resl - 1] << endl;
 97         }
 98     }
 99     return 0;
100 }

说远了,回到主席树:

这道题的线段树维护的数据是:这个区间中数出现的次数

对于每一个新加进序列的数,我们都要新开一个线段树

开始只建一个空线段树,后面不同通过加入新的数进入序列,从而根据这个空的线段树建立新的线段树

 

 

 每一个新的线段树,其根节点保存在roots[]中,在第i个线段树代表序列下标[1,i]中数的个数情况在线段树中的样子

 

 

 

  1 #include <iostream>
  2 #include <algorithm>
  3 #include <cstring>
  4 #include <vector>
  5 using namespace std;
  6 const int N = 100010, M = 10010;
  7 struct tree
  8 {
  9     // l,r是保存本节点的左右子节点在tr[]中的下标
 10     int l, r;
 11     //保存这一区间中的数的个数
 12     //带权线段树区间的含义是值域范围
 13     int cnt;
 14     //开5倍数组吧,不要问,问就是玄学
 15 } tr[N << 5];
 16 // tr[]的下标
 17 int idx;
 18 int a[N];
 19 vector<int> nums;
 20 //用来保存每个版本线段树的根节点
 21 int roots[N];
 22 
 23 // find函数返回在nums中不小于数x的下标位置
 24 int find(int x)
 25 {
 26     // lower_bound函数底层为二分,返回在数组中一个不小于x的位置
 27     return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
 28 }
 29 
 30 //既然是单点修改,那么就要用子节点更新父节点
 31 void pushup(int u)
 32 {
 33     tr[u].cnt = tr[tr[u].l].cnt + tr[tr[u].r].cnt;
 34 }
 35 
 36 //返回新建节点在tr[]中的下标
 37 int build(int l, int r)
 38 {
 39     int nidx = ++idx;
 40     if (l == r)
 41         return nidx;
 42     int mid = l + r >> 1;
 43     tr[nidx].l = build(l, mid);
 44     tr[nidx].r = build(mid + 1, r);
 45     return nidx;
 46 }
 47 
 48 //返回新开节点在tr[]中的下标位置
 49 int insert(int last, int l, int r, int x)
 50 {
 51     int nidx = ++idx;
 52     //先让本版本复制上一个版本
 53     tr[nidx] = tr[last];
 54     //接下来是对版本的更新
 55     if (l == r)
 56     {
 57         tr[nidx].cnt++;
 58         return nidx;
 59     }
 60     int mid = l + r >> 1;
 61     if (x <= mid)
 62         tr[nidx].l = insert(tr[last].l, l, mid, x);
 63     else
 64         tr[nidx].r = insert(tr[last].r, mid + 1, r, x);
 65     pushup(nidx);
 66     return nidx;
 67 }
 68 
 69 //返回在nums中的下标
 70 //所谓的版本相减是版本中维护的信息相减
 71 // l,r是维护的值域
 72 // br,bl是两个版本的线段树在tr[]中所代表的下标
 73 int query(int l, int r, int br, int bl, int k)
 74 {
 75     if (l == r)
 76         return l;
 77     int cnt = tr[tr[br].l].cnt - tr[tr[bl].l].cnt;
 78     int mid = l + r >> 1;
 79     if (cnt >= k)
 80         return query(l, mid, tr[br].l, tr[bl].l, k);
 81     else
 82         return query(mid + 1, r, tr[br].r, tr[bl].r, k - cnt);
 83 }
 84 
 85 int main()
 86 {
 87     int n, m;
 88     cin >> n >> m;
 89     for (int i = 1; i <= n; i++)
 90     {
 91         // a[]是真正的原数据
 92         // nums是用来离散化的数据
 93         scanf("%d", &a[i]);
 94         nums.push_back(a[i]);
 95     }
 96     sort(nums.begin(), nums.end());
 97     // unique是对已经排序的数组进行去重操作,并返回去重数组后的最后下标(指针形式)
 98     // erase是删除下标(指针形式)a->b的全部元素
 99     nums.erase(unique(nums.begin(), nums.end()), nums.end());
100 
101     //离散化后的数组nums大小为tr[]节点的区间,对于一个数x,我们
102     //等下要用find(x)找到,数x在nums中的位置;
103     // build操作只是建了个空树而已,方便insert建立新版本线段树
104     roots[0] = build(0, nums.size() - 1);
105 
106     //开始建立新版本线段树
107     //每一个版本i代表:a[]数组中下标1~i区间,在线段树中的区间中数的个数
108     for (int i = 1; i <= n; i++)
109         // roots[i-1]表示上一个版本,因为这个版本是以上个版本为样例
110         //进行改变,线段树初始范围0,nums.size()-1,
111         //上一个版本是1~a[i-1],这个版本是要1~a[i],
112         // find(a[i])找到数a[i]要插进线段树哪一个范围,相当于单点修改
113         roots[i] = insert(roots[i - 1], 0, nums.size() - 1, find(a[i]));
114 
115     while (m--)
116     {
117         int l, r, k;
118         scanf("%d%d%d", &l, &r, &k);
119         printf("%d\n", nums[query(0, nums.size() - 1, roots[r], roots[l - 1], k)]);
120     }
121     return 0;
122 }

 

posted @ 2022-08-15 23:53  次林梦叶  阅读(70)  评论(0)    收藏  举报