树状数组好题

树状数组的题目一般有两种。哪两种?我们可以看看树状数组的定义。
我们树状数组有个数组 \(f\)。那么这个 \(f\) 有哪几种情况呢?
如果 \(f\) 的下标存的是原序列的下标,那么树状数组就是求原序列区间和/min/max/.....
如果 \(f\) 的下标存的是原序列的值,那么树状数组就是用来二位数点的。
废话不多说,看题。

P3605 [USACO17JAN] Promotion Counting P

首先 dfs 在所难免。
考虑怎么统计答案。

  1. 考虑 dfs 后离线统计。
    我们考虑用 dfn 序来做。想要统计一个子树比自己权值大的数的个数,其实就是子树对应区间的比自己大的个数。简单来说,如果查询以 \(x\) 为根的子树比自己权值大的个数,我们就可以转换为序列上求 \([dfn[x],dfn[x]+sz[x]-1]\) 区间内比自己大的个数。主席树或线段树处理即可。
  2. 考虑 dfs 在线统计
    我们维护出 \(n\) 个树状数组,具体就是把以 \(x\) 为根的子树内的点的权值扔进树状数组 BIT[x],让 \(f\) 的下标存储点的值,然后二位数点就行了。
    但是我们的空间不能接受开 \(\mathcal O(n)\) 级别的树状数组,而且合并树状数组会多一个 \(log\)。怎么办呢?
    实际上我们只需要开一个树状数组。毕竟能在若干个树状数组上数点,为什么不直接在一个树状数组上数点呢?
    我们遍历完 \(x\) 的子树后去统计树状数组内大于自己权值的点的个数,最后也把 \(x\) 的权值扔进去。由于可能树状数组内部包含其他子树的信息,我们在遍历 \(x\) 之前先让 \(ans[x]\) 减去其他子树大于自己节点权值的个数即可。

两种方法均可,但是显然第二种好写,复杂度都是 \(\mathcal O(n\log n)\)

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=1e5+5;
struct BIT
{
    int T[N];
    void upd(int x)
    {
        for(;x;x-=x&-x)
            T[x]++;
    }
    int query(int x)
    {
        int res=0;
        for(;x<=N-5;x+=x&-x)
            res+=T[x];
        return res;
    }
}T;
vector<int> G[N];
int n,a[N],ans[N],b[N];
void dfs(int x,int fa)
{
    ans[x]-=T.query(a[x]);
    for(int v:G[x])
    {
        if(v==fa) continue;
        dfs(v,x);
    }
    ans[x]+=T.query(a[x]);
    T.upd(a[x]);
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i],b[i]=a[i];
    sort(b+1,b+1+n);
    int m=unique(b+1,b+1+n)-b;
    for(int i=1;i<=n;i++)
        a[i]=lower_bound(b+1,b+1+m,a[i])-b;
    for(int i=2;i<=n;i++)
    {
        int x;cin>>x;
        G[x].pb(i),G[i].pb(x);
    }
    dfs(1,0);
    for(int i=1;i<=n;i++)
        cout<<ans[i]<<"\n";
    return 0;
}

P1168 中位数

一眼对顶堆,但这不是我们今天的主角。
我们考虑能不能用树状数组做。首先考虑离散化,然后我们维护出一个树状数组。
这个时候就是求目前一个序列的第 \(k\) 大值。其中 \(k=\frac{i+1}{2}\)
然后在树状数组上二分就行了。
代码不放了,感觉很好写吧。

P3760 [TJOI2017] 异或和

