参考:《算法竞赛入门经典:训练指南》
连续和查询问题
给定一个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)
#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;
}
选自:
《算法竞赛入门经典训练指南》刘汝佳 陈锋
略有改动