树状数组、线段树(未完待续)

树状数组

基本概念

1、lowbit(x):取x二进制最低位的1

lowbit(x) = x & (-x)

eg : lowbit(1010) = 10
lowbit(1000) = 1000

2、和原始数组不同,树状数组存储的不是原始数据,而是原始数组中下标在一个区间的数之和。它像是前缀和和差分方法的优化。前缀和虽然可以 O(1) 进行区间求和,即查询操作,但是它的单次更新复杂度达到了 O(n) ;差分的更新为 O(1),而查询却为 O(n)。

3、对于树状数组 a[pos] ,它存储的是下标在区间 (pos-lowbit(pos), pos] 的数之和。(见下图)
也正是因为这点,树状数组在查询和更新时使用和 lowbit 相关的操作,实现了查询和更新 O(log n) 的复杂度。
p1

单点更新、区间查询

转载

单点更新

从图片中我们可以直观地感受到:对于x节点,最小的“管辖”它的节点(就是父亲节点,之前的想象力hh)应该是x+lowbit(x), 因此x+y,x+lowbit(x)节点也要+y,x+lowbit(x)+lowbit(x+lowbit(x))节点也要+y……

区间查询

要查询(0, x]内的和,由于x节点只“管辖”(x-lowbit(x), x],下一段应该是(x-lowbit(x)-lowbit(x-lowbit(x)), x-lowbit(x)],再下一段……,直到(0, c].

// 单点更新
void update(int x, ll y)
{
    while (x <= n)  // 表示维护区间的上限,不一定等于n
    {
        a[x] += y;
        x += lowbit(x);
    }
}

// 区间查询
ll getsum(int x)
{
    ll res = 0;
    while (x > 0)
    {
        res += a[x];
        x -= lowbit(x);
    }
    return res;
}

求逆序对

先将原序列离散化,用 1 - n 代替原序列但不改变相对位置的大小关系。为什么要离散化?很多情况下我们要研究的是一个元素在序列中的性质,并不关心它的实际值,这时可以将其映射至 1,2,3,4,…这个自然数序列。求逆序对,我们只关心序列中两个数的偏序关系(大小关系),因此做离散化处理。

    // 离散化: 用1-n替换原序列
    cin >> n;
    for(int i = 1; i <= n; i++){
        cin >> data[i];
        mp[data[i]] = i;	// 记录该数在原序列中的位置
    }

    sort(data + 1, data + n + 1);

    // 构造离散后的序列
    for(int i = 1; i <= n; i++){
        b[mp[data[i]]] = i;		// 第i小的数在原序列中位置,将其映射为i
    }

之后可以单点更新,统计答案。遍历时,t出现时,[1, t-1]还未出现的数能和t构成逆序对,这样数的个数为 t - 1 - getsum(t-1).此时树状数组a类似一个桶,维护的是一堆01,表示对应下标的数是否出现。

	//单点更新
	void update(int x){
	    while(x <= n){
	        a[x]++;
	        x += x & (-x);
	    }
	}
	
    for(int i = 1; i <= n; i++){
        update(b[i]);
        ans += b[i] - 1 - getsum(b[i] - 1);
    }

维护区间最值(最大值为例)

不同于线段树,树状数组不保存外部节点(原数组信息)。虽然在维护区间和时可以通过查询获得,但是在维护区间最值时就无法做到了。区间 A 的 max 为 a,将其分为A1,A2两个区间后无法求出各个区间的 max 。(不满足区间减法)因此树状数组在维护区间最值时要保存原始数组。

单点更新

更新后,遍历直接相连的孩子节点,重新确定该节点的值。这里归纳一个性质:节点x的直接孩子为 x-i ,其中 i=2^k<lowbit(x) 。 可以和之前父亲节点的性质一起理解。

// A[]为原始数组
// 赋值操作: A[x] = y
void update(int x, int y) {
	h[x] = y;
    while (x <= N) 
        // 重新求解区间(x - lowbit(x), x]最大值
        for (int i = 1; i < lowbit(x); i <<= 1) {
            h[x] = max(h[x], h[x - i]);
        }
        x += lowbit(x);
    }

区间查询

维护最值时,区间减的性质无了,但是区间加仍满足!

int query(int x, int y)
{
	int ans = 0;
	while (y >= x)
	{
		ans = max(A[y], ans);
		y--;
		for (; y - lowbit(y) >= x; y -= lowbit(y))
			ans = max(h[y], ans);
	}
	return ans;
}

区间修改、单点查询

