题解:CF2129C Interactive RBS

题解 CF2129C

肠子悔青了。因为上黄没多久想保一下分就没打这一场,结果发现是巨大交互上分爽场,能送我直接飞升 2200 的一场就这么被我埋葬在当晚和朋友的鹅鸭杀里了,警钟长鸣。


题意

有一个长度为 \(N\) 的括号序列 \(S\) ,你不知道它具体的形态,每次可以向交互库询问,选中其中不超过 \(1000\) 个可重复的位置,以任意顺序排列这些位置构成一个序列,提交给交互库。

然后交互库返回一个整数,表示你查询的序列对应构成的括号序列中有多少个连续子串是合法括号串,括号串的定义不再赘述。要求你在 \(K\) 次询问内获得 \(S\) 的所有信息,然后报告 \(S\) 。

保证 \(S\) 不全部由 ( 或 ) 中的某一种构成。

C1:\(K=550\)

C2:\(K=200\)

C3:\(K=100\)


题解

本题部分分设计非常精妙,前两个难度等级依次启发下一题的思路,因此本文也按顺序介绍三个难度的解法。

Solve #1

不妨扩大题目的限制,想想怎么在 \(1000\) 步左右解决问题,以及我们需要什么。

题目允许我们使用重复的下标,也允许我们任意排列这些下标,如果我们知道某个位置 \(p\) 具体是 ( 还是 ) 的话(这里假设是 ) ),那我们就可以对每个下标 \(i\) 询问 ? 2 i p,如果返回值是 \(1\) 那么 \(i\) 就是 (,返回值是 \(0\) 那么 \(i\) 就是 ) 。

该如何快速找到一个这样的位置 \(p\) 呢?以找 ) 为例,我们发现固定查询左端点为 \(1\) ,查询序列 \(1,2,3,\cdots,len\) 得到的返回值关于 \(len\) 的取值是单调不降的。所以可以二分 \(len\) 来找到第一个有 ( 与之匹配的 ),从而也就找到了一个 )。一个特例是如果原序列内不存在一个子串为合法括号序列的话,那么下标为 \(1\) 的位置一定是 ),原因是题目保证了至少分别存在一个 ( 和 )。

找到 \(p\) 这一步需要开头判一次刚刚说的特殊情况,加上二分的 \(\lceil{\log_2(1000)}\rceil=10\) 次查询,已经用去 \(11\) 次查询。不过这是简单版本,查询次数给足了余量。

接下来盯着题目给出的 \(550\) 次查询限制,提示我们试着在一次查询中确定两个位置的值。既然前面获得了 \(p\),我们先尝试构造形如 ?*)) 或者 ?)*) (问号和星号分别表示两个要查询的位置)的查询,但发现这样行不通,因为如果返回值是 \(1\) 的话我们无法区分是哪个位置没有匹配。那么我们尝试让返回值具有“差异化”,让我们能知道左右两边的匹配情况。一种合理的构造方案是 ?)**)),这样当结果为 \(4\) 时,说明两个位置都是 (,为 \(2\) 说明第一个位置是 ) 第二个位置是 (,为 \(1\) 反之,为 \(0\) 说明两个位置都是 )。

至此第一个难度的问题解决。\(N=1000\) 时使用不超过 \(511\) 次查询。

ll n;
ll query(ll l,ll r){
	cout<<"? "<<r-l+1<<" ";
	for(ll i=l;i<=r;i++)cout<<i<<" ";
	cout<<endl;
	ll res;cin>>res;
	return res;
}
string s;
void solve(){
	cin>>n;
	s.resize(n+1);
	ll res=query(1,n);
	ll it=0;
	if(res==0)it=1;
	else {
		ll l=1,r=n;
		while(l<=r){
			ll mid=(l+r)>>1;
			if(query(1,mid))it=mid,r=mid-1;
			else l=mid+1;
		}
	}
	for(ll i=1;i+1<=n;i+=2){
		cout<<"? 6"<<" ";
		cout<<i<<" "<<it<<" "<<i+1<<" "<<i+1<<" "<<it<<" "<<it<<endl;
		cin>>res;
		if(res==4)s[i]='(',s[i+1]='(';
		if(res==2)s[i]=')',s[i+1]='(';
		if(res==1)s[i]='(',s[i+1]=')';
		if(res==0)s[i]=')',s[i+1]=')';
	}
	if(n&1){
		cout<<"? 2"<<" ";
		cout<<n<<" "<<it<<endl;
		cin>>res;
		if(res)s[n]='(';
		else s[n]=')';
	}
	cout<<"! ";
	for(ll i=1;i<=n;i++)cout<<s[i];
	cout<<endl;
	return ;
}

