偏序与持久化数据结构
《树状数组》
首先来学习一下与偏序问题息息相关的持久化数据结构----树状数组(线段树也是,但这里我就不多说了)
想看详细原理证明,这是一个好博客:https://zhuanlan.zhihu.com/p/435561765
https://blog.csdn.net/a591027895/article/details/123951314
树状数组,其保存维护的值的数组是:c[]
通过上面的博客我们可以知道c[x]:表示区间[x-lowbit(x)+1,x]中,树状数组所维护的值
c[x]的父节点是:c[x+lowbit(x)]
c[x]的子节点是:c[x-1],c[x-1-lowbit(x-1)]......(直到【】里面的值到是0之间为止) a[x](a[]是原数组)
可以看出c[x]所代表的区间以x结尾,长度为lowbit(x),即:[x-lowbit(x)+1,x]
所谓的树状数组就是:
如果给定区间:[1,n],原数组为a[1....n]
那么可以用c[1....n]来维护原数组的一些性质:如a[]某些性质的前缀和等
c[x],作为树状数组的节点可以由原数组a[x]和c[x-1],c[x-1-lowbit(x-1)].......这些节点组成
而c[x]本身也为c[x+lowbit(x)],作出贡献(即其是子节点)
最终树状数组像这样:
其能干什么?
(1).可以通过query(x)操作知道在区间[1,x]中,树状数组维护的值是多少(区间查询)
时间复杂度:O(logn)
注意通过下面的操作,我们一定要保证我们知道要维护的区间的最小值为1
这个可以通过c[x]+c[x-lowbit(x)]+......来实现
因为c[x]代表范围:[x-lowbit(x)+1,x]
c[x-lowbit(x)]代表范围:[x-lowbit(x)-lowbit(x-lowbit(x)),x-lowbit(x)]
..............
这个范围应该可以看出是当组合起来的时候是连续的,最终组合成了区间[1,x]
ll query(int x)
{
ll ans = 0;
for (int i = x; i >= 1; i -= lowbit(i))
ans += c[i];
return ans;
}
(2).可以通过add(x,k)操作,对c[x]的值进行k个单位修改(单点修改),同时可以向上传递修改的结果,即同时可以修改父节点
时间复杂度:O(logn)
1 void add(int x, int k)
2 {
3 for (int i = x; i <= maxn; i += lowbit(i))
4 c[i] += k;
5 }
树状数组还可以实现区间修改和单点查询
这个的实现是靠利用差分,将区间修改,可以转化为两次单点修改
将单点查询,转化为了求区间的前缀和,即区间查询
《偏序问题》
好博客:https://zhuanlan.zhihu.com/p/112504092
以最经典的逆序对问题为例:
这里如果用最普通的想法要O(n^2)的时间复杂度,肯定会超时
这里的偏序关系是:i<j,ai>aj
如果满足这个关系,我们称(i,ai)(j,aj)
解决这个问题我们可以用归并排序O(nlogn)
也可以用树状数组或线段树,思想都是一样的:
对于a,我们可以将其看作一个区间
我们按照顺序,从1~n,将a插入树状数组中,即add(a,+1),表示将c[a]+=1,代表区间[a-lowbit(a)+1,a]内数的个数增加了1
当我们插入到下标j时,天然地满足了下标小于j的下标对应的数都被插入到了树状数组中,
然后我们只要找一下此时有多少个数满足>aj,
即只要query(n)-query(aj),因为query(x),代表区间[1,x]中有多少个数
那么 query(maxa)-query(aj)就代表:【aj+1,maxa】有多少个数,即现在大于aj的有多少个数,这就是对于j来说的逆序对的个数
注意:因为这里a的数值巨大,但是N较小,我们可以进行离散化,来保证数组可以存储区间
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 5 * 1e5 + 5;
int a[N], c[N], n, maxn;
vector<int> sa;
int find(int x)
{
int l = 0, r = sa.size() - 1, ans;
while (l <= r)
{
int mid = (l + r) >> 1;
if (sa[mid] >= x)
{
ans = mid;
r = mid - 1;
}
else
l = mid + 1;
}
return ans + 1;
}
int lowbit(int x)
{
return x & (-x);
}
void add(int x, int k)
{
for (int i = x; i <= maxn; i += lowbit(i))
c[i] += k;
}
ll query(int x)
{
ll ans = 0;
for (int i = x; i >= 1; i -= lowbit(i))
ans += c[i];
return ans;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
sa.push_back(a[i]);
}
sort(sa.begin(), sa.end());
sa.erase(unique(sa.begin(), sa.end()), sa.end());
maxn = sa.size();
ll ans = 0;
for (int i = 1; i <= n; i++)
{
int x = a[i];
int pos = find(x);
ans += (query(maxn) - query(pos));
add(pos, 1);
}
cout << ans;
return 0;
}
《进阶的逆序对问题》
很明显这个问题构成偏序:
假设尖端点为i,权值为ai
对于其左边的偏序:
1. j<i,aj>ai
2.j<i,aj<ai
对于其右边的偏序:
1.j>i,aj>ai
2.j>i,aj<ai
对于其左边的偏序很好做,与逆序对问题相同
但是对于其右边的偏序,如何作?很简单,只要将数反过来插入即可:for(int i=n;i>=1;i--)
1 #include <iostream>
2 #include <algorithm>
3 #include <cstring>
4 using namespace std;
5 typedef long long ll;
6 const int N = 2 * 1e5 + 5;
7 // c[x]:表示范围[x-lowbit(x)+1,x]内出现的数的个数;
8 // high[x]:表示在原数组中下标比x小,但是数比x大的个数
9 // low[x]:表示在原数组中下标比x小,数比x小的个数
10 int a[N], c[N], high[N], low[N];
11 int n;
12 // 令a[i]=x,high[i]与low[i]是可以用树状数组求出来的
13 // 具体做法是将原数组中的数顺序插入到树状数组中,此时树状数组记录的是各个范围中相应的数出现的次数
14 // 显然我们可以通过query(x-1)操作来询问出,当要插入数x时[1,x-1]范围内出现的数的个数
15 // 这个是low[x]的答案
16 // 我们可以通过query(n)-query(x)操作来询问出,当要插入数x时[x+1,n]范围内出现的数的个数
17 // 这个是high[x]的答案
18 // 插入数的操作为add(x,k):这个说明[x-lowbit(x)+1,x]树状数组维护的区间c[x]的值要改动k个单位了
19 // 而且连带的其父节点c[x+lowbit(x)](x+lowbit(x)<=n)也会受到改动
20 int lowbit(int x)
21 {
22 return x & (-x);
23 }
24 void add(int x, int k)
25 {
26 for (int i = x; i <= n; i += lowbit(i))
27 c[i] += k;
28 }
29 // 将返回[1,x]内树状数组维护的值
30 int query(int x)
31 {
32 int ans = 0;
33 for (int i = x; i >= 1; i -= lowbit(i))
34 ans += c[i];
35 return ans;
36 }
37 int main()
38 {
39 // 这道题树状数组维护的是区间内数出现的个数,建树的过程是逐个将出现的数
40 // 找到这个数的区间,然后让这个区间的值+1即可
41 cin >> n;
42 for (int i = 1; i <= n; i++)
43 cin >> a[i];
44 // 正序将数插入数组中,求的是在下标i的左边有多少个\(high记录)和/(low记录)
45 for (int i = 1; i <= n; i++)
46 {
47 int y = a[i];
48 low[i] = query(y - 1);
49 high[i] = query(n) - query(y);
50 add(y, 1);
51 }
52 memset(c, 0, sizeof(c));
53 ll ans1 = 0, ans2 = 0;
54 // 逆序将数插入数组中,求的是在下标i的右边有多少个\和/
55 for (int i = n; i >= 1; i--)
56 {
57 int y = a[i];
58 ans1 += (ll)high[i] * (query(n) - query(y));
59 ans2 += (ll)low[i] * (query(y - 1));
60 add(y, 1);
61 }
62 cout << ans1 << " " << ans2;
63 return 0;
64 }