树状数组的应用

 

树状数组应用:

我理解这个怎么更新最值问题也想了很久,我认为要完全理解树状数组的应用要理解透两点:

1.lowbit(i)是干什么用的。

2.c[i]是怎么保存状态的。

  • 区间求和类问题(前缀和)
  • 最大值/最小值
  • 求序列中第 K 大数
  • “图腾”类问题的统计

1.区间求和类问题(前缀和)上面讲了,就不举例了;

 

2.最大值/最小值(最大最小一样,改个max)

for(int x=1; x<lowbit(i); x<<=1)
 {
      c[i]=max(c[i],c[i-x]);
 }

对于c[i],每次都是往下更新最值的。而i是根据lowbit的规则往下的。举例:

lowbit(8)=8;

i==4,c[i-1]=c[7];  x<<=1==2;  

           c[i-2]=c[6],  x<<=1==4;

           c[i-4]=c[4],  x<<=1=8;

break;

说明每次c[i]都是把它的子树或者节点更新,若它的子树的最大值大于当前的值,则更新;

hdu1754AC代码

#include<bits/stdc++.h>
using namespace std;

#define e exp(1)
#define pi acos(-1)
#define mod 1000000007
#define inf 0x3f3f3f3f
#define ll long long
#define ull unsigned long long
#define mem(a,b) memset(a,b,sizeof(a))
int gcd(int a,int b){return b?gcd(b,a%b):a;}

const int maxn=2e5+10;

int n,m,a[maxn],c[maxn];

int lowbit(int x)
{
	return x&(-x);
}
void update(int i,int value)
{
	while(i<=n)
	{
		c[i]=value;
		for(int x=1; x<lowbit(i); x<<=1)
		{
			c[i]=max(c[i],c[i-x]);
		}
		i+=lowbit(i);
	}
	return ;
}
int query(int l,int r)
{
	int ans=0;
	while(l<=r)
	{
		ans=max(ans,a[r]);
		r--;
		for(; r-l>=lowbit(r); r-=lowbit(r))
		{
			ans=max(ans,c[r]);
		}
	}
	return ans;
}
int main()
{
	while(~scanf("%d%d",&n,&m))
	{
		for(int i=1; i<=n; i++)c[i]=0;
		for(int i=1; i<=n; i++)
		{
			scanf("%d",&a[i]);
			update(i,a[i]);
		}
		while(m--)
		{
			char s[5];int x,y;
			scanf("%s %d%d",s,&x,&y);
			if(s[0]=='Q')
			{
				printf("%d\n",query(x,y));
			}
			else
			{
				a[x]=y;
				update(x,y);
			}
		}
	}
	return 0;
}

3.求序列中第 K 大数

以POJ 2985为例,具体的写在程序里。思路都是基于二分的思想。

下面是(LogN)^2的方法

?
/*
    题意:某人养了很多猫,他会把一些猫合并成一组,并且会询问第k大组有几只猫

    算法:处理集合用并查集,动态更新第K值用树状数组,具体的看注释

    2011-07-21 19:59
*/

#include <stdio.h>

#define MAXN 300000

int a[MAXN], f[MAXN],c[MAXN];//c[maxn]表示值为i的数有i个; 
int n, m;

int lowbit(int x)
{
    return x & -x;
}

int find(int x)
{
    if (x != f[x])
        f[x] = find(f[x]);
    return f[x];
}

void add(int x, int num)
{
    for ( ; x <= n; x += lowbit(x))
        c[x] += num;
}

int sum(int x)
{
    int sum = 0;
    for ( ; x > 0; x -= lowbit(x))
        sum += c[x];
    return sum;
}




int main()
{
    int i, num, cmd, x, y, k, l, r;

    scanf("%d%d", &n, &m);
    for (i = 1; i <= n; i++)
        f[i] = i;
    for (i = 1; i <= n; i++)
        a[i] = 1;
    add(1, n);//初始状态值为1的数有n个, a[i]表示组内有i只猫的组数,
    num = n;
    for (i = 1; i <= m; i++)
    {
        scanf("%d", &cmd);
        if (cmd == 0)
        {
            scanf("%d%d", &x, &y);
            x = find(x);
            y = find(y);
            if (x == y)
                continue;
            add(a[x], -1);
            add(a[y], -1);
            add(a[y] = a[x] + a[y], 1);
            f[x] = y;
            num--;//合并集合
        }
        else
        {
            scanf("%d", &k);
            k = num - k + 1;//转换为找第k小的数
            l = 1;
            r = n;//二分逼近求第k大值,就是求第num - k + 1小的值
            while (l <= r)
            {
                int mid = (l + r) / 2;
                if (sum(mid) >= k)//注意这里是>=,因为是求第num - k + 1小的,所以尽量往左逼近
                    r = mid - 1;
                else
                    l = mid + 1;
            }
            printf("%d\n", l);
        }
    }
    return 0;
}

