重现赛链接 2019 ACM ICPC Xi'an University of Posts & Telecommunications School Contest
有幸参与2019XUPT-ACM校赛出题和裁判工作。过程还是蛮有意思的。
转载请注明出处和链接。
F-猜球球(3s)
Description
六一到了,为了庆祝这个节日,好多商家都推出了很多好玩的小游戏。Tongtong看到了一个猜球球的游戏,有n种除了颜色之外完全相同的球,商家从中拿出来一个球球放到了箱子里,已知第i种颜色的球出现在箱子里的概率为ai。Tongtong可以用下面这种方法来确定箱子中球的颜色:向商家提出猜测:“是第x种颜色的球球或第y种颜色的球球或...........中的一个”,商家会回答你的猜测是正确还是错误的,直到你有百分百的把握确定箱子里的球球,猜测的次数越少,Tongtong能够得到的礼物就更好。为了让Tongtong过一个开开心心的六一,请你找出一种最优的策略,尽可能少的向店主提出猜测来确定球的颜色,输出猜测次数的期望值。
策略“最优”是指:猜测次数的期望最小。当你有百分百的把握确定箱子里的球球颜色种类时,则不需要继续猜测。例如,如果有两种颜色的球球,箱子里放的是第二种颜色的球球,你可以猜测“是第一种颜色的球球”。商家会告诉你“错误”,所以你可以推测“箱子里的球球是第二种颜色的”,并且有百分百的把握,所以你就可以结束猜测而不需要额外的一次猜测。这种询问方式猜测次数为一次(不管你这一次有没有猜对)。
Input
第一行输入一个n,表示有n种颜色的球。 n<=2 000
第二行输入n个非负小数a1~an,表示是第i种颜色球球的概率,保证加和起来为1。
Output
输出最小期望,保留7位小数。
Sample Input1
3
0.5000000000 0.2500000000 0.2500000000
Sample Output1
1.5000000
Sample Input2
4
0.3 0.3 0.2 0.2
Sample Output2
2.0000000
Hint
对于样例1:
最佳策略下:第一次询问“是不是第二种颜色的球球或第三种颜色的球球中的一种”,如果回答“否”则可以知道是第一种颜色的球球,结束询问;如果回答是“是”则询问第二次“是不是第二种颜色的球球”。
对于样例2:
最佳策略下:第一次询问“是不是第一种颜色或第二种颜色球球中的一个”,如果回答“是”则询问第二次“是不是第一种颜色的球球”,根据回答可以得出球球的颜色;如果第一次询问回答“否”,则第二次询问“是不是第三种颜色的球球”,同理根据第二次回答也可以唯一得出球球的颜色。
本来让我出校赛的题目的话,是不想出一些乱七八糟的数据结构与算法的。像哈夫曼树这样的数据结构基本不会出现在小小的校赛里。但有时候你不得不相信这种叫做“缘分”的东西。
这个题目的想法实际上不是我自己想出来了的。事情的经过是这样的:
寒假里听另外一个学习AI方面技术的同学闲扯机器学习深度学习什么的东西,然后遇到的一个问题就是“猜球球”。那个同学说用神经网络*&%%&*+(省略一堆听不懂的话)什么的来解决这个问题。然后机智的我突然感觉到:这种类型的问题应该是有解的,也就是有一个确定的算法可以准确的得到最优解,而不是靠AI什么的得到近似最优解。
然后我考虑良久,经过跟出题组其他成员讨论,最终这个题就出现在了校赛上。
校赛的时候没人做出来这个题,我想了一下大概有以下原因:
1.思路确实有点绕
2.题面太长太复杂,导致大家一看就头晕
3.前面的题花费了太多时间,轮到了这个题没有时间和精力去做
4.出题者在完全放松没有压力的情况下思考、论证题解都想了几个小时,赛场上可能真的很难。(而且经过出题组的讨论,这个题被定义为【难】的等级,是本次比赛除去压轴题和失效题以外最难的题)
先贴出来讲题的PPT上的解题思路:
因为任意一次询问和回答,都可以确定其中一半的球球集合包含目标球,另一半则不包含目标球。然后再对包含目标球的球球集合进一步划分,直到包含目标球的集合里只包含一个球,就可以百分百确定了。这样就得到了一个决策树(二叉形状),二叉决策树根节点到每个叶子的路到都是期中一种情况的解决方案,显然深度就是询问次数。 则有:期望=∑(询问次数*每种情况出现的概率)=∑(叶子对应的深度*它出现在盒子里的概率)。 而我们知道:这个公式 ∑(深度*元素出现的概率 ) 与某种编码方案的编码长度期望公式 相同。询问次数的期望最小也就是编码长度的期望最小。而解决这个问题的经典方法就是——哈夫曼树
然后我稍稍再解释一遍:
由题意可知,tongtong的目的是确定箱子里的球球的颜色到底是什么。
对于策略问题,我们建立一颗“决策树”。树上每个节点代表当前状态,也就是“候选球”的集合。目的是寻找“目标球”
一开始,集合中的元素是全体球球。表示所有的球都有可能是箱子里的球球(当然要除去出现概率为0的球,这是一个出题者自己都踩进去的坑点)。
然后我们要开始询问。这一次询问会将集合中的元素分成两半,也就是两个子集,假设这两个子集分别为A和B,我们询问的格式等价为:“箱子里的球球是集合A中的一个?”,我们知道,不管回答是“正确”还是“错误”,我们都可以确定“目标球球”到底在哪一半中。(就像你询问“你是不是男生”这种答案是二选一的问题,只要对方是"诚实的"不管对方的答案是什么,你都可以确定正确答案)
也就是说:每次询问都会将当前的集合分成两部分。而回答的“正确”和“错误”会导致两种互斥的状态转移。这也就是决策树的分叉。直到我们得到一个只包含一个元素的集合,就可以百分百确定“目标球”到底是什么了。
现在我们要求的就是怎样询问(怎样决策)可以使平均询问次数最少(期望最小)。
在决策树上,每个叶子都代表一种球球颜色可能的情况,每种情况出现的概率就是球球出现在箱子里的概率,询问次数显然就是叶子所在的深度。
我们通过对比:本题的期望公式 和 哈夫曼编码求某种编码方案的平均编码长度(长度的期望)公式,发现两个公式是完全一样的(同构的),都是
期望 = ∑(深度*元素出现的概率 )
所以我们可以将题目转化为:
每个球球都是一个字符,每个字符出现的使用不同。给出所有字符使用频率,请给出一个前缀码的编码方案,使平均码长最短。
然后学过《数据结构》的大二同学应该知道,最佳编码就是“哈夫曼编码”,求该方案的算法就是构造“哈夫曼树”。
构造哈夫曼树的经典算法的时间复杂度为O(n*n),使用优先队列(堆)优化后只需要O(nlogn)。
为了降低难度,所以比赛设定O(n*n)就可以判对了。
#include
#include
using namespace std;
int const maxn=2e3+5;
double co[maxn];
struct node
{
int i;
double v;
struct node*lc,*rc,*self;
node()
{
lc=rc=NULL;
self=this;
}
};
node mem[maxn<<2];
int m=0;
node *alloc()
{
return &mem[m++];
}
node *lst[maxn];
bool operator<(node a,node b)
{
return a.vlc==NULL && rt->rc==NULL)
return d*rt->v;
double ret=0;
ret+=dfs(rt->lc,d+1);
ret+=dfs(rt->rc,d+1);
return ret;
}
int main()
{
double sum=0;
int n;
cin>>n;
double t;
for(int i=1;i<=n;i++)
{
cin>>t;
if(t==0)
{
n--;
i--;
continue;
}
sum+=t;
lst[i]=alloc();
lst[i]->v=t;
lst[i]->i=i;
}
priority_queueque;
while(que.empty()==0)
que.pop();
for(int i=0;i2)
que.pop();
}
}
node a=que.top();
que.pop();
node b=que.top();
que.pop();
node *rt=alloc();
rt->i=a.i;
rt->v=a.v+b.v;
rt->lc=a.self;
rt->rc=b.self;
lst[b.i]=NULL;
lst[a.i]=rt;
}
int root;
for(root=1;root<=n;root++)
if(lst[root])
break;
double ans=dfs(lst[root],0);
printf("%.7f\n",ans);
}