CF1746E Joking

CF1746E Joking

{1,2}\{1, 2\}

交互库最开始给定一个正整数 nn,并生成一个 x[1,n]x \in [1, n],你的目标是得到交互库中的 xx

你可以向交互库提出问题:

提问一个集合 SS,交互库回答的内容是 xSx \in S 的真假。该提问次数不能超过限制数 QQ

交互库可以骗人,也即交互库的回答不一定正确。但保证 交互库连续的两次回答中,至少有一次是正确的

你还可以向交互库提交答案,直接提交一个正整数 ii。你只有 22 次提交答案的机会。

保证 1n1051 \le n \le 10^5。交互库是自适应性的,它会通过某种神奇策略动态改变 xx,使得它仍然满足它所回答过你的所有问题,保证交互正确性的同时,尽可能让你慢地猜出答案。

E1 Easy Version

在 Easy Version 中,限制参数 Q=82Q = 82

观察到提交答案的机会只有 22 次,因此中途就提交答案并不是特别明智。但直接放弃一次多余的机会也不是很明智。

因此我的想法是:当知道 xx 的范围只有两个数的时候,直接把这两个数猜一遍就可以了。那么我们提问的最终目标从将 xx 锁定成一个数弱化成了锁定成两个数的一个小范围。


对交互的询问进行分析,我们考虑交互库不骗人的时候怎么做。

我们大概能知道这种交互问题都是不断缩小 xx 可能的范围得到答案的,不妨放称当前 xx 可能的范围为“目标范围”,最开始目标范围就是 [1,n][1, n] 的正整数全集,而我们的目标就是把目标范围减少到大小为 22。为了确保询问次数低于限制参数,我们期望每次能让目标范围降低的尽可能多。

这里让目标范围降低多少的计算,应该按照交互库的 YesNo 两种回答中,能让目标范围降低地最少的那个来计算,也即我们要按照最坏情况考虑。

比如当前的目标范围是 UU,你提问一次 S={1}S = \{1\}(我们先暂且假定交互库不骗人),那么交互库回答 Yes,不管 UU 多大都能降低成 11,也即 UU 的大小会降低 U1|U| - 1。这个降低效果没人敢说不高。但是如果交互库回答 No,目标范围大小只能降低 11,这就很不优秀了。根据按照最坏情况考虑的原则,这种询问策略的目标范围降低效果为降低 11,明显不是我们想要的策略。

提一嘴,这里交互库自适应,所以要按照最坏情况考虑;但如果交互库不自适应,那么可能可以有正确率不是 100%100\% 但是可以很高的交互策略:可以算一下交互库回答两种答案的期望,从而得到目标范围降低的期望,而不是最坏情况。

如果一个交互策略的最坏情况很不优秀,但是期望比较优秀,在交互库不自适应的情况下可能也是可以做的。这是后话了。


如果交互库不骗人,策略还是比较容易想到的,折半查找即可。

设当前目标范围是 UU,我们取 UU 的一个大小为 U2\left\lfloor\dfrac{|U|}2\right\rfloor 的子集 SS 进行提问。如果回答 Yes,那么目标范围 USU \gets S,大小变成 U2\left\lfloor\dfrac{|U|}2\right\rfloor,也即降低 U2\left\lceil\dfrac{|U|}2\right\rceil;如果回答 No,目标范围 UUSU \gets \complement_US,大小变成 U2\left\lceil\dfrac{|U|}2\right\rceil,也即降低 U2\left\lfloor\dfrac{|U|}2\right\rfloor

那么这种策略,每次可以将目标范围 UU 大小降低 U2\left\lfloor\dfrac{|U|}2\right\rfloor,我们可以在 log2n\log_2n 的询问次数解决问题。


如果交互库骗人呢?

首先我们要分析清楚的是,会骗人的交互库对于我们询问的回答,能给我们提供什么确定的信息。容易看出,在交互库骗人时,单独一次询问的回答什么信息都不能提供,如果我们只看不相邻的两次询问,也什么信息都不能提供。关键就是在于我们要利用好相邻两次询问最少有一次是真的,来看 相邻的两次询问能带来什么信息。

