编程之美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 500000
int 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);
int
main(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 500010
struct 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);
else
insert(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);
}
}
int
main(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 500010
struct 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;
}
int
main(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;
}
效率
总结:
从上面各种方法我们可以看到,分治算法的效率最高(没有线段树和树状数组中的排序离散化预处理),消耗内存最低,方法最优,推荐这种做法。线段树编码难度最大,且内存消耗巨大,效率也不算高。不推荐这种做法。树状数组效率和内存都位于分治算法和树状数组中间,树状数组的编码量小。