【动态规划算法】(斐波那契数列模型详解)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法学习的过程中,动态规划始终是一个绕不开的重要主题.它不仅是解决复杂问题的高效工具,也是面试和竞赛中出现频率极高的核心考点.对于初学者而言,动态规划之所以难,往往不在于代码实现本身,而在于如何理解"状态""转移"和"最优子结构"这些抽象概念.而斐波那契数列,正是理解动态规划思想最经典、最基础的入门模型.表面上看,斐波那契数列只是一个简单的递推问题:后一个数等于前两个数之和.但如果从算法设计的角度深入分析,就会发现其中蕴含着动态规划的核心思想------将原问题拆解为规模更小的子问题,并通过保存中间结果来避免重复计算,从而显著提升效率.也正因为如此,斐波那契数列常被视为学习动态规划的第一把钥匙.本文将围绕斐波那契数列模型展开,从暴力递归入手,逐步分析其时间复杂度问题,再引出记忆化搜索和自底向上的动态规划写法,帮助读者循序渐进地理解动态规划的本质与实现方式,为后续学习更复杂的动态规划问题打下坚实基础.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.动态规划算法思想背景

动态规划(Dynamic Programming,简称 DP)是一种通过分阶段求解问题保存中间结果来提高计算效率的算法设计思想.它的核心理念在于:对于一个复杂问题,如果其求解过程可以拆分为若干个相互关联的子问题,那么就可以先求解子问题,再由子问题的结果逐步推出原问题的答案,从而避免大量重复计算.

动态规划思想最早由美国数学家理查德·贝尔曼(Richard Bellman)在 20 世纪 50 年代提出,最初主要应用于运筹学、最优化理论以及决策过程研究中.随着计算机科学的发展,这一思想逐渐被广泛应用到算法设计领域,成为解决最优化问题、计数问题、路径问题和序列问题的重要方法之一.

从本质上讲,动态规划是对暴力递归的一种优化.许多问题在递归求解时,往往会重复计算相同的子问题,导致时间复杂度急剧上升.而动态规划正是通过记录已经求出的状态结果,在后续计算中直接复用这些结果,进而将原本低效的指数级算法优化为多项式级别的高效算法.

动态规划能够成立,通常依赖两个关键条件:

  • 问题具有最优子结构,即原问题的最优解可以由子问题的最优解构成;
  • 问题具有重复子问题,即在求解过程中,相同的子问题会被多次访问.只有同时具备这些特征,才能通过状态定义和状态转移方程,将问题系统化地拆解并高效求解.

在实际学习中,动态规划不仅是一种算法技巧,更是一种分析问题和构建解法的思维方式.它要求我们从整体问题中抽象出"状态",找出状态之间的递推关系,并根据计算顺序设计合理的求解过程.因此,掌握动态规划,不仅有助于解决具体算法题,也能够提升对复杂问题结构的理解能力.

正因为动态规划兼具理论价值与实践意义,它已成为算法学习中的核心内容之一.无论是经典的斐波那契数列、背包问题,还是路径规划、最长公共子序列、区间合并等问题,背后都体现了动态规划"以空间换时间""由小推大"的基本思想.


2.斐波那契数列模型背景介绍

斐波那契数列是数学与计算机科学中一个非常经典的数列模型,通常定义为:前两个数固定为0和1,从第三项开始,每一项都等于前两项之和.即:
F ( 0 ) = 0 , F ( 1 ) = 1 F(0)=0,\quad F(1)=1 F(0)=0,F(1)=1
F ( n ) = F ( n − 1 ) + F ( n − 2 ) ( n ≥ 2 ) F(n)=F(n-1)+F(n-2)\quad (n\ge 2) F(n)=F(n−1)+F(n−2)(n≥2)

这一数列最早来源于中世纪数学家斐波那契提出的兔子繁殖问题.该问题通过描述兔群数量随时间增长的规律,引出了这样一种递推关系:当前阶段的结果,依赖于前面两个阶段的状态.这种"当前问题由更小规模子问题推导而来"的特点,使斐波那契数列成为研究递归思想和动态规划算法的典型案例.