不过,在分析这个之前,我先介绍一个关键的思想:对于一次 SS 的询问:

  • 交互器回答 Yes:说明本轮交互器告诉我们 xxSS 中。
  • 交互器回答 No:说明本轮交互器告诉我们 xxUS\complement_US 中。

请注意这里对回答 No 的思考方式,我们不要直接思考 xx 不在 SS 中,这样会越思考越乱。正确想法应该是:对于一次 S\boldsymbol S 的询问,无论交互器的回答是什么,它提供我们的信息都是 x\boldsymbol x 在某个集合 S\boldsymbol{S'},只是回答 Yes 代表 S=SS' = S,回答 No 代表 S=USS' = \complement_US。这个想法的改变对于解题非常有帮助,后面大家会深刻感受到这点。

现在来看相邻的两次询问能带来什么信息。假设我们先询问一次,得到了 xSx \in S'(注意,根据上面的思想转变,这里 xSx \in S' 已经同时蕴含了交互器的两种回答),再询问一次,得到了 xTx \in T',我们能得到什么信息?

这两次询问的结果在交互库不骗人时,保证均是真的,我们可以得到 xSx \in S’xTx \in T' 为真,即 xSTx \in S’ \cap T'

而在交互库骗人时,这两条信息的真假并不确定,只保证至少有一个是真的,即:xSx \in S'xTx \in T' 为真。这样我们得到的信息是 xSTx \in S' \cup T'请记住这个结论: 本题中相邻的两次反馈为 xS\boldsymbol{x \in {S'}} xT\boldsymbol{x \in {T'}} 的询问,得到的确定性信息为 xST\boldsymbol{x \in {S' \cup T'}}这个结论尤为重要,是本题根据询问回答推出确定信息的基本方法。

总结一下:通过两次相邻的,反馈分别为 xSx \in S'xTx \in T' 的两次询问,可确定 xSTx \in S' \cup T',将目标范围 UU 缩小到 STS’ \cup T',换句话说,U(ST)=USUT\complement_U(S' \cup T') = \complement_US' \cap \complement_UT' 可以从全集中被删除了。

回想我们的目标,我们期望用这两次相邻的询问,让目标范围降低地尽可能多。

先梳理思路:SS'TT' 是由你的询问和交互器共同决定的,如果你决定做两次 SSTT 的询问,交互器对这两种询问可能有四种不同的回答,而你显然无法事先预知交互器的回答,并且交互库自适应,所以 S=SS' = SS=USS' = \complement_UST=TT' = TT=UTT' = \complement_UT 组成的共四种可能中,你需要按照最坏情况作为“先问 SS,再问 TT”的目标范围缩小效果。然后,再探求什么样的 SSTT 能使得这个目标范围缩小效果达到最高。

还是假设当前目标范围为 UU,连续两次分别询问了 SSTT,那么:

  • 两次均回答 Yes,可以将目标范围进一步锁定到 STS \cup T,也即 U(ST)=USUT\complement_U\left(S \cup T\right) = \complement_U S \cap \complement_U T 可以被删去。
  • 第一次回答 No,第二次回答 Yes,可以将目标范围进一步锁定到 UST\complement_U S \cup T,也即 U(UST)=SUT\complement_U\left(\complement_U S \cup T\right) = S \cap \complement_UT 可以被删去。
  • 第一次回答 Yes,第二次回答 No,可以将目标范围进一步锁定到 SUTS \cup \complement_UT,也即 U(SUT)=UST\complement_U\left(S \cup \complement_UT\right) = \complement_US \cap T 可以被删去。
  • 两次均回答 No,可以将目标范围进一步锁定到 USUT\complement_US \cup \complement_UT,也即 U(USUT)=ST\complement_U\left(\complement_US \cup \complement_UT\right) = S \cap T 可以被删去。

上面的等号用的是德摩根定律化简,读者处理集合问题应当熟练应用。

