动态规划(斐波拉契问题)

@TOC

        先说一下动态规划斐波拉契数列模型的解决常见办法:

        这类问题常见的解法就是以 i 位置为结尾,然后按照题目的题解总结状态转移方程,在进行基础的初始化,通过已有数据,推导出未来某一时刻的相关数据。总结如下:

1. 状态表示

2. 状态转移方程

3. 初始化

4. 填表顺序

5. 返回值

接下来通过几个题来进行讲解:

第N个泰波那契数

这是动态规划最基础的一道题

链接 : . - 力扣(LeetCode)

代码讲解:

        按照我们上面说的步骤来进行讲解:

状态表示:

        这个题我们就是创建一个dp数组,每一个状态就是以i 为结尾的dp[i]

状态转移方程:

        这个题的状态转移方程, 题目已经是给我们说明了 dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];

初始化

        题目上面的也是有提到 dp[0] = 0, dp[1] = 1, dp[2] = 1

填表顺序

        我们要知道第n个泰波那契数, 就必须知道n - 1, n - 2, n - 3, 依次类推, 顺序是从左往右逐渐推导

返回值

        我们需要的是第n个泰波那契数, 那么返回值自然是dp[n];

代码展示:

class Solution {
public:
    int tribonacci(int n) {
        if(n == 0)
        {
            return 0;
        }
        if(n == 1 || n == 2)
        {
            return 1;
        }
        vector<int> dp(n + 1);
        dp[0] = 0, dp[1] = 1, dp[2] = 1;
        for(int i = 3; i <= n; i++)
        {
            dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
        }
        return dp[n];
    }
};

三步问题

链接 : . - 力扣(LeetCode)

代码讲解:

状态表示:

        这个题我们就是创建一个dp数组,每一个状态就是以i 为结尾的dp[i]

状态转移方程:

        这里的转移方程我们需要分析一下:

        假如是 i 个台阶, 想要上到第 i 个台阶一共有三种方法:

        从 i - 3的台阶处,一口气走上来

        从 i - 2的台阶处, 一口气走俩台阶上来

        从i - 1的台阶处, 一步上来

        因此 dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3] 

初始化

        假如是一个台阶 : 那么就是一个一步到位 , 即 : dp[0] = 1

        假如是两个台阶 : 那么就是两种方法 : 一种是从一步一个台阶上来,一种是直接一步两个台阶上来 , 即 dp[1] = 2 

        假如是三个台阶,那么就有很多种方法 : 一步一个台阶上去, 一步三个台阶上去, 先一步一个台阶,然后一步两个台阶,后者先一步两个台阶, 然后一步一个台阶. 即 dp[2] = 4;

填表顺序

        既然是爬楼梯,自然是从左向右,依次推导

返回值:

        dp[0] 表示 1 个台阶, dp[1] 表示2 个台阶, n个台阶就是dp[n - 1] . 因此返回值是dp[n - 1]

代码展示:

class Solution {
public:
    int waysToStep(int n) {
        if(n == 1)
        {
            return 1;
        }
        if(n == 2)
        {
            return 2;
        }
        if(n == 3)
        {
            return 4;
        }
        vector<long long> dp(n);
        dp[0] = 1, dp[1] = 2, dp[2] = 4;
        for(int i = 3; i < n; i++)
        {
            dp[i] = (dp[i - 1] + dp[i - 2] + dp[i - 3]) % 1000000007;
        }
        return dp[n - 1] % 1000000007;
    }
};

        注意 : 结果可能会超出 int 的限制, 所以dp建议使用long long, 如果还会超出就是用题目中说明的1000000007

使用最小花费爬楼梯

链接 : . - 力扣(LeetCode)

代码讲解:

方法一:

状态表示:

        这个题我们就是创建一个dp数组,每一个状态就是以i 为结尾的dp[i]

状态转移方程

        我们创建的dp数组, 我们希望每个元素都是到达该阶梯最小的花费,而走楼梯的时候,我们可以选择走一个台阶或者两个台阶. dp[i] = min(dp[i - 1] + cost[i], dp[i - 2] + cost[i - 2])

初始化:

        0 号和1 号阶梯前面没有花费,登上他们花费的体力和cost[i]相等

        因此只需要初始化dp[0], dp[1]

填表顺序:

        想知道dp[i]的花费,需要知道dp[i - 1]和dp[i - 2], 依次类推,肯定是从左向右的

返回值

        自然是返回dp[n]

方法二:

状态表示:

        这个题我们就是创建一个dp数组,每一个状态就是以i 为结尾的dp[i]

状态转移方程

        这里我们需要的是从i到最后还需要花多少力气,那么越往后,还需要花费的力气越少,到了最后的n - 1, n -2位置的时候,直接使用该位置需要的力气就可以了,因此dp[i] =min(dp[i + 1] , dp[i + 2]) + cost[i];

初始化

        和方法一相反,需要知道最后两个位置的花费

返回值

        选择dp[0], dp[1]之中的最小值,因为到达这俩位置不花费力气,只需要知道这俩位置到最后谁花的少