这题应该没有紫。至少我能自己做出来的应该都不算紫。
首先考虑二进制拆分。然后就是统计每一位上是否最后异或能出 \(1\),也就是说看在这一位上,产生 \(1\) 的贡献的个数是不是奇数。那么怎么求个数呢?
我们首先维护一个前缀和数组,这样区间和就变为两个点的减法,处理起来会简单一些。然后一次枚举每一进制位。
首先开两个树状数组是显然的,一个存储的是这一位是 \(0\) 的信息,另一个存 \(1\)
然后我们考虑借位情况。我们假设对于对于某一位 \(i\),由于该位的对称性,我们这里讨论第 \(x\) 个数在 \(i\) 位上是 \(1\) 的情况,那么能与他产生 \(1\) 的贡献的有两种情况。

  1. \(i\) 位是 \(0\) 且不借位。
  2. \(i\) 位是 \(1\) 且借位。
    如果我们把前 \(n\) 个数 \(i-1\) 位的值看作数组 \(b\),那么就是:
  3. \(i\) 位是 \(0\) 且小于等于 \(b_x\)\(b\) 的个数。
  4. \(i\) 位是 \(1\) 且大于 \(b_x\)\(b\) 的个数。
    这就是二位数点了,因此我们对于每一进制位做一次二位数点即可。复杂度 \(\mathcal O(n\log ^2V)\)
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,V=1e6+5;
struct BIT
{
	int t[V][2];
	void clear(){memset(t,0,sizeof(t));}
	void upd(int x)
	{
		int y=x;
		for(;x<=V-5;x+=x&-x)
			t[x][0]++;
		for(;y;y-=y&-y)
			t[y][1]++;
	}
	int query1(int x)
	{
		int res=0;
		for(;x;x-=x&-x) res+=t[x][0];
		return res;
	}
	int query2(int x)
	{
		int res=0;
		for(;x<=V-5;x+=x&-x)
			res+=t[x][1];
		return res;
	}
}T0,T1;
int n,a[N],b[N],cnt,ans,cnt0,cnt1;
int main()
{
	cin>>n;
	for(int i=1,x;i<=n;i++)
		cin>>x,a[i]=a[i-1]+x;
	cnt0=1;
	for(int i=1;i<=n;i++)
	{
		int x=a[i]&1;
		if(x) cnt+=cnt0,cnt1++;
		else cnt+=cnt1,cnt0++;
	}
	if(cnt&1) ans=1;
	for(int i=1;i<=20;i++)
	{
		T0.clear(),T1.clear(),T0.upd(1),cnt=0;
		for(int j=1;j<=n;j++)
			b[j]|=((a[j]>>(i-1))&1)<<(i-1);
		for(int j=1;j<=n;j++)
		{
			int x=(a[j]>>i)&1;
			if(x) cnt+=T0.query1(b[j]+1)+T1.query2(b[j]+2),T1.upd(b[j]+1);
			else cnt+=T1.query1(b[j]+1)+T0.query2(b[j]+2),T0.upd(b[j]+1);
		}
		if(cnt&1) ans|=1<<i;
	}
	cout<<ans;
	return 0;
}

P3369 【模板】普通平衡树

???????????????????????????????????????????????????????????????
是的,这道题可以树状数组做!
考虑把这道题除了 \(4\) 操作外的值全部装入一个数组,然后离线离散化。
对值开树状数组,然后遇到加入和删除分别进行 \(upd(a[i],1)\)\(upd(a[i],-1)\) 即可。
后三个询问呢?\(4\) 直接 \(query(i)\)\(5,6\) 直接二分。
玄学树状数组,而且比块链复杂度优秀,比平衡树常数小。

#include<bits/stdc++.h>
using namespace std;
const int maxn=100050;
inline int read()
{
	int x=0,t=1;char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-')t=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
	return x*t;
}
int n,q[maxn],a[maxn],p[maxn],tot=0,c[maxn];
int hash(int x){return lower_bound(q+1,q+1+tot,x)-q;}
int lowbit(int x){return x&-x;}
void add(int x,int p)
{
	while(x<=tot)
	{
		c[x]+=p;
		x+=lowbit(x);
	}
}
int sum(int x)
{
	int res=0;
	while(x)
	{
		res+=c[x];
		x-=lowbit(x);
	}
	return res;
}
int query(int x)
{
	int t=0;
	for(int i=19;i>=0;i--)
	{
		t+=1<<i;
		if(t>tot||c[t]>=x) t-=1<<i;
		else x-=c[t];
	}
	return q[t+1];
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		p[i]=read(),a[i]=read();
		if(p[i]!=4) q[++tot]=a[i];
	}
	//hash
	sort(q+1,q+1+tot);
	tot=unique(q+1,q+1+tot)-(1+q);
	for(int i=1;i<=n;i++)
	{
		if(p[i]==1) add(hash(a[i]),1);
		if(p[i]==2) add(hash(a[i]),-1);
		if(p[i]==3) printf("%d\n",sum(hash(a[i])-1)+1);
		if(p[i]==4) printf("%d\n",query(a[i]));
		if(p[i]==5) printf("%d\n",query(sum(hash(a[i])-1)));
		if(p[i]==6) printf("%d\n",query(sum(hash(a[i]))+1));
	}
	return 0;
} 