在算法学习中,斐波那契数列之所以重要,并不仅仅因为它形式简单,而是因为它完整体现了动态规划问题的基本特征.首先,它具有明显的重复子问题,例如在递归求解 F ( n ) F(n) F(n)时, F ( n − 1 ) F(n-1) F(n−1)和 F ( n − 2 ) F(n-2) F(n−2)的计算过程中会反复出现相同的子问题;其次,它满足最优子结构或递推结构,即原问题的解可以由子问题的解直接构成.正因如此,斐波那契数列常被用作讲解暴力递归、记忆化搜索、状态转移方程以及空间优化等内容的入门模型.

从教学角度来看,斐波那契数列模型是连接"数学递推思想"和"程序设计实现"之间的重要桥梁.通过这个模型,学习者不仅可以直观理解递归到动态规划的优化过程,还能够逐步掌握如何抽象状态、设计转移方程以及分析算法复杂度.因此,斐波那契数列不仅是动态规划的起点,也为后续学习背包问题、路径问题、区间动态规划等更复杂的模型奠定了基础.


3.第N个泰波那契数(OJ题)


算法流程解法(动态规划)

  1. 状态表示 :

    这道题可以根据题目的要求直接定义出状态表示:
    dp[i] 表示:第 i 个泰波那契数的值.

  2. 状态转移方程

    题目已经非常贴心的告诉我们了:
    dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]

  3. 初始化

    从我们的递推公式可以看出,dp[i]i = 0 以及 i = 1 的时候是没有办法进行推导的,因为 dp[-2]dp[-1] 不是一个有效的数据.

    因此我们需要在填表之前,将 0, 1, 2 位置的值初始化.题目中已经告诉我们 dp[0] = 0,dp[1] = dp[2] = 1.

  4. 填表顺序 :

    毫无疑问是从左往右.

  5. 返回值 :

    应该返回 dp[n] 的值.

核心代码

cpp 复制代码
//使用一维数组
class Solution {
public:
    //函数功能:求解第n个泰波那契数
    //参数n:目标泰波那契数的下标
    //返回值:第n个泰波那契数的值
    int tribonacci(int n) {
        //边界条件1:n=0返回0,n=1返回1(题目给定的初始值)
        if (n == 0 || n == 1) 
            return n;

        //定义dp数组:dp[i] 表示第i个泰波那契数
        //数组大小为n+1,保证能存储到下标为n的元素
        vector<int> dp(n + 1); 

        //初始化dp数组:题目给定的前3个泰波那契数初始值
        dp[0] = 0;   //第0个泰波那契数
        dp[1] = 1;   //第1个泰波那契数
        dp[2] = 1;   //第2个泰波那契数

        //核心循环:从左往右填充dp数组(从第3个开始递推)
        //泰波那契数递推公式:T(n) = T(n-1) + T(n-2) + T(n-3)
        for (int i = 3; i <= n; i++)
            dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];

        //返回最终结果:第n个泰波那契数
        return dp[n];
    }
};

