「Luogu P1975 [国家集训队]排队」

题目大意

给出一个序列 \(h\),支持交换其中的两数,求出每一时刻的逆序对个数.

分析

求逆序对是 \(O(N\log_2N)\) 的,有 \(M\) 个操作,如果暴力求的话时间复杂度就是 \(O(MN\log_2N)\) 虽然数据范围不大,但是还是可能因为评测机浮动而TLE,所以就不要想着折腾这些东西了,还是要用一些正经点的方法去过这种题.

求逆序对的方法大致可以分成两种:

  1. 用一些数据结构维护大于某个数的数在这个数之前出现过几次,只需要将这些数一个一个放入就好了,优点很明显,可以计算出每个数对于结果的贡献,但是缺点也很致命,常数较大,如luogu上的模板题就过不了了.
  2. 利用归并排序的性质来计算,但是这个方法中的每个数的贡献是无法单独计算的,跑起来确实会比第一种方法快.

第二种方法用在本题显然不合适,所以本题需要用第一种方法.

先不考虑交换两数有什么特殊的性质,单纯考虑删除一个数对于逆序对个数的影响(P3157 [CQOI2011]动态逆序对).

因为是删除一个数,其影响到的只有其他数与它产生的逆序对,而产生逆序对的条件就是 \(j<i\)\(a_j>a_i\),所以在这个数前面且大于它的数会和它产生一个逆序对,在这个数后面且小于它的数也会和它产生一个逆序对,那么问题就变成了计算前面有多少大于它的数,后面有多少小于它的数,不考虑范围的话可以直接用权值线段树解决,然而这里是一个区间问题+单点修改,那么就很容易想到利用树状数组维护前缀每个数出现的次数,只要差分一下就可以得出结果.

至于需要重新加上一个数那还是一样,只要找到前面大于这个数的个数,后面小于这个数的个数,相加就是这个数对于这个序列的逆序对个数产生的贡献,因为树状数组维护的是前缀,所以交换两数带来的良好性质自然也没什么用了,但是,为了让这个没什么用的性质看似有用一点,还是放一道可以用这个性质的题.

代码

