题目链接https://atcoder.jp/contests/arc101/tasks/arc101_b?lang=en
•大概题意:
给定一个整数序列,对其所有子区间分别取中位数,由这些中位数构成新的序列,求这个序列的中位数。
•题意分析:
要特别注意的是,原序列是不用排序的,而每个子区间和最终序列都要排序。
这意味着是直接从原序列取子区间,而根据中位数的定义,求中位数之前是要排序的,所以整个过程应该是:
•直接对原序列取子区间;
•对于每个子区间执行以下操作:
1.对该子区间排序;
2.求该子区间的中位数;
•将每个区间的中位数组成一个序列,并对其排序;
•求这个最终序列的中位数。
•具体算法:
不难发现,最后的中位数在最终序列(排序后)中的位置是n*(n+1)/4(n是原序列元素个数),且无论从左数还是从右数都是。
•如何判断某个数在最终序列的位置呢?
•首先最终序列的第n*(n+1)/4个数即为最终答案,则在原序列中:①有n*(n+1)/4个数<=它,
②有n*(n+1)/4个数>=它。
(这句话说得不够严谨,我问过某位巨佬,至于n是奇是偶对最终位置n*(n+1)/4可能的+1-1的影响在二分的时候不用纠结,按n*(n+1)/4即可。我尝试后发现把最终答案看作从左到右的第n*(n+1)/4个数和从右到左的第n*(n+1)/4个数都可以)
•那么只需考虑①,②中的一种情况即可。在这里对①讨论。
这时候,二分的思路就出来了,对于某个mid,
看看在最终序列中>=mid的数的个数是不是n*(n+1)/4,如果大于n*(n+1)/4,说明mid太小,反之mid太大。
•二分中check(mid)函数的思路:
check函数的作用是求出在最终序列中,比mid大的数的个数,并返回。
如何判断最终序列中的某个数比mid大还是小?
要知道最终序列的每一个数都来自于原序列的某一个子区间的中位数。
对于某个区间,如果这个区间中大于等于mid的数多于小于mid的数,那么这个区间的中位数必然>=mid,反之比mid小的数更多则这个区间的中位数<mid。当这种中位数个数为n*(n+1)/4时,表示这个mid在原序列中为第n*(n+1)/4个数,即为最终的答案。
所以,在check(mid)的函数中,遍历原序列,>=mid的数用1表示,<mid的用-1,存在一个数组名为pre[]。那么对于上述的某个区间,如果这个区间对应的pre[]区间内元素之和>=0,
说明这个区间中大于等于mid的数多于小于mid的数。反之亦然。
这时候就想到了前缀和,上述问题就转化成满足pre[i]-pre[j]>=0(i>j)的区间个数,
即满足pre[i]>=pre[j](i>j)的个数。这不就是求顺序对的对数吗?
求顺序对可以用树状数组,可以总结出具体模板。
/*
树状数组求顺序对的原理:(懂的可以跳过这里了😅)
*/
•求顺(逆)序对的时候要注意的细节是:
①可能有(最终序列元素个数-1)个数比mid小,这时存入树状数组的就是负数了,而树状数组下标要>0,所以每个数都要+M防止负下标出现。M的值要根据n估算一下。
②循环里的i要从0开始,不然就会漏掉所有以单个元素为区间的情况:pre[i]-pre[0]
研究的时候发现求逆序对也是可以的。改一下二分的符号即可~
注意:i从0开始,则求前缀和时要 i+1(原因分析见相应代码注释)
•AC代码:(顺序对版)
#include<algorithm>
#include<math.h>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<map>
#define ll long long
#define INF 0x3f3f3f3f
#define For(i,a,b) for(int i=a;i<b;i++)
#define lm(x) 1<<(x)
using namespace std;
#define N 200003
#define M 100005
ll n,e[N],a[N],pre[N];
ll lbt(int x){
return x&(-x);
}
void upd(int x,int v){
for(;x<=N;x+=lbt(x)){
e[x]+=v;
}
}
ll tsum(int x){
ll ret=0;
for(int i=x;x>0;x-=lbt(x))
ret+=e[x];
return ret;
}
ll check(int x){
memset(e,0,sizeof(e));//重置树状数组
ll ret=0;
For(i,1,n+1)//遍历一遍原序列
pre[i]=pre[i-1]+(a[i]>=x?1:-1);//pre[i]表示原序列中从头开始到i这段区间
//求pre[]顺序对个数
For(i,0,n+1){//从零开始,以包括原数列只含一个元素的区间即所有pre[i]-pre[0]
ret+=tsum(pre[i]+M);
upd(pre[i]+M,1);//+M防止负数下标
}
return ret;
}
int main()
{ios::sync_with_stdio(false);
cin>>n;ll l=1,r=0,mid,ans;
For(i,1,n+1){
cin>>a[i];
r=max(r,a[i]);//记录二分右边界
}
while(l<=r){//二分答案,对最终的中位数二分
mid=(l+r)>>1;
if(check(mid)>=n*(n+1)/4)ans=mid,l=mid+1;
else r=mid-1;
}
cout<<ans<<endl;
return 0;
}
•AC代码:(逆序对版)
#include<algorithm>
#include<math.h>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<map>
#define ll long long
#define INF 0x3f3f3f3f
#define For(i,a,b) for(int i=a;i<b;i++)
#define lm(x) 1<<(x)
using namespace std;
#define N 200003
#define M 100005
ll n,e[N],a[N],pre[N];
ll lbt(int x){
return x&(-x);
}
void upd(int x,int v){
for(;x<=N;x+=lbt(x)){
e[x]+=v;
}
}
ll tsum(int x){
ll ret=0;
for(int i=x;x>0;x-=lbt(x))
ret+=e[x];
return ret;
}
ll check(int x){
memset(e,0,sizeof(e));//重置树状数组
ll ret=0;
For(i,1,n+1)//遍历一遍原序列
pre[i]=pre[i-1]+(a[i]>=x?1:-1);//pre[i]表示原序列中从头开始到i这段区间
//求pre[]逆序对个数。/*对求顺序对版的代码改这个for循环和二分的符号即可*/
For(i,0,n+1){ //从零开始,以包括原数列只含一个元素的区间即所有pre[i]-pre[0]
upd(pre[i]+M,1); //+M防止负数下标
ret+=i+1-tsum(pre[i]+M);
} /*这里要注意,i是从0开始的,根据求逆序对的原理,
这里的i被赋予“第i个插入”的涵义,没有“第0个插入”的说法,所以在这里+1*/
return ret;
}
int main()
{ios::sync_with_stdio(false);
cin>>n;ll l=1,r=0,mid,ans;
For(i,1,n+1){
cin>>a[i];
r=max(r,a[i]);//记录二分右边界
}
while(l<=r){//二分答案,对最终的中位数二分
mid=(l+r)>>1;
if(check(mid)<=n*(n+1)/4)ans=mid,l=mid+1;//求逆序对的话,符号换成<=即可
else r=mid-1;
}
cout<<ans<<endl;
return 0;
}