洛谷P8849 『JROI-7』hibernal 二分法题解
题目大意:
交互题,给定 N = 2 或 18 或 64 或 512 或 1000,其中你要通过 19 次以内的询问在数列 1 - N 中找到给定的未知的两个数 x 和 y(本题解中设 x < y),每次询问可以选择任意个数,若这任意个数中含有 x 和 y 中的一个则返回 1,否则返回 2。
10pts:
当 N = 2 时,我们可以轻易地发现 x 和 y 必分别为 1 和 2,直接输出就解决了。
20pts:
当 N = 18时,我们可以从 1 开始询问,每次询问后加上后一个数字,这样当返回值从 0 变为 1 时,则代表加入的数字为 x,当返回值再次从 1 变回 0 时,则代表加入的数字为 y。因为总共仅有 18 个数,故 19 次询问足以解决这种情况。
30pts:
好了,从现在起,前面的乱搞做法算是落下尾声了。正式拿到这道题,首先看到的就是极具特色的数据规模,除却前两个水点和最后一个接近但不完全是的 N = 1000 以外,所有的数据规模均为 2 的次幂。显然,这道题将与 二分 或是 倍增 有很强的联系。然而倍增由于需要再向后重新减去,相当于有一个常数为 4,这基本上排除了倍增的可行性,所以我们将目光射向二分。
显而易见的是,一旦程序得到了 1 的返回值,很快就可以用二分解决掉,几乎可以不做讨论,如果同学们有所疑惑,可以先在这里想一下,并不复杂,在这里不再赘述。
然而同学们只要简单地动手尝试一下平常的二分并采用最坏的不到最后不会出现 1 的情况就会发现,19 次的二分不足以支持这道题哪怕是 30pts 的解决,进而我们要尝试利用这道题目与普通二分的不同点来优化二分。而一个不太明显的特征是,本题的二分并不需要使用平常二分的单调性,所以我们可以把选取的段放在前一次二分的两段的中间,这样就可以利用上一步二分来只用一步变将整段化作四段。语言可能相对难以理解,上图!
图中红色部分为第一次询问,绿色部分为第二次询问。
在第一次询问中,返回值为 0,那么我们知道要么是 ① + ② 中有 x 和 y,要么是 ③ + ④ 中有 x 和 y。
在第二次询问中,返回值为 0,那么我们知道要么是 ① + ④ 中有 x 和 y,要么是 ② + ③ 中有 x 和 y。
综上所述,x 和 y 要么在 ① 中,要么在 ② 中,要么在 ③ 中,要么在 ④ 中,绝不会出现在某两个四分之一块中的情况,而使用一般的二分,达到这一步需要询问三次。
接着,我们在 ① 和 ② 两个块中间再使用一次绿色询问,就可以再把这两块分成四块,以此类推,我们可以节省一半的询问次数,30pts 成功解决。
100pts(笔者直接 30 -> 100 了XD,思路还比较清晰):
此时我们把上文中的线段卷成一个圆来观察一下
这个和上面的图是等效的,其中我们把整个圆通过取或是不取划分成部分,就像切蛋糕一样。然而如果我们接着像上面的思路一样去切一刀,图案是这样的:
不过,我们联想一下切蛋糕,当你的刀是直的,也就是只能取一段连续的数进行询问时,这大概就是最好的切法了。
但如果你能自定义刀的形状呢?很明显,本题没有限制我们的询问必须连续,我们怎么样用一把自定义的刀一刀把已经分成四块的蛋糕切成八块呢?请大家自己大胆的尝试一下,然后接着往下看:
我的想法是这样的:
当然你也可以直接扔个矩形上来啊什么的,总之,我们要做到的是,将原来的 ①,②,③,④,块皆进行二分!
接下来大家应该懂得都懂了,通过这种方式,我们能够在 9 次询问下将整个数列分成 500 个两两配对的块。接下来的事情就只有怎么用剩下的询问次数找出来 x 和 y 的位置了。当我们拿到一个返回 1 的数据时,我们其实需要的是将剩下的几几配对的块中各选取一半,然后就可以进行正常的二分,最多为 9 次询问(即块两两配对,需要从 500 中找 1),找到一个后,另一个一定在他拿到返回值 1 的那个几几配对的块里,再进行二分即可,这个操作需要的次数与前面找输出值 1 时的次数和为 10,很容易证明,不再赘述。
好了,这道题用不着离谱的卡交互机自适应也用不着繁杂的二进制的一种漂亮的二分做法就完成了!
下面贴代码!我的很丑,你要忍一下:
1 #include <bits/stdc++.h> 2 #define ll long long 3 #define ull unsigned long long 4 #define re register 5 6 const int INF = 0x3f3f3f3f; 7 using namespace std; 8 int t,n; 9 int b[2000],bb; 10 int d,e; 11 int main(){ 12 cin>>t; 13 while(t--) 14 { 15 bb=0; 16 cin>>n; 17 if(n==1000) n=1024; 18 int a=n,k; 19 while(1) 20 { 21 k=0; 22 a/=2; 23 for(int q=1;q<=n;q++) 24 { 25 if(q>1000) break; 26 k++; 27 if(q%a==0) q+=a; 28 } 29 cout<<"? "<<k<<" "; 30 for(int q=1;q<=n;q++) 31 { 32 if(q>1000) break; 33 cout<<q<<" "; 34 if(q%a==0) q+=a; 35 } 36 cout<<endl; 37 int x; 38 cin>>x; 39 cout<<flush; 40 if(x==1) 41 { 42 break; 43 } 44 } 45 for(int q=1;q<=n;q++) 46 { 47 if(q>1000) break; 48 b[++bb]=q; 49 if(q%a==0) q+=a; 50 } 51 int l=1,r=bb,mid; 52 while(1) 53 { 54 if(l==r) break; 55 mid=(l+r)>>1; 56 cout<<"? "<<mid-l+1<<" "; 57 for(int q=l;q<=mid;q++) cout<<b[q]<<" "; 58 cout<<endl; 59 int x; 60 cin>>x;cout<<flush; 61 if(x==1) r=mid; 62 else l=mid+1; 63 } 64 d=b[l]; 65 bb=0; 66 for(int q=1;q<=n;q++) 67 { 68 if(q==d) 69 { 70 int v=q; 71 if(v%a==0) v--; 72 while(v%a!=0) v--; 73 v++; 74 for(int w=v;;w++) 75 { 76 if(w+a>1000) break; 77 b[++bb]=w+a; 78 if(w%a==0) break; 79 } 80 } 81 } 82 l=1,r=bb; 83 while(1) 84 { 85 if(l==r) break; 86 mid=(l+r)>>1; 87 cout<<"? "<<mid-l+1<<" "; 88 for(int q=l;q<=mid;q++) cout<<b[q]<<" "; 89 cout<<endl; 90 int x; 91 cin>>x;cout<<flush; 92 if(x==1) r=mid; 93 else l=mid+1; 94 } 95 e=b[l]; 96 if(d>=e) swap(d,e); 97 cout<<"! "<<d<<" "<<e<<endl; 98 } 99 return 0; 100 }
第一次写交互题,术语很可能用的完全不对,希望大家谅解啊qwq。