这里我们应该按照最坏情况讨论,即如果我们钦定询问第一次询问 SS,第二次询问 TT

  • 最坏情况下,目标范围的大小至少会缩小到 max(ST,UST,SUT,USUT)\max\left(\left|S \cup T\right|, \left|\complement_U S \cup T\right|, \left|S \cup \complement_UT\right|, \left|\complement_US \cup \complement_UT\right|\right)
  • 最坏情况下,目标范围的大小至少会减少 min(ST,UST,SUT,USUT)\min\left(\left|S \cap T\right|, \left|\complement_U S \cap T\right|, \left|S \cap \complement_UT\right|, \left|\complement_US \cap \complement_UT\right|\right)

当然,上面两条是等价的,这里我选择第二条处理(选第一条处理也很好做,读者可以自行尝试)。

我们的目标是最大化缩小效果,也即选定 SSTT,最大化 min(ST,UST,SUT,USUT)\min\left(\left|S \cap T\right|, \left|\complement_U S \cap T\right|, \left|S \cap \complement_UT\right|, \left|\complement_US \cap \complement_UT\right|\right)

对于集合问题我们可以考虑绘制韦恩图。

设上面黄色区域代表的集合为 S1S_1,大小为 aa;紫色区域代表的集合为 S2S_2,大小为 bb;棕色区域代表的集合为 S3S_3,大小为 cc;白色区域代表的集合为 S4S_4,大小为 dd

容易发现,S1S_1S2S_2S3S_3S4S_4 互不相交,并集为 UU,即 a+b+c+d=Ua +b +c + d = |U|

观察目标最值的四项:min(ST,UST,SUT,USUT)\min\left(\left|S \cap T\right|, \left|\complement_U S \cap T\right|, \left|S \cap \complement_UT\right|, \left|\complement_US \cap \complement_UT\right|\right)

我们可以发现,S1=SUTS_1 = S \cap \complement_UTS2=USTS_2 = \complement_US \cap TS3=STS_3 = S \cap TS4=USUTS_4 = \complement_US \cap \complement_UT,也即目标最值中的四项恰好和韦恩图中的四个小区域是完全对应的。

那么目标最值其实就是 min(a,b,c,d)\min(a, b, c, d),我们期望它最大。

aabbccdd 作为和为 U|U| 的正整数,它们的最小值最大就是 U4\left\lfloor\dfrac{|U|}4\right\rfloor。构造也比较简单,先令 a=b=c=d=U4a = b = c = d = \left\lfloor\dfrac{|U|}4\right\rfloor,此时 a+b+c+da +b +c +d 距离 U|U| 还剩 U mod4|U| \bmod 4,将 U mod4|U| \bmod 4 随意地加在这四个数中即可。

目标最值已求出,那么我们将 UU 尽可能平均分为四个子集 S1S_1S2S_2S3S_3S4S_4(最小的子集大小为 U4\left\lfloor\dfrac{|U|}4\right\rfloor 即视作完成平均分),然后令 S=S1S3S = S_1 \cup S_3T=S2S3T = S_2 \cup S_3,这样就构造出了一组可以让目标集合 UU 最坏情况下也至少减少 U4\left\lfloor\dfrac{|U|}4\right\rfloor 的询问策略。

回顾我们的构造方案,事实上,我们是将 UU 分成了四个集合,然后选取了一种询问策略,使得无论回答如何,我们总能将四个集合之一扔出目标集合。并且这四个集合中最小的那个也至少有四分之一,使得最坏情况下我们总能将目标集合减少四分之一。

现在我们检验这样的询问策略能否达成目标。写程序判断需要用多少询问才能把目标集合大小缩小到 22 以内(别忘了我们有两次猜测机会):

int main() {
    int n = (int)1e5;
	for (int i = 0; i <= 83; i += 2, n - (n >> 2))
		std :: cout << i << ' ' << n << std :: endl;
	return 0;
}

我们发现用 7676 次询问即可让目标集合的大小降至 33,似乎下降的速度还可以,但后面无论使用多少次询问目标集合大小仍然是 33,想一下为什么。

