bzoj 3289 Mato的文件管理 (莫队算法+区间逆序数)

传送门bzoj 3289



题目大意:求区间逆序数。



前置技能

1.用树状数组求逆序数。其思路为:树状数组每个节点有个对应的区间,每个节点表示它所表示的下标区间内数字(或者说比节点下标小的数)出现的次数 。当插入第 i 个数字 a[i] 的时候,它会和之前插入的比它大的数形成逆序数,所以插入该数产生的逆序数数为 区间长度 - 已经插入的比该数小的数已经插入的比该数小的数也就是下标为 a[i] 的节点所表示的区间内数的个数。


2.莫队算法。可以处理没有加和性质的区间问题(线段树可以处理有加和性质的区间问题)。这个没法直接套用模板,因为莫队算法是一种思想。简单的来说就是将可以离线处理的问题的所有询问排序,每次由上一次区间进行小范围的转移到当前查询区间。这样避免了很多重复计算,所以时间复杂度很乐观。


排序的时候一般按照分块(每块大小为根号 n)思想来做(也可以按照最小曼哈顿距离做),具体做法是对于询问区间所属的分块相同的,按照区间右端点升序排列,不同的按照区间左端点排列。具体介绍详见:莫队巨神推荐的文章。


其基本步骤可以总结为:输入数据 -> 分块 -> 将询问按照分块排序 -> 通过小范围移动上次的区间得到当前区间的结果  -> 将询问按照编号排序还原原来的顺序 -> 输出结果。关于莫队算法建议找个题按别人的代码敲一遍就明白了。




思路

下面说下本题的思路,知道了如何用树状数组求解逆序数和莫队算法的思想,其实就没啥问题了。唯一的难点是如果在左右端点移动的时候确定区间逆序数的增减情况。


假设区间 [L,R] 内的逆序数已知,当右端点往前移动一格,即 R=R+1时,设新加入的数为 x ,则 x 和在它之前插入的比它大的数会形成逆序数,这个数量之前说过了,为区间长度 - 已插入的比它小的数。右端点退后一格的时候,情况类似,不再讨论。

当左端点前进一格,即 L=L+1 时,区间变小,逆序数减少,减少的数量是下标为 L 的数与其后面比它小的数产生的逆序数,为 query(a[L]-1) 。当左端点退后一格时,情况类似,也不再讨论。



代码:

//求区间逆序数 
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<math.h>
#define N 50010
using namespace std;

int n,ans,a[N],b[N],T[N];
int blk[N]; //记录分块 

struct node
{ //询问 
	int l,r,x,ans; //l,r为询问区间,x为询问编号,ans为询问答案 
} q[N];

int cmp1(node x,node y)
{ //如果询问的左端处于相同块则按右端排序,否则按左端排序 
	if(blk[x.l]==blk[y.l]) return x.r<y.r;
	else return x.l<y.l;
}

int cmp2(node x,node y)
{ //按照编号排序 
	return x.x<y.x;
}

void add(int x,int val)
{ //将包含第 x 位置的加上 val
	int i;
	for(i=x;i<=n;i+=i&-i)
		T[i]+=val;
}

int query(int x)
{ //查找当前插入的数中比 x 小的数的个数 
	int i,sum=0;
	for(i=x;i;i-=i&-i)
		sum+=T[i];
	return sum;
}

int main()
{
	int i,m,l,r,t,num;
	while(~scanf("%d",&n))
	{
		for(i=1;i<=n;i++)
		{ //文件大小 
			scanf("%d",&a[i]);
			b[i]=a[i];
		}
		sort(b+1,b+n+1); //将b排序,然后		 
		for(i=1;i<=n;i++) //用 a[i]在排序后的下标代替 a[i] 
			a[i]=lower_bound(b+1,b+n+1,a[i])-b;
		scanf("%d",&m);
		for(i=1;i<=m;i++)
		{ //询问区间 
			scanf("%d%d",&q[i].l,&q[i].r);
			q[i].x=i; //记录询问编号 
		}
		t=sqrt(n); //每块大小为 sqrt(n) 
		for(i=1;i<=n;i++) blk[i]=(i-1)/t+1; //分块 
		sort(q+1,q+m+1,cmp1); //将询问排序 
		//开始时设区间长度为负 
		l=1;
		r=0;
		ans=0;		
		for(i=1;i<=m;i++)
		{ //加上插入每个数产生的逆序数 
			while(r<q[i].r)			
			{
				r++;
				add(a[r],1);
				//逆序数增加的数量为当前插入的所有数-比 a[r]小的数 
				ans+=r-l+1-query(a[r]); 
			}
			while(r>q[i].r)
			{
				add(a[r],-1);
				ans-=r-l-query(a[r]);
				r--;
			}
			while(l<q[i].l)
			{
				add(a[l],-1);
				//逆序数减少的数量为当前插入的所有数中比 a[l]-1小的 
				ans-=query(a[l]-1);
				l++;
			}
			while(l>q[i].l)
			{
				l--;
				add(a[l],1);
				ans+=query(a[l]-1);
			}
			q[i].ans=ans;
		}
		sort(q+1,q+m+1,cmp2); //按照编号排序还原原顺序 
		for(i=1;i<=m;i++) printf("%d\n",q[i].ans);
	}
	return 0;
}

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值