2021-02-12 洛谷 P2911 [USACO08OCT]Bovine Bones G 题解

洛谷 P2911 [USACO08OCT]Bovine Bones G 题解(题解区一日游)

Bessie loves board games and role-playing games so she persuaded Farmer John to drive her to the hobby shop where she purchased three dice for rolling. These fair dice have S1, S2, and S3 sides

respectively (2 <= S1 <= 20; 2 <= S2 <= 20; 2 <= S3 <= 40).

Bessie rolls and rolls and rolls trying to figure out which three-dice sum appears most often.

Given the number of sides on each of the three dice, determine which three-dice sum appears most frequently. If more than one sum can appear most frequently, report the smallest such sum.

题目大意:

贝茜喜欢玩棋盘游戏和角色扮演游戏,所以她说服了约翰开车带她去小商店.在那里她买了三个骰子。这三个不同的骰子的面数分别为 s1,s2, s3.

对于一个有 S 个面的骰子每个面上的数字是 1,2,3,…,S。每个面(上的数字)出现的概率均等。贝茜希望找出在所有“三个面上的数字的和”中,哪个和的值出现的概率最大。

现在给出每个骰子的面数,需要求出哪个所有“三个面上的数字的和”出现得最频繁。如果有很多个和出现的概率相同,那么只需要输出最小的那个。

数据范围:
2 <= s1 <= 20, 2 <= s2 <= 20, 2 <= s3 <= 40

输入输出样例:

Sample Input:
3 2 3

Sample Output:
5

说明/提示:

这些是所有可能的结果:

1 1 1 -> 3
1 2 1 -> 4
2 1 1 -> 4
2 2 1 -> 5
3 1 1 -> 5
3 2 1 -> 6
1 1 2 -> 4
1 2 2 -> 5
2 1 2 -> 5
2 2 2 -> 6
3 1 2 -> 6
3 2 2 -> 7
1 1 3 -> 5
1 2 3 -> 6
2 1 3 -> 6
2 2 3 -> 7
3 1 3 -> 7
3 2 3 -> 8

5 和 6 都出现了五次,所以输出较小的 5
水题
没错,这是一道红题,但我今天要为它专门打一篇题解。

难度是不大的,只要理解题意加以一点点的优化,你就可以得到这样的AC代码:

#include<cstdio>
int s1,s2,s3;
int a[16001],maxx=-1,maxs;
int main()
{
    scanf("%d%d%d",&s1,&s2,&s3);
    for(int i=1;i<=s1;i++)
      for(int j=1;j<=s2;j++)
        for(int q=1;q<=s3;q++)
          a[i+j+q]++;
    for(int i=s1*s2*s3-1;i>=3;i--)
    {
        if(a[i]>=maxx)
        {
            maxx=a[i];
            maxs=i;
        }
    }
    printf("%d",maxs);
    return 0;
}

做完了,AC 了, 准备下一道。
好吧,还是得给自己一个交代,不能就这样稀里糊涂的屈服于 O(n3) 算法。太不严谨了。
是的,是可恶的O(n3)的复杂度,众所周知三次方的曲线大概是这样的(大红的色那条):
时间复杂度

  • 大红色是O(n3), 粉色O(n2),蓝色O(n),青色O(log2n),绿色是O(1).

因为红题的数据规模实在是太友好,竟然AC了,但是良心不允许我打出时间复杂度高达O(n3)的代码。因为y = x3 这样一条陡峭的曲线足以把所有程序扼杀在摇篮里。试问但凡这道题的数据规模给的稍微大一些,我还能AC吗?

把红的那条(三次方)变成粉色那条(平方),蓝色那条(一次方),甚至青色那条(对数),这是我们需要在设计每一个程序的时候努力去实现的。

就像这道题一样,虽然做出来了,AC了,而且三次方也是题目引导我去设计的算法,但用三次方的算法总归还是让人不爽

大部分人就到此为止了,但是我还要再进一步,尝试去降它的时间复杂度不试试怎么知道呢?

10分钟过去了,感觉陷入了思维定势,抓狂。
20分钟过去了,想到了能优化一点点的算法(约等于没有优化),依旧抓狂。
30分钟,不想了,准备去看一看神秘的题解区有没有神犇解决了这个问题。

存在即合理,于是我点开了题解(日常膜拜题解区大佬 )。
果然自古题解区才人出,29篇题解都是风格独特,奇招净出。

