Loading [MathJax]/jax/element/mml/optable/BasicLatin.js

【做题】arc070_f-HonestOrUnkind——交互+巧妙思维

做的第一道交互题……
首先,有解的一个必要条件是a>b。否则,即当a<=b时,可以有a个unkind的人假装自己就是那a个honest的人。(彼此之间都说是honest的,说别人都是unkind的)
那么,现在我们考虑处理a>b的情况。

标算

一个显然的思路就是在n+1次询问内确定一个人是honest的。这样可以再花n1询问确定答案。
注意到一个性质:

pq是unkind的,那么pq不可能都是honest的。

我们发现,如果直接删除pq减小问题规模,那么新问题仍然满足a>b,也一定存在honest的人。
那么,我们不禁想知道,对于一个有k个元素的序列a1a2a3...ak,怎样的大概k回答均为Y的询问能让我们确信某个ai一定是honest的?
宛如是一只小鸟(题解)把答案告诉了我们,我们发现

i,1<=i<k, aiai+1是honest的
ak是honest的。

这是因为若ai是honest的,那么ai+1也是honest的。并且,这里至少有一个人是honest的。那么,我们就可以以如下算法生成a1a2a3...ak

维护一个栈。枚举每一个人:

  • 如何当前栈为空,将其加入栈。
  • 如果栈顶说它是honest的,将其加入栈。
  • 如果栈顶说它是unkind的,放弃它且取出栈顶。

时间复杂度O(n)

Copy
#include <bits/stdc++.h> #define gc() getchar() using namespace std; const int N = 4010; int sta[N],top,a,b,ans[N],t,n; bool ask(int x,int y) { static char tmp; printf("? %d %d\n",x,y); fflush(stdout); for (tmp = gc() ; tmp != 'Y' && tmp != 'N' ; tmp = gc()); return (tmp == 'Y'); } int main() { scanf("%d%d",&a,&b); if (a <= b) return 0 * puts("Impossible\n"); n = a + b; for (int i = 0 ; i < n ; ++ i) { if (!top) sta[++top] = i; else { if (ask(sta[top],i)) sta[++top] = i; else top--; } } t = sta[top]; for (int i = 0 ; i < n ; ++ i) ans[i] = ask(t,i); printf("! "); for (int i = 0 ; i < n ; ++ i) printf("%d",ans[i]); puts(""); return 0; }

顺便一提,还有个高妙的做法。 #### 高妙做法 这个做法基于一个结论:

pq互相说对方是honest的,那么它们的状态相同。

状态也就是指unkind或honest。显然,这些信息可以用并查集维护。
注意到询问上限2n=k>=0n2k,故考虑不断把问题个规模缩小一倍。
下面,直接给出算法及代码:

考虑现在有k个元素集合,每个集合保证其中的元素状态相同。我们把它们每两个分成一组,丢弃多余出来的一个。对于每一组,花两次询问判断这两个集合是否一定状态相同,是则将它们合并;否则丢弃它们。
对新产生的集合再递归进行这个操作。
最后,令t为最后一次在分组时因多余而被丢弃的集合。可以证明,t是honest的。我们向t询问上面所有被丢弃的集合,得出答案。

Copy
#include <bits/stdc++.h> #define gc() getchar() using namespace std; const int N = 4010; int a,b,n,t,ans[N],q[N],tmp[N],cnt,tcnt,fa[N]; vector<int> threw; int get_fa(int x) { return x == fa[x] ? x : fa[x] = get_fa(fa[x]); } bool ask(int x,int y) { static char tmp; printf("? %d %d\n",x,y); fflush(stdout); for (tmp = gc() ; tmp != 'Y' && tmp != 'N' ; tmp = gc()); return (tmp == 'Y'); } void solve() { if (cnt == 0) return; tcnt = 0; for (int i = 0 ; i+1 < cnt ; i += 2) { if (ask(q[i],q[i+1]) == 1 && ask(q[i+1],q[i]) == 1) { fa[q[i+1]] = q[i]; tmp[tcnt++] = q[i]; } else threw.push_back(q[i]), threw.push_back(q[i+1]); } if (cnt&1) t = q[cnt-1], threw.push_back(q[cnt-1]); for (int i = 0 ; i < tcnt ; ++ i) q[i] = tmp[i]; cnt = tcnt; solve(); } int main() { scanf("%d%d",&a,&b); if (a <= b) return 0 * printf("Impossible\n"); n = a + b; for (int i = 0 ; i < n ; ++ i) q[i] = i, fa[i] = i; cnt = n; solve(); for (int i = 0 ; i < n ; ++ i) get_fa(i); for (int i = 0 ; i < (int)threw.size() ; ++ i) { if (threw[i] == t) continue; ans[threw[i]] = ask(t,threw[i]); } ans[t] = 1; for (int i = 0 ; i < n ; ++ i) ans[i] = ans[fa[i]]; printf("! "); for (int i = 0 ; i < n ; ++ i) printf("%d",ans[i]); puts(""); return 0; }

接下来证明一下为什么这个算法是正确的。

首先证明总询问次数是不多于2n的。
我们考虑递归中的第d+1层,此时最多有n2d个元素。那么,单是在这一层的处理中每一个元素平均下来的对询问次数的贡献是12d。我们记ad表示某一个元素从第d+1层起所能产生的最大贡献。下面考虑两种情况:

  • 这个元素与另一个元素合并,为下一层递归添加了一个元素,也为最后的确定答案产生了12d+1的贡献。那么,有ad=12d+ad+112d+1
  • 这个元素未能与另一个元素合并。那么,有ad=12d

故我们得出

ad={max

稍微YY一下我们就得到,当0 <= d < \log_2n时,a_d = \frac {1} {2^d}
那么,总询问次数不多于n \times a_0 + n = 2n

然后,证明为什么t就是honest的。
首先声明,在递归过程中,可能会出现a=b。要出现这情况,一个必要条件是在分组时丢弃了一个honest的集合。当然,容易想到不会再出现有a<b的情况。
我们讨论递归的最后一层,集合个数可能是1或2。

  • 集合个数为1。那么,t将成为这个集合。因为a>=b,所以这个集合一定是honest的。
  • 集合个数为2。此时,a=b=1。考虑前面最后一层导致了a=b的递归。在那里,因分组而丢弃了一个honest的集合S。那之后的每一层递归中,一定有a=b(否则那就不是最后一层导致a=b的递归),则集合个数为偶数,不会再因分组而丢弃集合。因此,t就是S,是honest的。

现在,我们就可以确信,这个算法没问题了。
时间复杂度O(n \alpha (n))


小结:感觉根本想不出那么巧妙的东西。或许是思维能力还不够吧。
posted @   莫名其妙的aaa  阅读(274)  评论(0编辑  收藏  举报
编辑推荐:
· .NET 9 new features-C#13新的锁类型和语义
· Linux系统下SQL Server数据库镜像配置全流程详解
· 现代计算机视觉入门之:什么是视频
· 你所不知道的 C/C++ 宏知识
· 聊一聊 操作系统蓝屏 c0000102 的故障分析
阅读排行:
· 不到万不得已,千万不要去外包
· C# WebAPI 插件热插拔(持续更新中)
· 会议真的有必要吗?我们产品开发9年了,但从来没开过会
· 如何打造一个高并发系统?
· 【译】我们最喜欢的2024年的 Visual Studio 新功能
点击右上角即可分享
微信分享提示