其实很简单,我们每次能让目标集合减少 U4\left\lfloor\dfrac{|U|}4\right\rfloor,然而 U=3|U| = 3 的时候这个值是 00,也就是压根无法减少。

分析原因,其实是因为 U=3|U| = 3 的时候,不妨设 U={p,q,r}U = \{p, q, r\},我们将其划分成四个子集,发现其中必须有一个是空的。这意味着我们的询问可能有一种回答,对目标集合的变化是:从 UU 里扔掉了一个空集。这是无法化简问题的。

更具体来说,假设我们的划分是 S1={p}S_1 = \{p\}S2={q}S_2 = \{q\}S3={r}S_3 = \{r\}S4=S_4 = \varnothing,问题为 S=S1S3S = S_1 \cup S_3T=S2S3T = S_2 \cup S_3。那么在 SSTT 均回答 Yes 时,我们的推论是目标集合变小到 STS \cup T,也即扔掉了 S4S_4,这相当于什么也没扔。

或者假设划分是 S1={p}S_1 = \{p\}S2={q}S_2 = \{q\}S3=S_3 = \varnothingS4={r}S_4 = \{r\},问题为 S=S1S3S = S_1 \cup S_3T=S2S3T = S_2 \cup S_3。那么在 SSTT 均回答 No 时,我们的推论是目标集合变小到 USUT\complement_US \cap \complement_UT,也即扔掉了 S3S_3,也造成了扔空集。

那么我们只能换一种策略了。既然 7676 次询问就能让目标集合大小减少到 33,我们还有 66 次询问机会让它的大小减少到 22

这里你可以尝试手玩一下(笔者其实手玩花了不少时间)。这里提供一种 33 次询问解决问题的策略:

先问一次 {p}\{p\},如果是 No,再问一次 {p}\{p\},如果还是 No,那么目标集合可缩减到 {q,r}\{q, r\}

否则,上一个状态应该是问了一次 {p}\{p\} 得到了一个 Yes。然后直接问 {q}\{q\} 即可。回答 Yes 目标集合为 {p,q}\{p, q\},否则为 {p,r}\{p, r\}

其实这个策略是可以推广的,上面的 ppqqrr 换成集合,我们就得到了一种使用 33 个问题让目标集合大小减少 U3\left\lfloor\dfrac{|U|}3\right\rfloor 的做法。事实上这个减少速率是比使用 22 个问题让目标集合大小减少 U4\left\lfloor\dfrac{|U|}4\right\rfloor 要慢的,最坏需要 8484 次询问才能让目标集合降低到 22 以内,不过这题好像没卡这个做法。

于是,我们使用了不多于 7979 次询问解决了本题。

#include <bits/stdc++.h>
inline int read() {} // 读入数字的函数,省略
inline std :: string rest() {} // 读入字符串的函数,省略

inline bool query(std :: vector <int> v) {
	printf("? %d ", (int)v.size());
	for (int x : v)
		printf("%d ", x);
	puts("");
	fflush(stdout);
	return (rest() == " YES");
}

inline void solve(std :: vector <int> v) {
	int n = (int)v.size();
	if (n <= 2) {
		for (int x : v) {
			printf("! %d\n", x);
			fflush(stdout);
			if (rest() == " :)")
				exit(0);
		}
	} else if (n == 3) {
		if (!query({v[0]}) && !query({v[0]})) {
			solve({v[1], v[2]});
		} else if (query({v[1]})) {
			solve({v[0], v[1]});
		} else
			solve({v[0], v[2]});
	} else {
		std :: vector <int> S;
		std :: vector <int> T;
		for (int i = 0; i < n; ++i) {
			if (i & 1)
				S.push_back(v[i]);
			if (i & 2)
				T.push_back(v[i]);
		}
		std :: vector <int> nxt;
		int s = query(S) ? 1 : 0, t = query(T) ? 2 : 0;
		for (int i = 0; i < n; ++i)
			if (((i & 1) == s) || ((i & 2) == t))
				nxt.push_back(v[i]);
		solve(nxt);
	}
}