通过“差分”,转化为单点更新区间查询。查询和修改函数部分的代码和后者相同。
区间修改、单点查询推导

    a[0] = 0;
    for(int i = 1; i <= n; i++){
        cin >> a[i];
        update(i, a[i] - a[i - 1]);   // 差分
    }
    
    // 更新操作
	cin >> c1 >> c2 >> d;
	update(c1, d);
	update(c2 + 1, -d);
	// 查询操作
	cin >> c1;
	cout << get(c1) << '\n';	

区间修改、区间查询

区间更新、区间查询推导

void update(int x, ll y)
{
    int cur = x;
    while (x <= n)
    {
        s1[x] += y;
        s2[x] += y * (cur - 1);
        x += x & (-x);
    }
}

ll getsum(int x)
{
    ll res = 0, cur = x;
    while (x > 0)
    {
        res += cur * s1[x] - s2[x];
        x -= x & (-x);
    }
    return res;
}

线段树

线段树简介

01 线段树功能非常强大,且拓展性也很强,通过各种操作的组合可以实现在O(log n)的时间内维护区间信息。
02 支持单点修改以及一些特殊的区间操作,包括区间修改(区间加、减、乘)、区间查询(区间求和、求区间最大值、求区间最小值)等。
03 线段树维护的信息,需要满足可加性,即能以可以接受的速度合并信息和修改信息。

线段树的建立

// d[now]维护原数组下标[l,r]的区间
void build(int now, int l, int r)
{
	if (l == r)
	{
		tree[now] = a[l];
		return;
	}

	int mid = (l + r) >> 1;
	build(now * 2, l, mid);
	build(now * 2 + 1, mid + 1, r);
	tree[now] = tree[now * 2] + tree[now * 2 + 1];
	// 维护最大值
	// tree[now] = max(tree[now * 2], tree[now * 2 + 1]);
}

区间查询

ll query(int now, int l, int r, int ql, int qr)
{
	ll sum = 0;
	// 完整地包含在询问区间中的极大区间,若不则还要拆分
	if (ql <= l && qr >= r)
	{
		sum += tree[now];
		// 维护最大值
		// res = max(res, tree[now]);
		return sum;
	}

	pd(now, l, r);

	int mid = (l + r) >> 1;
	if (ql <= mid)
		sum += query(now * 2, l, mid, ql, qr);
		// 维护最大值,qr>mid情形同理
		// res = max(res, query(now * 2, l, mid, ql, qr))
	if (qr > mid)
		sum += query(now * 2 + 1, mid + 1, r, ql, qr);
	return sum;
}

单点修改

// 递归找到要修改的x位置
void update(int now, int l, int r, int x, int uy)
{
	if (l == r)
	{
		tree[now] += uy;
		// 维护最大值
		// tree[now] = uy;
		return;
	}

	int mid = (l + r) >> 1;
	if (x <= mid)
		update(now * 2, l, mid, x, uy);
	else
		update(now * 2 + 1, mid + 1, r, x, uy);

	tree[now] = tree[now * 2] + tree[now * 2 + 1];
	// tree[now] = max(tree[now * 2], tree[now * 2 + 1]);
}

区间修改(加)

void update(int now, int l, int r, int xl, int xr, int c)
{
	if (xl <= l && xr >= r)
	{
		tree[now] += (r - l + 1) * c;
		tag[now] += c;
		return;
	}

	pd(now, l, r);

	int mid = (l + r) >> 1;
	if (xl <= mid)
		update(now * 2, l, mid, xl, xr, c);
	if (xr >= mid + 1)
		update(now * 2 + 1, mid + 1, r, xl, xr, c);

	tree[now] = tree[now * 2] + tree[now * 2 + 1];
}

懒标记优化

void pd(int now, int l, int r)
{
	int ls = now * 2;
	int lr = now * 2 + 1;

	int mid = (l + r) >> 1;
	tag[ls] += tag[now];
	tag[lr] += tag[now];

	tree[ls] += tag[now] * (mid - l + 1);
	tree[lr] += tag[now] * (r - mid);

	tag[now] = 0;

区间修改(乘)

void update1(int now, int l, int r, int xl, int xr, int c)
{
	if (xl <= l && xr >= r)
	{
		tree[now] = tree[now] * c % p;
		tag1[now] = tag1[now] * c % p;
		tag2[now] = tag2[now] * c % p;

		return;
	}

	pd(now, l, r);

	int mid = (l + r) >> 1;
	if (xl <= mid)
		update1(now * 2, l, mid, xl, xr, c);
	if (xr >= mid + 1)
		update1(now * 2 + 1, mid + 1, r, xl, xr, c);

	tree[now] = (tree[now * 2] + tree[now * 2 + 1]) % p;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

u小鬼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值