【CometOJ4303】小 C 的可重集(线段树)(随机二分)

传送门


题解:

这种不可枚举的数量里面求第 K K K 大显然是二分。

问题在于这道题似乎并不好直接二分,没法对 O ( n 2 ) O(n^2) O(n2) 个串确定顺序。

首先注意到这里的大小关系给的非常的迷,这就导致有一点很妙,对于一个集合,新加一个数,得到的集合一定比原集合小。

于是确定左端点的时候,集合大小关于右端点单调。

现在还有一个无法二分的问题,这个完全不用在意,在可能集合中随机一个集合即可,容易证明期望随机次数 O ( log ⁡ n ) O(\log n) O(logn)

那么只剩最后一个问题,计算选中集合的排名。

考虑计算有多少个比它大的,也就是它是第几大的。

由于集合在单独增加一个元素或减少一个元素时候的单调性有保证,做法看下面的代码。

比较两个集合大小可以值域作差,具体实现可以考虑线段树。

还有要注意就是二分不一定要二分到只剩下一个,剩下的全部相等的时候也可以直接退出。

不过懒的话就像我一样直接次数爆炸的时候退出即可。


代码:

#include<bits/stdc++.h>
#define ll long long
#define re register
#define cs const

namespace IO{
	inline char gc(){
		static cs int Rlen=1<<22|1;static char buf[Rlen],*p1,*p2;
		return (p1==p2)&&(p2=(p1=buf)+fread(buf,1,Rlen,stdin),p1==p2)?EOF:*p1++; 
	}template<typename T>T get_integer(){
		char c;bool f=false;while(!isdigit(c=gc()))f=c=='-';T x=c^48;
		while(isdigit(c=gc()))x=((x+(x<<2))<<1)+(c^48);return f?-x:x;
	}inline int gi(){return get_integer<int>();}
	inline ll gl(){return get_integer<ll>();}
}using namespace IO;

using std::cerr;
using std::cout;

cs int N=1e5+7;

std::mt19937 rn(time(0));
std::uniform_int_distribution<int> rnd(0,(int)1e9);

ll K;
int n,a[N];

namespace SGT{

cs int M=1<<17;
int ct[M<<1|1];
int les[M<<1|1];
void clr(){
	memset(ct,0,sizeof ct);
	memset(les,0,sizeof les);
}
bool push_up(int p){
	int c=ct[p],l=les[p];
	int lc=p<<1,rc=lc|1;ct[p]=ct[lc]||ct[rc];
	les[p]=les[lc]||(!ct[lc]&&les[rc]);
	return c!=ct[p]||l!=les[p];
}
void add(int p,int v){
	ct[p+=M]+=v,les[p]=ct[p]<0;
	while((p>>=1)&&push_up(p));
}

}

int lim[N];
ll solve(int ql,int qr){
	ll ct=0;SGT::clr();
	for(int re i=ql;i<=qr;++i)
		SGT::add(a[i],1);
	for(int re i=1;i<=n;++i){
		lim[i]=std::max(lim[i-1],i-1);
		while(lim[i]<=n&&!SGT::les[1])
			SGT::add(a[++lim[i]],-1);
		if(i<=lim[i])SGT::add(a[i],1);
		ct+=lim[i]-i;
	}return ct;
}

int st[N],tp,L[N],R[N];
void Main(){
	n=gi(),K=gl();
	for(int re i=1;i<=n;++i)
		a[i]=gi();K=n*(n+1ll)/2+1-K;
	for(int re i=1;i<=n;++i)
		L[i]=i,R[i]=n,st[++tp]=i;
	for(int re T=1;T<=80;++T){
		int p=st[rnd(rn)%tp+1];
		int pos=rnd(rn)%(R[p]-L[p]+1)+L[p];
		if(solve(p,pos)>=K)
			for(int re i=1;i<=tp;++i)
				R[st[i]]=std::min(R[st[i]],lim[st[i]]-1);
		else 
			for(int re i=1;i<=tp;++i)
				L[st[i]]=std::max(L[st[i]],lim[st[i]]);
		int ct=0;
		for(int re i=1;i<=tp;++i)
			if(L[st[i]]<=R[st[i]])
				st[++ct]=st[i];
		tp=ct;
		if(tp==1&&L[st[1]]==R[st[1]])break;
	}int l=st[1],r=R[st[1]];
	std::sort(a+l,a+r+1);
	for(int re i=l;i<=r;++i)
		cout<<a[i]<<" ";
}

inline void file(){
#ifdef zxyoi
	freopen("multisets.in","r",stdin);
#else
#ifndef ONLINE_JUDGE
	freopen("multisets.in","r",stdin);
	freopen("multisets.out","w",stdout);
#endif
#endif
}signed main(){file();Main();return 0;}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值