//滚动数组优化
class Solution {
public:
    //函数功能:计算第n个泰波那契数(滚动数组优化版,空间复杂度O(1))
    //泰波那契数列规则:T(0)=0, T(1)=1, T(2)=1, T(n)=T(n-1)+T(n-2)+T(n-3)
    int tribonacci(int n) {
        //边界条件1:n=0时,直接返回0
        if(n == 0) return 0;
        //边界条件2:n=1或n=2时,直接返回1
        if(n == 1 || n == 2) return 1;

        //滚动变量定义:
        //a 保存 T(i-3) 的值
        //b 保存 T(i-2) 的值
        //c 保存 T(i-1) 的值
        //d 保存当前计算的 T(i) 的值
        int a = 0, b = 1, c = 1, d = 0;

        //从第3项开始,循环递推计算到第n项
        for(int i = 3; i <= n; i++)
        {
            //核心递推公式:当前项 = 前三项之和
            d = a + b + c;
            //滚动更新变量:为下一次循环做准备
            a = b;  //原T(i-2) 变成 新的T(i-3)
            b = c;  //原T(i-1) 变成 新的T(i-2)
            c = d;  //新计算的T(i) 变成 新的T(i-1)
        }

        //循环结束后,d即为第n个泰波那契数
        return d;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>  
using namespace std;

class Solution {
public:
    int tribonacci(int n) {
        if(n == 0) return 0;
        if(n == 1 || n == 2) return 1;

        int a = 0, b = 1, c = 1, d = 0;
        for(int i = 3; i <= n; i++)
        {
            d = a + b + c;
            a = b;
            b = c;
            c = d;
        }
        return d;
    }
};

int main() {
    Solution sol;  //创建解法对象

    //定义测试用例(泰波那契数列标准值)
    int testCases[] = {0, 1, 2, 3, 4, 5, 6, 10};
    //标准结果:T0=0, T1=1, T2=1, T3=2, T4=4, T5=7, T6=13, T10=149

    cout << "泰波那契数列测试结果:" << endl;
    //遍历测试所有用例
    for(int n : testCases) {
        int res = sol.tribonacci(n);
        cout << "n = " << n << " -> 结果:" << res << endl;
    }

    return 0;
}

4.三步问题(OJ题)


算法思路:解法(动态规划)

  1. 状态表示

    这道题可以根据经验 + 题目要求直接定义出状态表示:
    dp[i] 表示:到达 i 位置时,一共有多少种方法.

  2. 状态转移方程

    i位置状态的最近的一步,来分情况讨论:

    如果 dp[i] 表示小孩上第 i 阶楼梯的所有方式,那么它应该等于所有上一步的方式之和:

    i. 上一步上一级台阶,dp[i] += dp[i - 1];

    ii. 上一步上两级台阶,dp[i] += dp[i - 2];

    iii. 上一步上三级台阶,dp[i] += dp[i - 3];

综上所述,dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3].

需要注意的是,这道题目说明,由于结果可能很大,需要对结果取模.

在计算的时候,三个值全部加起来再取模,即 (dp[i - 1] + dp[i - 2] + dp[i - 3]) % MOD 是不可取的,同学们可以试验一下,n 取题目范围内最大值时,网站会报错 signed integer overflow.

对于这类需要取模的问题,我们每计算一次(两个数相加/乘等),都需要取一次模.否则,万一发生了溢出,我们的答案就错了.

  1. 初始化

    从我们的递推公式可以看出,dp[i]i = 0i = 1 以及 i = 2 的时候是没有办法进行推导的,因为 dp[-3]dp[-2]dp[-1] 不是一个有效的数据.

    因此我们需要在填表之前,将 1, 2, 3 位置的值初始化.

    根据题意,dp[1] = 1,dp[2] = 2,dp[3] = 4.

  2. 填表顺序

    毫无疑问是从左往右.

  3. 返回值

    应该返回 dp[n] 的值.

核心代码

cpp 复制代码
class Solution {
public:
    //取模常量:防止数值溢出,题目要求结果对 1e9+7 取模
    const int MOD = 1e9 + 7;

