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

编程之美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;
}


效率

总结:

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


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值