zoukankan      html  css  js  c++  java
  • 剑指Offer解题报告(Java版)——n个骰子的点数 43

    问题

       

    n个骰子朝上的数之和为s,求s的所有可能以及概率

       

    分析问题

       

    如果是用笨方法,一般人最开始都会想到笨方法,那就是枚举法

       

    举个例子,比如两个骰子,第一个骰子的结果为1,2,3,4,5,6,两个骰子的结果是2,3,4,5,6,7;3,4,5,6,7,8;4,5,6,7,8,9;……7,8,9,10,11,12,共三十六种,用n平方size的数组记录这36个结果

       

    仔细分析可以发现其实这其中有很多是重复的,所以去除重复,考虑最小的应该是2,也就是n,最大的应该是12,也就是6n

       

    所以所有的结果应该只有6n-n+1=5n+1种,如果我们开辟一个最大index=6n的数组也就是size为6n+1的数组就可以放下这所有的结果,但是其中index为0-(n-1)的位置上没有数放,这里我们有两种解决方案,一种是就让它空着,这样的好处是,结果为s的就可以直接放在index为s的位上,不过如果我们想节省这部分的空间,可以将所有数据往前移一下,也就是把和为s的放在s-n上即可,这样我们就只需要size为5n+1的数组

       

    所以我们再声明一个结果数组,5n+1大小,通过遍历前面的n平方大小的数组,出现和为s就在5n+1大小的s-n位上加1即可

       

    这样的方式,时间复杂度为n平方,可见并不理想,我们可以降低时间复杂度

       

    首先想到是否能退化问题,比如n个骰子与n-1个骰子之间的关系,比如n个骰子的结果是n-1个骰子的结果分别加上1-6而得,于是n-1个骰子的结果又是n-2个骰子的结果分别再加上1-6所得

       

    但递归的方法并不是很好,很多重复计算,重复计算的问题可以考虑斐波拉契计算过程,我们最后提出一种以空间换时间的方法,也是传统的记录中间结果的方法,根斐波拉契的优化很像,将某些中间结果存起来以减少递归过程的重复计算问题

       

    解决问题

       

    主体在如何计算次数,将次数存到数组中,由于要用到递归,我们最好单独写一个base函数,这是我的经验,base函数中的参数要包括递归的时候要用到的那些变量,比如总的n,现在的n,以及现在的sum,以及贯穿始终的次数数组

       

    static void baseProbabilities(int numTotal,int numCur,int sum,int[] probabilities){

    if (numCur==1) {

    probabilities[sum-numTotal]++;

    }else {

    for (int i = 1; i <=g_maxValue ; i++) {

    baseProbabilities(numTotal, numCur-1, sum+i, probabilities);

    }

    }

    }

    而计算次数的时候就是去调用这个base的函数

       

    for (int i = 1; i <=g_maxValue; i++) {

    baseProbabilities(number, number, i, probabilities);

    }

       

    考虑n=2时的递归过程,首先nT=2,nC=2,sum=1,表明第一个骰子甩出一个1,由于nC=2表明现在有两个骰子,所以进入else部分,i又从1到6循环,表明这是进入到第二个骰子在甩了,首先i为1,表明又甩出一个1,这时候nC=1,就将2-n的位置上加1,表明结果为2的次数加1,然后退到上一层,i++,此时还是第二个骰子在甩,甩出一个2,此时sum=3,nC=1,所以在和为3的位置上加1,一直这样,到了和为7的位置上加1的时候,会退到在上一次循环,这时候表明第一个骰子甩出了一个2,此时进入第二个骰子,依次会出现和为3,4,5,6,7,8的结果,然后再在相应位置上加1即可

       

    优化方法

       

    我们需要将中间值存起来以减少递归过程中的重复计算问题,可以考虑我们用两个数组ABAB之上得到,B又在A之上再次得到,这样AB互相作为对方的中间值,其实这个思想跟斐波拉契迭代算法中用中间变量保存n-1,n-2的值有异曲同工之妙

       

    我们用一个flag来实现数组AB的轮换,由于要轮转,我们最好声明一个二维数组,这样的话,如果flag=0时,1-flag用的就是数组1,如果flag=1时,1-flag用的就是数组0,

       

    int[][] probabilities=new int[2][];

    probabilities[0]=new int[g_maxValue*number+1];

    probabilities[1]=new int[g_maxValue*number+1];

       

    我们以probabilities[0]作为初始的数组,那么我们对这个数组进行初始化是要将1-6都赋值为1,说明第一个骰子投完的结果存到了probabilities[0]

       

    然后就是第二个骰子,第二个骰子的结果存到probabilities[1],是以probabilities[0]为基础的,此时和为s的次数就是把probabilities[0]中和为s-1,s-2,s-3,s-4,s-5,s-6的次数加起来即可

       

    int temp=0;

    for(int j=1;j<=i && j<=g_maxValue;++j)

    temp+=probabilities[flag][i-j];

    probabilities[1-flag][i]=temp;

       

    而第k次用k个骰子那么要更新的结果范围就是k到maxValue*k

       

    所以连起来就是

       

    for(int i=k;i<=g_maxValue*k;++i)

    {

    int temp=0;

    for(int j=1;j<=i && j<=g_maxValue;++j)

    temp+=probabilities[flag][i-j];

    probabilities[1-flag][i]=temp;

    }

       

    然后就需要把probabilities[1]作为中间值数组,这里我们把flag赋值为1-flag即可,是不是很神奇!

       

    flag=1-flag;

  • 相关阅读:
    PAT Advanced 1067 Sort with Swap(0, i) (25分)
    PAT Advanced 1048 Find Coins (25分)
    PAT Advanced 1060 Are They Equal (25分)
    PAT Advanced 1088 Rational Arithmetic (20分)
    PAT Advanced 1032 Sharing (25分)
    Linux的at命令
    Sublime Text3使用指南
    IntelliJ IDEA创建第一个Groovy工程
    Sublime Text3 安装ftp插件
    Sublime Text3配置Groovy运行环境
  • 原文地址:https://www.cnblogs.com/keedor/p/4474471.html
Copyright © 2011-2022 走看看