int main() {
	int n = read();
	std :: vector <int> v;
	for (int i = 1; i <= n; ++i)
		v.push_back(i);
	solve(v);
	return 0;
}

E2 Hard Version

在 Hard Version 中,限制参数 Q=53Q = 53

观察我们刚才的询问策略哪里可以优化。我们从全局上查看刚才的询问策略,发现第一次询问和第二次询问是一组;第三次询问和第四次询问是一组;第五次询问和第六次询问是一组。每一组询问,我们将目标范围缩小。

然而这样“相邻两个询问必有一真”这个条件只在一组询问中的两个询问被用到;而上一组的最后一个询问和下一组的第一个询问也是相邻的,这两个询问之间也是有条件的,但我们没用到,造成了条件的浪费。

现在我们考虑怎么让每两个相邻询问的限制都用上。

一般地,假设当前的目标范围为 UU,上一次询问交互库告诉我们 xxTT 中,我们探求这次该怎么问。

很明显,我们问的集合一定是 UU 的子集,并且一部分(可空)在 TT 中,一部分(可空)在 UT\complement_UT 中。这里设 F=UTF = \complement_UT,并且询问集合在 TT 中的部分为 T0T_0,在 FF 中的部分为 F0F_0,即这次问的集合为 T0F0T_0 \cup F_0。并设 T1=TT0T_1 = T \setminus T_0F1=FF0F_1 = F \setminus F_0

如果这次的回答是 Yes,即这次询问交互库告诉我们 xxT0F0T_0 \cup F_0 中,我们可以确认 xxT(T0F0)=TF0T \cup (T_0 \cup F_0) = T \cup F_0 中,因此目标集合 UTF0U \gets T \cup F_0F1F_1 被丢出目标集合。同时,TT0F0T \gets T_0 \cup F_0,以便下一次询问知道上一次询问交互库告知的信息。

如果这次的回答是 No,即这次询问交互库告诉我们 xxT1F1T_1 \cup F_1 中,可以确认 xxT(T1F1)=TF1T \cup (T_1 \cup F_1) = T \cup F_1 中,UTF1U \gets T \cup F_1F0F_0 被丢出目标集合,同时 TT1F1T \gets T_1 \cup F_1

这是一个类似 dp 的转移,考虑设计状态 f(U,T)f(U, T) 表示当前全集为 UU,上一次询问交互库告诉我们 xTx \in T 时,最少的询问次数。这样并无问题,然而 F=UTF = U \setminus T 和其子集的信息在转移中大量用到,UU 反而不怎么用到,如果每次表示 FF 和其子集都要用 UTU \setminus T 来做中间桥梁,不是很直观。

因为 FFUUTT 显然知二求一,不妨设 f(T,F)f(T, F) 表示上一次询问交互库告诉我们 xTx \in T 中,且 F=UTF = U \setminus T 时,最少的询问次数。

这样以来我们还要得知 FF 转移时的变化,简单推理一下:

  • 回答是 Yes,则 UTF0U \gets T \cup F_0TT0F0T \gets T_0 \cup F_0FF 仍应该是新的 UUTT 的差集,并且 TF0T \cup F_0T0F0T_0 \cup F_0 中的并集都是两个不交集合的并,所以 FTT0F \gets T \setminus T_0,即 FT1F \gets T_1
  • 回答是 No,则 UTF1U \gets T \cup F_1TT1F1T \gets T_1 \cup F_1,所以 FTT1F \gets T \setminus T_1。即 FT0F \gets T_0

于是我们可以写出转移方程:

f(T,F)=1+minT0T,F0Fmax(f(T0F0,T1),f(T1F1,T0))f(T, F) = 1 + \min_{T_0 \subseteq T, F_0 \subseteq F} \max(f(T_0 \cup F_0, T_1), f(T_1 \cup F_1, T_0))

这里 min\min 的含义是:在当前 (T,F)(T, F) 这个状态下,我们可以且仅可以在 TT 中选出子集 T0T_0,以及在 FF 中选出子集 F0F_0 来询问。在不同的询问方式中,我们期望一个能让接下来的步数最小的询问方式来询问。

