0
点赞
收藏
分享

微信扫一扫

C语言每日一练 —— 第22天:零基础学习动态规划

文章目录

一、前言

二、递推

    首先让我们来看一下,零基础学习动态规划前必须要看的一道题。

1、斐波那契数列

1)题目描述

2)算法分析

3)源码详解

int fib(int n) {
    int i;                      // (1)
    int f[31] = {0, 1};         // (2)
    for(i = 2; i <= n; ++i) {   // (3)
        f[i] = f[i-1] + f[i-2]; // (4)
    }
    return f[n];                // (5)
}
  • ( 1 ) (1) (1) 首先定义一个循环变量;
  • ( 2 ) (2) (2) 再定义一个数组记录斐波那契数列的第 n n n 项,并且初始化第 0 0 0 项 和 第 1 1 1 项。
  • ( 3 ) (3) (3) 然后一个 for 循环,从第 2 项开始;
  • ( 4 ) (4) (4) 利用递推公式逐步计算每一项的值;
  • ( 5 ) (5) (5) 最后返回第 n n n 项即可。

4)简单复盘

2、爬楼梯

1)题目描述

2)算法分析

3)源码详解

int climbStairs(int n){
    int i;                      // (1)
    int f[46] = {1, 1};         // (2)
    for(i = 2; i <= n; ++i) {   // (3)
        f[i] = f[i-1] + f[i-2]; // (4)
    }
    return f[n];                // (5)
}
  • ( 1 ) (1) (1) 首先定义一个循环变量;
  • ( 2 ) (2) (2) 再定义一个数组 f [ i ] f[i] f[i] 代表从第 0 0 0 阶爬到第 i i i 阶的方案数;
  • ( 3 ) (3) (3) 然后一个 for 循环,从第 2 项开始;
  • ( 4 ) (4) (4) 利用递推公式逐步计算每一项的值;
  • ( 5 ) (5) (5) 最后返回第 n n n 项即可。

4)简单复盘

三、线性DP

    递推也是某种意义上的线性DP,线性DP的最大特征就是状态是用一个一维数组表示的,一般状态转移的时间复杂度为 O ( 1 ) O(1) O(1) 或者 O ( n ) O(n) O(n)
    让我们来看一个线性DP的经典例子来加深理解。

1、使用最小花费爬楼梯

1)题目描述

2)算法分析

3)源码详解

int min(int a, int b) {
    return a < b ? a : b;                   // (1)
}

int minCostClimbingStairs(int* cost, int n){
    int i;                                  // (2)
    int f[1001] = {0, 0};                   // (3)
    for(i = 2; i <= n; ++i) {               // (4)
        f[i] = min(f[i-1] + cost[i-1], f[i-2] + cost[i-2]);
    }
    return f[n];                            // (5)
}
  • ( 1 ) (1) (1) 为了方便求最小值,我们实现一个最小值函数 m i n min min,直接利用 C语言 的 条件运算符 就可以了;
  • ( 2 ) (2) (2) 然后开始动态规划的求解,首先定义一个循环变量;
  • ( 3 ) (3) (3) 再定义一个数组 f [ i ] f[i] f[i] 代表从第 0 0 0 阶爬到第 i i i 阶的最小花费,并且初始化第 0 0 0 项 和 第 1 1 1 项;
  • ( 4 ) (4) (4) 然后一个for循环,从第 2 2 2 项开始,直接套上状态转移方程就能计算每一项的值了;
  • ( 5 ) (5) (5) 最后返回第 n n n 项即可;

4)简单复盘

2、打家劫舍

1)题目描述

2)算法分析

3)源码详解

int max(int a, int b) {
    return a > b ? a : b;            // (1)
}

int rob(int* nums, int n){
    int i;                           // (2)
    int dp[110];
    dp[0] = nums[0];                 // (3)
    for(i = 1; i < n; ++i) {         // (4)
        if(i == 1) {                 // (5)
            dp[1] = max(nums[0], nums[1]);
        }else {                      // (6)
            dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
        }
    }
    return dp[n-1];                  // (7)
}
  • ( 1 ) (1) (1) 首先要求的是最大值,所以我们用条件运算符简单实现一个求最大值的函数;
  • ( 2 ) (2) (2) 然后开始动态规划的求解,首先定义一个循环变量;
  • ( 3 ) (3) (3) 定义一个状态数组 d p [ i ] dp[i] dp[i],初始状态 d p [ 0 ] dp[0] dp[0] n u m s [ 0 ] nums[0] nums[0]
  • ( 4 ) (4) (4) 然后一层循环执行状态转移;
  • ( 5 ) (5) (5) 为了防止调用 d p [ i − 2 ] dp[i-2] dp[i2] 时的数组下标越界,当 i == 1的情况需要特殊处理,也比较简单啦,就是要么取第 0 个要么取第 1 个;
  • ( 6 ) (6) (6)i > 1时直接套用刚才研究出来的状态转移方程就可以啦;
  • ( 7 ) (7) (7) 最后返回 d p [ n − 1 ] dp[n-1] dp[n1] 就是我们要求的答案了;

