Leetcode70——爬楼梯(斐波那契类型)(C语言)(通过该问题讲解动态规划基本思想)

本文详细介绍了动态规划的基本思想,包括定义状态、状态转移方程、边界条件等,并通过爬楼梯问题(如Fibonacci数列)实例演示了递归、记忆化递归和动态规划的解法,强调了动态规划在处理重叠子问题上的优势和空间换时间策略。
摘要由CSDN通过智能技术生成

目录

原题链接:        

动态规划的基本思想:

爬楼梯题目讲解:

解法1(递归):

解法2(记忆化递归):

解法3(Fibonacci数列的动态规划算法):

总结:


原题链接:        

原题链接:70. 爬楼梯 - 力扣(LeetCode)


动态规划的基本思想:

        动态规划(Dynamic Programming,简称DP)是一种算法设计技术,它通过将复杂问题分解为更小的子问题来解决优化问题。动态规划通常用于解决具有重叠子问题和最优子结构特性的问题。
        动态规划的基本思路可以概括为以下几个步骤:
                1. 定义状态:首先,需要定义问题中的状态。状态是指在特定步骤或阶段的问题的某种描述。通常,状态可以用一个或多个变量来表示,并且状态之间存在依赖关系。
                2. 状态转移方程:其次,确定状态转移方程,也称为递推关系。这个方程描述了状态如何从之前的某个状态或多个状态转移而来。在形式上,这个方程通常表示为 `dp[i] = f(dp[i-1], dp[i-2], ...)`,其中 `dp[i]` 是到达第 `i` 个状态的方法数或成本,而 `f` 是一个关于前状态的函数。
                3. 边界条件:确定初始状态或边界条件。这些是问题的基础情况,通常可以直接计算得到,或者由问题的性质直接给出。
                4. 计算顺序:确定状态的计算顺序。通常,状态的计算顺序应该能够充分利用之前计算的结果,避免重复计算。
                5. 构建DP表:根据状态转移方程和边界条件,构建一个DP表(或数组),并按顺序填充表中的每个状态。
                6. 输出结果:最后,根据DP表中的最终状态输出结果。


        动态规划的关键优点是它能够显著减少计算复杂度,将指数级问题转化为多项式级问题。这主要是因为动态规划避免了重复计算相同的状态,而是利用已计算的状态结果。


        动态规划适用于具有最优子结构特性的问题,即一个问题的最优解包含了其子问题的最优解。此外,动态规划也适用于具有重叠子问题的问题,即在求解问题的过程中,相同的子问题会被多次计算。通过存储这些子问题的解,动态规划可以有效地减少计算量。


爬楼梯题目讲解:

        Fibonacci(斐波那契)数列问题,它是一个简单而典型的分治问题,Fibonacci数列的递归方程表示为:

        F(n) = F(n-1) + F(n-2);

        其中,F(n) 表示数列的第n项,F(n-1) 表示数列的第n-1项,F(n-2) 表示数列的第n-2项。

        注意,这个递归方程是从第三项开始的,因为前两项有明确的定义:

        F(0) = 0 ;

        F(1) = 1。

解法1(递归):

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
int climbStairs(int n) {
	if (n < 0) return 0;
	if (n == 1) return 1;
	if (n == 2) return 2;
	return climbStairs(n - 1) + climbStairs(n - 2);
}
int main() {
	int n;
	printf("输入台阶数>");
	scanf("%d", &n);
	printf("有%d种方法可以爬到楼顶。", climbStairs(n));
	return 0;
}

        该程序实现简单,但非常低效。以求解该题目不难发现,在递归调用过程中,每次产生的子问题并不是新问题,有些子问题被反复计算多次,比如climbStairs(3)调用了两次,climbStairs(2)调用了三次,这种现象被称为 重叠子问题。

        当输入参数n较大时,其重复计算的子问题数目将爆照式增长,最终需要计算的子问题个数(或者递归调用次数)将增长为指数级。



那么该如何减少实际求解的子问题的数目呢?

        如果用一个数组保存已解决的子问题的答案,而在需要时再从该数组中查找已经求解的子问题的答案,这样就可以避免大量的重复计算,从而得到多项式复杂性算法。这就是动态规划的基本思路。


        记忆化(缓存):由于递归会多次计算相同的子问题,这会导致大量的重复计算,特别是在 n 较大时。为了避免重复计算,我们可以使用一个数组 arr 来存储已经计算过的结果。如果 arr[n] 已经被计算过,就直接返回它的值,否则就计算它并存储起来。

解法2(记忆化递归):

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>

int arr[10];

int climbStairs(int n) {
	int tmp;
	if (n < 0) {
		return 0;
	}
	else if (arr[n] != 0) {
		return arr[n];
	}
	else {
		tmp = climbStairs(n - 1) + climbStairs(n - 2);
		return tmp;
	}
}

int main() {
	int n, i;
	for (i = 0; i < 50; i++) {
		arr[i] = 0;
	}
	arr[1] = 1;
	arr[2] = 2;
	printf("输入台阶数>");
	scanf("%d", &n);
	printf("有%d种方法可以爬到楼顶。", climbStairs(n));
	return 0;
}

解法3(Fibonacci数列的动态规划算法):

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <malloc.h>

// 动态规划解决爬楼梯问题
int climbStairs(int n) {
    if (n <= 2) {
        return n;
    }

    int* dp = (int*)malloc((n + 1) * sizeof(int));
    dp[1] = 1;
    dp[2] = 2;

    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    int result = dp[n];
    free(dp); // 释放分配的内存
    return result;
}

int main() {
    int n;
    printf("输入台阶数: ");
    scanf("%d", &n);
    printf("有 %d 种方法可以爬到楼顶。\n", climbStairs(n));
    return 0;
}

        该代码基本思路是:要到达第 `n` 阶,你可以从第 `n-1` 阶爬 1 阶上来,或者从第 `n-2` 阶爬 2 阶上来。因此,到达第 `n` 阶的方法数等于到达第 `n-1` 阶和第 `n-2` 阶方法数的和。

动态规划的步骤如下:
        1. 初始化:首先,我们需要定义一个数组 `dp` 来存储到达每一阶楼梯的方法数。由于到达第一阶和第二阶的方法数是固定的,我们可以直接初始化 `dp[1] = 1` 和 `dp[2] = 2`。
        2. 递推计算:然后,我们从第三阶开始,逐个计算到达每一阶的方法数。对于每一阶 `i`(`i` 从 3 开始到 `n`),它的方法数 `dp[i]` 等于前两阶方法数的和,即 `dp[i] = dp[i-1] + dp[i-2]`。
        3. 输出结果:最后,当计算完所有阶数后,`dp[n]` 就是到达楼顶的方法数。我们将这个值输出即可。
        在代码中,我们使用了一个循环来逐阶计算方法数,并在循环结束后返回 `dp[n]` 的值。在主函数中,我们接收用户输入的楼梯阶数 `n`,并调用 `climbStairs` 函数来计算并输出结果。
        注意,我们使用了 `malloc` 来动态分配内存,这是因为我们在编译时不知道 `n` 的值,因此不能使用静态数组。在使用完动态分配的内存后,我们使用 `free` 函数来释放内存,这是一个良好的编程习惯,可以防止内存泄露。


总结:

        Fibonacci数列的例子可以得到,动态规划的关键在于解决重叠子问题的重复计算,将原来指数级复杂度的分治算法改进多项式级的计算。

        在实现过程中,动态规划算法需要存储各子问题的解,所以它的空间复杂度大于其他算法,这是一种空间换时间的策略。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值