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

Posted 英雄哪里出来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言每日一练 —— 第22天:零基础学习动态规划相关的知识,希望对你有一定的参考价值。

文章目录

一、前言

    之前的文章中,我有讲过一些经典的动态规划,例如:最长单调子序列最长公共子序列最小编辑距离背包问题记忆化搜索区间DP数位DP,其中不乏一些较难的内容,不太好理解,所以这篇文章,我会对基础的动态规划再做一个梳理,从最简单的线性DP开始讲起,来谈谈零基础如何一步一步搞清楚动态规划。
    点击文末【阅读原文】可跳转到视频讲解。

二、递推

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

1、斐波那契数列

1)题目描述

    给定一个 n ( 0 ≤ n ≤ 30 ) n (0 \\le n \\le 30) n(0n30),求斐波那契数列的第 n n n 项。

2)算法分析

    斐波那契数列就是一个从 0 0 0 1 1 1 开始,其后每一项都等于前两项的和,就像这样:
F ( 0 ) = 0 F ( 1 ) = 1 F ( n ) = F ( n − 1 ) + F ( n − 2 ) , 其 中 n > 1 F(0) = 0 \\\\ F(1) = 1 \\\\ F(n) = F(n - 1) + F(n - 2),其中 n > 1 F(0)=0F(1)=1F(n)=F(n1)+F(n2)n>1

    拿到这个题目,我们首先来看题目范围, n n n 最多不超过 30,那是因为斐波那契数的增长速度很快,是指数级别的。所以如果 n n n 很大,就会超过 c语言 中32位整型的范围。这是一个最基础的递推题,递推公式都已经告诉你了,我们要做的就是利用一个循环来实现这个递推。

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)题目描述

    给定一个 n ( 1 ≤ n ≤ 45 ) n (1 \\le n \\le 45) n(1n45) 代表总共有 n n n 阶楼梯,一开始在第 0 0 0 阶,每次可以爬 1 1 1 或者 2 2 2 个台阶,问总共有多少种不同的方法可以爬到楼顶。

2)算法分析

    我们定义一个数组 f [ 46 ] f[46] f[46],其中 f [ i ] f[i] f[i] 表示从第 0 0 0 阶爬到第 i i i 阶的方案数。

    由于每次可以爬 1 1 1 或者 2 2 2 个台阶,所以对于第 i i i 阶楼梯来说,所以要么是从第 i − 1 i-1 i1 阶爬过来的,要么是从 i − 2 i-2 i2 阶爬过来的,如图所示:

    于是得出一个递推公式
f [ i ] = f [ i − 1 ] + f [ i − 2 ] f[i] = f[i-1] + f[i-2] f[i]=f[i1]+f[i2]
    我们发现这个就是斐波那契数列,你可以叫它递推公式,也可以叫它状态转移方程。这里的 f [ i ] f[i] f[i] 就是状态的概念,从一个状态到另一个状态就叫状态转移。
    当然我们还要考虑初始状态, f [ 0 ] f[0] f[0] 代表从第 0 0 0 阶到第 0 0 0 阶的方案数,当然就是 1 1 1 啦, f [ 1 ] f[1] f[1] 代表从第 0 0 0 阶到第 1 1 1 阶的方案数,由于只能走 1 1 1 阶,所以方案数也是 1 1 1

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)题目描述

    给定一个 n ( n ≤ 1000 ) n(n \\le 1000) n(n1000),再给定一个 n n n 个整数的数组 c o s t cost cost, 其中 c o s t [ i ] cost[i] cost[i] 是从楼梯第 i i i 个台阶向上爬需要支付的费用。一旦支付此费用,即可选择向上爬一个或者两个台阶。
    可以选择从下标为 0 0 0 或下标为 1 1 1 的台阶开始爬楼梯,请计算并返回达到楼梯顶部的最低花费。

2)算法分析

    我们发现这题和之前的爬楼梯很像,只不过从原来的计算方案数变成了计算最小花费。
    我们尝试用一个数组来表示状态: f [ i ] f[i] f[i] 表示爬到第 i i i 层的最小花费。

由于每次只能爬 1 1 1 个或者 2 2 2 个台阶,所以 f [ i ] f[i] f[i] 这个状态只能从 f [ i − 1 ] f[i-1] f[i1] 或者 f [ i − 2 ] f[i-2] f[i2] 转移过来:
    1)如果从 i − 1 i-1 i1 爬上来,需要的花费就是 f [ i − 1 ] + c o s t [ i − 1 ] f[i-1] + cost[i-1] f[i1]+cost[i1]
    2)如果从 i − 2 i-2 i2 爬上来,需要的花费就是 f [ i − 2 ] + c o s t [ i − 2 ] f[i-2] + cost[i-2] f[i2]+cost[i2]
    没有其他情况了,而我们要 求的是最小花费,所以 f [ i ] f[i] f[i] 就应该是这两者的小者,得出状态转移方程:
f [ i ] = m i n ( f [ i − 1 ] + c o s t [ i − 1 ] , f [ i − 2 ] + c o s t [ i − 2 ] ) f[i] = min(f[i-1] + cost[i-1], f[i-2] + cost[i-2]) f[i]=min(f[i1]+cost[i1],f[i2]+cost[i2])
    然后考虑一下初始情况 f [ 0 ] f[0] f[0] f [ 1 ] f[1] fC语言每日一练——第90天:青蛙跳台阶(升级版)

C语言每日一练——第161天:冒泡排序算法

C语言每日一练——第154天:牛顿迭代法求方程根

C语言每日一练——第105天:杨辉三角形

C语言每日一练——第126天:佩奇借书问题

C语言每日一练——第147天:兔子产子问题