具体定义和性质见蓝皮书P202页(有时间再添过来)
印象笔记可能更好看一点:https://app.yinxiang.com/fx/c...

树状数组的应用

树状数组与逆序对

当然归并排序也可以求逆序对
树状数组求逆序对时间复杂度为O((N+M)logM)----M为数据范围
归并排序求逆序对的时间复杂度为O(Nlogn)
PS:
归并排序就是每次把序列二分,递归对左右两半排序,然后合并两个有序序列
归并排序求逆序对就是递归对左右两半排序时,可以把左右两半各自内部的逆序对数作为子问题计算,因此我们只需要在合并时考虑"左边一半里一个较大的数“与“右边一半里一个较小的数”构成逆序对的情形,求出这种情形的个数。合并两个有序序列A[l~mid]与A[mid+1~r]可以采用两个指针i和j分别对二者进行扫描的方式,不断比较两个指针所指向的数值a[i]和a[j]的大小,将小的那个加入到排序的结果数组中。若小的是a[j],则a[i~mid]都比a[j]要大,他们都会与a[j]构成逆序对,顺便统计到答案中。
如果数据范围较大,即M较大,可以对序列进行离散化处理,不过离散化需要排序,不如直接用归并排序求。
题目链接:https://www.acwing.com/proble...
分析:
以找'V'标志为例:
image.png
AC代码:

#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;
const int N = 200010;
int n;
int a[N];
long long int w1[N],w2[N],w3[N],w4[N];
long long int c[N];
long long int ask(int x)//查询操作
{
    long long int ans = 0;
    for(int i=x;i;i-=(i&-i))
    {
        ans += c[i];
    }
    return ans;
}
void add(int x)//添加操作
{
    for(;x<=n;x+=(x&-x))
    {
        c[x]+=1;
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=n;i++)
    {
        w1[i]=ask(n)-ask(a[i]);//利用前缀和思想求出区间[1,i-1]中[a[i]+1,n]范围内的数
        w3[i]=ask(a[i]-1);//利用前缀和思想求出区间[1,i-1]中[1,a[i]-1]范围内的数
        add(a[i]);
    }
    memset(c,0,sizeof(c));
    for(int i=n;i>=1;i--)
    {
        w2[i]=ask(n)-ask(a[i]);
        w4[i]=ask(a[i]-1);
        add(a[i]);
    }
    long long int ans1 = 0,ans2=0;
    for(int i=2;i<=n-1;i++)
    {
        ans1+=w1[i]*w2[i];
        ans2+=w3[i]*w4[i];
    }
    printf("%lld %lld\n",ans1,ans2);
    return 0;
}

下一个问题:树状数组的扩展应用,题目链接:https://www.acwing.com/proble...
树状数组仅支持单点增加和区间查询
但本题问的是单点查询和区间增加,所以我们就要转化一下。
考虑差分的思想,新建一个数组b[],初始全部为0,对于每条“C l r d”指令,b[l]加上d,b[r+1]减去d(如果不明白差分,可以看这个https://app.yinxiang.com/fx/0...),这样区间查询时就可以变成单点查询。
举个例子
初始状态:
image.png

#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;
const int N = 100010;
int a[N], b[N];
int n,m;
char ch;
int l,r,d;
int ask(int x)
{
    int ans = 0;
    for(;x;x-=(x&-x))
    {
        ans+=b[x];
    }
    return ans;
}
void add(int x,int d)
{
    for(;x<=n;x+=(x&-x))
    {
        b[x]+=d;
    }
}

int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    while (m--)
    {
        getchar();
        scanf("%c",&ch);
        if(ch=='Q')
        {
            scanf("%d",&l);
            printf("%d\n",a[l]+ask(l));
        }
        else
        {
            scanf("%d %d %d",&l,&r,&d);
            add(l,d);
            add(r+1,-d);
        }
    }
    
    return 0;
}

