题目解析
是考试的题目。
虽然是个签到题但我还是挣扎了很久,而且我个智障把文件名打错了,签到失败嘤嘤嘤
首先简化一下题意:找到一个最大的(k),使得(a[k+1])至(a[2k])中的数都能在(a[1])到(a[k])中匹配到一个比它严格小的数(每个数都只能和一个数匹配)。如果没有这样的(k),输出(0)。
(也可以理解成左半边的数去在右半边找一个比它大的数,是一样的,不过题意理解不同后面的思考分析应该也会相应有所变化)
首先考虑暴力枚举(k),从大到小枚举,然后去(check)它是否符合情况。
(k)的范围是(min(frac n 2,pos))
(pos)是最大值第一次出现的位置,因为最大值不可能出现在前半部分,没有箱子可以装它。注意存在整个数列没有(m)的数据。(出题人大骗子嘤嘤嘤
下面来考虑一下这个(check)该怎么写。
贪心地想,我们应该给每个右半部分的数安排一个小于它的最大的数,暴力做这个的话,我们就要每次排序,然后(lower\_bound),想想就很可怕。
对于这种查找左边小于某个数的问题,我们可以考虑一下权值树状数组。
我们把左半边的数插进权值(BIT)里面,然后对于右半边的数查询有多少个比它小的值。
由于左半边的数可能会被“用掉”,所以我们把右半边的数从小到大排序,然后查询有多少个比它小的左半边的数。
而对于第(i)个数而言,它前面有(i-1)个数用掉了左半边的一些数,而且用掉的这些数一定是小于它自己的(用掉的这些数小于它前面的数),所以查到的数的个数减去(i-1)是第(i)个数可以用到的数,如果这个数值小于(0),那么这个(k)是不可行的。
然后你就得到了(75pts)的好成绩。
bool check(int x)
{
if(x==0) return 1;
//memset(tree,0,sizeof(tree));
//for(int i=1;i<=x;i++)
// Update(a[i],1);
if(x!=lim) Update(a[x+1],-1);
memcpy(b,a,sizeof(a));
sort(b+x+1,b+2*x+1);
for(int i=x+1;i<=2*x;i++)
{
int tmp=Query(b[i]-1)-(i-x-1);
if(tmp<=0) return 0;
}
return 1;
}
而整个(k)变化的过程中,左半边的数可以通过权值(BIT)维护,可以可持续发展。但是右边的数的话,我们每次要排序,要查询,最坏复杂度可以达到(n^2)。而我们又必须要重新排序再枚举右半边的数才能知道它具体能不能匹配得到一个数字。
我们想想能不能让右半边的数也可持续发展一下,换句话说,能不能让查询与排序无关。
还是利用权值(BIT),把右边的数的查询变成一个修改((a[i]-1,-1)),而现在(k)的变化只会影响到这些位置:(假设(k-1)变成$k $,这里显然顺着做要顺一点,不用提前插入一些元素
- (a[2k-1],a[2k])加入右半部分
- (a[k])从前半部分跳到了右半部分
对应以下修改:
- ((a[2k-1]-1,-1))
- ((a[2k]-1,-1))
- ((a[k]-1,1))((a[k])跑走了,所以把它之前的查询撤回
- ((a[k],1))
如何判断当前的(k)是否可行呢?
查询权值(BIT)的每一个位置(的前缀和),如果有小于零的地方,说明有一个右半部分的数找不到匹配的数,不可行,否则就可行。
所有可行解取(max)即可。
看了一圈没有和我写法一样的,倒是有两位初三的同学写了权值线段树,一个(60pts),一个(100pts),不过我看了一下,修改的查询的(±1)正好和我是相反的,大概是因为权值线段树是区间加减,所以较大的区间应该为(+1)才能保证某一段区间和小于零时不成立吧。
别人的写法好巧妙的,但对于我来说不太自然。
瞪了好久才想到优化,不过最后自己独立瞪出来了,还顺手拿了个luogu最优解,在洛谷享受到了coding的快乐(虽然这题很水
理论复杂度应该是(O(nmlogm)),跑得比(O(nm))的算法快我也不知道为什么。
►Code View
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define N 20005
#define M 1005
#define LL long long
int rd()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1; c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
return f*x;
}
int m,n,lim=-1,ans=0;
int a[N],b[N];
int tree[M];
inline int lowbit(int x)
{
return x&(-x);
}
inline int Query(int x)
{
int res=0;
for(;x;x-=lowbit(x))
res+=tree[x];
return res;
}
inline void Update(int x,int val)
{
for(;x<M-5;x+=lowbit(x))
tree[x]+=val;
}
int main()
{
//freopen("bins.in","r",stdin);
//freopen("bins.out","w",stdout);
m=rd(),n=rd();
m++;
for(int i=1;i<=n;i++)
{
a[i]=rd()+1;
if(a[i]==m&&lim==-1)
lim=i;
}
lim=min(lim,n/2);
//if(lim==-1) lim=n/2;
//数据里居然有整个数列都没有m的 出题人大骗子嘤嘤嘤
//其实这个m就是来告诉你值域很小的吧 可这也不需要专门输入一个m啊 直接在数据范围里说不好么233
for(int k=1;k<=lim;k++)
{
Update(a[2*k]-1,-1);
Update(a[2*k-1]-1,-1);
Update(a[k]-1,1);//a[k]不在后半部分了 得撤回
Update(a[k],1);
bool flag=0;
for(int i=1;i<=m;i++)
{
int tmp=Query(i);
if(tmp<0)
{
flag=1;
break;
}
}
if(!flag) ans=max(ans,k);
}
printf("%d
",ans);
return 0;
}
►Code View 75pts TLE
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define N 20005
#define M 1005
#define LL long long
int rd()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1; c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
return f*x;
}
int m,n,lim=-1;
int a[N],b[N];
int tree[M];
inline int lowbit(int x)
{
return x&(-x);
}
inline int Query(int x)
{
int res=0;
for(;x;x-=lowbit(x))
res+=tree[x];
return res;
}
inline void Update(int x,int val)
{
for(;x<M-5;x+=lowbit(x))
tree[x]+=val;
}
bool check(int x)
{
if(x==0) return 1;
//memset(tree,0,sizeof(tree));
//for(int i=1;i<=x;i++)
// Update(a[i],1);
if(x!=lim) Update(a[x+1],-1);
memcpy(b,a,sizeof(a));
sort(b+x+1,b+2*x+1);
for(int i=x+1;i<=2*x;i++)
{
int tmp=Query(b[i]-1)-(i-x-1);
if(tmp<=0) return 0;
}
return 1;
}
int main()
{
//freopen("bins.in","r",stdin);
//freopen("bins.out","w",stdout);
m=rd(),n=rd();
for(int i=1;i<=n;i++)
{
a[i]=rd();
if(a[i]==m&&lim==-1)
lim=i;
}
lim=min(lim,n/2);
if(lim==-1) lim=n/2;
for(int i=1;i<=lim;i++)
Update(a[i],1);
for(int k=lim;k>=0;k--)
{
if(check(k))
{
printf("%d
",k);
break;
}
}
return 0;
}