solve #2

在上面的做法中,我们是通过差异化给两个位置分配的右括号数量,来让返回值里包含具有两个位置区分度的信息。注意到单次查询的序列长度可以达到 \(1000\),那么顺着这个思路,加入更多的查询位置,然后将其也和前面的位置“差异化”分配右括号就行。

不难想到二进制拆分。形如 ?) 的查询得到的返回值是 \(0/1\),??)) 得到的是 \(0/2\),????)))) 得到的返回值是 \(0/4\),…将这些查询序列“拼”起来就可以根据返回值的二进制表示得到哪些位置是 ( 还是 )。然而我们不能简单地将它们直接连接起来,原因是括号串 ()(()) 的返回值是 \(4\) 而不是 \(3\),首尾相连的合法串会对答案产生额外贡献。因此我们需要在每个 ???...?)))..) 后再单独添加一个 ) 来保证这样的合法串无法首尾连接产生贡献。

那么我们在长度 \(1000\) 的询问序列中可以同时获得几个位置的答案呢?很遗憾只有至多 \(8\) 个位置(而不是直觉得到的 \(10\) 个位置),原因是假设我们要同时获得 \(a\) 个位置的答案,那么由等比数列求和,按照上面的构造方式我们至少需要长度为 \(2^{a+1}+a-3\) 的询问序列,在 \(a=9\) 时已经超出限制。

但是一次查询获取 \(8\) 个位置的信息足以我们通过本题中等难度的版本。我们的总查询数量不超过 \(\lceil{\frac{1000}{8}}\rceil+11=136\) 次。

ll n;
ll query(ll l,ll r){
	cout<<"? "<<r-l+1<<" ";
	for(ll i=l;i<=r;i++)cout<<i<<" ";
	cout<<endl;
	ll res;cin>>res;
	return res;
}
string s;
void solve(){
	cin>>n;
	s.resize(n+20);
	ll res=query(1,n);
	ll it=0;
	if(res==0)it=1;
	else {
		ll l=1,r=n;
		while(l<=r){
			ll mid=(l+r)>>1;
			if(query(1,mid))it=mid,r=mid-1;
			else l=mid+1;
		}
	}
	for(ll i=1;i<=n;i+=8){
		vector<ll>lst;
		ll lim=min(8ll,n-i+1);
		for(ll j=0;j<lim;j++){
			for(ll k=1;k<=(1<<j);k++)lst.push_back(min(i+j,n));
			for(ll k=1;k<=(1<<j)+1;k++)lst.push_back(it);
		}
		cout<<"? "<<lst.size()<<" ";
		for(auto x:lst)cout<<x<<" ";
		cout<<endl;
		cin>>res;
		for(ll j=0;j<lim;j++){
			if(res&(1<<j))s[i+j]='(';
			else s[i+j]=')';
		}
	}
	cout<<"! ";
	for(ll i=1;i<=n;i++)cout<<s[i];
	cout<<endl;
	return ;
}

solve #3

离正解只差最后一步了。可以感觉到二进制拆分的思路是对的,但是长度限制让上面的构造很难继续优化了。

这个时候考虑二进制拆分为什么可以用一个非负整数映射集合。因为二进制下每个位置加权后的值严格大于所有低位权值之和,也就是 \(2^k>2^k-1=\sum_{i=0}^{k-1}2^i\),不存在一个位置的值可以被低位值的某一种组合加出,从而保证每个位置信息独立。