现在问题升级,不是单点询问了,变为了区间询问和区间修改
依旧是采用差分的思想,区间修改是与上一个题亦同,当区间询问时,即执行语句“Q l r”时,
我们要计算的是a[1,r]减去a[1,l-1],但由于我们在修改时是对差分数组b[]进行操作,所以询问时,我们仍然要加上b[]
以计算a[1,r]为例:
i=1 b1 a[1]要加的值
i=2 b1 b2 a[2]要加的值
i=3 b1 b2 b3 a[3]要加的值
...... ......
i=r b1 b2 b3 .... br a[r]要加的值
补全矩阵:
i=0 b1 b2 b3 .... br
i=1 b1 b2 b3 .... br
i=2 b1 b2b3 .... br
i=3 b1 b2 b3 .... br
......
i=r b1 b2 b3 .... br
矩阵和为(r+1)*(b1+b2+.....+br)
再减去补充的部分,即得到之前矩阵的和(r+1)*(b1+b2+.....+br)-(b1+2b2+3b3+....+rbr)
要有两个树状数组tr1[]维护b[],tr2[]维护i*b[]

#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <string.h>
#define lowbit(x) x&-x

using namespace std;
const int N = 100010;
int a[N];
long long int sum[N];
long long int tr[2][N];
int n,m;
char ch[2];
int l,r,d;
long long int ask(int k,int x)
{
    long long int ans = 0;
    for(;x;x-=(x&-x))
    {
        ans+=tr[k][x];
    }
    return ans;
}
void add(int k,int x,long long int d)
{
    for(;x<=n;x+=(x&-x))
    {
        tr[k][x]+=d;
    }
}

int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        sum[i] = sum[i-1]+a[i];
    }
    while (m--)
    {
        getchar();
        scanf("%s",ch);
        if(ch[0]=='Q')
        {
            scanf("%d %d",&l,&r);
            long long int ret = sum[r]-sum[l-1];
            ret+=(r+1)*ask(0,r)-l*ask(0,l-1);
            ret-=ask(1,r)-ask(1,l-1);
            printf("%lld\n",ret);
        }
        else
        {
            scanf("%d %d %d",&l,&r,&d);
            add(0,l,d);
            add(0,r+1,-d);
            add(1,l,l*d);
            add(1,r+1,-(r+1)*d);
        }
    }
    
    return 0;
}

下一题链接:https://www.acwing.com/proble...
分析:
image.png
image.png
image.png
image.png
image.png
image.png
后面不再枚举......
由上分析可知,我们要在1-n范围内建立树状数组,然后从后往前找第a[i]+1小的数,找完之后再在树状数组中删掉这个数。
在树状数组中删掉一个数很简单,只需要执行单点增加操作,增加-1即为删掉这个数。
现在最麻烦的就是怎么找第k小的数,我们可以利用二分查找,设这个数为x,如果x是第k小的,那么区间[1,x]上的数一定为k。
代码:

#include <algorithm>
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <vector>
#define lowbit(x) x & -x

using namespace std;
const int N = 100010;
int n;
int t[N];
void add(int x,int d)
{
    for(;x<=n;x+=lowbit(x))
    {
        t[x]+=d;
    }
}
bool ask(int x,int k)
{
    int ans = 0;
    for(;x;x-=lowbit(x))
    {
        ans += t[x];
    }
    if(ans>=k)
    {
        return true;
    }
    return false;
}
int b[N],a[N];

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        add(i,1);//每个数加1个
    }
    for(int i=2;i<=n;i++)
    {
        scanf("%d",&b[i]);
    }
    for(int i=n;i>=1;i--)
    {
        int l=0,r=n;
        int k = b[i]+1;
        while (l<r)
        {
            int mid = (l+r)>>1;
            if(ask(mid,k))
            {
                r=mid;
            }
            else
            {
                l=mid+1;
            }
        }
        a[i]=l;
        add(l,-1);
    }
    for(int i=1;i<=n;i++)
    {
        cout<<a[i]<<endl;
    }
    return 0;
}

拾柒
1 声望1 粉丝

引用和评论

0 条评论