动态规划学习之路(1)

今天来分享两道动态规划的题目, 题目都出自于近期各大互联网公司的春招真题,希望和大家一起分享~~

1:来自网易今年的实习生招聘题目,题目题干如下:
易老师购买了一盒饼干,盒子中一共有k块饼干,但是数字k有些数位变得模糊了,看不清楚数字具体是多少了。易老师需要你帮忙把这k块饼干平分给n个小朋友,易老师保证这盒饼干能平分给n个小朋友。现在你需要计算出k有多少种可能的数值
输入描述:
输入包括两行:

第一行为盒子上的数值k,模糊的数位用X表示,长度小于18(可能有多个模糊的数位)
第二行为小朋友的人数n

输出描述:
输出k可能的数值种数,保证至少为1

输入例子:
9999999999999X
3
输出例子:
4

分析:大致看一下这个题,感觉直接暴力做,必然要超时,想想都很可怕,这就体现动态规划的功力了,可是这种题跟平时见过的动态规划还是有一些差别的,听大神说,是类似数位dp的东东。。然而并不是很了解,后来经过大神指点,明白这道题的思路。不管怎么说,用到动态规划了,必然要设置dp数组,如果大家没见过下面这种设置的方式,就当学习一个新的知识吧,确实比较难理解,至少对我来说是这样。
定义一个二维数组dp[i][j], i表示输入数值的长度,明显i取在0到输入数值长度之间的值,比如输入的数是10位,那么i应该取在0到10之间的数; j表示余数,则j应该取在0到n-1之间的数,比如,小盆友的人数是3,则j可以取到0,1,2。
理解了这个二维数组的大致情况,接下来要说dp[i][j]到底表示的是啥,这里我们是这么定义的:dp[i][j]表示长度为i的数 除以小朋友的人数之后得到的余数为j的个数。其实如果数字确定(如果不包含X),则对于确定的i,只有一个dp[i][j]为1,但是由于X位可以从0-9中选择,所以不止有一个,最后返回dp[length][0]就是长度为length,余数为0(整除)的个数。这里length就是数值K的长度。
那么状态转移方程该怎么写呢,举个例子,对于确定的数值K=1244,小朋友个数n=3来说,如果当前的i为走到了3,也就是我们只看124这三位,那么算出余数j为1,也就是说对于这个特殊的例子dp[3][1]=1;那么当i=4的时候,我们现在要看1244了,那我们怎么更新呢,来看1244除以3的过程,其实前面完全就是124除以3,只不过得到过程中的余数1之后,要和最后的4拼接构成14,再和3除,得到最后的余数2,那这时候其实,新的余数是j=2,也就是dp[4][2]+=dp[3][1].
上述情况如果理解了,现在就把最后那个4 换成X,那么这时候,由于X可以从0到9,所以我们得到的新余数应该是newJ=(1*10+X)%3,这样由于X不定,我们可以更新很多值,但是这更新的新余数,都来自这个旧余数,或者依赖于他,也就是说,现在
dp[4][newJ]+=dp[3][1];
这样的话,我们就分情况,当某一位确定,我们是一种做法,某位为X,我们又是一种做法。
下面附上代码,应该可以理解的。

public class DistributeBingGan {
    public static void main(String[] args) {    
        Scanner in = new Scanner(System.in);
        String line = in.nextLine();
        int n = Integer.parseInt(in.nextLine());
        System.out.println(combinationCount(line,n));
    }

    public static long combinationCount(String s,int n){
        int len = s.length();
        long[][] dp = new long[len+1][];
        for(int i = 0; i <= len; i++){
            dp[i] = new long[n];
        }
        dp[0][0] = 1;

        for(int i = 1; i <= len; i++){
            for(int j = 0; j < n; j++){
                if(s.charAt(i-1) == 'X'){
                    for(int k = 0; k <= 9; k++){
                        int newJ = (j*10+k) % n;
                        dp[i][newJ] += dp[i-1][j]; 
                    }
                }
                else
                {
                    int newJ = (j*10+(s.charAt(i-1)-'0'))% n;
                    dp[i][newJ] += dp[i-1][j]; 
                }
            }
        }

        for(int i=0;i<len+1;i++)
        {
            for(int j=0;j<n;j++)
                System.out.print(dp[i][j]+"   ");

           System.out.println();    
            }
        return dp[len][0];


    }
}