其中 一位大神的暴力算法

暴力算法

还有稍进一步的 哈希法 或桶排序标记法:

桶排序

还有高深莫测的STL中的map(本人对STL有天然的恐惧):

map

甚至有大牛跳出思维惯势,犀利地指出了题目本身的漏洞

漏洞

但是扯了这么多(不还是都和我一样是O(n3)的复杂度吗 ),前面的都是在O(n3)的框架上在数据结构上做功夫,还没有看见一个能从时间复杂度上创新的。

这时另一篇题解映入眼帘:

一个名为 :P31pr的大佬
(先在此膜拜一下)

大佬

乍一看很正常,但当我继续往下看,发现他和我一样执着于降时间复杂度。
但和我不一样的是,他用一张狂放的稿纸,寥寥数百字就推出了方法,并且成功的,并且完美的把这道题降到了匪夷所思的 O( 1 )!!!!!

大佬的稿纸

一步到位,直接降到O(1)

狂放的字迹掩盖不住精妙的思想

O(1)算法思路讲解

(下面直接转大佬的题解,有删改,有意者可直接点击跳转:原题解 侵删

事实上第一眼看到这个题目,立刻想到了自己小学时在某本数学趣味书上看到的一个喜欢赌博的数学家,他玩过这样一个游戏:掷两个骰子(正方体的那种),两个人分别对两个骰子的点数之和进行猜测,谁猜对了谁赢。而他发现:在这样的规则下,猜7胜率最高。于是他凭着这个发现赢了好多钱。但现在我觉得这个故事很可能是假的() 根据这个发现,很自然地,我们先研究两个骰子的情况。对于上述问题,我们可以列个表来解决:
在这里插入图片描述
可以观察到,7出现的次数最多。再想一想,就可以知道:当掷出两个相同的骰子时,若骰子有 nn 个面,则概率最大的和为 1+n。

接下来看一看当两个骰子面数不一样时会怎样(事实上大部分人应该已经能够得出亿些结论了,但我太菜了,只好继续研究)。仍然采用列表法,我们用面数分别为5和8的骰子试试看:

在这里插入图片描述
根据这个表格,我们可以得出较为一般的结论了 (明明根据上个表格就行了) 。为了更清楚地说明,我给这张表格上了色:
在这里插入图片描述
好了!现在任何人都能毫无障碍地看出其中的规律,不用说大家应该也知道,但我还是啰嗦一下免得过不了审核

若有两个骰子,它们的面数分别为 a,b(a ≤ b),则可以得出以下结论:

  • 不同的点数之和的情况总数为 a+b−1,从小到大具体是 2,3,4,…,a+b;
  • 点数之和出现次数最多的是 1+a,1+a+1,1+a+2,…,1+b,且总共有 b−a+1个出现次数最多的和;
  • 当和为 m 的出现次数不是最多时,设 m 的出现次数为 f(m),则
    在这里插入图片描述
    进行到这里,我们已经把两个骰子的问题研究得较为清楚了。事实上,最重要的结论是第二条,因为题目只要求我们求出和,而并不要求知道这个和出现的次数(或概率)。

接下来,我们加入第三个骰子。

如果现在在原来的表格的基础上直接加入一个骰子,那我们将会得到一个长方体,那样不太利于我们的思考(事实上这个长方体中数字的分布也有着和上文表格类似的规律,具体是怎样的请读者自行想象)。因此我们对这个表格再简化一下。由于我们只关心骰子的点数之和,并不关心这个和是如何得出的,我们可以将两个骰子构成的表格压缩成一条线段。根据上面的第一条结论,这条线段的长度应为 a+b−1(仍然设两个骰子的面数分别为 a,b)。我们在这条线段上从左往右把 2,3,4,…,a+b 都标上,这样我们的压缩就完成了!为了便于说明,我们把这条线段叫做 AB。(这个图大家应该想象得出来,我就不画了) 但这样的表示还是有很明显的缺陷,大家也一定能看出来,那就是这条线段上没有各个和出现次数的信息。没关系,我们可以把它变成一个柱状图:

柱状图
这其实类似于频数分布直方图,但并不真的是。当我们做完这一步工作以后,再加入第三个骰子,事情将会变得容易许多。我们设这个骰子有 c 个面,那么利用刚才的线段 AB,我们又可以画出一个表格,但这个表格上关于和的出现次数的信息是不准确的,因此它其实不太重要。最重要的是接下来的这一步,请让你的脑子转起来

仍然要使用刚才的线段 AB,或者使用上述柱状图的横轴,对于上面的每个数字,我们可以给它加上 1 或 2 或 …或 c,这样就得到三个骰子的点数之和。若是要得到相同的和,我们随意选取线段上的一个点,给这个点所对应的数加 1,给它左边相邻的数加 2,再左边的数加 3……,以此类推。通过这样的方法,我们得到的和都是相等的。事实上我们可以把这样的过程想象成一条长度为 c 的线段 s 在线段 AB 上滑动,线段 s 上的数从右到左是 1,2,…,c,不过线段 s 不一定完全被包含在 AB 内,它也可以部分滑到外面。如果理解了这一步,我们就非常接近最终答案了!由于线段 AB 上每一个数的出现次数并不相同,那么很容易想到,当线段 s 在比较靠中间的位置上时,所得出的相同的和的个数会比较多。更进一步地,要使相同的和的个数最多(这正是题目所求的!),我们应使线段 s 最多地落在 [1+a,1+b] 这个范围内。由此,我们已经可以得出最终答案了!

当线段 s 的长度不足以完全覆盖 [1+a,1+b] 或恰好完全覆盖(即 c ≤ b−a+1)时,为了使和最小(题目要求),我们要尽量使它靠左。具体来讲,我们要使它的左端点与 1 + a 重合,那么答案就是 1 + a + c;

当线段 s 的长度比 [1+a,1+b] 更大(即 c>b−a+1)时,同理仍应使它尽量靠左,但也要保证尽量靠中间,那么此时线段 s 在线段 AB 上应该是这样一种状态:

在这里插入图片描述
当然这里 c − ( b−a+1 ) 除以2不一定是整数,那么根据尽量靠左的原则,可以给左边多分配一点,答案应为 1+ b + ⌊ c − ( b − a + 1 ) ⌋ 2 \frac {⌊ c - ( b - a + 1 ) ⌋} {2} 2c(ba+1) + 1。(这里的 ⌊ ⌋ 是下取整符号,即 ⌊x⌋ 表示小于等于 x 的最大整数)

好啦,我们几乎大功告成啦!为什么是几乎呢,因为这里 a,b,c 的大小关系还不明确(当然你也可以求三次后取最小值),我们回头看看只有两个骰子时的情况:

点数之和出现次数最多的是 1+a,1+a+1,1+a+2,…,1+b,且总共有b−a+1个出现次数最多的和;

要使出现次数最多,那么 b−a 应最大,对于三个骰子的情况同样如此(这里还是需要稍微思考一下的),这样我们很容易得出 b 应取为三个数中最大的数,a 应取为三个数中最小的数,c自然就是中间的那个数。至此,我们已经完全解决了这个问题!

最后的最后是代码

#include<iostream>
#include<math.h>
using namespace std;
void swap(int &x,int &y) 
//swap必须加上&取地址才能真正交换,类型必须写void
{ int t=x; x=y; y=t; }
int main()
{
    int a,b,c;
    cin>>a>>b>>c;
    if(a<b) swap(a,b);
    if(b<c) swap(b,c);
    if(a<b) swap(a,b);
    //冒泡排序
    if(b<=a-c+1)
        cout<<1+b+c;
    else
        cout<<2+a+(b-a+c-1)/2;//int除法自动下取整
    return 0;
}

需要说明的是,这种方法也可以求出和的出现次数(概率),当然式子长得不太好看就是了,大家可以自己想一想哦!(明明想都不用想)

-----------------------------------------------完结大吉------------------------------------------------------
一道题的解法是死的,但是思维和意识是可以锻炼的。百尺竿头,何不更进一步?。我们需要锻炼的就是迎难而上(不怕秃头 )的勇气。当你不再满足于 AC ,当你开始驻足回望,哪怕是红题,也可以让你欲罢不能,因为我相信任何一道题,都有它存在的价值。认真对待每一道练习,把每一次训练当成比赛,于是最后的比赛也就算不得什么了。

Update :2021/2/11 增加了很多细节
Update :2021/2/12 发现了大佬的代码,高兴
Update:2021/2/13 最终稿成功完结

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值