【牛客】动态规划入门

1. 斐波那契数列

这似乎是一个老生常谈的题目

在古老的年代,天地间诞生了一个会自己增长的数列。大地赐予它生命,告诉它1为万物之始。天空则传授了它数列增长之道,告诉它永远以前一刻的自己为基石,方可不断超越自我,实现数列之永恒。于是这个数列就这样开始了它艰辛的增长之路,1,1,2,3,5,8,13,21,34 ... 功夫不负有心人,终于有一天,一个数学家发现了它,将它命名为Fibonacci数列,并将这个数列的传奇故事介绍给了世人。

所以,在座的程序员们,第n个数字是几呢,请写出你的代码 -_-

当我看到这个题目,第一反应是,就这?一个递归不就搞定了

public:
    int Fibonacci(int n) {
         
        if(n==1||n==2){
            return 1;
        }else{
            return Fibonacci(n-1)+Fibonacci(n-2);
        }
    }
};

在牛客上一运行,确实通过了所有测试用例,但是运行时间也很吓人,237ms !好家伙 !

这时我想起来我貌似选的是动态规划练习题...好吧,我不会动态规划啊,于是首先去百度了下动态规划。再回到这个问题上来,上面这种简单递归的写法,有两个问题:

1)存在太多的重复计算,比如n=5,f(5)=f(4)+f(3),显然这里要算一次f(3),然后f(4)=f(3)+f(2),这里又要算一次f(3),而类似的重复计算还有很多。n越大,整个递归树就越大,重复计算就越多。

2)会出现递归的栈溢出,因为函数的调用是通过栈实现的。

这里加一嘴,请原谅我的偏题:如果编程语言对尾递归有优化,我们将递归写成尾递归可以避免栈溢出。什么是尾递归呢,就是在函数返回的时候,调用函数本身,并且return语句不能包含表达式。所以...很遗憾的是java和python语言都未对尾递归做优化。但是python的话可以使用@tail_call_optimized这个装饰器实现尾递归优化。另外再看看c++,我们只需在gcc后面加上-O2参数,就可以实现尾递归优化。

那么如何使用动态规划计算斐波那契数列呢,这是我的代码

 public int Fibonacci(int n) {
        if(n==1||n==2)
            return 1;
        int dp[] = new int[n];
        dp[0] = 1;//dp[i],i=n-1
        dp[1] = 1;
        for(int i=2;i<n;i++){
            dp[i] = dp[i-1]+dp[i-2];
        }
        return dp[n-1];
 }

大致就是,之前用递归,是一个自顶向下的过程,比如n=5,我们是从5到1去推进。而这里是一个自底向上的过程,先计算n=1,n=2 ... 保存到数组里,然后自然而然地算出n=5的情况,很显然这样有效避免了重复计算,也不需要使用递归。

 2. 跳台阶

再做一个简单的动态规划题巩固一下

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。1<=n<40

以下是简单的java解法

public int jumpFloor(int target) {

      int[] dp = new int[target+1];
      dp[0]=1;
      dp[1]=2;
      for(int i=2;i<target;i++){
          dp[i]=dp[i-2]+dp[i-1];
      }
      return dp[target-1];
}

3. 换钱币

这个题目和跳台阶非常相似,一起做为简单入门题了

给定数组arr,arr中所有的值都为正整数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个aim,代表要找的钱数,求组成aim的最少货币数。
如果无解,请返回-1
这个代码我当时是抄的答案,理解后加上了我的注释
 public int coinChange(int[] coins, int amount) {
        int max = amount + 1;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, max);
        dp[0] = 0;//0元无法组合,返回0个硬币
        System.out.println(Arrays.toString(dp));
        //外循环从金额为1开始,自底向上,一直到需要的金额
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                //假设已知dp[i]的最优组合,组合中最后一枚硬币面值为coins[j]
                //面值大于总金额的硬币不可能是组合成员之一,用条件语句过滤掉
                //dp[i]可以通过dp[i-x]+1计算出
                //但是不清楚x为哪个面值,所以通过该内循环试验出
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        System.out.println(Arrays.toString(dp));
        return dp[amount] > amount ? -1 : dp[amount];
    }

好了,动态规划入门到此为止。以后有时间再继续做牛客上那几个动态规划中等难度的题吧。

posted @ 2022-03-14 23:41  方山客  阅读(273)  评论(0编辑  收藏  举报