也就是说,如果我们存在一种分配每位权值的方案,满足上面的条件,那么这样的分配也是可以用一个非负整数去映射集合的。

回到刚才的构造,如果我们能用更少的串长让一个位置的权值更大(也就是合法括号串的数量更大),且满足上面的条件,就可以在单次查询塞下更多位置。

那么我们的目光移向形如 ()()()...() 的串,用长度为 \(2·len\) 的串长即可获得 \(\frac{len·(len+1)}{2}\) 个合法括号串。然而给第一个位置的 \(len\) 设为 \(1\) ,给第二个位置的 \(len\) 设为 \(2\),第三个位置设 \(3\),……是行不通的,容易发现这样得到的每个位置的权值不满足本文第二段提出的要求。

那不满足我们就让它满足好了。打表,令 \(len_1\) 为 \(1\),要求 \(len_i\) 满足 $2·\frac{len_{i-1}·(len_{i-1}+1)}{2} \le \frac{len_{i}·(len_{i}+1)}{2} $ 的前提下尽可能小,然后我们看看每个位置最小的 \(len\) 是多少,以及要用多少查询长度。

对前 \(12\) 位打表代码如下。

void solve(){
	ll x=1,sum=1;
	cout<<x<<' ';
	for(ll i=2;i<=12;i++){
		ll y=x;
		while(y*(y+1)<x*(x+1)*2)y++;
		cout<<y<<' ';
		x=y;
		sum+=y;
	}
	cout<<endl;
	cout<<sum*2<<endl;
	return ;
}

输出结果:

1 2 3 5 8 12 18 26 37 53 76 108 
698

那么也就是说,按照输出结果第一行的方式分配每一位的权值,只需要使用 \(698\) 个长度就可以用得到的结果表示 \(12\) 个不同的位置是 ( 还是 ),同时我们还要额外的 \(11\) 个位置在每一位间塞入 ) 来防止中等难度问题解法中提到的额外贡献问题。可以根据输出结果发现塞 \(13\) 个位置就不行了。

那么总查询数量不超过 \(\lceil\frac{1000}{12}\rceil+11=95\) 次,足以通过本题全部难度版本。

ll mul[]={1,2,3,5,8,12,18,26,37,53,76,108};
ll n;
ll query(ll l,ll r){
	cout<<"? "<<r-l+1<<" ";
	for(ll i=l;i<=r;i++)cout<<i<<" ";
	cout<<endl;
	ll res;cin>>res;
	return res;
}
string s;
void solve(){
	cin>>n;
	s.resize(n+20);
	ll res=query(1,n);
	ll it=0;
	if(res==0)it=1;
	else {
		ll l=1,r=n;
		while(l<=r){
			ll mid=(l+r)>>1;
			if(query(1,mid))it=mid,r=mid-1;
			else l=mid+1;
		}
	}
	for(ll i=1;i<=n;i+=12){
		ll lim=min(12ll,n-i+1);
		vector<ll>lst;
		for(ll j=0;j<lim;j++){
			for(ll k=1;k<=mul[j];k++){
				lst.push_back(i+j),lst.push_back(it);
			}
			lst.push_back(it);
		}
		cout<<"? "<<lst.size()<<" ";
		for(auto x:lst)cout<<x<<" ";
		cout<<endl;
		cin>>res;
		for(ll j=lim-1;j>=0;j--){
			if(res>=mul[j]*(mul[j]+1)/2){
				res-=mul[j]*(mul[j]+1)/2;
				s[i+j]='(';
			}
			else s[i+j]=')';
		}
	}
	cout<<"! ";
	for(ll i=1;i<=n;i++)cout<<s[i];
	cout<<endl;
	return ;
}

实现没有注意最后一位后不需要添加 ) 分隔的细节,对答案没有影响。

posted @ 2025-08-02 19:17  Rnfmabj  阅读(350)  评论(0)    收藏  举报