区间信息的维护与查询_1 2016.4.24
来源:互联网 发布:linux 压缩文件夹bz2 编辑:程序博客网 时间:2024/06/05 14:36
参考:《算法竞赛入门经典:训练指南》
连续和查询问题
给定一个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;}
选自:
《算法竞赛入门经典训练指南》刘汝佳 陈锋
略有改动
- 区间信息的维护与查询_1 2016.4.24
- 区间信息的维护与查询
- Splay 区间信息的维护与查询 静态模板
- 区间信息的维护与查询1(树状数组)
- 区间信息维护与查询 2016.10.13
- 区间信息的查询与维护(一)树状树组
- 区间信息的维护和查询系列算法-树状数组
- 【转载】区间信息的维护与查询(一)——二叉索引树(Fenwick树、树状数组)
- 白书~区间信息维护习题
- 《电脑编程技巧与维护》2011年第24期刊登出《DB 查询分析器》批量执行DML语句并返回更详细的信息
- 树状数组区间维护及单点查询
- 【小结】树状数组的区间修改与区间查询
- 树状数组的区间修改与区间查询
- ZOJ Monthly, March 2014,3765 Lights (Splay 基本操作,并维护区间上的信息 * 模板)
- sqlserver里的统计信息与维护计划
- 用于修改学生的信息_1
- 面试信息获取_1——进程与线程的区别
- 树的区间查询与更新(线段树)
- java.io.IOException:stream closed 异常的原因及处理
- 大整数的乘法
- 获取网络域名的IP地址
- `
- vim强大工具YCM安装汇总
- 区间信息的维护与查询_1 2016.4.24
- 主机存活探测方式
- Uboot学习笔记
- java面试题--转载留用
- JavaScript的模块化:封装(闭包),继承(原型) 介绍
- C语言中如何将数转化为字符串
- (礼拜五log)前端开发:overflow
- IntentService源码分析
- 设计模式系列——三个工厂模式(简单工厂模式,工厂方法模式,抽象工厂模式)