这里 max\max 的含义是:在我们决定问 T0F0T_0 \cup F_0 后,交互库会返回 YesNo 两种情况。交互库返回什么是我们不可预测的,并且因为交互库的自适应性,我们必须按照最坏的一种情况考虑。也即,对于 T0F0T_0 \cup F_0 的询问集合,它的后继询问步数应该取本次询问回答 Yes 和本次询问回答 No 后,两个后继询问步数的最大值。

边界状态是:TF2|T \cup F| \le 2 时,f(T,F)=0f(T, F) = 0

询问的方式是:在当前 (T,F)(T, F) 的状态下,考虑能让这个 min\min 取到最小值的一对 (T0,F0)(T_0, F_0),然后询问 T0F0T_0 \cup F_0 即可。

可以认为在第一次询问之前,交互器告诉了我们 x{1,2,,n}x \in \{1, 2, \ldots, n\} 整个全集,因此初始状态是 ({1,2,,n},)(\{1, 2, \ldots, n\}, \varnothing)

然而这样的状态本身的复杂度就能达到 4n4^n 量级,显然无法承受。观察到,我们的询问策略显然和 TTFF 这两个集合的具体形态没什么关系,只和它们的大小有关。也即,对于 T1=T2|T_1| = |T_2|F1=F2|F_1| = |F_2|,我们有 f(T1,F1)=f(T2,F2)f(T_1, F_1) = f(T_2, F_2)

因此,我们考虑不将集合设入状态,而是将集合的大小设入状态。于是我们可以得到如下转移方程:

f(a,b)=1+min0ca,0dbmax(f(c+d,ac),f(a+bcd,c))f(a, b) = 1 + \min_{0 \le c \le a, 0 \le d \le b}\max(f(c + d, a - c), f(a +b - c - d, c))

边界状态是:a+b2a + b \le 2 时,f(a,b)=0f(a, b) = 0

询问的方式是,在当前 (T,F)(T, F) 的状态下,考虑 a=Ta = |T|b=Fb = |F|,以及能让 f(a,b)f(a, b) 这个 min\min 取到最小值的一对 (c,d)(c, d),然后在 TT 中随便选一个大小为 cc 的子集 T0T_0FF 中随便选一个大小为 dd 的子集 F0F_0,询问 T0F0T_0 \cup F_0 即可。

初始状态是 f(n,0)f(n, 0)

观察转移的顺序并检查转移的无环性:对于任意 f(a,b)f(a, b),为了转移计算它的值用到的 f(a,b)f(a', b') 一定满足 c+da+bc + d \le a + b,因为每次询问后目标全集一定没有变大。为验证,计算一下 f(c+d,ac)f(c+ d, a - c) 中两个维上值的和 a+da + d,以及 f(a+bcd,c)f(a +b - c - d, c) 中两个维上值的和 a+bda + b - d,它们确实不会超过 a+ba + b。所以,我们的转移大体顺序一定是先转移并计算 a+ba + b 小的 f(a,b)f(a, b)

那么对于 a+b=a+ba + b = a' + b'f(a,b)f(a, b)f(a,b)f(a, b') 应该先转移谁?考虑到如果我们花费了一次提问,使得当前的全集 TFT \cup F 没有增加,但是 FF 反而变少了,这样是一定不优的。因为每次我们都是在 FF 中选择一个子集抛出全集,因此我们当然希望 FF 尽可能大,从而使得能选择抛出全集的 FF 的子集尽量大。因此对于 a+b=a+ba + b = a' + b',不妨令 b>bb > b',我们应该令 f(a,b)f(a, b) 去转移 f(a,b)f(a',b')f(a,b)f(a', b') 要比 f(a,b)f(a, b) 后转移到)。

事实上,f(a,b)f(a, b) 若用到 a+b=a+ba' +b' = a + bf(a,b)f(a', b') 转移,意味着 d=0d = 0d=bd = b,也就是我们选择的 F0=F_0 = \varnothingF0=FF_0 = F(即 F1=F_1 = \varnothing),这均会导致计算分别扔掉 F0F_0F1F_1 时至少有一种丢了空集,也即目标集合大小不变。

