树状数组的应用
偏序问题
逆序对 洛谷 1908
题目描述:对于给定的一段正整数序列,逆序对就是序列中ai > aj且i < j的有序对。计算一段正整数序列中逆序对的数目。序列中可能有重复数字。
输入格式:第一行,一个数 n,表示序列中有 n个数。第二行n个数,表示给定的序列。序列中每个数字不超过 109。n <= 5×105。
输出格式:输出序列中逆序对的数目。
输入样例:
6
5 4 2 6 3 1
输出样例:
11
题解:
直接暴力循环O(n2),利用树状数组O(nlogn)。
用树状数组解逆序对用到了一个小技巧,将数字看做一个数组的下标。每处理一个数字就在对应的下标加一,统计前缀和就是这个数的逆序对个数。
(1)倒序。用树状数组倒序处理数列,当前数字的前一个数的前缀和即为以该数为较大数的逆序对的个数。例如样例的{5, 4, 2, 6, 3, 1},倒序处理数字:
1)数字1。把a[1]加一,计算a[1]前面的前缀和sum(0),逆序对数量ans=ans+sum(0)=0;
2)数字3。把a[3]加一,计算a[3]前面的前缀和sum(2),逆序对数量ans=ans+sum(2)=1;
3)数字6。把a[6]加一,计算a[6]前面的前缀和sum(5),逆序对数量ans=ans+sum(5)=1+2=3;
等等。
(2)正序。正序,当前已经处理的数字个数减掉当前数字的前缀和即为以该数为较小数的逆序对个数。例如样例的{5, 4, 2, 6, 3, 1},正序处理数字:
1)数字5。把a[5]加一,当前处理了1个数,ans=ans+(1-sum(5))=0;
2)数字4。把a[4]加一,当前处理了2个数,ans=ans+(2-sum(4))=0+1=1;
3)数字2。把a[2]加一,ans=ans+(3-sum(2))=1+2=3;
4)数字6。把a[6]加一,ans=ans+(4-sum(6))=3+0=3;
等等。
不过这样有个问题,将数字看做数组下标,如果数字很大的话那么数组就太大了。例如数字等于109,那么树状数组的空间也要开到109 = 1G,这远远超过了题目限制的空间。用“离散化”这个小技巧能解决这个问题。
所谓离散化,就是把原来的数字,用它们的相对大小来替换原来的数值,而它们的顺序仍然不变,不影响逆序对的计算。例如{1, 20000,10, 300, 890000000},它们的相对大小是{1, 4, 2, 3, 5},这两个序列的逆序对数量是一样的。前者需要极大的空间,后者空间很小。有多少个数字,离散化后开的空间就是多大。
#include<iostream>
#include<algorithm>
#define lowbit(x) x&-x
using namespace std;
const int MAX = 5e5 + 10;
int tree[MAX], ran[MAX];
struct num
{
int num, val;
}a[MAX];
bool cmp(num n, num m)
{
if (n.val == m.val)return n.num < m.num;
return n.val > m.val;
}
void update(int x, int d)
{
while (x <= MAX)
{
tree[x] += d;
x += lowbit(x);
}
}
int sum(int x)
{
int ans = 0;
while (x > 0)
{
ans += tree[x];
x -= lowbit(x);
}
return ans;
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i].val;
a[i].num = i;
}
sort(a + 1, a + 1 + n, cmp);
for (int i = 1; i <= n; i++)
{
ran[a[i].num] = i;
}
long long ans = 0;
for (int i = 1; i <= n; i++)
{
update(ran[i], 1);
ans += sum(ran[i] - 1);
}
cout << ans << endl;
system("pause");
return 0;
}
区间最值
I Hate It hdu 1754
题目描述:求区间内最大值。
输入:第一行是正整数N,M ( 0<N<=200000,0<M<5000 ),代表数字个数和操作数。第二行包含N个整数,接下来M行,每行有一个询问,格式为:
Q A B 代表一个询问,询问从第A到第B个数字中的最大值。
U A B 代表一个更新,把第A个数字改为B。
输出:对每个询问,输出区间最大值。
用暴力法搜索复杂度为O(MN)。
下面使用树状数组求解。
在标准的树状数组里tree[x]中储存的是x-lowbit(x)+1到x的和,这里将树状数组进行一些修改令tree[x]储存x-lowbit(x)+1到x的最大值。
**修改:**修改update(x,value)用value修改tree[x]的最大值,并修改数组中其他被影响的节点。
例如修改x=4
(1)修改tree[4],与直接相连的tree[2]与tree[3]比较取最大值。
(2)修改父节点tree[8],与直接相连的tree[7],tree[6],tree[4]比较最大值。
每一步的复杂度是O(logn),共logn步,总共复杂度O((logn)2)。注意,初始化需要修改n,O(n(logn)2)。
void update1(int x,int value){
while(x <= n){
tree[x] = value;
for(int i=1; i<lowbit(x); i<<=1) //用子结点更新自己
tree[x] = max(tree[x], tree[x-i]);
x += lowbit(x); //父结点
}
查询查询query(L,R)。选取区间【L,R】,有两种情况讨论。
(1)R-L>=lowbit(x),根据树状数组原理,即[L, R]范围包含了tree[R]直连子结点的个数,此时直接使用tree[R]的值。
query(L,R)=max(tree[R],query(L,R-lowbit®)。
(2)R-L<lowbit(x),上述关系不成立,使用a[R],然后递推。
query(L,R)=max(a[R],query(L,R-1))。
int query1(int L,int R){
int ans = 0;
while(L<=R) {
ans = max(ans,a[R]);
R--;
while(R-L>=lowbit(R)){
ans = max(ans,tree[R]);
R-=lowbit(R);
}
}
return ans;
}
代码
#include<iostream>
#include<algorithm>
using namespace std;
#define lowbit(x) x&-x
const int MAX = 2e5 + 10;
int n, m, tree[MAX], a[MAX];
void update(int x, int value)
{
while (x <= n)
{
tree[x] = value;
for (int i = 1; i < lowbit(x); i<<=1)
{
tree[x] = max(tree[x], tree[x - i]);
}
x += lowbit(x);
}
}
int query(int L, int R)
{
int ans = 0;
while (L <= R)
{
ans = max(ans, a[R]);
R--;
while (R - L >= lowbit(R))
{
ans = max(ans, tree[R]);
R -= lowbit(R);
}
}
return ans;
}
int main()
{
cin >> n >> m;
memset(tree, 0, sizeof(tree));
memset(a, 0, sizeof(a));
for (int i = 1; i <= n; i++)
{
cin >> a[i];
update(i, a[i]);
}
while (m--)
{
char flag;
int A, B;
cin >> flag >> A >> B;
if (flag == 'Q')
{
cout << query(A, B);
}
else
{
update(A, B);
}
}
system("pause");
return 0;
}
离线处理
No Pain No Game hdu 4630
题目描述:给出一个序列,这个序列是1~n这n个数字的一个全排列。给出一个区间[L, R],求区间内任意两个数的GCD(最大公约数)的最大值。
输入:第一行包括一个数T,后面有T个测试。每个测试的第一行是数字n,1<=n<=50000,第二行包括n个数,是1~n这n个数字的一个全排列。第三行包括数字Q,1<=Q<=50000,表示Q个询问。后面有Q行,每行有2个整数L,R,1<=L<=R<=n,表示一个询问。
输出:每个询问的结果打印一行。
题解:
在区间[L, R]内,先求出区间内所有数的因子,出现2次的因子是公约数,最大的那个就是答案。
有Q个区间询问,而Q很大,所以每次查询的复杂度需要达到O(logn)才行。但是如果对每个询问都单独计算这个区间内的最大公约数,最快也是O(n) 的,Q个询问就是O(n2) ,超时。
此时需要用离线处理,即先读取所有的询问,然后统一处理,计算结束后一起输出。
前面的标准树状数组的代码,只能求区间和。能否改成求区间最值?把update()、sum()简单地改写成:
#include<iostream>
#include<algorithm>
#include<vector>
#define lowbit(x) x&-x
using namespace std;
const int MAX = 5e4 + 10;
int T, n, Q;
int tree[MAX * 2], num[MAX], pre[MAX], ans[MAX];
struct Query
{
int r, l;
int id;
bool operator < (const Query &t)const { return r < t.r; }
}q[MAX];
void update(int x, int d)
{
while (x <= MAX)
{
tree[x] = max(tree[x], d);
x += lowbit(x);
}
}
int query(int x)
{
int ans = 0;
while (x > 0)
{
ans += max(tree[x], ans);
x -= lowbit(x);
}
return ans;
}
vector<int>divs[MAX];
void caldivs()
{
for (int i = 1; i < MAX; i++)
{
for (int j = i; j < MAX; j += i)
{
divs[i].push_back(i);
}
}
}
void init()
{
memset(tree, 0, sizeof(tree));
memset(pre, 0, sizeof(pre));
}
int main()
{
caldivs();
cin >> T;
while (T--)
{
init();
cin >> n >> Q;
for (int i = 1; i <= n; i++)
{
cin >> num[i];
}
for (int i = 1; i <= Q; i++)
{
cin >> q[i].l >> q[i].r;
q[i].id = i;
}
sort(q + 1, q + 1 + Q);
for (int i = 1, k = 1; i <= n; i++)
{
int x = num[i];
for (int j = 0; j < divs[i].size(); j++)
{
int y = divs[i][j];
if (pre[y])
{
update(pre[y], y);
}
pre[y] = i;
}
while (q[k].r == i && k <= Q)
{
ans[q[k].id] = query(q[k].l);
k++;
}
}
for (int i = 1; i <= Q; i++)
{
cout << ans[i] << endl;
}
}
system("pause");
return 0;
}