区间信息的维护与查询_1 2016.4.24

参考:《算法竞赛入门经典:训练指南》

连续和查询问题

给定一个n个元素的数组A1,A2,...,An,你的任务是设计一个数据结构,支持一个查询操作Query(L, R):计算AL + AL+1+ ... + AR


如何做呢?

如果每次用循环来计算,单次查询需要O(n)的时间,效率太低

如果借助前缀和思想,可以花O(n)时间事先计算好Si = A1 + A2 + ... + Ai(定义S0 = 0),因为Query(L, R) = SR - SL-1,每次查询都只需O(1)时间

我们用O(n) - O(1)来描述这样的时间复杂度:预处理时间为O(n),单次查询时间为O(1)


上面的问题本身虽然不难,但是只要稍作修改,就可以引申出一系列经典算法的数据结构


一、二叉索引树(树状数组)

二叉索引树俗称树状数组,或者 Fenwick树,因为作者是 Fenwick


动态连续和查询问题

给定一个 n 个元素的数组 A1、A2、...,An,你的任务是设计一个数据结构,支持以下两种操作

  • Add(x, d)操作:让 Ax 增加 d
  • Query(L, R):计算AL + AL+1+ ... + AR

如何让 Query 和 Add 都能快速完成呢?

按照刚才的思路,每次执行 Add 操作都要修改一批 Si ,还是会很慢


有一种称为二叉索引树(Binary Indexed Tree,BIT)的数据结构可以很好地解决这个问题


(1)lowbit

对于正整数 x ,我们定义 lowbit(x) 为 x 的二进制表达式中最右边的1所对应的值(而不是这个比特的序号)

比如,38288的二进制是 1001010110010000,所以 lowbit(38288) = 16(二进制是 10000)


在程序实现中,lowbit(x) = x&(-x)


为什么呢?

回忆一下,计算机里的整数采用补码表示,因此 -x 实际上是 x 按位取反,末尾加 1 以后的结果

 388288 = 1001010110010000

-388288 = 0110101001110000

二者按位取“与之后,前面的部分全部变为0,之后 lowbit 保持不变

灰色结点是 BIT 中的结点(白色长条的含义稍后叙述),每一层结点的 lowbit 相同,
而且 lowbit 越大,越靠近根。

对于结点 i,如果它是左子结点,那么父结点的编号就是 i+lowbit(i);如果它是右子结
点,那么父结点的编号是 i-lowbit(i)

搞清楚树的结构之后,构造一个辅助数组 C,其中




换句话说,C 的每个元素都是 A 数组中的一段连续和


到底是哪一段呢?在 BIT 中,每个灰色结点 i 都属于一个以它自身结尾的水平长条(对于 lowbit=1 的那些点,“长条”就是那个结点自己),这个长条中的数之和就是 Ci

比如结点 12 的长条就是从 9~12,即C12 = A9+A10+A11+A12

同理,C6=A5+A6

这个等式极为重要

Ci就是以 i 结尾的水平长条内的元素之和


有了 C 数组后,如何计算前缀和 Si呢?顺着结点 i 往左走,边走边“往上爬”(注意并不一定沿着树中的边爬),把沿途经过的 Ci累加起来就可以了

沿途经过的 Ci所对应的长条不重复不遗漏地包含了所有需要累加的元素

如图 3-4 所示


而如果修改了一个 Ai,需要更新 C 数组中的哪些元素呢?从 Ci开始往右走,边走边“往上爬”(同样不一定沿着树中的边爬),沿途修改所有结点对应的 Ci即可

有且仅有这些结点对应的长条包含被修改的元素

如图 3-5 所示




两个操作的实现代码如下

int sum(int x)
{
    int ret = 0;
    while (x > 0) {
        ret += C[x];
        x -= lowbit(x);
    }
    return ret;
}

void add(int x, int d)
{
    while (x <= n) {
        C[x] += d;
        x += lowbit(x);
    }
}

不难证明,两个操作的时间复杂度均为 O(logn)

预处理的方法是先把 A 和 C 数组清空,然后执行 n 次 add 操作,总时间复杂度为 O(nlogn)



HDU 1166 敌兵布阵


#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int maxn = 50000 + 5;
int a[maxn];
char cmd[10];
int N;

int lowbit(int x);
void Update(int x, int d);
int Query(int x);