#include<bits/stdc++.h>
#define REP(i,first,last) for(int i=first;i<=last;++i)
#define DOW(i,first,last) for(int i=first;i>=last;--i)
using namespace std;
const int MAXN=114514;
int N,M;
int arr[MAXN];
int tot=0;
map<int,int>Hash;//用于离散化
int sor[MAXN];//用于离散化
int root[MAXN];//树状数组上每个位置的线段树的根节点
long long answer=0;
//线段树部分
struct SegmentTree
{
	int sum,lson,rson;
}sgt[MAXN*32];
int sgt_cnt=0;
#define LSON sgt[now].lson
#define RSON sgt[now].rson
#define MIDDLE ((left+right)>>1)
#define LEFT LSON,left,MIDDLE
#define RIGHT RSON,MIDDLE+1,right
void PushUp(int now)//合并信息
{
	sgt[now].sum=sgt[LSON].sum+sgt[RSON].sum;
}
void Updata(int num,int add,int &now,int left=1,int right=tot)//修改操作,和普通动态开点权值线段树相同
{
	if(num<left||right<num)//不包含修改位置就返回
	{
		return;
	}
	if(!now)//如果当前位置没有节点就新建一个节点
	{
		now=++sgt_cnt;
	}
	if(left==right)//到叶节点就直接修改
	{
		sgt[now].sum+=add;
		return;
	}
	//继续修改
	Updata(num,add,LEFT);
	Updata(num,add,RIGHT);
	PushUp(now);
}
int num_add,num_dec;
int add_sgt[MAXN],dec_sgt[MAXN];//树状数组来维护,需要用到差分,需要记录下当前需要加上的线段树的当前根节点的编号,以及需要减去的线段树的当前编号
int GetSum()//得到当期范围中的数的个数,和树状数组计算区间和同理
{
	int result=0;
	REP(i,1,num_add)
	{
		result+=sgt[add_sgt[i]].sum;
	}
	REP(i,1,num_dec)
	{
		result-=sgt[dec_sgt[i]].sum;
	}
	return result;
}
//左右子树中的数的个数计算方法同理
int GetSumL()
{
	int result=0;
	REP(i,1,num_add)
	{
		result+=sgt[sgt[add_sgt[i]].lson].sum;
	}
	REP(i,1,num_dec)
	{
		result-=sgt[sgt[dec_sgt[i]].lson].sum;
	}
	return result;
}
int GetSumR()
{
	int result=0;
	REP(i,1,num_add)
	{
		result+=sgt[sgt[add_sgt[i]].rson].sum;
	}
	REP(i,1,num_dec)
	{
		result-=sgt[sgt[dec_sgt[i]].rson].sum;
	}
	return result;
}
//将当前的根节点变为左子节点
void GetRootL()
{
	REP(i,1,num_add)
	{
		add_sgt[i]=sgt[add_sgt[i]].lson;
	}
	REP(i,1,num_dec)
	{
		dec_sgt[i]=sgt[dec_sgt[i]].lson;
	}
}
//变为右子节点
void GetRootR()
{
	REP(i,1,num_add)
	{
		add_sgt[i]=sgt[add_sgt[i]].rson;
	}
	REP(i,1,num_dec)
	{
		dec_sgt[i]=sgt[dec_sgt[i]].rson;
	}
}
int QuerySmall(int num,int left=1,int right=tot)//查询小于的数的个数,计算方法和权值线段树同理,不多讲
{
	if(num<=left)
	{
		return 0;
	}
	if(right<num)
	{
		return GetSum();
	}
	if(num<=MIDDLE)
	{
		GetRootL();
		return QuerySmall(num,left,MIDDLE);
	}
	int result=GetSumL();
	GetRootR();
	return result+QuerySmall(num,MIDDLE+1,right);
}
int QueryBig(int num,int left=1,int right=tot)//计算大于的数的个数,同理
{
	if(right<=num)
	{
		return 0;
	}
	if(left>num)
	{
		return GetSum();
	}
	if(MIDDLE+1<=num)
	{
		GetRootR();
		return QueryBig(num,MIDDLE+1,right);
	}
	int result=GetSumR();
	GetRootL();
	return result+QueryBig(num,left,MIDDLE);
}
#undef LSON
#undef RSON
#undef MIDDLE
#undef LEFT
#undef RIGHT
int Lowbit(int now)//树状数组要用的lowbit
{
	return now&-now;
}
void BeforeQuery(int left,int right)//在查询前的预处理,将需要加上的线段树的根节点和需要减去的线段树的根节点编号记录下来
{
	num_add=0,num_dec=0;
	for(int now=right;now;now-=Lowbit(now))
	{
		add_sgt[++num_add]=root[now];
	}
	for(int now=left-1;now;now-=Lowbit(now))
	{
		dec_sgt[++num_dec]=root[now];
	}
}
int Small(int num,int left,int right)//查询区间内小于的数的个数
{
	BeforeQuery(left,right);
	return QuerySmall(num);
}
int Big(int num,int left,int right)//查询区间内大于的数的个数
{
	BeforeQuery(left,right);
	return QueryBig(num);
}
void Change(int p1,int p2)//交换两数,就是删掉一个数,再放上一个数的操作最两遍
{
	int ha=arr[p1];
	int hb=arr[p2];
	answer-=Big(ha,1,p1-1)+Small(ha,p1+1,N);//删除一个数所减去的贡献
	for(int now=p1;now<=N;now+=Lowbit(now))//在线段树中减去这个数
	{
		Updata(ha,-1,root[now]);
	}
	answer+=Big(hb,1,p1-1)+Small(hb,p1+1,N);//同理加上这个数
	for(int now=p1;now<=N;now+=Lowbit(now))
	{
		Updata(hb,1,root[now]);
	}
	answer-=Big(hb,1,p2-1)+Small(hb,p2+1,N);
	for(int now=p2;now<=N;now+=Lowbit(now))
	{
		Updata(hb,-1,root[now]);
	}
	answer+=Big(ha,1,p2-1)+Small(ha,p2+1,N);
	for(int now=p2;now<=N;now+=Lowbit(now))
	{
		Updata(ha,1,root[now]);
	}
	swap(arr[p1],arr[p2]);
}
int main()
{
	scanf("%d",&N);
	REP(i,1,N)
	{
		scanf("%d",&arr[i]);
		sor[i]=arr[i];
	}
	sort(sor+1,sor+1+N);//离散化
	sor[0]=114514233;
	REP(i,1,N)
	{
		if(sor[i]!=sor[i-1])
		{
			Hash[sor[i]]=++tot;
		}
	}
	REP(i,1,N)//逆序对的计算中只需要考虑相对大小,所以不需要保留原来的值
	{
		arr[i]=Hash[arr[i]];
	}
	REP(i,1,N)//建树
	{
		for(int now=i;now<=N;now+=Lowbit(now))
		{
			Updata(arr[i],1,root[now]);
		}
	}
	REP(i,2,N)//计算最开始的逆序对
	{
		answer+=Big(arr[i],1,i-1);
	}
	printf("%lld\n",answer);//记得输出最开始的逆序对个数
	scanf("%d",&M);
	int l,r;
	REP(i,1,M)
	{
		scanf("%d%d",&l,&r);
		Change(l,r);
		printf("%lld\n",answer);
	}
	return 0;
}
posted @ 2020-02-24 18:59  SxyLimit  阅读(107)  评论(0编辑  收藏  举报