P2717 寒假作业

很多人都把这道题作为一道 cdq 入门题。
那么这道题树状数组怎么做呢?我们考虑把每个数都减去 \(k\)
那么就是问有多少个子区间,平均值大于等于 \(0\)。换句话说,就是区间和大于等于 \(0\)
我们考虑维护出前缀和数组,然后就是问 \((i,j)\) 的数量,使得 \(i\le j,sum_j\ge sum_{i-1}\)。然后就是经典二位数点了。
二位数点写的有点多了不想写了(。

P1972 [SDOI2009] HH的项链

首先我要说一句,莫队可做!!!!
这道题之前说了在线做法碾爆树状数组和莫队的主席树,但是我们还是要了解一下好写的树状数组做法。
这道题比较难的地方就是怎么让它区间颜色只产生 \(1\) 的贡献。我们考虑扫描线做法,然后每一次维护颜色的最靠右的贡献即可。
假设我们这个时候遇到了一个之前没遇到的颜色,我们就在这里打上 \(1\) 的标记;如果遇到一个之前遇到过的颜色,那么就撤销之前打过的标记,然后在这里打上 \(1\) 的标记。像这种打撤销标记的做法是扫描线经典做法了,省选也很喜欢出。
然后就做完了。查询的时候就是 \(query(r)-query(l-1)\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N = 1e6 + 5;
ll n, a[N], nxt[N];
struct ANS
{
    ll id, l;
    ll r, ans;
} A[N];
bool CMP(ANS x, ANS y)
{
    return x.r < y.r;
}
bool CMP2(ANS x, ANS y)
{
    return x.id < y.id;
}
namespace BIT
{
    ll T[N];
    ll lowbit(ll x) {return x & -x;}
    void add(ll x, ll k)
    {
        while (x <= n)
        {
            T[x] += k;
            x += lowbit(x);
        }
    }
    ll query(ll x)
    {
        ll res = 0;
        while (x)
        {
            res += T[x];
            x -= lowbit(x);
        }
        return res;
    }
}
int main()
{
    scanf("%lld", &n);
    for (ll i = 1; i <= n; i++) scanf("%lld", a + i);
    ll q;
    scanf("%lld", &q);
    for (ll i = 1; i <= q; i++)
    {
        scanf("%lld%lld", &A[i].l, &A[i].r);
        A[i].id = i;
    }
    sort(A + 1, A + 1 + q, CMP);
    ll pos = 1;
    for (ll i = 1; i <= q; i++)
    {
        ll l = A[i].l, r = A[i].r;
        while (pos <= r)
        {
            if (nxt[a[pos]]) BIT::add(nxt[a[pos]], -1);
            BIT::add(pos, 1);
            nxt[a[pos]] = pos;
            pos++;
        }
        A[i].ans = BIT::query(r) - BIT::query(l - 1);
    }
    sort(A + 1, A + 1 + q, CMP2);
    for (ll i = 1; i <= q; i++) cout << A[i].ans << endl;
    return 0;
}

补充一下。如果这个时候给序列加上一个属性值 \(x_i\),每一次问区间内每一种颜色的最大属性值之和,怎么做呢?我们还是打撤销贡献,不同的是,我们还需要记录一个属性值的贡献即可。可以补充一下代码:

#include <bits/stdc++.h>//deepseek写的,老子懒得写了
using namespace std;

typedef long long ll;
const int MAXN = 5e5 + 5;

struct Query {
    int l, r, idx;
};

int n, q;
int c[MAXN], x[MAXN];
Query queries[MAXN];
ll ans[MAXN];

vector<int> by_r[MAXN];
map<int, int> last_pos;
map<int, int> color_max;

// Fenwick Tree for prefix sums
struct FenwickTree {
    vector<ll> bit;
    int n;

    FenwickTree(int n) : n(n), bit(n + 1, 0) {}

    void update(int idx, ll delta) {
        for (; idx <= n; idx += idx & -idx)
            bit[idx] += delta;
    }

    ll query(int idx) {
        ll res = 0;
        for (; idx > 0; idx -= idx & -idx)
            res += bit[idx];
        return res;
    }

    ll query_range(int l, int r) {
        return query(r) - query(l - 1);
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> q;
    for (int i = 1; i <= n; ++i) {
        cin >> c[i] >> x[i];
    }

    for (int i = 0; i < q; ++i) {
        cin >> queries[i].l >> queries[i].r;
        queries[i].idx = i;
        by_r[queries[i].r].push_back(i);
    }

    FenwickTree ft(n);

    for (int r = 1; r <= n; ++r) {
        int color = c[r];
        int value = x[r];

        // Update the current max for the color
        if (color_max.find(color) != color_max.end()) {
            if (value > color_max[color]) {
                // Subtract the old max
                ft.update(last_pos[color], -color_max[color]);
                // Add the new max
                ft.update(r, value);
                color_max[color] = value;
                last_pos[color] = r;
            }
        } else {
            color_max[color] = value;
            last_pos[color] = r;
            ft.update(r, value);
        }

        // Answer all queries ending at r
        for (int idx : by_r[r]) {
            int l = queries[idx].l;
            ans[queries[idx].idx] = ft.query_range(l, r);
        }
    }

    for (int i = 0; i < q; ++i) {
        cout << ans[i] << '\n';
    }

    return 0;
}

当然这道补充题也可以莫队做,这里推荐莫队做了,因为如果不卡常的话莫队会更好写一些。

P6225 [eJOI 2019] 异或橙子

线段树唐题。那么现在不让用线段树否则斩立决,该怎么办呢?
那肯定是跟 \(l,u\) 的奇偶性有关呗。
如果 \(l,u\) 的奇偶性相同,\(l,u\) 中所有元素答案皆为 \(0\)
如果奇偶性不同,那么答案就是 \(a_l\oplus a_{l+2}\oplus \cdots\oplus a_u\)
开两个树状数组维护奇偶下标即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N = 2e5 + 5;
ll tree_0[N], tree_1[N];
ll n;
ll a[N];
ll lowbit(ll x) {
	return (x & -x);
}
ll query(ll x) {
	ll cnt = 0;
	for (ll i = x; i; i -= lowbit(i)) {
		cnt ^= tree_0[i];
	}
	return cnt;
}
void update(ll x, ll k) {
	for (ll i = x; i <= n; i += lowbit(i)) {
		tree_0[i] ^= k;
	}
}
ll queryy(ll x) {
	ll cnt = 0;
	for(ll i = x; i; i -= lowbit(i)) {
		cnt ^= tree_1[i];
	}
	return cnt;
}
void updatee(ll x, ll k) {
	for (ll i = x; i <= n; i += lowbit(i)) {
		tree_1[i] ^= k;
	}
}
int main() {
	ll m;
	scanf("%lld%lld", &n, &m);
	for (ll i = 1; i <= n; ++i) {
		scanf("%lld", a + i);
		if ((i & 1) == 0) {
			update(i, a[i]);
		} else {
			updatee(i, a[i]);
		}
	}
	while (m--) {
		ll op;
		scanf("%lld", &op);
		if (op == 1) {
			ll x, value;
			scanf("%lld%lld", &x, &value);
			ll k = x & 1;
			if (k == 0) {
				update(x, a[x] ^ value);
			} else {
				updatee(x, a[x] ^ value);
			}
			a[x] = value;
		} else {
			ll left, right;
			scanf("%lld%lld", &left, &right);
			if ((left + right) & 1) {
				cout << 0 << endl;
				continue;
			}
			ll k = left & 1;
			if (k == 0) {
				printf("%lld\n", query(right) ^ query(left - 1));
				continue;
			}
			printf("%lld\n", queryy(right) ^ queryy(left - 1));
		}
	}
	return 0;
}
posted @ 2025-05-10 14:53  I_AK_CTSC  阅读(23)  评论(0)    收藏  举报