POJ 2299 线段树/树状数组求逆序数
2016-03-15 20:07
267 查看
题意: 给你一组无序数 (小于等于500,000) ,让你通过相邻两数的交换,使其变成升序排列。(这些数两两都是不同的)。
刚拿到这道题的时候,不知道该如何下手,后来经过一位善良美丽的学姐的启发,想到了用逆序数。 那么什么是逆序数呢 ? 这在线性代数中有提到过: 在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。
例如给定一组数 9 1 0 5 4 那么这组数的逆序数就是 6 。 因为 9比1大,9比0大,9比5大,9比4大,1比0大,5比4大。 共有6组逆序数。 那么很显然,如果某个数之前有 n 个比它大的数,则这 n 个数肯定要经过他被换到后面。 如此操作直到逆序数为 0 时,排序完成。在这里就不给出严格的数学证明了。
那么怎么去求一组数的逆序数呢? 最简单的方式就是暴力了,逐一排查,不过这样的时间复杂度为 O(n^2)对于本题来说肯定要TLE。 那么我们需要用一个小技巧来解决,本题的技巧就在于用线段树或者树状数组来维护一个区间和来求逆序数。这里我使用的是树状数组,下面我给出详细的解释。
思路很简单,逐一排查要耗费大量的时间,但是如果我们能够直接知晓某个数身后有多少个比它小的数,我们就只需要对数组进行一次遍历就能够获得整个数列的逆序数了。 我们可以按照小到大的顺序将这组数插入树状数组中,每次插入的数只需要知道他的身后已经插入了多少个数即可,我们设插入数的坐标为 i ,那么只需求出(i,n)中有多少个插过的数。在每次插入的时候我们更新当前坐标点的数值为 1 ,表示此坐标已经插入了一个数,这样做便于我们求区间和去统计插入的数的个数。
问题分析至此,我们就可以通过去求出每次插入的元素 i 后(i,n)的区间和来求出这个元素的逆序数。(i,n)的区间和就等于(1,n)的区间和减去(1,i)的区间和。最后,我们累加所有元素的区间和即可得出题解(即需要操作的最少次数)。
代码如下:
刚拿到这道题的时候,不知道该如何下手,后来经过一位善良美丽的学姐的启发,想到了用逆序数。 那么什么是逆序数呢 ? 这在线性代数中有提到过: 在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。
例如给定一组数 9 1 0 5 4 那么这组数的逆序数就是 6 。 因为 9比1大,9比0大,9比5大,9比4大,1比0大,5比4大。 共有6组逆序数。 那么很显然,如果某个数之前有 n 个比它大的数,则这 n 个数肯定要经过他被换到后面。 如此操作直到逆序数为 0 时,排序完成。在这里就不给出严格的数学证明了。
那么怎么去求一组数的逆序数呢? 最简单的方式就是暴力了,逐一排查,不过这样的时间复杂度为 O(n^2)对于本题来说肯定要TLE。 那么我们需要用一个小技巧来解决,本题的技巧就在于用线段树或者树状数组来维护一个区间和来求逆序数。这里我使用的是树状数组,下面我给出详细的解释。
思路很简单,逐一排查要耗费大量的时间,但是如果我们能够直接知晓某个数身后有多少个比它小的数,我们就只需要对数组进行一次遍历就能够获得整个数列的逆序数了。 我们可以按照小到大的顺序将这组数插入树状数组中,每次插入的数只需要知道他的身后已经插入了多少个数即可,我们设插入数的坐标为 i ,那么只需求出(i,n)中有多少个插过的数。在每次插入的时候我们更新当前坐标点的数值为 1 ,表示此坐标已经插入了一个数,这样做便于我们求区间和去统计插入的数的个数。
问题分析至此,我们就可以通过去求出每次插入的元素 i 后(i,n)的区间和来求出这个元素的逆序数。(i,n)的区间和就等于(1,n)的区间和减去(1,i)的区间和。最后,我们累加所有元素的区间和即可得出题解(即需要操作的最少次数)。
代码如下:
#include <iostream> #include <cstring> #include <algorithm> using namespace std; ///****整个算法的时间复杂度是 O(nlogn)。 const int maxn=5e5+5; struct Num{ ///储存数列和数列坐标的结构体。 int zb,v; ///zb储存坐标,v储存数列的值。 } p[maxn]; int bit[maxn],n; int cmp(const Num &a,const Num &b){ /** 当两数相等时将坐标小的那个先处理, 这样就不会对逆序数的计算产生影响。 虽然题目说了不会出现重复的数。 */ if(a.v == b.v){ return a.zb < b.zb; } return a.v < b.v; ///让坐标随着值的大小进行从小到大排序。 } void init(){ ///多组输入别忘了初始化。 memset(p,0,sizeof(p)); memset(bit,0,sizeof(bit)); } ///********************************树状数组维护区间和********************** int sum(int i){ int s=0; while(i > 0){ s+=bit[i]; i-=i&-i; } return s; } void add(int i,int x){ while(i <= n){ bit[i]+=x; i+=i&-i; } } ///************************************************************************** int main() { ios::sync_with_stdio(false); while(cin>>n&&n){ init(); for(int i=1;i<=n;i++){ cin>>p[i].v; p[i].zb=i; } sort(p+1,p+n+1,cmp); long long res=0; ///res用来记录逆序数的个数,数据量大要使用long long。 for(int i=1;i<=n;i++){ add(p[i].zb,1); ///坐标处更新为 1 ( Ps :初始化状态时为 0) res+=sum(n)-sum(p[i].zb); /// (i+1,n)=(1,n)-(1,i) } cout<<res<<endl; } return 0; }
相关文章推荐
- Java ThreadLocal
- 进程和线程的区别以及联系
- 使用TableLayout
- Code Review的重要性
- 操作系统项目(一)安装qemu
- iOS中的事件传递和响应者链条
- 进程学习——父进程子进程ID
- GCD应用及其他方法
- gulp使用笔记
- #import、#include、#import<>和#import””的区别
- BZOJ 1607 轻拍牛头
- fastjson 使用笔记
- Mac 使用brew 安装软件
- HDU 2089 不要62
- Python琐碎知识总结
- gradle 跨工程引用
- AngularJS介绍 - 下一个大框架
- 不同vlan间通信的三种配置方式
- Java泛型中extends和super的理解(转)
- Spark源码