至于 f(a,b)f(a, b) 转移用到自己的情况,意味着最坏情况下这次询问可能会让 (F,T)(F, T) 毫无变化,显然这样的询问是浪费的,不可取,直接不转移即可(其实不用特殊处理,因为 f(a,b)+1f(a, b) + 1 一定无法作为更小的值更新 f(a,b)f(a, b))。

a+ba + b 升序为第一优先级,bb 降序为第二优先级,这样转移是一定无环的,转移顺序就解决了。

考虑到上面的状态复杂度为 n2n^2,转移时间复杂度为 Θ(n4)\Theta(n^4),显然无法承受。怎么办?

这里有一种启发式的思想,不妨从 E1 尽可能让全集大小降速更快的角度思考转移的过程。

当前的状态是 (T,F)(T, F),每次我们选定 T0F0T_0 \cup F_0,根据上面的讨论,交互库回答 Yes 时,状态变为 (T0F0,T1)(T_0 \cup F_0,T_1)F0F_0 被丢出集合;否则变为 (T1F1,T0)(T_1 \cup F_1, T_0)F1F_1 被丢出集合。我们希望最坏情况下被丢的尽可能多,所以令 min(F0,F1)\min(|F_0|, |F_1|) 取最大,让 F0F_0 均分 FF 比较合适。而且我们还希望最坏情况下,下次的 FF 也能尽量大(这样下一次丢的 F0F_0F1F_1 也能尽量大)所以令 min(T0,T1)\min(|T_0|, |T_1|) 最大,还是让 T0T_0 均分 TT 比较合适。

因此,我们考虑让转移方程中的 ccdd 只在 a2\dfrac a 2b2\dfrac b 2 附近几个整数枚举,这样以来枚举复杂度就会降低成 Θ(n2)\Theta(n^2)。这样以来 f(a,b)f(a, b) 的内容可能不再是最少步数,但经试验,它的内容也是一个相对较少的步数(只不过不是最优解而已)。本题中,我们并不关心最优步数的具体精确值,只需要得到一个足够少步数的方案,所以这样是可行的。

这里我取 c[a12,a2+1]c \in \left[\left\lfloor\dfrac{a - 1}2\right\rfloor, \left\lceil\dfrac{a}{2} \right\rceil + 1\right]d[b12,b2+1]d \in \left[\left\lfloor\dfrac{b - 1}2\right\rfloor, \left\lceil\dfrac b 2 \right\rceil + 1\right]

然而 n2n^2 还是过不了 10510^5。我们不妨再次启发式一下,真的需要用到所有状态吗?如果我们只想知道 f(n,0)f(n, 0) 的值,实际需要的状态数是不是会少于 n2n^2,乃至于可以接受呢?我们可以用记忆化搜索进行尝试。

typedef std :: pair <int, int> pii;
std :: map <pii, int> f;
std :: map <pii, pii> g;

inline int F(int a, int b) {
	if (a + b <= 2)
		return 0;
	if (f.count({a, b}))
		return f[{a, b}];
	int ans = 1000, ansc = -1, ansd = -1;
	for (int c = std :: max(0, (a - 1) >> 1); c <= std :: min(a, (a + 3) >> 1); ++c)
		for (int d = std :: max(0, (b - 1) >> 1); d <= std :: min(b, (b + 3) >> 1); ++d) {
			if ((c + d) + (a - c) == a + b && a - c <= b)
				continue;
			if ((a + b - c - d) + c == a + b && c <= b)
				continue;
            // 上面两行是为了控制 a + b = a' + b' 时的转移顺序
            // 注意 a - c <= b, c <= b 的等号不能省略,这是为了防止 F(a, b) 的求解访问自身导致死递归
			int now = 1 + std :: max(F(c + d, a - c), F(a + b - c - d, c));
			if (now < ans) {
				ans = now;
				ansc = c;
				ansd = d;
			}
		}
	g[{a, b}] = {ansc, ansd};
	return f[{a, b}] = ans;
}

