「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;
}
posted @ 2024-08-12 22:26  To_Carpe_Diem  阅读(27)  评论(4编辑  收藏  举报