下面是LogN的方法

/*
    题意:某人养了很多猫,他会把一些猫合并成一组,并且会询问第k大组有几只猫

    算法:处理集合用并查集,动态更新第K值用树状数组,具体的看注释

    2011-07-21 20:42
*/

#include <stdio.h>

#define MAXN 300000

int a[MAXN], c[MAXN + 5], f[MAXN];
int n, m;

int lowbit(int x)
{
    return x & -x;
}

int find(int x)
{
    if (x != f[x])
        f[x] = find(f[x]);
    return f[x];
}

void add(int x, int num)
{
    for ( ; x <= MAXN; x += lowbit(x))
        c[x] += num;
}

int sum(int x)
{
    int sum = 0;
    for ( ; x > 0; x -= lowbit(x))
        sum += c[x];
    return sum;
}


/*
    求第K小的值。a[i]表示值为i的个数,c[i]当然就是管辖区域内a[i]的和了。

    神奇的方法。不断逼近。每次判断是否包括(ans,ans + 1 << i]的区域,
    不是的话减掉,是的话当前的值加上该区域有的元素。
    注意MAXN是更新到的最大值,如果上面只更新到n的话取n就行了。

    乍一看循环的量是常数,难道是O(1)的吗?实际上i应该遍历到LogN,所以该算法是LogN的。比线段树、平衡树代码量少多了。
*/

int find_kth(int k)
{
    int ans = 0, cnt = 0, i;
    for (i = 20; i >= 0; i--)
    {
        ans += (1 << i);
        if (ans >= MAXN|| cnt + c[ans] >= k)
            ans -= (1 << i);
        else
            cnt += c[ans];
    }
    return ans + 1;
}


int main()
{
    int i, num, cmd, x, y, k, l, r;

    scanf("%d%d", &n, &m);
    for (i = 1; i <= n; i++)
        f[i] = i;
    for (i = 1; i <= n; i++)
        a[i] = 1;
    add(1, n);//a[i]表示组内有i只猫的组数
    num = n;
    for (i = 1; i <= m; i++)
    {
        scanf("%d", &cmd);
        if (cmd == 0)
        {
            scanf("%d%d", &x, &y);
            x = find(x);
            y = find(y);
            if (x == y)
                continue;
            add(a[x], -1);
            add(a[y], -1);
            add(a[y] = a[x] + a[y], 1);
            f[x] = y;
            num--;//合并集合
        }
        else
        {
            scanf("%d", &k);
            printf("%d\n", find_kth(num - k + 1));//第k大就是第num - k + 1小的
        }
    }
    return 0;
}

4.“图腾”类问题的统计

这个例题很多,就说一个求逆序对的吧;

1、什么是逆序数?

         在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序数的总数就是这个排列的逆序数。

 

2、用树状数组求逆序数的总数

         2.1该背景下树状数组的含义

         我们假设一个数组A[n],当A[n]=0时表示数字n在序列中没有出现过,A[n]=1表示数字n在序列中出现过。A对应的树状数组为c[n],则c[n]对应维护的是数组A[n]的内容,即树状数组c可用于求A中某个区间的值的和。

         树状数组的插入函数(假设为 void insert(int i,int x) )的含义:在求逆序数这个问题中,我们的插入函数通常使用为insert( i , 1 ),即将数组A[i]的值加1 (A数组开始应该初始化为0,所以也可以理解为设置A[ i ]的值为1,即将数字i 加入到序列的意思 )。,同时维护c数组的值。

         树状数组中区间求和函数(假设函数定义为: int getsun(int i ) )的含义:该函数的作用是用于求序列中小于等于数字 i 的元素的个数。这个是显而易见的,因为树状数组c 维护的是数组A的值,则该求和函数即是用于求下标小于等于 i 的数组A的和,而数组A中元素的值要么是0要么是1,所以最后求出来的就是小于等于i的元素的个数。

         所以要求序列中比元素a大的数的个数,可以用i - getsum(a)即可( i 表示此时序列中元素的个数)。

 

         2.2如何使用树状数组求逆序数总数

         首先来看如何减小问题的规模:

         要想求一个序列 a b c d,的逆序数的个数,可以理解为先求出a b c的逆序数的个数k1,再在这个序列后面增加一个数d,求d之前的那个序列中值小于d的元素的个数k2,则k1+k2即为序列a b c d的逆序数的个数。

         举个例子加以说明:

  假设给定的序列为 4 3 2 1,我们从左往右依次将给定的序列输入,每次输入一个数temp时,就将当前序列中大于temp的元素的个数计算出来,并累加到ans中,最后ans就是这个序列的逆序数个数。

 