调用 F(1e5, 0) 我们可以发现,这样运行后 ff 的大小为 39473947,这个状态数无疑是十分优秀的。

注意到初始时我们应该调用 F(n,0)F(n, 0) 而非 F(105,0)F(10^5, 0),这是因为 F(105,0)F(10^5, 0) 访问到的所有状态可能不含有 F(n,0)F(n, 0) 所需的状态。另外,F(n,0)F(n, 0) 并不是 nn 越大状态数越多,比如 F(1053,0)F(10^5 - 3, 0) 访问到的状态数为 39533953。然而,nn 越大状态数确实呈现越多的趋势,不可能 nn 变小时状态数突然暴涨,因此我们可以推断 F(n,0)F(n, 0) 的状态数大概就是 40004000 的量级。

而且 f(105,0)f(10^5, 0) 恰好为 5353,恰好满足要求,至此本题解决。

由于本题过于启发式,时间复杂度笔者写不出来。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2023-06-20 19:58:38 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2023-06-20 21:59:36
 */
#include <bits/stdc++.h>
inline int read() {
	int x = 0;
	bool f = true;
	char ch = getchar();
	for (; !isdigit(ch); ch = getchar())
		if (ch == '-')
			f = false;
	for (; isdigit(ch); ch = getchar())
		x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}
inline std :: string rest(bool space = true) {
	std :: string s;
	char ch = getchar();
	for (; !isgraph(ch); ch = getchar());
	for (; isgraph(ch); ch = getchar())
		s.push_back(ch);
	return space ? (" " + s) : s;
}

typedef std :: pair <int, int> pii;
std :: map <pii, int> f;
std :: map <pii, pii> g;

inline int F(int a, int b) {
	if (a + b <= 2)
		return 0;
	if (f.count({a, b}))
		return f[{a, b}];
	int ans = 1000, ansc = -1, ansd = -1;
	for (int c = std :: max(0, (a - 1) >> 1); c <= std :: min(a, (a + 3) >> 1); ++c)
		for (int d = std :: max(0, (b - 1) >> 1); d <= std :: min(b, (b + 3) >> 1); ++d) {
			if ((c + d) + (a - c) == a + b && a - c <= b)
				continue;
			if ((a + b - c - d) + c == a + b && c <= b)
				continue;
			int now = 1 + std :: max(F(c + d, a - c), F(a + b - c - d, c));
			if (now < ans) {
				ans = now;
				ansc = c;
				ansd = d;
			}
		}
	g[{a, b}] = {ansc, ansd};
	return f[{a, b}] = ans;
}

inline bool ask(std :: basic_string <int> S) {
	printf("? %d ", (int)S.size());
	for (int x : S)
		printf("%d ", x);
	puts("");
	fflush(stdout);
	return (rest() == " YES");
}

inline void solve(std :: basic_string <int> T, std :: basic_string <int> F) {
	int a = (int)T.size(), b = (int)F.size();
	if (a + b <= 2) {
		std :: basic_string <int> ans = T + F;
		for (int x : ans) {
			printf("! %d\n", x);
			fflush(stdout);
			if (rest() == " :)")
				exit(0);
		}
	} else {
		auto p = g[{a, b}];
		int c = p.first, d = p.second;
		std :: basic_string <int> T0, T1, F0, F1;
		T0.assign(T.begin(), T.begin() + c);
		T1.assign(T.begin() + c, T.end());
		F0.assign(F.begin(), F.begin() + d);
		F1.assign(F.begin() + d, F.end());

		if (ask(T0 + F0))
			solve(T0 + F0, T1);
		else
			solve(T1 + F1, T0);
	}
}

int main() {
	int n = read();
	F(n, 0);
	std :: basic_string <int> S;
	S.resize(n);
	std :: iota(S.begin(), S.end(), 1);
	solve(S, {});
	return 0;
}
posted @   dbxxx  阅读(70)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2020-06-20 [luogu p3743] kotori的设备
点击右上角即可分享
微信分享提示