4)简单复盘

3、删除并获得点数

1)题目描述

2)算法分析

3)源码详解

int max(int a, int b) {
    return a > b ? a : b;
}

int rob(int* nums, int n){
    int i;
    int dp[10010];
    dp[0] = nums[0];
    for(i = 1; i < n; ++i) {
        if(i == 1) {
            dp[1] = max(nums[0], nums[1]);
        }else {
            dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
        }
    }
    return dp[n-1];
}

int deleteAndEarn(int* nums, int n){
    int i;                         // (1)
    int sum[10010], val[10010];    // (2)
    memset(sum, 0, sizeof(sum));   // (3)
    for(i = 0; i < n; ++i) {
        ++sum[ nums[i] ];          // (4)
    }
    for(i = 0; i <= 10000; ++i) {
        val[i] = i * sum[i];       // (5)
    }
    return rob(val, 10001);        // (6)
}
  • ( 1 ) (1) (1) 首先定义一个循环变量;
  • ( 2 ) (2) (2) 然后定义两个辅助数组 s u m [ i ] sum[i] sum[i] v a l [ i ] val[i] val[i], 后面我会解释它们的作用。
  • ( 3 ) (3) (3) s u m sum sum 数组利用 m e m s e t memset memset 归零;
  • ( 4 ) (4) (4) 一层循环将所有数字映射到 s u m sum sum 数组中, s u m [ i ] sum[i] sum[i] 的值代表的是 i i i 在数组中的个数;
  • ( 5 ) (5) (5) 然后填充 v a l [ i ] val[i] val[i] v a l [ i ] val[i] val[i] 的值代表选取 i i i 这个数以后能够获得的点数,当然就是它本身的值乘上它的个数,即 i × s u m [ i ] i \times sum[i] i×sum[i]
  • ( 6 ) (6) (6) 然后直接把 打家劫舍的代码拷过来,修改一下数组范围,直接调用即可;

4)简单复盘

4、最大子数组和

1)题目描述

2)算法分析

3)源码详解

int max(int a, int b) {
    return a > b ? a : b;               // (1)
}

int maxSubArray(int* nums, int n){
    int i;                              // (2)
    int dp[100001];                     // (3)
    int maxValue = nums[0];             // (4)
    dp[0] = nums[0];                    // (5)
    for(i = 1; i < n; ++i) {            // (6)
        dp[i] = max(dp[i-1] + nums[i], nums[i]);
        maxValue = max(maxValue, dp[i]);// (7)
    }
    return maxValue;                    // (8)
}
  • ( 1 ) (1) (1) 定义一个求最大值的函数;
  • ( 2 ) (2) (2) 定义一个循环变量;
  • ( 3 ) (3) (3) 定义一个状态数组 d p [ i ] dp[i] dp[i]
  • ( 4 ) (4) (4) 定义一个我们要返回的最大值,初始化为第一个整数的值;
  • ( 5 ) (5) (5) 计算初始状态 d p [ 0 ] dp[0] dp[0]
  • ( 6 ) (6) (6) 利用一层循环,执行状态转移;
  • ( 7 ) (7) (7) 然后在所有以第 i i i 个整数结尾的子数组中取一个最大值;
  • ( 8 ) (8) (8) 最后返回这个最大值就是我们要求的答案了;

4)简单复盘

    这道题显然和前面的题难度有所增加,可以多看几遍,多想想,利用所有的碎片时间来进行学习,迟早会想出来的,那么好好想想吧,祝你好运!

四、总结复盘

1、状态

    如果你学过编译原理,那么你应该会知道 DFA (有限状态自动机),没错,这里的状态就可以理解成状态机中的状态,即 DFA 上的某个结点。

2、状态转移

    状态转移则对应了 DFA 上的边,即从一个状态到另一个状态,边上也有可能有条件,也就对应了状态转移的条件。

3、时间复杂度

    动态规划的时间复杂度分为两部分:状态计算的时间复杂度,每个状态的状态转移时间复杂度。
    所有状态计算的时间复杂度为 O ( a ) O(a) O(a),单个状态的状态转移时间复杂度为 O ( b ) O(b) O(b),则整个动态规划的求解过程的时间复杂度就是 O ( a b ) O(ab) O(ab)
    线性DP 的状态数就是 O ( n ) O(n) O(n),状态转移的时间复杂度一般为 O ( 1 ) O(1) O(1) 或者 O ( n ) O(n) O(n),也有 O ( l o g 2 n ) O(log_2n) O(log2n) 的,可能利用二分枚举进行状态转移,比如最长单调子序列。

4、线性DP拓展

    常见的线性DP有最长单调子序列、前缀最值、前缀和、背包问题等等。

5、刷题路线

    可以通过公众号 「 夜深人静写算法 」 回复「 题集 」 获取。


举报

相关推荐

0 条评论