序列的变化(下划线为新增加元素)

序列中大于新增加的数字的个数

操作

{ }

0

初始化时序列中一个数都没有

{4 }

0

往序列中增加4,统计此时序列中大于4的元素个数

{4 3 }

1

往序列中增加3,统计此时序列中大于3的元素个数

{4 3 2}

2

往序列中增加2,统计此时序列中大于2的元素个数

{4 3 2 1}

3

往序列中增加1,统计此时序列中大于1的元素个数

         当所有的元素都插入到序列后,即可得到序列{4 3 2 1}的逆序数的个数为1+2+3=6.

        

         2.3 C++实现代码如下:

#include <iostream>
#include <string>
using namespace std;
#define N 1010
int c[N]; 
int n;
int lowbit(int i)
{
    return i&(-i);
}
int insert(int i,int x)
{
    while(i<=n){
        c[i]+=x;
        i+=lowbit(i);
    }
    return 0;
}

int getsum(int i)
{
    int sum=0;
    while(i>0){
        sum+=c[i];
        i-=lowbit(i);
    } 
    return sum;
}
void output()
{
    for(int i=1;i<=n;i++) cout<<c[i]<<" ";
    cout<<endl;
}
int main()
{
    while(cin>>n){
        int ans=0;
        memset(c,0,sizeof(c));
        for(int i=1;i<=n;i++){
            int a;
            cin>>a;
            insert(a,1);
            ans+=i-getsum(a);//统计当前序列中大于a的元素的个数
        }
        cout<<ans<<endl;
    }
    return 0;
}

我们会发现要是这里的a很大,那怎么办;这里的a相当于A[a]下标。这时候我们就要用到离散化了。(不懂离散化的--点击离散化的思想

 

知道什么是逆序对后就好办了,树状数组的功能就是可以单点更新,区间查询,这样你把每个数该在的位置离散化出来,然后每次把每个数该在的位置上加上1,比如一个数该在的位置为x,那么用add(x)把这个位置加上1,然后再用区间查询read(x)查询1~x的和,也就是可以知道前面有多少个数是比他小的了(包括那个数自己),再用已经插入的数的个数减去read(x),就算出了前面有多少个数比他大了。

下面举个例子来详细的看一下过程:

第一次插入的时候把5这个位置上加上1,read(x)值就是1,当前已经插入了一个数,所以他前面比他大的数的个数就等于 i - read(x) = 1 - 1 = 0,所以总数 sum += 0

 

第二次插入的时候,read(x)的值同样是1,但是 i - read(x) = 2 - 1 = 1,所以sum += 1

第三次的时候,read(x)的值是2,i - read(x) = 3 - 2 = 1,所以sum += 1

第四次,read(x)的值是1,i - read(x) = 4 - 1 = 3,所以sum += 3

第五次,read(x)的值是1,i - read(x) = 5 - 1 = 4,所以sum += 4

这样整个过程就结束了,所有的逆序对就求出来了。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define LL long long
using namespace std;
 
int n,tree[100010];
void add(int k,int num)
{
    while(k<=n)
    {
        tree[k]+=num;
        k+=k&-k;
    }
}
 
int read(int k)
{
    int sum=0;
    while(k)
    {
        sum+=tree[k];
        k-=k&-k;
    }
    return sum;
}
struct node
{
    int val,pos;
}a[100010];
bool cmp(node a,node b)
{
    return a.val < b.val;
}
int main(void)
{
    int i,j;
    int b[100010];
    while(scanf("%d",&n)==1)
    {
        memset(tree,0,sizeof(tree));
        for(i=1;i<=n;i++)
        {
            scanf("%d",&a[i].val);
            a[i].pos = i;
        }
        sort(a+1,a+1+n,cmp);
        int cnt = 1;
        for(i=1;i<=n;i++)
        {
            if(i != 1 && a[i].val != a[i-1].val)
                cnt++;
            b[a[i].pos] = cnt;
        }
        LL sum = 0;
        for(i=1;i<=n;i++)
        {
            add(b[i],1);
            sum += (i - read(b[i]));
        }
        printf("%lld\n",sum);
    }
 
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值