树状数组-UVa1428 Ping pong题解

算法分析

一、为何看到这道题想到用树状数组?
首先看到题目上有一句话:For some reason, the contestants can’t choose a referee whose skill rank is higher or lower than both of theirs.也就是说裁判的技能值必须要在两个玩家之间。但是这句话还不能让我们确定用树状数组来做这道题。还有一个关键在于:The contestants have to walk to the referee’s house, and because they are lazy, they want to make their total walking distance no more than the distance between their houses. 也就是说,裁判的位置还得在两个玩家之间。那么我们就可以直观地得到算法:从头到尾遍历裁判的位置,针对每次裁判的位置,在裁判的两端找到符合题目要求的玩家,没找到一种合法方案,就令最终答案加1即可。在这个过程中,我们大胆猜想需要用到的数据结构是树状数组。
二、决策问题的算法分析
那么现在的问题是,当第i个人当裁判的时候,如何找到合法的决策方案数。首先,决策可能分为两种情况,要么左边的人能力小,右边的能力大,要么左边大右边小。
我们假设左边比裁判能力小的人有ci个,右边比裁判能力小的人有di个,则:
对于情况1:裁判左边合法人数为ci,右边合法人数为(n-i)-di
由乘法原理,合法的决策数目为:ci*((n-i)-di)
对于情况2:裁判左边合法人数为(i-1)-ci,右边合法人数为di
由乘法原理,合法的决策数目为:di*((i-1)-ci)
然后由加法原理,我们可以得到第i个人作为裁判时的合法决策数目(即能组织的比赛数目)为:
ci*((n-i)-di)+di*((i-1)-ci)
显然ci和di可以通过树状数组以较低的时间复杂度求出,这也证明了我们在上面的猜想

代码及注释

有了算法分析之后,直接套用树状数组模板即可解决问题,代码如下:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int maxn=200005;
int T,n;
int c[maxn],d[maxn];                               //算法分析中的ci和di存在这个里面 
int a[maxn];                                       //离线读入数据,先读完再操作
int s[maxn];                                       //树状数组中的辅助数组
int N=0;                                           //树状数组的上界 
//这几个树状数组的函数都是模板了(模板参考蓝书即紫书的训练指南) 
int lowbit(int x){
	return x & -x;                                   //笔者自己写&的时候,一般会空格,但是写别的一般不空格。。。 
}
void add(int x,int dd){
	while(x<=N){
		s[x]+=dd;x+=lowbit(x);
	}
}
int sum(int x){
	int ret=0;
	while(x>0){
		ret+=s[x];x-=lowbit(x);
	}
  return ret;
}

int main(){
	
	//freopen("in.txt","r",stdin);
	//freopen("out.txt","w",stdout);

	cin>>T;
	while(T--){
	  memset(s,0,sizeof(s));                          //记得每次到新的一组数据的时候,该重新初始化的地方一定要考虑到
		cin>>n;
	  for(int i=1;i<=n;i++){
	  	cin>>a[i];
	  	if(a[i]>N) N=a[i];
		}
		for(int i=1;i<=n;i++){                          //正着来一遍再倒着来一遍,就能算出左右两边比它小的。 这个思路借鉴于另外一篇博客(后面会引用到) 
			add(a[i],1);
			c[i]=sum(a[i]-1);                             //注意这里要减1,因为不能包括裁判自己
		}
		memset(s,0,sizeof(s));
		for(int i=n;i>=1;i--){
			add(a[i],1);
			d[i]=sum(a[i]-1);
		}
		ll ans=0;                                       //答案可能会很大,记得开到long long 
		for(int i=1;i<=n;i++){
			ans+=(ll)(i-1-c[i])*d[i]+(ll)(n-i-d[i])*c[i]; //即前面”算法分析“中得出的表达式 
		}
		cout<<ans<<endl; 
	}
	
	return 0;
} 

这里面有个小插曲,因为求ci之后,要求di的时候偷了一下懒(直接把求ci的代码复制了一遍),结果i++忘记改成i–了,于是就一直死循环。这里补充一下死循环下的调试问题:遇到死循环的时候,需要在每个循环的位置分别设置调试点进行检测(可以看看中间变量,如果输出很多很多中间变量,则是死循环的位置)。发现add函数死循环之后,就一直在看add哪里错了,始终找不到错,才突然看到包含add的那个第二个for循环不对。
正着遍历一遍再倒着遍历一遍这样的优美思路,来源于:
添加链接描述
在这里提别鸣谢该文章作者。

问题:为什么遍历一遍就能得到比裁判能力小的人的数目?(也就是说ci到底是怎么求出来的)
这个问题,蓝书上讲的还是比较清楚的,大致可以描述为:令一个x[j]代表目前为止已经考虑过的所有ai中,是否存在一个a[i]=j(x[j]=0代表不存在,x[j]=1代表存在),那么c[i]就是前缀和x[1]+x[2]+…+x[ai-1]。计算ci时,需要先设x[ai]=1,然后求前缀和。(此段参考自刘汝佳《算法竞赛入门经典——训练指南》)

作者

Bowen

发布了50 篇原创文章 · 获赞 7 · 访问量 1145

猜你喜欢

转载自blog.csdn.net/numb_ac/article/details/103303034
今日推荐