代码展示

方法一

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size() + 1);
        for(int i = 2; i <= cost.size(); i++)
        {
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[cost.size()];
    }
};

方法二

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();
        vector<int> dp(n);
        dp[n - 1] = cost[n - 1];
        dp[n - 2] = cost[n - 2];
        for(int i = dp.size() - 3; i >= 0; i--)
        {
            dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i];
        }
        return min(dp[0], dp[1]);
    }
};

解码方法

链接:. - 力扣(LeetCode)

代码讲解:

状态表示:

        我们创建一个dp数组,这个数组,每一个元素表示从0到当前位置的解码方法。每一个dop[i]就是当前所需数据的状态

状态转移方程:

        字母A-Z,26个字母中,有的只需要一个下标就可以定位,有的需要两个下表才能定位,因此这个状态转移方程需要分情况说明

        首先是解码的成功和失败,假如解码失败,那么直接变成0,

失败的情况有很多,以下进行列举

  • 当字符串的首元素为0, 那么这串字符串就无法进行解码,结果为0
  • 当字符串在进行单一下标的解码的时候,如果出现了 1<= a <= 9 之外的字符的时候,解码失败,结果为0
  • 当字符串在进行两个下标的解码的时候,如果出现   10 <= s <= 26, 那么解码失败,结果为0

假如成功就会分成两个情况

  • 当以一个下标来进行解码, 那么解码成功出来的结果种类数就是dp[i - 1]
  • 当以两个下标来进行解码,那么解码成功出来的结果种类就是dp[i - 2]  

状态转移方程就是 dp[i] = dp[i - 1] + dp[i - 2]

初始化

        要解码从0 到 i 位置的字符串,必须知道dp[i - 1] 和 dp[i - 2],那么依次类推,dp[0]和dp[1]是必须初始化的

返回值

        返回的是dp[i]

代码展示

class Solution {
public:
    bool OneIsOK(const char s)
    {
        int number = s - '0';
        if(number >= 1 && number <= 9)
        {
            return true;
        }
        return false;
    }

    bool DoubleAreOK(string s)
    {
        if(s[0] == '0')
        {
            return false;
        }
        int number = stoi(s);
        if(number >= 10 && number <= 26)
        {
            return true;
        }
        return false;
    }

    int numDecodings(string s) {
        if(s[0] == '0')
        {
            return 0;
        }
        if(s.size() == 1)
        {
            return 1;    
        }
        int n = s.size();
        vector<int> dp(n);
         string sub;
        //设置两个函数
        //判断单一位置是否有效
        if(OneIsOK(s[0]))
        {
            dp[0]++;
        }
        if(OneIsOK(s[1]))
        {
            dp[1]++;
        }
         //判断该位置和前面的位置一起是否有效
        sub = s.substr(0, 2);
        if(DoubleAreOK(sub))
        {
            dp[1]++;
        }
        for(int i = 2; i < dp.size(); i++)
        {
            if(OneIsOK(s[i]))
            {
                dp[i] += dp[i - 1];
            }
            sub = s.substr(i - 1, 2);
            if(DoubleAreOK(sub))
            {
                dp[i] += dp[i - 2];
            }
        }
        return dp[n - 1];
    }
};

通过这个代码很容易发现,这个动态规划初始化 dp[0]和dp[1]的时候非常的繁琐,而且·可以看到在初始化dp[1]的时候,和后续的推导其实差不多,那到底有没有什么办法,让dp[1]和后面的dp[i]一起推导出来,答案是有的————虚拟节点

优化:

        我们在前面加一个节点dp[-1], 通过dp[-1]和dp[0]来推导出来后面的数据

        注:这里的dp[-1]是为了清晰,才这么讲,实际上应该是dp[0],s[i] 等效到dp数组就是dp[i +1]

        那么dp[-1]到底应该是一个怎么样的值呢

        我们看前面的状态转移方程的推导,当以一个下标来进行解码, 那么解码成功出来的结果种类数就是dp[i - 1], 因此假如第一个位置解码成功了,那么他的解码方法数就是dp[-1],dp[0] 我们知道是1,因此dp[-1]肯定是1

因此这个代码的优化就是

class Solution {
public:
    bool OneIsOK(const char s)
    {
        int number = s - '0';
        if(number >= 1 && number <= 9)
        {
            return true;
        }
        return false;
    }

    bool DoubleAreOK(string s)
    {
        if(s[0] == '0')
        {
            return false;
        }
        int number = stoi(s);
        if(number >= 10 && number <= 26)
        {
            return true;
        }
        return false;
    }

    int numDecodings(string s) {
        if(s[0] == '0')
        {
            return 0;
        }
        int n = s.size();
        vector<int> dp(n + 1);
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2; i < dp.size(); i++)
        {
            if(OneIsOK(s[i - 1]))
            {
                dp[i] += dp[i - 1];
            }
            string sub = s.substr(i - 2, 2);
            if(DoubleAreOK(sub))
            {
                dp[i] += dp[i - 2];
            }
        }
        return dp[n];
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值