P6186 [NOI Online #1 提高组] 冒泡排序


前言

在学习了如何求解逆序对之后,我又找到了一个逆序对拓展的题目


洛谷P6186 [NOI Online #1 提高组] 冒泡排序

题目描述

给定一个 1 ∼ n 的排列 pi,接下来有 m 次操作,操作共两种:

交换操作:给定 x,将当前排列中的第 x 个数与第 x+1 个数交换位置。
询问操作:给定 k,请你求出当前排列经过 k 轮冒泡排序后的逆序对个数。 对一个长度为 n 的排列 pi。​

进行一轮冒泡排序的伪代码如下:

for i = 1 to n-1:
if p[i] > p[i + 1]:
swap(p[i], p[i + 1]);

输入格式

第一行两个整数 n,m,表示排列长度与操作个数。
第二行 n 个整数表示排列 pi
接下来 m 行每行两个整数 ti,ci ,描述一次操作:
若 ti=1,则本次操作是交换操作,x=ci
若 ti=2,则本次操作是询问操作,k=ci

输出格式

对于每次询问操作输出一行一个整数表示答案。

输入输出样例
输入 #1

3 6
1 2 3
2 0
1 1
1 2
2 0
2 1
2 2

输出 #1

0
2
1
0

题解

思路

显然首先是要求解原序列的逆序对数的,且要将每一个数所对应的逆序对数量储存起来,这里有两种情况,①一个数所对应的逆序对数量可以是以自己为较大数的逆序对,即ai>aj,i<j时,将该对逆序对储存在i所对应的元素中(ans[i]++),②或者是以自己为较小数的逆序对,即ai>aj,i<j时,将该对逆序对储存在j所对应的元素中(ans[j]++)。

那么,到底用哪一种呢?

这就要考虑冒泡排序中逆序对的变化了,每一次冒泡排序,最大的数都会放到最后,但是在许多中间的数的位置也发生了变化,一些局部较大的数也往后移了,若以①的方式储存逆序对数,那么每次排序后,不同位置上的数所对应的逆序对数量变化也不统一,若以②的方式储存,那么每次排序后的逆序对数量变化情况如下:

a n s [ i ] = { a n s [ i ] − 1 , a n s [ i ] > 0 0 , a n s [ i ] = 0 ans[i]= \left\{\begin{matrix} ans[i]-1,ans[i]>0 \\ 0,ans[i]=0 \end{matrix}\right. ans[i]={ans[i]1,ans[i]>00,ans[i]=0

每一排序时,大数往后移,但因为不储存逆序对数量,所以大数所对应的逆序对数不变,而小数往前移,且只可能移一位,所以小数所对应的逆序对数减一,而不移动的数,则是对应逆序对数为0的数,保持不变。

所以,逆序对的储存方式应该是②
那么,交换第x个数和第x+1个数时,逆序对数量的变化:

if(a[x]>a[x+1]){
	int tmp=ans[x];
	ans[x]=ans[x+1]-1;
	ans[x+1]=tmp;
}else(a[x]<a[x+1]){
	int tmp=ans[x];
	ans[x]=ans[x+1];
	ans[x+1]=tmp+1;
}

求第k次询问的逆序对总数为:

∑ i = 1 n m a x ( a n s [ i ] − k , 0 ) \sum_{i=1}^{n} max(ans[i]-k,0) i=1nmax(ans[i]k,0)

那么,接下来的关键就是如何求出第k次询问的逆序对总数以及修改 a n s [ x ] ans[x] ans[x]和第 a n s [ x + 1 ] ans[x+1] ans[x+1]的值,求总数?修改值?显然,这又是用线段树来维护,所以这个题需要两个线段树。但是这个查询不太一样,,我们需要求 m a x ( a n s [ i ] − k , 0 ) max(ans[i]-k,0) max(ans[i]k,0)的和,那么我们可以先将 a n s [ i ] ans[i] ans[i]放到 a n s [ i ] ans[i] ans[i]的位置上(一个数的逆序对的数量一定小于总个数,所以改线段树占用的空间在规定范围内),然后计算第k次循环后的逆序对总数时,只需要计算[k+1,n]区间内的总值再减去k*该区间中的数量。

线段树查询部分代码如下:

int query(int i,int x,int y){
	if(x<=s[i].l && s[i].r<=y){
		return s[i].sum;
	}
	pushdown(i);
	int res=0;
	if(s[i<<1].r>=x) res+=query(i<<1,x,y);
	if(s[i<<1|1].l<=y) res+=query(i<<1|1,x,y);
	return res;
}//查询总数
int query2(int i,int x,int y){
	if(x<=s[i].l && s[i].r<=y){
		return s[i].num;
	}
	pushdown(i);
	int res=0;
	if(s[i<<1].r>=x) res+=query2(i<<1,x,y);
	if(s[i<<1|1].l<=y) res+=query2(i<<1|1,x,y);
	return res;
}//查询数量
	
	query(1,c+1,n)-c*uery2(1,c+1,n);
	//计算第k次循环后的逆序对数量

线段树修改,加法部分代码如下:

struct tree{
	int l,r,sum;
	int lz,llz;
	int num;
};
const int maxn=2e5+10;
tree s[4*maxn];
int a[maxn],ans[maxn];
vector<int> v;
void pushup(int i){
	s[i].sum=s[i<<1].sum+s[i<<1|1].sum;
	s[i].num=s[i<<1].num+s[i<<1|1].num;
}
void build(int i,int x,int y){
	s[i].l=x;s[i].r=y;
	if(x==y){
		s[i].sum=0;
		s[i].num=0;
		return ;
	}
	int mid=(x+y)/2;
	build(i<<1,x,mid);
	build(i<<1|1,mid+1,y);
	pushup(i);
	return ;
}
void pushdown(int i){
	if(s[i].lz){
		int k=s[i].lz;
		s[i<<1].sum   = (s[i<<1].r-s[i<<1].l+1)*k;
		s[i<<1|1].sum = (s[i<<1|1].r-s[i<<1|1].l+1)*k;
		s[i<<1].lz   = k;
		s[i<<1|1].lz = k;
		s[i].lz=0;
	}
	if(s[i].llz){
		int k=s[i].llz;
		s[i<<1].sum   += (s[i<<1].r-s[i<<1].l+1)*k;
		s[i<<1|1].sum += (s[i<<1|1].r-s[i<<1|1].l+1)*k;
		s[i<<1].llz   += k;
		s[i<<1|1].llz += k;
		s[i].llz=0;
	}
}
void add(int i,int x,int m){
	if(!x) return ;
	if(s[i].l == s[i].r){
		s[i].sum+=m;
		s[i].llz+=m;
		if(m>0) s[i].num++;
		else if(m<0) s[i].num--;
		return ;
	}
	pushdown(i);
	if(s[i<<1].r>=x) add(i<<1,x,m);
	else add(i<<1|1,x,m);
	pushup(i);
}//加法
void change(int i,int x,int y,int m){
	if(s[i].l>=x && s[i].r<=y){
		s[i].sum=m*(s[i].l-s[i].r+1);
		s[i].lz=m;
		return ;
	}
	pushdown(i);
	if(s[i<<1].r>=x) change(i<<1,x,y,m);
	if(s[i<<1|1].l<=y) change(i<<1|1,x,y,m);
	pushup(i);
}//修改

离散化和主函数代码如下:

int getid(int x){
	return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}
signed main(){
	int n=read();int m=read();
	v.clear();
	build(1,1,n);//构建空的线段树
	for(int i=1;i<=n;++i){
		a[i]=read();
		v.push_back(a[i]);
	}
	//离散化
	sort(v.begin(),v.end());
	v.erase(unique(v.begin(),v.end()),v.end());
	//计算逆序对
	for(int i=1;i<=n;++i){
		int x=getid(a[i]);
		ans[i]=(i-1)-query(1,1,x);//a[i]存储以b[i]为较大数的逆序对
		change(1,x,x,1);
	}
	memset(s,0,sizeof(s));
	build(1,1,n);
	for(int i=1;i<=n;++i){
		add(1,ans[i],ans[i]);
	}
	while(m--){
		int t=read(),c=read();
		if(t==1){
			add(1,ans[c],-ans[c]);
			add(1,ans[c+1],-ans[c+1]);
			if(a[c]>a[c+1]){
				int tmp=ans[c];
				ans[c]=ans[c+1]-1;
				ans[c+1]=tmp;
			}else{
				int tmp=ans[c];
				ans[c]=ans[c+1];
				ans[c+1]=tmp+1;
			}//修改ans
			int tmp=a[c];a[c]=a[c+1];a[c+1]=tmp;//修改a
			add(1,ans[c],ans[c]);
			add(1,ans[c+1],ans[c+1]);//修改线段树s
		}else if(t==2){
			if(c>=n-1)
				c=n-1;
			int num=query2(1,c+1,n);
			cout<<query(1,c+1,n)-c*num<<"\n";
		}
	}
	return 0;
}

总结

个人认为该题的综合型较强,非常锻炼思维能力,需要能够灵活地使用线段树或者树状数组。对于本篇题解其实还有可以改进的地方,比如查询部分就有更简单的方法进行维护。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值