    //函数功能:计算上 n 级台阶的总方法数
    //规则:每次可以走 1 步、2 步 或 3 步
    int waysToStep(int n) {
        //动态规划标准解题四步走
        //1. 创建 dp 表
        //2. 初始化
        //3. 填表
        //4. 返回结果

        //边界条件:n 为 1/2/3 时,直接返回固定结果(提前处理,避免数组操作)
        if(n == 1 || n == 2) return n;
        if(n == 3) return 4;

        //1.创建 dp 表
        //dp[i] 表示:上到第 i 级台阶,总共有多少种方法
        //数组大小 n+1,保证下标能取到 n
        vector<int> dp(n + 1);

        //2.初始化 dp 数组(基础值,无法通过递推公式得到)
        dp[1] = 1;  //上1级台阶:只有1种方法(走1步)
        dp[2] = 2;  //上2级台阶:2种方法(1+1 / 2)
        dp[3] = 4;  //上3级台阶:4种方法(1+1+1 / 1+2 / 2+1 / 3)

        //3.填表:从第4级开始,从左往右递推计算
        //状态转移方程:dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
        //每一步相加后都取模,避免整数溢出(核心优化)
        for(int i = 4; i <= n; i++)
            dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;

        //4.返回结果:第n级台阶的总方法数
        return dp[n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>  
#include <vector>   
using namespace std;

class Solution {
public:
    const int MOD = 1e9 + 7;

    int waysToStep(int n) {
        //1. 创建 dp 表
        //2. 初始化
        //3. 填表
        //4. 返回

        //处理边界情况
        if(n == 1 || n == 2) return n;
        if(n == 3) return 4;

        vector<int> dp(n + 1);
        dp[1] = 1, dp[2] = 2, dp[3] = 4;
        for(int i = 4; i <= n; i++)
            dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;
        return dp[n];
    }
};

int main() {
    Solution sol;
    //测试用例:台阶数 1~6
    int testCases[] = {1, 2, 3, 4, 5, 6};

    cout << "三步上楼梯 测试结果:" << endl;
    cout << "每次可走1/2/3级台阶,求上n级台阶的方法数" << endl;
    cout << "----------------------------------------" << endl;

    //遍历测试所有用例
    for(int n : testCases) {
        int res = sol.waysToStep(n);
        cout << "台阶数 n = " << n << " → 总方法数:" << res << endl;
    }

    return 0;
}

5.使用最小花费爬楼梯(OJ题)


算法思路:解法(动态规划)解法⼀:

  1. 状态表示

    这道题可以根据经验 + 题目要求直接定义出状态表示:

    第一种:以 i 位置为结尾,
    dp[i] 表示:到达 i 位置时的最小花费.(注意:到达 i 位置的时候,i 位置的钱不需要算上)

  2. 状态转移方程

    根据最近的一步,分情况讨论:

  • 先到达 i - 1 的位置,然后支付 cost[i - 1],接下来走一步走到 i 位置:
    dp[i - 1] + cost[i - 1];
  • 先到达 i - 2 的位置,然后支付 cost[i - 2],接下来走一步走到 i 位置:
    dp[i - 2] + cost[i - 2].
  1. 初始化

    从我们的递推公式可以看出,我们需要先初始化 i = 0,以及 i = 1 位置的值.容易得到 dp[0] = dp[1] = 0,因为不需要任何花费,就可以直接站在第 0 层和第 1 层上.

  2. 填表顺序

    根据状态转移方程可得,遍历的顺序是从左往右.

  3. 返回值

    根据状态表示以及题目要求,需要返回 dp[n] 位置的值.

核心代码

cpp 复制代码
class Solution
{
public:
    //参数cost:每一级台阶对应的花费数组,每次可以爬1级或2级台阶
    //目标:爬到楼梯顶部(最后一级台阶的上方)的最小花费
    int minCostClimbingStairs(vector<int>& cost)
    {
        //获取台阶的总数量
        int n = cost.size();
        
        //1.创建dp表
        //dp[i] 表示:爬到第 i 级台阶位置的最小花费
        //数组大小为 n+1:因为要爬到顶部(第n级位置),需要覆盖0~n所有位置
        vector<int> dp(n + 1, 0);
        
        //2.初始化dp数组
        //初始状态:站在第0级、第1级台阶,不需要任何花费,直接站立
        dp[0] = dp[1] = 0;
        
        //3.填表:从第2级台阶开始,从左往右递推计算
        for (int i = 2; i < n + 1; i++)
            //状态转移方程:
            //到达第i级台阶有两种方式:
            //①从i-1级爬1步上来:总花费 = 到i-1级的最小花费 + i-1级台阶的花费
            //②从i-2级爬2步上来:总花费 = 到i-2级的最小花费 + i-2级台阶的花费
            //取两种方式的最小值,作为到达i级台阶的最小花费
            dp[i] = min(cost[i - 1] + dp[i - 1], cost[i - 2] + dp[i - 2]);
        
        //4.返回结果
        //dp[n] 就是爬到楼梯顶部的最小花费
        return dp[n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> 
using namespace std;

class Solution
{
public:
    int minCostClimbingStairs(vector<int>& cost)
    {
        int n = cost.size();
        //初始化一个 dp 表
        vector<int> dp(n + 1, 0);
        //初始化
        dp[0] = dp[1] = 0;
        //填表
        for (int i = 2; i < n + 1; i++)
            //根据状态转移方程得
            dp[i] = min(cost[i - 1] + dp[i - 1], cost[i - 2] + dp[i - 2]);
        //返回结果
        return dp[n];
    }
};

int main() {
    Solution sol;

    //测试用例1:力扣官方示例1
    vector<int> cost1 = {10, 15, 20};
    cout << "测试用例1:cost = [10,15,20]" << endl;
    cout << "最小花费:" << sol.minCostClimbingStairs(cost1) << endl << endl;

    //测试用例2:力扣官方示例2
    vector<int> cost2 = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
    cout << "测试用例2:cost = [1,100,1,1,1,100,1,1,100,1]" << endl;
    cout << "最小花费:" << sol.minCostClimbingStairs(cost2) << endl;

    return 0;
}


算法思路:解法(动态规划)解法二:

  1. 状态表示:

    这道题可以根据经验 + 题目要求直接定义出状态表示:

    第二种:以 i 位置为起点.
    dp[i] 表示:从 i 位置出发,到达楼顶,此时的最小花费.

  2. 状态转移方程:

    根据最近的一步,分情况讨论:

  • 支付 cost[i],往后走一步,接下来从 i + 1 的位置出发到终点:dp[i + 1] + cost[i];
  • 支付 cost[i],往后走两步,接下来从 i + 2 的位置出发到终点:dp[i + 2] + cost[i];

我们要的是最小花费,因此 dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i].

  1. 初始化:

    为了保证填表的时候不越界,我们需要初始化最后两个位置的值,结合状态表示易得:
    dp[n - 1] = cost[n - 1], dp[n - 2] = cost[n - 2]

  2. 填表顺序:

    根据状态转移方程可得,遍历的顺序是从右往左.

  3. 返回值:

    根据状态表示以及题目要求,需要返回 dp[n] 位置的值.

核心代码

cpp 复制代码
class Solution
{
public:
    // 函数功能:计算爬楼梯的最小花费(反向动态规划解法)
    // 参数cost:每级台阶的花费数组,每次可爬1级或2级台阶
    // 核心思路:从楼顶倒推,计算每个位置到楼顶的最小花费
    int minCostClimbingStairs(vector<int>& cost)
    {
        //动态规划解题四步模板
        //1.创建 dp 表
        //2.初始化
        //3.确定填表顺序
        //4.确定返回值

        //获取台阶的总数量
        int n = cost.size();
        
        //1.创建 dp 表
        //dp[i] 表示:从第 i 级台阶出发,到达楼顶的最小花费
        vector<int> dp(n);
        
        //2.初始化 dp 数组(最后两个台阶)
        //最后一级台阶(n-1):直接跳1步到楼顶,花费=当前台阶费用
        dp[n - 1] = cost[n - 1];
        //倒数第二级台阶(n-2):直接跳2步到楼顶,花费=当前台阶费用
        dp[n - 2] = cost[n - 2];
        
        //3.填表顺序:从右往左(从 n-3 遍历到 0)
        //状态转移方程:dp[i] = min(跳1步, 跳2步) + 当前台阶花费
        for(int i = n - 3; i >= 0; i--)
            dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i];
        
        //4.返回值
        //可以选择从第0级 或 第1级台阶开始,取两者的最小值
        return min(dp[0], dp[1]);
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> 
using namespace std;

class Solution
{
public:
    int minCostClimbingStairs(vector<int>& cost)
    {
        //1.创建 dp 表
        //2.初始化
        //3.填表顺序
        //4.返回值

        int n = cost.size();
        //dp[i] 表示:从第i级台阶出发,到达楼顶的最小花费
        vector<int> dp(n);
        //初始化最后两级台阶:直接跳到底楼顶,花费为当前台阶费用
        dp[n - 1] = cost[n - 1], dp[n - 2] = cost[n - 2];
        //从右往左填表,倒推计算每一级台阶的最小花费
        for(int i = n - 3; i >= 0; i--)
            dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i];
        // 可以从0级或1级台阶起步,取最小花费
        return min(dp[0], dp[1]);
    }
};

int main() {
    Solution sol;

    // 测试用例1:力扣官方示例1
    vector<int> cost1 = {10, 15, 20};
    cout << "测试用例1:cost = [10,15,20]" << endl;
    cout << "最小花费:" << sol.minCostClimbingStairs(cost1) << endl << endl;

    // 测试用例2:力扣官方示例2
    vector<int> cost2 = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
    cout << "测试用例2:cost = [1,100,1,1,1,100,1,1,100,1]" << endl;
    cout << "最小花费:" << sol.minCostClimbingStairs(cost2) << endl;

    return 0;
}

6.解码方法(OJ题)


算法思路:解法(动态规划):

  1. 状态表示:

    根据以往的经验,对于大多数线性 dp,我们经验上都是以某个位置结束或者开始做文章,这里我们继续尝试用 i 位置为结尾结合题目要求来定义状态表示.
    dp[i] 表示:字符串中 [0, i] 区间上,一共有多少种编码方法.

  2. 状态转移方程:

    定义好状态表示,我们就可以分析 i 位置的 dp 值,如何由前面或者后面的信息推导出来.

    关于 i 位置的编码状况,我们可以分为下面两种情况:

    i. 让 i 位置上的数单独解码成一个字母;

    ii. 让 i 位置上的数与 i - 1 位置上的数结合,解码成一个字母.

下面我们就上面的两种解码情况,继续分析:

  • i 位置上的数单独解码成一个字母,就存在解码成功和解码失败两种情况:

    i. 解码成功:当 i 位置上的数在 [1, 9] 之间的时候,说明 i 位置上的数是可以单独解码的,那么此时 [0, i] 区间上的解码方法应该等于 [0, i - 1] 区间上的解码方法.因为 [0, i - 1] 区间上的所有解码结果,后面填上一个 i 位置解码后的字母就可以了.此时 dp[i] = dp[i - 1];

    ii. 解码失败:当 i 位置上的数是 0 的时候,说明 i 位置上的数是不能单独解码的,那么此时 [0, i] 区间上不存在解码方法.因为 i 位置如果单独参与解码,但是解码失败了,那么前面做的努力就全部白费了.此时 dp[i] = 0.

  • i 位置上的数与 i - 1 位置上的数结合在一起,解码成一个字母,也存在解码成功和解码失败两种情况:

    i. 解码成功:当结合的数在 [10, 26] 之间的时候,说明 [i - 1, i] 两个位置是可以解码成功的,那么此时 [0, i] 区间上的解码方法应该等于 [0, i - 2] 区间上的解码方法,原因同上.此时 dp[i] = dp[i - 2];

    ii. 解码失败:当结合的数在 [0, 9][27, 99] 之间的时候,说明两个位置结合后解码失败(这里一定要注意 00 01 02 03 04 ...... 这几种情况),那么此时 [0, i] 区间上的解码方法就不存在了,原因依旧同上.此时 dp[i] = 0.

综上所述:dp[i] 最终的结果应该是上面四种情况下,解码成功的两种的累加和(因为我们关心的是解码方法,既然解码失败,就不用加入到最终结果中去),因此可以得到状态转移方程(dp[i] 默认初始化为 0):

i. 当 s[i] 上的数在 [1, 9] 区间上时:dp[i] += dp[i - 1]

ii. 当 s[i - 1]s[i] 上的数结合后,在 [10, 26] 之间的时候:dp[i] += dp[i - 2];

如果上述两个判断都不成立,说明没有解码方法,dp[i] 就是默认值 0.

  1. 初始化:
    方法一(直接初始化):
    由于可能要用到 i - 1 以及 i - 2 位置上的 dp 值,因此要先初始化前两个位置.

初始化 dp[0]

i. 当 s[0] == '0' 时,没有编码方法,结果 dp[0] = 0;

ii. 当 s[0] != '0' 时,能编码成功,dp[0] = 1

初始化 dp[1]

i. 当 s[1][1, 9] 之间时,能单独编码,此时 dp[1] += dp[0](原因同上,dp[1] 默认为 0 )

ii. 当 s[0]s[1] 结合后的数在 [10, 26] 之间时,说明在前两个字符中,又有一种编码方式,此时 dp[1] += 1

方法二(添加辅助位置初始化):

可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:

i. 辅助结点里面的值要保证后续填表是正确的;

ii. 下标的映射关系

  1. 填表顺序:

    毫无疑问是从左往右

  2. 返回值:

    应该返回 dp[n - 1] 的值,表示在 [0, n - 1] 区间上的编码方法.

核心代码

cpp 复制代码
//使⽤直接初始化的⽅式解决问题
class Solution
{
public:
    //规则:1-26 分别对应 A-Z,0不能单独解码,0开头的数字无法解码
    int numDecodings(string s)
    {
        int n = s.size();        //获取字符串长度
        vector<int> dp(n);       //1.创建dp表
                                 //dp[i] 表示:字符串 s[0...i] 区间的解码方法总数

        //2.初始化dp数组:处理第一个字符
        // 第一个字符非0,有1种解码方法;为0,无法解码(0种)
        dp[0] = s[0] != '0';
        //边界情况:字符串只有1个字符,直接返回结果
        if(n == 1) return dp[0];

        //初始化第二个字符 dp[1]
        //情况1:第二个字符单独解码(要求 1<=s[1]<=9),方法数 += 第一个字符的解码数
        if(s[1] <= '9' && s[1] >= '1') 
            dp[1] += dp[0];
        //情况2:前两个字符组合解码(要求数值在 10~26 之间),方法数 +1
        int t = (s[0] - '0') * 10 + s[1] - '0'; //字符转数字,计算组合值
        if(t >= 10 && t <= 26) 
            dp[1] += 1;

        //3. 填表:从第三个字符开始,从左往右递推
        for(int i = 2; i < n; i++)
        {
            //情况1:当前字符单独解码(1-9有效),方法数 += 前一位的解码数
            if(s[i] <= '9' && s[i] >= '1') 
                dp[i] += dp[i - 1];
            //情况2:当前字符与前一个字符组合解码(10-26有效)
            int t = (s[i - 1] - '0') * 10 + s[i] - '0';
            if(t >= 10 && t <= 26) 
                dp[i] += dp[i - 2]; //方法数 += 前两位的解码数
        }

        //4. 返回结果:整个字符串的解码方法总数
        return dp[n - 1];
    }
};


//使⽤添加辅助结点的⽅式初始化
class Solution
{
public:
    //优化点:使用 n+1 大小的dp数组 + 虚拟下标,简化初始化逻辑,避免边界判断
    int numDecodings(string s)
    {
        int n = s.size(); //获取字符串的长度
        //1.创建dp表:dp[i] 表示 字符串前 i 个字符的解码方法总数
        vector<int> dp(n + 1);
        
        //2.初始化dp数组
        dp[0] = 1; //虚拟节点:前0个字符(空字符串)有1种解码方法,保证后续计算合法
        dp[1] = s[0] != '0'; //前1个字符:非0则1种方法,为0则0种

        //3.填表:从左往右遍历,i 代表前i个字符
        for (int i = 2; i <= n; i++)
        {
            //情况1:第 i 个字符(s[i-1])单独编码
            //字符不为0时有效,解码方法数 += 前i-1个字符的方法数
            if (s[i - 1] != '0') 
                dp[i] += dp[i - 1];
            
            //情况2:第 i-1 和 i 个字符(s[i-2]、s[i-1])联合编码
            int t = (s[i - 2] - '0') * 10 + s[i - 1] - '0';
            //组合数字在 10~26 之间时有效,解码方法数 += 前i-2个字符的方法数
            if (t >= 10 && t <= 26) 
                dp[i] += dp[i - 2];
        }

        //4.返回结果:整个字符串(前n个字符)的解码方法总数
        return dp[n];
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Solution
{
public:
    int numDecodings(string s)
    {
        //优化版动态规划:使用虚拟节点简化初始化
        int n = s.size();
        //dp[i] 表示:字符串前 i 个字符的解码方法总数
        vector<int> dp(n + 1);
        dp[0] = 1; //虚拟节点:空字符串的解码方法为1,保证后续计算合法
        dp[1] = s[0] != '0'; //第一个字符非0则1种方法,为0则0种

        //填表:从左往右递推计算
        for (int i = 2; i <= n; i++)
        {
            //情况1:当前字符单独编码(非0有效)
            if (s[i - 1] != '0')
                dp[i] += dp[i - 1];
            //情况2:当前字符与前一字符联合编码(10~26有效)
            int t = (s[i - 2] - '0') * 10 + s[i - 1] - '0';
            if (t >= 10 && t <= 26)
                dp[i] += dp[i - 2];
        }
        //返回整个字符串的解码方法总数
        return dp[n];
    }
};

int main() {
    Solution sol;

    //测试用例1:力扣官方示例1
    string s1 = "12";
    cout << "测试用例1:s = \"" << s1 << "\"" << endl;
    cout << "解码方法数:" << sol.numDecodings(s1) << endl << endl;

    //测试用例2:力扣官方示例2
    string s2 = "226";
    cout << "测试用例2:s = \"" << s2 << "\"" << endl;
    cout << "解码方法数:" << sol.numDecodings(s2) << endl << endl;

    //测试用例3:包含无效0的情况
    string s3 = "06";
    cout << "测试用例3:s = \"" << s3 << "\"" << endl;
    cout << "解码方法数:" << sol.numDecodings(s3) << endl << endl;

    //测试用例4:边界情况-单个字符0
    string s4 = "0";
    cout << "测试用例4:s = \"" << s4 << "\"" << endl;
    cout << "解码方法数:" << sol.numDecodings(s4) << endl << endl;

    //测试用例5:边界情况-单个有效字符
    string s5 = "1";
    cout << "测试用例5:s = \"" << s5 << "\"" << endl;
    cout << "解码方法数:" << sol.numDecodings(s5) << endl;

    return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【动态规划算法】(从入门到精通:路径问题)


每日心灵鸡汤:"我们吞咽了太多意义,其实生命只需要呼吸"
人们总是喜欢为各种事情赋予特定的意义,以显示其存在价值.于是,为着这三言两语的意义,我们久经风霜又翻山越岭,遍体鳞伤还要咬着牙说一句"坚持不懈,久炼成钢".像是往鸟儿的足上绑了块石头,谁飞得更高,谁就是最值得嘉奖的鸟儿.我们都忘了,其实一开始,它们只需要飞翔,而不需要比较飞翔之高低.
一如我们的生命,只需要呼吸就够了,不用驮着石头蹉跎.

相关推荐
南境十里·墨染春水2 小时前
linux学习进展 网络基础
linux·网络·学习
笨笨饿2 小时前
# 67_MCU的几大分区
数据结构·单片机·嵌入式硬件·算法·机器人·线性回归·个人开发
玖笙&2 小时前
✨WPF编程进阶【9.1】:WPF资源完全指南(附源码)
c++·c#·wpf·visual studio
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 230. 二叉搜索树中第 K 小的元素 | C++ 栈迭代中序遍历
c++·算法·leetcode
RunningBComeOn2 小时前
001学习笔记
学习
大熊背2 小时前
ISP Pipeline中Lv实现方式探究之六--lv值计算再优化
网络·算法·自动曝光·lv
RTC老炮2 小时前
WebRTC下FlexFEC算法架构及原理
网络·算法·音视频·webrtc
xin_nai2 小时前
LeetCode热题100(Java)(2)双指针
算法·leetcode·职场和发展
东京老树根2 小时前
SAP学习笔记 - BTP SAP Build08 - BPA Condition,Decision Table
笔记·学习