int main()
{
//    freopen("in.txt", "r", stdin);
    int T;

    scanf("%d", &T);
    int Case = 0;
    while (T--) {
        memset(a, 0, sizeof(a));
        ++Case;
        printf("Case %d:\n", Case);
        scanf("%d", &N);
        int t;
        for (int i=1; i<=N; ++i) {
            scanf("%d", &t);
            Update(i, t);
        }
        while (scanf("%s", cmd) && strcmp(cmd, "End") != 0) {
            int i, j;
            scanf("%d%d", &i, &j);
            if (strcmp(cmd, "Add") == 0) {
                Update(i, j);
            } else if (strcmp(cmd, "Sub") == 0) {
                Update(i, -j);
            } else if (strcmp(cmd, "Query") == 0) {
                int ans = Query(j) - Query(i-1);
                printf("%d\n", ans);
            }
        }
    }
    return 0;
}

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

void Update(int x, int d)
{
    while (x <= N) {
        a[x] += d;
        x += lowbit(x);
    }
}

int Query(int x)
{
    int ret = 0;
    while (x > 0) {
        ret += a[x];
        x -= lowbit(x);
    }
    return ret;
 }



乒乓比赛(Ping pong, Beijing 2008, LA 4329)


摘自lrj大白


一条大街上住着 n 个乒乓球爱好者,经常组织比赛切磋技术。

每个人都有一个不同的技能值 ai。每场比赛需要 3 个人:两名选手,一名裁判。

他们有一个奇怪的规定,即裁判必须住在两名选手的中间,并且技能值也在两名选手之间。问一共能组织多少种比赛。


【输入格式】
输入第一行为数据组数 T(1≤T≤20)。每组数据占一行,首先是整数 n(3≤n≤20 000),
然后是 n 个不同的整数,即 a1, a2, …, an(1≤ai≤100 000),按照住所从左到右的顺序给出每个乒乓爱好者的技能值。

【输出格式】
对于每组数据,输出比赛总数的值。


【分析】
考虑第 i 个人当裁判的情形。假设 a1到 ai-1中有 ci个比 ai小,那么就有(i-1)-ci个比 ai大;

同理,假设 ai+1到 an中有 di个比 ai小,那么就有(n-i)-di个比 ai大。

根据乘法原理和加法原理,i 当裁判有 ci(n-i-di)+(i-ci-1)di种比赛。这样,问题就转化为求 ci和 di。

ci可以这样计算:

从左到右扫描所有的 ai,令 x[j]表示目前为止已经考虑过的所有 ai中,是否存在一个 ai=j (x[j]=0 表示不存在,x[j]=1 表示存在),

则 ci就是前缀和 x[1]+x[2]+…+x[ai-1]。


初始时所有 x[i]=0,在计算 ci时,需要先设 x[ai]=1,然后求前缀和。

换句话说,我们需要动态地修改单个元素值并求前缀和——这正是 BIT 的标准用法。


这样,就可以在 O(nlogr)(这里的 r 是 ai的上限)的时间内计算出所有 ci

类似地,可以计算出 di,然后在 O(n)时间里累加出最后的答案。

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

typedef long long LL;

const int maxn = 100000 + 5;
LL a[maxn];
LL x[maxn];
LL c[maxn], d[maxn];
int n;

int lowbit(int x);
void Update(int x, int d);
int Query(int x);

int main()
{
//    freopen("in.txt", "r", stdin);
    int T;

    scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        for (int i=1; i<=n; ++i) {
            scanf("%lld", &a[i]);
        }
        memset(x, 0, sizeof(x));
        for (int i=1; i<=n; ++i) {
            c[i] = Query(a[i]);
            Update(a[i], 1);
        }
        memset(x, 0, sizeof(x));
        for (int i=n; i>0; --i) {
            d[i] = Query(a[i]);
            Update(a[i], 1);
        }
        LL ans = 0;
        for (int i=1; i<=n; ++i) {
            ans += c[i] * (n-i-d[i]);
            ans += d[i] * (i-1-c[i]);
        }
        printf("%lld\n", ans);
    }
    return 0;
}

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

void Update(int a, int d)
{
    while (a <= maxn) {
        x[a] += d;
        a += lowbit(a);
    }
}

int Query(int a)
{
    int ret = 0;
    while (a > 0) {
        ret += x[a];
        a -= lowbit(a);
    }
    return ret;
}

选自:

《算法竞赛入门经典训练指南》刘汝佳 陈锋

略有改动



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值