该博客参考于:https://www.cnblogs.com/xiongmao-cpp/p/5043340.html
逆序数(也叫逆序对)
逆序对通俗的来说,就是如果 i > j && a[i] < a[j] ,这两个就算一对逆序列,简单的来说,所有逆序对的个数和就是找一个数的前面有几个比他大的数,他们加起来的和就是逆序对的总数。
用树状数组求逆序数的总数
该背景下树状数组的含义
这里我们用数组c[n],表示数字 n 是否在序列中出现过,c[n]==1表示出现,c[n]==0 表示未出现。c 对应的树状数组为 a 的内容,则 a[n] 对应维护的是数组 c[n] 的内容,即树状数组 a 可用于求 a 中某个区间的值的和。
树状数组的插入更新函数(update(int pos,int k) )的含义:在求逆序数这个问题中,我们的更新函数一般形式是update(i,1),即将数组 c[i] 的值 +1(其初始化为0,所以就是让c[i]==1),同时维护 a 数组的值。
int lowbit(int x)
{
return x&(-x);
}
void update(int pos,int k) //这里是在pos位置之后,在相应的位置上不断添加k
{
while(pos<=n){
c[pos] += k;
pos += lowbit(pos);
}
}
树状数组中的区间求和函数(getsum(int pos))的含义:该函数的作用就是用来求序列中 <=i 的元素的个数。因为树状数组 a 维护的是 数组 c 的值,则对应的求和函数即是用于求下标 <=i 的数组 c 的和,而数组 c 的元素要么是0要么是,所以最后求出来的就是 <=i 的元素的个数。
ll getsum(int pos) //寻找在pos之前一共有多少数字小于pos
{
ll res = 0;
while(pos){
res += c[pos];
pos -= lowbit(pos);
}
return res;
}
所以求序列中比元素 p 大的的数的个数,可以用 i - getsum( p ) 求出( i 表示此时序列中元素的个数)。
如何使用树状数组求逆序数总数
首先如何看待减小问题的规模:
要想求一个序列 a b c d 的逆序数的个数k,可以理解为先求出 a b c 的逆序数的个数 k1 ,再在序列后面加一个数 d,求出 d 的元素小于 d 的元素的个数 k2,则k1 + k2 即为序列 a b c d 的逆序列的个数。
举个例子加以说明:
假设给定的序列为 9 1 0 5 4,我们从左往右依次将给定的序列输入,每次输入一个数temp时,就将当前序列中大于temp的元素的个数计算出来,并累加到ans中,最后ans就是这个序列的逆序数个数。
序列的变化(下划线为新增加元素) | 序列中大于新增加的数字的个数 | 操作 |
---|---|---|
{ } | 0 | 初始化时序列中一个数都没有 |
{9} | 0 | 往序列中增加9,统计此时序列中大于9的元素个数 |
{9,1} | 1 | 往序列中增加1,统计此时序列中大于1的元素个数 |
{9,1,0} | 2 | 往序列中增加0,统计此时序列中大于0的元素个数 |
{9,1,0,5} | 1 | 往序列中增加5,统计此时序列中大于5的元素个数 |
{9,1,0,5,4} | 2 | 往序列中增加4,统计此时序列中大于4的元素个数 |
当所有的元素都插入到序列后,即可得到序列{9,1,0,5,4}的逆序数的个数为1+2+1+2=6.
逆序对经常用于从小到大排序交换次数的求解
为什么这么说呢,因为从小到大排序交换次数等于每个数后面比自己小的数的个数之和,也就等于每个数前面比自己大的数的个数这和,也就是序列逆序对的个数
话不多说直接上题:
例题1:POJ 2299 http://poj.org/problem?id=2299
In this problem, you have to analyze a particular sorting algorithm. The algorithm processes a sequence of n distinct integers by swapping two adjacent sequence elements until the sequence is sorted in ascending order. For the input sequence 9 1 0 5 4 ,
Ultra-QuickSort produces the output 0 1 4 5 9 .
Your task is to determine how many swap operations Ultra-QuickSort needs to perform in order to sort a given input sequence.
Input
The input contains several test cases. Every test case begins with a line that contains a single integer n < 500,000 – the length of the input sequence. Each of the the following n lines contains a single integer 0 ≤ a[i] ≤ 999,999,999, the i-th input sequence element. Input is terminated by a sequence of length n = 0. This sequence must not be processed.
Output
For every input sequence, your program prints a single line containing an integer number op, the minimum number of swap operations necessary to sort the given input sequence.
Sample Input
5
9
1
0
5
4
3
1
2
3
0
Sample Output
6
0
题意
给出n个数,求将这n个数从小到大排序,求需要交换的次数。
分析
上面的推导,我们知道就是让你求序列偏序对的个数
AC代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<string>
#include<queue>
#include<vector>
#include<map>
#include<utility>
#include<algorithm>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int maxn = 500010;
const int inf = 0x3f3f3f3f;
struct node{
int pos,val;
bool operator < (const node& x)const{
return val < x.val; //以权值排序
}
}a[maxn]; //树状数组
int b[maxn],c[maxn],n; //b为离散化数组,c为记录数组
int lowbit(int x)
{
return x&(-x);
}
void update(int pos,int k) //这里是在pos位置之后,在相应的位置上不断添加k
{
while(pos<=n){
c[pos] += k;
pos += lowbit(pos);
}
}
ll getsum(int pos) //寻找在pos之前一共有多少数字小于pos,这里用 long long
{
ll res = 0;
while(pos){
res += c[pos];
pos -= lowbit(pos);
}
return res;
}
int main(void)
{
while(~scanf("%d",&n),n){
memset(c,0,sizeof(c));
for(int i=1;i<=n;i++){
scanf("%d",&a[i].val);
a[i].pos = i;
}
sort(a+1,a+n+1); //排序
for(int i=1;i<=n;i++){
b[a[i].pos] = i;
}
ll ans = 0; //开 long long
for(int i=1;i<=n;i++){
update(b[i],1);
ans += (i-getsum(b[i]));
}
printf("%lld\n",ans);
}
return 0;
}
树状数组 + 离散
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<string>
#include<queue>
#include<vector>
#include<map>
#include<utility>
#include<algorithm>
typedef long long ll;
using namespace std;
const int maxn = 500010;
struct node{
int val;
int pos;
}tmp[maxn];
ll tree[maxn];
int c[maxn],n;
bool cmp(node a,node b)
{
return a.val < b.val;
}
void update(int x,int y)
{
for( ;x <= n;x += (x&-x))
tree[x] += y;
}
ll getsum(int x)
{
ll ans = 0;
for( ;x;x -= (x&-x))
ans += tree[x];
return ans;
}
int main(void)
{
int x;
while(~scanf("%d",&n),n){
for(int i=0;i<=n;i++)
tree[i] = 0;
for(int i=1;i<=n;i++){
scanf("%d",&tmp[i].val);
tmp[i].pos = i;
}
sort(tmp+1,tmp+n+1,cmp);
for(int i=1;i<=n;i++){
if(i==1||tmp[i].val!=tmp[i-1].val)
c[tmp[i].pos] = i;
else c[tmp[i].pos] = c[tmp[i-1].pos];
}
ll ans = 0;
for(int i=1;i<=n;i++){
update(c[i],1);
ans += (i-getsum(c[i]));
}
printf("%lld\n",ans);
}
return 0;
}
归并排序
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<string>
#include<vector>
#include<queue>
#include<stack>
#include<utility>
#include<map>
#include<algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long llu;
const int maxn = 500010;
int a[maxn],b[maxn];
ll cnt;
void Merge(int l,int mid,int r)
{
int i = l,j = mid+1;
for(int k=l;k<=r;k++){
if(j>r||i<=mid&&a[i]<=a[j]) b[k]=a[i++];
else{
b[k] = a[j++];
cnt += (mid-i+1);
}
}
for(int k=l;k<=r;k++) a[k] = b[k];
}
void Merge_sort(int l,int r)
{
if(l<r){
int m = (l + r)>>1;
Merge_sort(l,m);
Merge_sort(m+1,r);
Merge(l,m,r);
}
}
int main()
{
int n;
while(~scanf("%d",&n)&&n){
cnt = 0;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
Merge_sort(1,n);
printf("%lld\n",cnt);
}
return 0;
}
例题2:楼兰图腾 https://www.acwing.com/problem/content/243/
在完成了分配任务之后,西部314来到了楼兰古城的西部。
相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(‘V’),一个部落崇拜铁锹(‘∧’),他们分别用V和∧的形状来代表各自部落的图腾。
西部314在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了N个点,经测量发现这N个点的水平位置和竖直位置是两两不同的。
西部314认为这幅壁画所包含的信息与这N个点的相对位置有关,因此不妨设坐标分别为(1,y1),(2,y2),…,(n,yn),其中y1~yn是1到n的一个排列。
西部314打算研究这幅壁画中包含着多少个图腾。
如果三个点(i,yi),(j,yj),(k,yk)满足1≤i<j<k≤n且yi>yj,yj<yk,则称这三个点构成V图腾;
如果三个点(i,yi),(j,yj),(k,yk)满足1≤i<j<k≤n且yi<yj,yj>yk,则称这三个点构成∧图腾;
西部314想知道,这n个点中两个部落图腾的数目。
因此,你需要编写一个程序来求出V的个数和∧的个数。
输入格式
第一行一个数n。
第二行是n个数,分别代表y1,y2,…,yn。
输出格式
两个数,中间用空格隔开,依次为V的个数和∧的个数。
数据范围
对于所有数据,n≤200000,且输出答案不会超过int64。
输入样例:
5
1 5 3 2 4
输出样例:
3 4
题意
给定 n 个点的以横坐标从小到大排序后的纵坐标,让你找寻坐标呈现 v 和 ∧ 的特点的对数,
思路
说白了就是:
对于 v ,就是假设有一点 p 让你找 p 前纵坐标大于 yp 的个数,以及 p 后面纵坐标大于 yp 的个数,然后求乘积。
对于 ∧,就是假设有一点 p 让你找 p 前纵坐标小于 yp 的个数,以及 yp 后面纵坐标小于 p 的个数,然后求乘积。
- 倒序扫描序列 a ,利用树状数组,求每一个 a[i] 后边有几个数比它大,记作 ri1[i],有几个数比他小,记作 ri2[i]。
- 正序扫描序列 a,利用树状数组,求每一个 a[i] 前边有几个数比它大,记作 le1[i],有几个数比他小,记作 le2[i]。
- v的数目:∑le1[i] * ri1[i] (i∈[1,n])
- ∧的数目:∑le2[i] * ri2[i] (i∈[1,n])
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<cmath>
#include<set>
#include<map>
#include<stack>
#include<queue>
#include<ctype.h>
#include<vector>
#include<algorithm>
#include<sstream>
#define PI acos(-1.0)
using namespace std;
typedef long long ll;
typedef unsigned long long llu;
const int inf = 0x3f3f3f3f;
const ll lnf = 0x3f3f3f3f3f3f3f;
const int maxn = 201000;
int a[maxn],n;
ll c[maxn<<2];
ll le1[maxn],ri1[maxn];
ll le2[maxn],ri2[maxn];
void add(int x,ll y)
{
for( ;x<maxn;x+=(x&-x))
c[x] += y;
}
ll ask(int x)
{
ll ans = 0;
for( ;x;x-=(x&-x))
ans += c[x];
return ans;
}
int main(void)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",a+i);
for(int i=n;i;i--){ //倒叙扫描
ri2[i] = ask(a[i]-1); //后面小于a[i]的数目
ri1[i] = (n-i) - ri2[i]; //后面大于a[i]的数目
add(a[i],1);
}
memset(c,0,sizeof(c)); //清空
for(int i=1;i<=n;i++){
add(a[i],1);
le1[i] = (i-ask(a[i])); //前面大于a[i]的数目
le2[i] = (i-1) - le1[i]; //前面小于a[i]的数目
}
ll ans1,ans2; //注意用 ll
ans1 = ans2 = 0;
for(int i=1;i<=n;i++){
ans1 += le1[i]*ri1[i];
ans2 += le2[i]*ri2[i];
}
printf("%lld %lld\n",ans1,ans2);
return 0;
}