由《编程之美》光影切割问题引出的-----求数组逆序数

来源:互联网 发布:mysql入门很简单 光盘 编辑:程序博客网 时间:2024/04/27 19:36

编程之美1.7的一道光影切割问题,经过分析之后,可以简化为一个求逆序数的问题,当然求逆序数可以用非常暴力的O(N^2)的解法,但是,如果你正在接受一个面试,给出了O(N^2)的解法的话,面试官一定不会满意,对你的印象也大打折扣,所以这里主要是讲解一些求逆序数的一些高效方法。

以poj上的一道题目为例http://poj.org/problem?id=2299

方法一:分治算法

我们知道排序中有一种高效的排序算法是归并排序,归并排序在最坏情况下是O(NlogN)的,另外需要O(N)的空间。分治算法是一种万能的高效的算法,很多问题都可以用分治的思想去解决,求逆序数当然也可以利用分治思想去解决。对于数组a1,a2,a3............aN,将数组分为两个子问题求解,分别求解a1,a2.....a(1+N)/2的逆序数,和a(3+N)/2的逆序数,然后再求解两个子数组之间形成的逆序数。这里就要用到归并排序了,这里假设左边和右边的子数数组经过递归以后已经是有序的了(只有一个元素时子数组逆序数为1),然后将两个子数组合并,对于右边子数组中的每个元素,当这个元素加到临时数组中时,只要求出左边比它大的元素个数是多少就可以了,然后将所有这个个数相加既是答案。左边比该元素大的元素个数为mid-i+1,i为左边数组此时的下标。

#include<stdio.h>#define MAX_NUM 500000int N;int sequence[MAX_NUM];int copy[MAX_NUM];long long merge(int p1_start,int p1_end, int p2_start,int p2_end);long long  reverse_num(int left,int right);intmain(void){int i;while(scanf("%d",&N) && N){for(i = 0;i<N;i++)scanf("%d",&sequence[i]);printf("%lld\n",reverse_num(0,N-1));}return 0;}long long  merge(int p1_start,int p1_end, int p2_start,int p2_end){int i = p1_start,j = p2_start;int count = 0;long long ans_count = 0;while(i <= p1_end && j <= p2_end){if(sequence[i] <= sequence[j])copy[count++] = sequence[i++];else{copy[count++] = sequence[j++];ans_count += p1_end - i + 1;}}while(i<=p1_end)copy[count++] = sequence[i++];while(j <= p2_end)copy[count++] = sequence[j++];int k = 0;while(k<count)sequence[p1_start+k] = copy[k++];return ans_count;}long long  reverse_num(int left,int right){if(left == right)return 0;int mid = (left+right)>>1;return reverse_num(left,mid) + reverse_num(mid+1,right) + merge(left,mid,mid+1,right);}
运行效率如图

方法二:线段树+离散化

这道题目可以利用线段树去做,我们知道线段树处理区间问题是比较方便的,效率也很高,所以线段树也叫区间树,但是这道题目0<=a[i]<999999999,但是共有不超过500000个数据,所以很显然要用到数据的离散化,所谓离散化就是将数据映射到排序后它的下表,即它是第几小的。离散化之后存入数组hash,然后将数组中每个数插入到线段树中,并且ans加上区间(hash[i]+1,N)的值

#include<cstdio>#include<iostream>#include<cstdlib>#include<algorithm>using namespace std;#define MAX_NUM 500010struct node{int value;int id;};int cmp(struct node a, struct node b){return a.value < b.value;}struct node values[MAX_NUM];int hash[MAX_NUM];struct tree_node{int left,right;long long times;};struct tree_node seg_trees[MAX_NUM<<2];void build_tree(int root,int left,int right){seg_trees[root].left = left;seg_trees[root].right = right;seg_trees[root].times = 0;if(left == right){return;}int mid = (left + right)>>1;build_tree(root*2,left,mid);build_tree(root*2+1,mid+1,right);}void insert(int root,int id){if(seg_trees[root].left == seg_trees[root].right){seg_trees[root].times +=1;return ;}int mid = (seg_trees[root].left + seg_trees[root].right)>>1;if(id <= mid)insert(root*2,id);elseinsert(root*2+1,id);seg_trees[root].times +=1;}long long query(int root,int left, int right){if(seg_trees[root].left == left && seg_trees[root].right == right)return seg_trees[root].times;int mid = (seg_trees[root].left + seg_trees[root].right)>>1;if(right <= mid)return query(root*2,left,right);else if(left > mid)return query(root*2+1,left,right);else{return query(root*2,left,mid)+query(root*2+1,mid+1,right);}}intmain(void){int N;while(scanf("%d",&N) && N){int i,j,k;for(i = 0;i<N;i++){scanf("%d",&values[i].value);values[i].id = i+1;}sort(values,values+N,cmp);for(i = 1;i<=N;i++){hash[values[i-1].id] = i;}build_tree(1,1,N);long long ans = 0;insert(1,hash[1]);for(i = 2;i<=N;i++){insert(1,hash[i]);if(hash[i] ==  N)continue;ans += query(1,hash[i]+1,N);}printf("%lld\n",ans);}return 0;}

效率

方法三:树状数组

从上面可以看到,利用线段树编码量大,编码难度高,浪费内存,而且运行效率并不高。所以,自然而然可以想到用树状数组去做,树状数组做法和线段树做法极其相似,这里不做详述。

#include<cstdio>#include<string.h>#include<iostream>#include<cstdlib>#include<algorithm>using namespace std;#define MAX_NUM 500010struct node{int value;int id;};int cmp(struct node a, struct node b){return a.value < b.value;}struct node values[MAX_NUM];int hash[MAX_NUM];long long tree_arr[MAX_NUM];int lowbit(int x){return x&(-x);}void update(int pos,int up){while(pos<=up){tree_arr[pos] +=1;pos = pos + lowbit(pos);}}int get_sum(int pos){long long ans = 0;while(pos>0){ans +=tree_arr[pos];pos -= lowbit(pos);}return ans; }intmain(void){int N;while(scanf("%d",&N) && N){int i;long long ans = 0;for(i = 0;i<N;i++){scanf("%d",&values[i].value);values[i].id = i+1;}sort(values,values+N,cmp);for(i = 1;i<=N;i++){hash[values[i-1].id] = i;}memset(tree_arr,0,sizeof(tree_arr));update(hash[1],N);for(i = 2;i<=N;i++){update(hash[i],N);if(hash[i] == N)continue;ans += get_sum(N)- get_sum(hash[i]);}printf("%lld\n",ans);}return 0;}


效率

总结:

从上面各种方法我们可以看到,分治算法的效率最高(没有线段树和树状数组中的排序离散化预处理),消耗内存最低,方法最优,推荐这种做法。线段树编码难度最大,且内存消耗巨大,效率也不算高。不推荐这种做法。树状数组效率和内存都位于分治算法和树状数组中间,树状数组的编码量小。


原创粉丝点击