2:这道题来自京东今年春招的一道题,题目是这样的:
小明同学要参加一场考试,考试一共有n道题目,小明必须做对至少60%的题目才能通过考试。考试结束后,小明估算出每题做对的概率,p1,p2,…,pn。你能帮他算出他通过考试的概率吗?

输入
输入第一行一个数n(1<=n<=100),表示题目的个数。第二行n个整数,p1,p2,…,pn。表示小明有pi%的概率做对第i题。(0<=pi<=100)

输出
小明通过考试的概率,最后结果四舍五入,保留小数点后五位。

样例输入
4
50 50 50 50

样例输出
0.31250

分析:有人说这也能动态规划? 确实啊,我一开始也是完全没往动态规划上想,就直接暴力枚举了,虽然能得到正确答案,但是直接超时的节奏。至于怎么暴力枚举的,我用的是回溯的思想,其实,在数据规模不是很大的时候,这样的方法还是很好的,并且可以解决一系列问题,具体的回溯套路,可以参见另一篇博客上的讲解[(http://blog.csdn.net/versencoder/article/details/52071930)],学会它,可以解决leetcode上很多道类似的题。我们今天还是来说动态规划。
大家也不要畏难吧,设置dp数组,并且找到状态转移方程确实比较难,也很不好想,所以只能见多识广才可以熟练运用,对于我们从没见过的设置方式或者思考方法,我们应当静下心来学习,以后碰到相关的题目,至少可以联想到,反正我拿到这道题是没什么动态规划的想法的,但是做的多的人,会想到背包问题,因为可以这么看,在背包问题中的当前物品放与不放可以与此处当前的这道考试题做对或者做错相对应,这样来思考,就有点动归的意思在里面了。
在这道题中我们同样设置dp[i][j],其中i表示前i道题,j表示做对的题目数量,这里dp[i][j]表示的就是dp[i][j]在前i道题中做对j道题的概率,接下来再来分析具体的动态转移方程。
首先,考虑一下初始的条件,当i,j为0的时候,我们有dp[0][0]=1,而i=0但是j!=0的时候,明显dp[0][j]=0;那dp[i][j]可以怎么得到呢,假定第i道题做对了那么dp[i][j]=dp[i-1][j-1]pi[i-1]/100; 转移这里pi[i-1]代表的是做对第i道题的概率; 如果第i道题做错了,那么dp[i][j]=dp[i-1][j](100-pi[i-1])/100; 所以dp[i][j]的值应该是这两种情况之和,这样我们就得到了状态转移方程:
这里写图片描述

然后,我们在计算出通过考试至少所要做对的题目数量threshold,那么我们计算的就是从dp[n][threshold]一直加到dp[n][n]的和。这样代码就很容易写出来了:

public class PassTheExam {
    public static void main(String[] args) {
      Scanner sc=new Scanner(System.in);
      int n=sc.nextInt();
      int[] pi=new int[n];
      double[][] dp=new double[n+1][n+1];
      int right=(int) Math.ceil(n*0.6);
      for(int i=0;i<n;i++)
      {
          pi[i]=sc.nextInt();
      }
     dp[0][0]=1.0;
      for(int i=1;i<n+1;i++)
      {
          dp[0][i]=0;
      }
      for(int i=1;i<n+1;i++)
      {  
          for(int j=0;j<n+1;j++)
          {
              if(j==0)
              {
                  dp[i][j]=dp[i-1][j]*(100-pi[i-1])/100;
              }
              else
              {
                  dp[i][j]=dp[i-1][j]*(100-pi[i-1])/100+dp[i-1][j-1]*pi[i-1]/100;

              }

          }

      }
      double sum=0.0;
     for(int i=right;i<=n;i++) 
     {
         sum=sum+dp[n][i];
     }

     DecimalFormat f=new DecimalFormat("0.00000");
     String format=f.format(sum);

      System.out.println(format);
    }

}

希望大家一起学习算法这一块的知识,共同进步!!!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值