「Day 7—离散化 & 树状数组 & 线段树」
离散化
定义
离散化本质是一种哈希,是一种用来处理数据的方法。
1.创建原数组的副本。
2.将副本中的值从小到大排序。
3.将排序好的副本去重。
4.查找原数组的每一个元素在副本中的位置,位置即为排名,将其作为离散化后的值。
B3694 数列离散化
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 1e5 + 5;
int n;
int T;
int a[MAXN],tmp[MAXN];
void lsh(){
for(int i = 1;i <= n;i ++){
//复制一个数组用来去重用
tmp[i] = a[i];
}
sort(tmp + 1,tmp + n + 1);
//去重
int len = unique(tmp + 1,tmp + n + 1) - tmp - 1;
for(int i = 1;i <= n;i ++){
//离散
a[i] = lower_bound(tmp + 1,tmp + len + 1,a[i]) - tmp;
}
for(int i = 1;i <= n;i ++){
cout << a[i] << " ";
}
cout << "\n";
}
int main(){
cin >> T;
while(T --){
cin >> n;
for(int i = 1;i <= n;i ++){
cin >> a[i];
}
lsh();
}
return 0;
}
树状数组
定义
树状数组是一种支持 单点修改 和 区间查询 的数据结构。
下图是一个树状数组的示意图,
具体的看代码
P3374 【模板】树状数组 1
代码
#include<iostream>
using namespace std;
const int MAXN = 5 * 1e5 + 5;
int n,m;
int sum[MAXN];
int lowbit(int x) {
// x 的二进制中,最低位的 1 以及后面所有 0 组成的数。
// lowbit(0b01011000) == 0b00001000
// ~~~~^~~~
// lowbit(0b01110010) == 0b00000010
// ~~~~~~^~
return x & -x;
}
//将x加上k
void add(int x,int k){
while(x <= n) sum[x] += k,x += lowbit(x);
}
//计算从1 ~ x的和
int f(int x){
int ans = 0;
while(x) ans += sum[x],x -= lowbit(x);
return ans;
}
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++){
int x;
cin >> x;
add(i,x);
}
for(int i = 1;i <= m;i ++){
int op,x,y;
cin >> op >> x >> y;
if(op == 1) add(x,y);
else cout << f(y) - f(x - 1) << "\n";
}
}
P3368 【模板】树状数组 2
代码
#include<iostream>
using namespace std;
const int MAXN = 5 * 1e5 + 5;
int n,m;
int sum[MAXN],a[MAXN];
int lowbit(int x){
return x&-x;
}
void add(int x,int k){
while(x <= n) sum[x] += k,x += lowbit(x);
}
int f(int x){
int ans = 0;
while(x) ans += sum[x],x -= lowbit(x);
return ans;
}
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++){
cin >> a[i];
}
for(int i = 1;i <= m;i ++){
int op,x,y,k;
cin >> op >> x;
if(op == 1){
cin >> y >> k;
//运用差分的思想 差分数组在x处+k,在y+1处-k就完成了对x~y的区间加k问题
add(x,k);
add(y + 1,- k);
}
else cout << a[x] + f(x) << "\n";
}
}
例题
P5057 [CQOI2006] 简单题
思路
这个题看起来是个板子,但又不全是,我们可以用树状数组或者线段树来完成,但是这个题树状数组比较方便,我们考虑如何进行取反,一次是 \(1\),第二次就是 \(0\)。于是我们便发现,如果每次区间加 \(1\),最后输出时判断其奇偶性即可。
代码
点击查看代码
#include<iostream>
using namespace std;
const int MAXN = 5 * 1e5 + 5;
int n, m;
int a[MAXN], sum[MAXN];
int lowbit(int x){
return x & - x;
}
void add(int x, int k){
while(x <= n){
sum[x] += k;
x += lowbit(x);
}
return;
}
int fine(int x){
int ans = 0;
while(x){
ans += sum[x];
x -= lowbit(x);
}
return ans;
}
int main(){
cin >> n >> m;
for(int i = 1;i <= m;i ++){
int op;
cin >> op;
if(op == 1){
int l, r;
cin >> l >> r;
add(l, 1);
add(r + 1, -1);
}
else{
int k;
cin >> k;
cout << fine(k) % 2 << "\n";
}
}
return 0;
}
P2068 统计和
思路
没啥好说的吧,差不多和板子一样,可以练一练,就单点 + 区间求和
代码
点击查看代码
#include <iostream>
using namespace std;
#define int long long
const int MAXN = 100005;
int n,w;
int sum[MAXN];
int lowbit(int x){
return x & -x;
}
void add(int x, int k){
while(x <= n){
sum[x] += k;
x += lowbit(x);
}
return;
}
int fine(int x){
int ans = 0;
while(x){
ans += sum[x];
x -= lowbit(x);
}
return ans;
}
signed main(){
cin >> n >> w;
for(int i = 1;i <= w;i ++){
char op;
cin >> op;
if(op == 'x'){
int x, k;
cin >> x >> k;
add(x, k);
}
else{
int l, r;
cin >> l >> r;
cout << fine(r) - fine(l - 1) << "\n";
}
}
return 0;
}
P4939 Agent2
思路
这个题也挺简单的,就不说了。
代码
点击查看代码
#include <iostream>
using namespace std;
#define int long long
const int MAXN = 1e7 + 5;
int n, m;
int sum[MAXN];
int lowbit(int x){
return x & - x;
}
void add(int x, int k){
while(x <= n){
sum[x] += k;
x += lowbit(x);
}
return;
}
int fine(int x){
int ans = 0;
while(x){
ans += sum[x];
x -= lowbit(x);
}
return ans;
}
signed main(){
cin >> n >> m;
for(int i = 1;i <= m;i ++){
int op;
cin >> op;
if(op == 0){
int l, r;
cin >> l >> r;
add(l, 1);
add(r + 1, -1);
}
else{
int x;
cin >> x;
cout << fine(x) << "\n";
}
}
return 0;
}
线段树
定义
线段树是为了维护区间信息的一种数据结构,线段树可以在 \(O(\log N)\) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
P3372 【模板】线段树 1
思路
我们用递归建树来完成,具体的看代码。
代码
定义结构体
struct node{
//l是左子树的节点编号,r是右子树的节点编号,sum为当前节点的区间和(l~r),add是当前节点的懒惰标记。
int l,r,sum,add;
}t[MAXN * 4];
上传操作
void pushup(int p){
//当前节点的区间和=左子树的区间和+右子树的区间和
t[p].sum = t[lc].sum + t[rc].sum;
return;
}
下传操作
void pushdown(int p){
//判断当前节点是否有懒惰标记,如果有就下放给其左右子树
if(t[p].add){
//下放左子树
t[lc].add += t[p].add;
//下放右子树
t[rc].add += t[p].add;
//同时也要将懒惰标记的数值下放给左右子树的区间值
//左子树的区间+区间元素数量*懒惰标记
t[lc].sum += t[p].add * (t[lc].r - t[lc].l + 1);
//右子树的区间+区间元素数量*懒惰标记
t[rc].sum += t[p].add * (t[rc].r - t[rc].l + 1);
//下放完成后将根节点的懒惰标记清0
t[p].add = 0;
}
}
建树
void build(int p,int l,int r){
//我们采用递归建树的方式
t[p] = {l,r,0,0};
//l = r说明这是一个叶子结点,则区间值也就是(l~r)的值为其本身的值x[l]或x[r],这个无所谓
if(l == r){
t[p].sum = x[l];
return;
}
//位运算避免溢出,等同于(l + r) / 2 或者 (l + r) >> 1
int mid = (l & r) + ((l ^ r) >> 1);
//以lc(p << 1)为左子树的根建立以l为区间左端点,mid为区间右端点的左子树
build(lc,l,mid);
//以rc (p << 1 | 1) 为右子树的根建立以mid+1为区间左端点,r为区间右端点的右子树
build(rc,mid + 1,r);
//进行上传操作,上传的原因是因为既然左子树和右子树都建立了,且其区间值也计算完毕,那么就应该利用左右子树向上传递其区间值。
//例如p为【1~2】的区间,左子树lc【1~1】的值为4,右子树rc【2~2】的值为6.
//那么p的值此时为lc.sum + rc.sum,即4 + 6 = 10,。
pushup(p);
}
区间查询
int query(int p,int l,int r){
//如果当前节点 p 代表的区间 [t[p].l, t[p].r] 完全在我们查询的区间 [l, r] 之内
//(即 l <= t[p].l 且 t[p].r <= r),那么我们可以直接返回这个节点的和 t[p].sum,而不需要进一步递归地查询子节点。
if(l <= t[p].l && t[p].r <= r) return t[p].sum;
//依旧是位运算,求的是当前节点p的区间中点
int mid = (t[p].l & t[p].r) + ((t[p].l ^ t[p].r) >> 1);
//下传一下懒惰标记,防止没有下传导致查询出错
pushdown(p);
int sum = 0;
//如果要查的区间的左端点比mid小,说明还要向左子树继续递归求值
if(l <= mid){
sum += query(lc,l,r);
}
//如果要查的区间的右端点比mid大,说明还要向右子树继续递归求值
if(r > mid){
sum += query(rc,l,r);
}
//最后返回左右子树的值之和
return sum;
}
区间修改
void change(int p,int l,int r,int k){
//如果区间刚好对上了,那么就让当前的节点的区间值+区间元素*k
if(l <= t[p].l && t[p].r <= r){
t[p].sum += (t[p].r - t[p].l + 1) * k;
//懒惰标记,方便下传
t[p].add += k;
return;
}
//如果没对上说明还是有左右子树需要继续修改
//又双叒是位运算,求的是当前节点p的区间中点
int mid = (t[p].l & t[p].r) + ((t[p].l ^ t[p].r) >> 1);
//下传懒惰标记,防止在修改子树是区间值不对
pushdown(p);
//如果要修改的区间的左端点比mid小,说明还要向左子树继续递归修改
if(l <= mid) change(lc,l,r,k);
//如果要修改的区间的右端点比mid大,说明还要向右子树继续递归修改
if(r > mid) change(rc,l,r,k);
//修改完别忘了去给p更新值
pushup(p);
}
完整代码
点击查看代码
#include<iostream>
#define lc p << 1
#define rc p << 1 | 1
#define int long long
using namespace std;
const int MAXN = 1e5 + 5;
int n,m,x[MAXN];
struct node{
int l,r,sum,add;
}t[MAXN * 4];
//上传
void pushup(int p){
t[p].sum = t[lc].sum + t[rc].sum;
return;
}
//下传
void pushdown(int p){
if(t[p].add){
t[lc].add += t[p].add;
t[rc].add += t[p].add;
t[lc].sum += t[p].add * (t[lc].r - t[lc].l + 1);
t[rc].sum += t[p].add * (t[rc].r - t[rc].l + 1);
t[p].add = 0;
}
}
//建树
void build(int p,int l,int r){
// t[p] = {l,r,x[l],0};
t[p] = {l,r,0,0};
if(l == r){
t[p].sum = x[r];
return;
}
int mid = (l & r) + ((l ^ r) >> 1);
build(lc,l,mid);
build(rc,mid + 1,r);
pushup(p);
}
int query(int p,int l,int r){
if(l <= t[p].l && t[p].r <= r) return t[p].sum;
int mid = (t[p].l & t[p].r) + ((t[p].l ^ t[p].r) >> 1);
// int mid = (t[p].l + t[p].r) / 2;
pushdown(p);
int sum = 0;
if(l <= mid){
sum += query(lc,l,r);
}
if(r > mid){
sum += query(rc,l,r);
}
return sum;
}
void change(int p,int l,int r,int k){
if(l <= t[p].l && t[p].r <= r){
t[p].sum += (t[p].r - t[p].l + 1) * k;
t[p].add += k;
return;
}
int mid = (t[p].l & t[p].r) + ((t[p].l ^ t[p].r) >> 1);
pushdown(p);
if(l <= mid) change(lc,l,r,k);
if(r > mid) change(rc,l,r,k);
pushup(p);
}
signed main(){
cin >> n >> m;
for(int i = 1;i <= n;i ++){
cin >> x[i];
}
build(1,1,n);
for(int i = 1;i <= m;i ++){
int op,x,y,k;
cin >> op >> x >> y;
if(op == 1){
cin >> k;
change(1,x,y,k);
}
else{
cout << query(1,x,y) << "\n";
}
}
return 0;
}
P3373 【模板】线段树 2
思路
和刚才那个差不多,具体看代码。
代码
定义结构体
struct Tree{
//sum是该子树的区间和
//add是加法的懒惰标记
//mul是乘法的懒惰标记
int l, r, sum, add, mul;
}t[MAXN * 4];
上传操作
void pushup(int u){
//区间和 = 左子树区间和 + 右子树区间和
t[u].sum = (t[lc].sum + t[rc].sum) % p;
}
处理标记
void init(Tree &tree, int md, int ad){
//先乘再加,处理标记
tree.sum = (tree.sum * md + ad * (tree.r - tree.l + 1)) % p;
//将mul的标记处理,便于下次处理
tree.mul = (tree.mul * md) % p;
//将add的标记处理
tree.add = (tree.add * md + ad) % p;
}
下传操作
void pushdown(int u){
//左子树标记下传
init(t[lc], t[u].mul, t[u].add);
//右子树标记下传
init(t[rc], t[u].mul, t[u].add);
//记得清空标记
t[u].mul = 1;
t[u].add = 0;
}
建树
void build(int u, int l, int r){
//我们依然采用递归建树的方式
//初始值
t[u] = {l, r, 0, 0, 1};
//如果到达叶子节点,就让其区间和(l ~ r[l == r])等于其本身。
if(l == r){
t[u].sum = x[l];
return;
}
//位运算计算中点
int mid = (l & r) + ((l ^ r) >> 1);
//递归左子树
build(lc, l, mid);
//递归右子树
build(rc, mid + 1, r);
//在左右两边的树建完的同时别忘了上传到根节点上
pushup(u);
}
区间修改
void change(int u, int l, int r, int md, int ad){
//如果当前的点在我们的目标区间外,那不就纯纯玩脱了吗
if(l > t[u].r || r < t[u].l) return;
//如果刚好在区间里,那就没问题了,更新一下该点懒惰标记
if(l <= t[u].l && t[u].r <= r){
init(t[u], md, ad);
return;
}
//给以u为根的树一整个更新懒惰标记
pushdown(u);
//向左递归更改
change(lc, l, r, md, ad);
//向右递归更改
change(rc, l, r, md, ad);
//别忘了上传给根节点
pushup(u);
}
区间查询
int query(int u, int l, int r){
//如果当前的点在我们的目标区间外,那不就纯纯玩脱了吗
if(l > t[u].r || r < t[u].l) return 0;
//如果刚好在目标区间里,直接返回sum[u],没必要继续调用了
if(l <= t[u].l && t[u].r <= r) return t[u].sum;
//递归查询前要先将标记下传,更新一下
pushdown(u);
//返回左右子树的区间和
return (query(lc, l, r) + query(rc, l, r)) % p;
}
完整代码
点击查看代码
#include <iostream>
using namespace std;
#define int long long
#define lc u<<1
#define rc u<<1|1
const int MAXN = 1e5 + 5;
int n, q, p;
int x[MAXN];
struct Tree{
int l, r, sum, add, mul;
}t[MAXN * 4];
void pushup(int u){
t[u].sum = (t[lc].sum + t[rc].sum) % p;
}
void init(Tree &tree, int md, int ad){
tree.sum = (tree.sum * md + ad * (tree.r - tree.l + 1)) % p;
tree.mul = (tree.mul * md) % p;
tree.add = (tree.add * md + ad) % p;
}
void pushdown(int u){
init(t[lc], t[u].mul, t[u].add);
init(t[rc], t[u].mul, t[u].add);
t[u].mul = 1;
t[u].add = 0;
}
void build(int u, int l, int r){
t[u] = {l, r, 0, 0, 1};
if(l == r){
t[u].sum = x[l];
return;
}
int mid = (l & r) + ((l ^ r) >> 1);
build(lc, l, mid);
build(rc, mid + 1, r);
pushup(u);
}
void change(int u, int l, int r, int md, int ad){
if(l > t[u].r || r < t[u].l) return;
if(l <= t[u].l && t[u].r <= r){
init(t[u], md, ad);
return;
}
pushdown(u);
change(lc, l, r, md, ad);
change(rc, l, r, md, ad);
pushup(u);
}
int query(int u, int l, int r){
if(l > t[u].r || r < t[u].l) return 0;
if(l <= t[u].l && t[u].r <= r) return t[u].sum;
pushdown(u);
return (query(lc, l, r) + query(rc, l, r)) % p;
}
main(){
cin >> n >> q >> p;
for(int i = 1;i <= n;i ++){
cin >> x[i];
}
build(1, 1, n);
for(int i = 1;i <= q;i ++){
int op, x, y, k;
cin >> op >> x >> y;
if(op == 1){
cin >> k;
change(1, x, y, k, 0);
}
else if(op == 2){
cin >> k;
change(1, x, y, 1, k);
}
else{
cout << query(1, x,y) << "\n";
}
}
return 0;
}
本文来自一名初中牲,作者:To_Carpe_Diem