动态规划

动态规划题目特点

1.计数

有多少种方式走到右下角

有多少种方法选出K个数使得和是sum

2.求最大最小值

从左上角到右下角路径的最大数字和

最长上升子序列长度

3.求存在性

取石子游戏,先手是否取胜

能不能选出k个数使得和是sum

例题:

你有三种硬币,面值分别是2元、5元和7元,每种硬币都足够多。

买一本书要27元,如何用最少的硬币组合正好付清,不需要对方找钱。

动态规划组成部分一:确定状态

状态在动态规划中的作用属于定海神针

简单的说,解动态规划的时候需要开一个数组,数组的每一个元素f[i]或者f[i][j]代表什么。这类似于接数学题中,X,Y,Z代表什么。

由于刚开始拿到题目时,我们往往不知道这些数组中的元素代表什么,所以就要确定状态。

确定状态需要两个意识:
-最后一步

-子问题

最后一步:我们肯定知道我们的目标是什么,我们把实现我们的目标的策略称为最优策略。而最后一步就是我们最优策略中的最后一步决策。

在本题中,我们的目标是求一种硬币数量最少的硬币组合。我们把这种硬币组合称为最优策略。我们不知道最优策略,也就是这个组合是什么样子的,但是最优策略一定是K枚硬币a1, a2, ....., ak面值加起来是27。最后一步就是最后的一枚硬币ak.

那么除去最后最后一步硬币ak,前面的硬币面值加起来就是27-ak。

关键点1

我们不关心前面的k-1枚硬币是怎么拼出27-ak的(可能有1种拼法,也可能有100种拼法),而且现在我们甚至还不知道ak和k,但是我们确定前面的硬币拼出了27-ak。

关键点2

因为这个是最优策略,所以拼出27-ak的硬币数也一定要最少,否则就不是最优策略了。

可以用反证法来证明。设现在拼出27-ak的硬币数为a。如果存在一种硬币组合方式使得拼出27-ak的硬币数更少,设硬币数为b。所以由b < a。则可得,我们的最优策略的总的硬币数为a+1,前面假设的组合硬币数为b+1。显然可得 a+1 >b + 1。这就与我们的最有策略相矛盾。所以拼出27-ak的硬币数也一定是最少的。否则就不再是最优策略。

也就是,最优策略中把最后一步去掉之后拼出27-ak的组合仍然是最优的。

子问题

所以我们就要求:最少用多少枚硬币可以拼出27-ak

原问题是最少用多少枚硬币拼出27,现在我们将原问题转换成了一个子问题,而且规模更小:27-ak。

为了简化定义,我们设状态f(x)=最少用多少枚硬币可以拼出X。

那么我们原来是要求f(27),现在则是转换成为了f(27-ak)。

但是现在我们还不知道最后那枚硬币ak是多少,但是最后最后那枚硬币只可能是2、5或7.

如果ak是2, 那么f(27)应该是f(27-2) +1

如果ak是5, 那么f(27)应该是f(27-5) +1

如果ak是7, 那么f(27)应该是f(27-7) +1

除此之外没有其他的可能了

需要求最少的硬币数。所以

使用递归方式来求解此问题,程序如下

//递归解法
//f(x)是最少用多少枚硬币拼出x
//返回值res就是拼出x用的最少硬币数
int f(int x){
    //0元钱只要0枚硬币
    if (x == 0)
        return 0;
    //把结果res初始化为无穷大。发现有比res小的值则进行进一步更新
    int res = MAX_VALUE;
    
    //如果要拼凑出3块钱,也就是f(3),我们肯定不会使用5块钱来拼凑,只会用小于3块钱的来拼凑
    //注意,这里的if语句是平行关系,并不会上面的满足条件后就跳过后面的if语句。
    //而是从上往下依次执行.当x>=7时,实现res = min(f(x - 2) + 1, f(x - 5) + 1, f(x - 7) + 1, res)

    //最后一枚硬币是2元
    if(x >= 2)
        res =  Math.min(f(x - 2) + 1, res);
    //最后一枚硬币是5元
    if(x >= 5)
        res =  Math.min(f(x - 5) + 1, res);
    //最后一枚硬币是7元
    if(x >= 7)
        res =  Math.min(f(x - 7) + 1, res);
    return res;
    
}

递归解法中的问题

我们在求解f(27)时,程序要去递归f(25),f(22),f(20)。然后再进一步求解f(25)时就会进一步递归求解,从而形成下面的树

我们会发现,出现大量的重复计算。如f(20)出现了3次,这里不是仅仅f(20)出现了3次,而是计算f(20)下面的递归,就是以f(20)为根节点的整棵树都计算了3次。当递归数值较大的时候计算量就是天文数字。所以递归解法做了很多的重复计算,使得效率低下。

那么动态规划是如何来实现呢?它是通过将计算结果保存下来,并改变计算顺序来实现的。

动态规划组成部分二:转移方程


设状态f[x] = 最少用多少枚硬币拼出x

那么对于任意x, 均有

f[x] = min{f[x - 2] + 1, f[x - 5] + 1, f[x - 7] + 1}

 

动态规划组成部分三:初始条件和边界情况

f[x] = min{f[x - 2] + 1, f[x - 5] + 1, f[x - 7] + 1}

两个问题:

一:x - 2, x - 5, 或者x - 7小于0怎么办?

二:什么时候停下来?

如果不能拼出Y,就定义f[Y] = 正无穷

例如f[-1] = f[-2] = ... = 正无穷

对于1块钱,我们也是无法拼出来的,所以f[1] = min{f[-1] + 1, f[-4] + 1, f[-6] + 1} = 正无穷,表示拼不出来1。

初始条件:f[0] = 0。0块钱用0个硬币来拼凑。这是一个很经常用的初始条件

初始条件其实是用状态方程算不出来,但是需要的条件,这就需要手工来定义。

动态规划组成部分四:计算顺序

拼出x所需要的最少硬币数:f[x] = min{f[x - 2] + 1, f[x - 5] + 1, f[x - 7] + 1}

初始条件:f[0] = 0

然后计算f[1], f[2], ..., f[27]

大部分动态规划的计算顺序都是从小到大,二维的计算则是从上到下,从左到右

我们计算顺序的确定只有一个原则,就是当计算方程左边的f[x]时,方程右边的式子中的整式都已经计算过了,或者说都是已知量了。在本题中,我们计算式f[x] = min{f[x - 2] + 1, f[x - 5] + 1, f[x - 7] + 1},要计算方程左边的f[x], 等式右边的f[x - 2] , f[x - 5] , f[x - 7] 都已经有计算结果了。所以计算顺序是从小到大。

则本题的计算顺序为

计算f[1], f[1]无法拼凑出来

计算f[2]

f[3]使用了f[1], f[-2], f[-4]拼不出来

f[4]使用了f[2], f[-1], f[-3],通过两个f[2]拼凑出来

最后计算出f[27]

每一步都尝试计算三种硬币,一共27步(计算f(0), f(1), ....., f(27))。

与递归算法相比,没有任何重复计算

算法时间复杂度(即需要进行的步数):27 * 3

小结

求最值型动态规划

动态规划组成部分

  1.确定状态

    最后一步(最优策略中使用的最后一枚硬币ak)

    化成子问题(最少的硬币拼出更小的面值27 - ak)

  2.转移方程

    f[x] = min{f[x - 2] + 1, f[x - 5] + 1, f[x - 7] + 1}

  3.初始条件和边界情况

    f[0] = 0,如果不能拼出Y,则f[Y] = 正无穷

    初始条件其实是用状态方程算不出来,但是需要的条件,这就需要手工来定义。并且意义很清晰,能够明确得出

  4.计算顺序

    我们计算顺序的确定只有一个原则,就是当计算方程左边的f[x]时,方程右边的式子中的整式都已经计算过了,或者说都是已知量了。

    大部分都是从小到大计算,在二维中则多是从上到下,从左到右计算。

消除冗余,加速计算。

本题代码

public class Solution{
    
    public: 
    //硬币放在数组A里面,
    //M代表要拼出的钱数
        int coinchange(int A[], int M){
            //在c++语言中,数组下标从0开始,我们要计算到M,那么开辟的内存空间大小应该是M+1
            int f[M + 1];
            //硬币种类的数量
            int n = sizeof(A) / sizeof(int); 
            
            //定义一个非常大的不可能的数当作无穷大
            very_large = 100000;
            
            //initialization
            //指定初始条件
            f[0] = 0;
            
            int i, j;
            //f[1], f[2], ... , f[27]
            for(i = 1; i <= M;++i)
            {
                //把初始值设置为无穷大,有情况比它好就去更改
                f[i] = very_large;
                //这里是最后一步。也就是我们要拼出i块钱,最后一枚硬币A[j]的值
                //f[i] = min(f[i - A[0]] + 1, ..., f[i - A[n - 1]] + 1)
                for(j = 0; j < n; j++)
                {
                    //由于i - A[j]可能是负数
                    //在数学上,无穷大加一还是无穷大,但是在编程中,则就会出现溢出问题
                    //所以如果是其它语言,在if语句内还要加个判断f[i - A[j]] != Interger.MAX_VALUE
                    if(i >= A[j])
                    {
                    f[i] = min(f[i - A[j]] + 1, f[i]);
                    }
                }
                
                
            }
            
            if(f[M] >= 99000)
                f[M] = -1;
            
            return f[M]            
        }
}

(二)

题意:

给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者向右走一步,问有多少种不同的方式走到右下角。

注意:不管什么样的动态规划都可以使用前面介绍的四个部分来求解。

动态规划组成部分一:确定状态

最后一步:无论机器人用何种方式到大右下角,总有最后挪动的一步:向下 或者向右。

右下角坐标设为(m - 1, n - 1)

那么前一步机器人一定是在(m - 2, n - 1)或者在(m - 1, n - 2)

那么,如果机器人右x中方式从左上角走到(m - 2, n - 1),有y种方式走到(m - 1, n - 2), 则机器人有x + y 种方式走到(m - 1, n - 1).

这里为什么是x + y呢?就是加法原理得到的。

问题转化为:机器人有多少种方式从左上角走到(m - 2, n - 1)和(m - 1, n - 2)

原题要求有多少种方式从左上角走到(m - 1, n - 1)

子问题

状态:设f[i][j]为机器人有多少种方式从左上角走到(i,j).。数组下标[i][j]就是网格(i,j)

注:至于数组为几维,则应按照原问题种有几个变量来设置。原问题中有几个变量,则设置为几维。在本题中有m-1, n-1两个变量,则开二维数组。

动态规划组成部分二:转移方程

 动态规划组成部分三:初始条件和边界情况

初始条件:f[0][0] = 1, 因为机器人只有一种方式到左上角。

边界情况:i = 0或j = 0时,则前一步只能由一个方向过来,所以f[i][j] = 1

动态规划组成部分四:计算顺序

f[0][0] = 1

计算第0行: f[0][0], f[0][1], ... , f[0][n-1]

计算第1行: f[1][0], f[1][1], ... , f[1][n-1]

.....

计算第m - 1行: f[m - 1][0], f[m - 1][1], ... , f[m - 1][n-1]

这样的计算顺序是为了保证在计算(i,j)格子时,前面的问题都已经计算过。

答案是f[m - 1][n - 1]

时间复杂度(计算步数):O(MN),空间复杂度(数组大小):O(MN)

本题代码

class Solution{
    public:
        int uniquePaths(int m, int n){
            int f[][] = new int[m][n];
            int i, j;
            for(i = 0; i < m; i++){ //row: up to down
                for(j = 0; j < n; j++){ //column: left to right
                    if(i == 0 || j == 0)
                    {
                        //这里也包含了初始条件f[0][0] = 1
                        f[i][j] = 1;
                    }
                    else
                    {
                        f[i][j] = f[i - 1][j] + f[i][j - 1];
                    }
                }
            }
            return f[m - 1][n - 1];
        }
}

 (三):存在型动态规划

有n块石头分别摆在x轴的0, 1, ... , n-1位置,如果一只青蛙在石头0,想要跳到石头n-1, 如果青蛙在第i块石头,它最多可以向右跳跃距离ai

问题:青蛙能否跳到石头n - 1

例子:输入:a = [2, 3, 1, 1, 4] 输出:True         输入:a = [3, 2, 1, 0, 4]    输出:False

组成部分一:确定状态

最后一步:如果青蛙能够到最后一块石头n-1, 我们考虑他的最后一步

这一步时从石头i跳过来, i < n - 1

这需要同时满足两个条件:

  青蛙可以跳到石头i

  最后一步不超过跳跃的最大距离: n - 1 - i <= ai

两个条件中第二个条件很容易判断。那么剩下的就是第一个条件:我们需要知道青蛙能不能跳到石头i(i < n-1)

而我们原来要求青蛙能不能跳到石头n - 1。这就成为一个子问题

状态:设f[j]表示青蛙能不能跳到石头j

组成部分二:转移方程

设f[j]表示青蛙能不能跳到石头j

f[j] = ORo<=i<j(f[i] AND i + a[i] > = j)

 

式中:OR表示进行for循环的时候只要有一个满足,就输出True,否则就输出False

组成部分三:初始条件和边界情况

设f[j]表示青蛙能不能跳到石头j

初始条件: f[0] = True, 因为青蛙一开始就在石头0

没有边界情况

计算顺序

设f[j]表示青蛙能不能跳到石头j

f[j] = ORo<=i<j(f[i] AND i + a[i] > = j)

初始化f[0] = True

计算f[1], f[2], ... , f[n - 1]

答案是f[n - 1]

class Solution{
    public:
        boolean canjump(int A[]){
            int n = sizeof(A) / sizeof(int);
            bool f[] = new bool[n];
            f[0] = true;
            for(int j = 1; j < n; j++)
            {
                f[j] = false;
                //previous stone i
                //last jump is from i to j
                for(int i = 0; i < j; i++)
                {
                    if(f[i] && i + A[i] >= j)
                    {
                        f[j] = true;
                        break;
                    }
                }
            }
            return f[n - 1]
        }
}

(三)

给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者想右走一步

网格中有一些障碍,机器人不能通过障碍

问有多少种不同的方式走到右下角

组成部分一:确定状态

机器人最后到达右下角(m - 1, n - 1)处,那么上个位置一定是在(m - 2, n - 1)或者(m - 1, n - 2)处。那么机器人到达右下角(m - 1, n - 1)的方式数量为到达(m - 2, n - 1)方式数量和到达(m - 1, n - 2)的方式数量之和。

原问题为:有多少种不同方式到达右下角

子问题为:有多少种方式到达位置(m - 2, n - 1)和位置(m - 1, n - 2)

组成部分二:状态方程

设f[i][j]为走到网格[i][j]处的方式数目。那么有f(m-1)(n-1)为到达位置(m -  1, n - 1)处的方式数量,那么到达(m - 2, n - 1)和(m - 1, n - 2)处的方式数量分别为f(m - 2)(n - 1)和f(m - 1)( n -2)

则状态方程为

f(i)(j) = f(i-1)(j) + f(i)(j-1)

组成部分三:初始条件和边界情况

当机器人位于点(0, 0 )处时,方式数数目为1。也就是机器人只有一种方式到达点(0,0),即f(0)(0) = 1

如果左上角和右下角有障碍,那么机器人永远无法到达,直接输出零

如果(i, j)处右障碍,则f[i][j] = 0,表示机器人无法到达此网格。

那么转移方程为

 

组成部分四:计算顺序

计算顺序按照从上到下,从左到右的顺序计算进行

class Solution{
    
    public:
        int uniquepathswithobstacles(int A[][])
        {
            if(A == NULL)
            {
                return 0;
            }
            int m = sizeof(A) / sizeof(A[0]);
            int n = sizeof(A[0]) / sizeof(A[0][0]);
            
            int f[][] = new int[m][n];
            //做一个数组,下标为[i][j]的数组就是到达网格(i,j)处的方式数量。
            //而且按照从上到下、从左到右的顺序计算,在进行计算(i,j)处的值时,
            //前j - 1行中的值全部计算完成,并且第j行前i-1个元素也已经计算完成。
            //在计算第(i, j)处的值时可通过下标来使用计算过的值
            
            for(int i = 0; i < m; i++)                //行从左到右
            {
                for(int j = 0; j < n; j++)  //列从上到下
                {
                    //一票否决
                    //如果A[i][j] = 1表示此处有障碍,将f[i][j]置为0,
                    //同时使用continue语句进行下轮循环进行下一个网格的计算
                    if(A[i][j] = 1)
                    {
                        f[i][j] = 0;
                        continue;
                    }
                    
                    if( i == 0 && j == 0)
                    {
                        f[i][j] = 0;
                        continue;
                    }
                    
                    //计算f[i][j]时先将f[i][j]置为0,然后再更新
                    f[i][j] = 0;
                    
                    //如果j=0,表示在第1列,不执行此语句。此时f[i][j] = 0 + f[i - 1][j]
                    //如果i=0,表示在第1行,不执行下一个if语句,此时f[i][j] = f[i][j - 1]
                    //如果i,j都大于0,则两个if语句同时执行,有f[i][j] = f[i][j - 1] + f[i - 1][j]
                    if( j >0)
                    {
                        f[i][j] += f[i][j - 1];
                    }
                    
                    if( i > 0)
                    {
                        f[i][j] += f[i - 1][j];
                    }
                    
                }
            }
            return f[m - 1][n - 1]
        }
}

序列型规划

题目:

有一排N栋房子,每栋房子要漆成三种颜色中的一种:红、蓝、绿

任何两栋相邻的房子不能漆成同样的颜色

第i栋房子漆成红色、蓝色、绿色的花费分别时cost[i][0], cost[i][1], cost[i][2]

问最少需要花多少钱漆完这些房子

例子:

输入:

N = 3

cost = [[14, 2, 11], [11, 14, 5], [14, 3, 10]]

输出

-10 (第0栋房子蓝色,第1栋房子绿色,第2栋房子蓝色。 2 + 5 + 3 = 10)

为了便于理解,首先统一一下编号问题。

前N栋房子,是指房子共有N栋。是生活中习惯用语,前1栋就只有一栋,前2栋就是两栋房子。

而在java和c++中,数组下标是从0开始,所以房子N-1指的是第N栋房子(前N栋房子的最后一栋)。

综上所述:前N栋房子并且房子N-1是红色、蓝色、绿色的最小花费是指把前N栋房子都油漆完并且将第N栋(前N栋中的最后一栋)漆成红色、蓝色、绿色的花费。

组成部分一:确定状态

最优策略时花费最小的策略

在这里,第一栋房子编号为0,所以,前N栋房子的最后一栋房子编号为N-1。

最后一步:最优策略中房子N-1一定染成了红、蓝、绿中的一种。

但是相邻两栋房子不能漆成一种颜色

所以如果最优策略中房子N-1是红色,房子N-2只能是蓝色或绿色

所以如果最优策略中房子N-1是蓝色,房子N-2只能是红色或绿色

所以如果最优策略中房子N-1是绿色,房子N-2只能是蓝色或红色

如果直接套用以前的思路,记录油漆前N栋房子的最小花费

根据套路,也需要记录油漆前N-1栋房子的最小花费。

但是,前N-1栋房子的最小花费的最优策略中,不知道房子N-2(所有房子的倒数第二栋房子)是什么颜色,所以房子N-2有可能与房子N-1撞色。

为解决这个问题,所以有

不知道房子N-2是什么颜色,就把它记录下来。就是在状态里面增加一个维度,用来记录房子N-2的颜色。

分别记录油漆前N-1栋房子并且房子N-2(倒数第二栋)是红色、蓝色、绿色的最小花费。这个过程中,我们需要记录三个数值。就是我们记录油漆前N-1栋房子并且N-2栋房子是红色的最小花费、记录油漆前N-1栋房子并且N-2栋房子是蓝色的最小花费、记录油漆前N-1栋房子并且N-2栋房子是绿色的最小花费。

如果N-1栋房子是红色,那么在N-2栋房子我们只需看蓝色和绿色

如果N-1栋房子是蓝色,那么在N-2栋房子我们只需看红色和绿色

如果N-1栋房子是绿色,那么在N-2栋房子我们只需看红色和蓝色

原问题:

求油漆前N栋房子且房子N-1(最后一栋)是红色、蓝色、绿色的最小花费。

子问题:

需要知道求油漆前N-1栋房子且房子N-2(倒数第二栋)是红色、蓝色、绿色的最小花费。

状态:

设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别为f[i][0]、f[i][1]、f[i][2]

组成部分二:转移方程

设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别为f[i][0]、f[i][1]、f[i][2]

以第一个式子为例来理解这个等式

下标的第二个维度是颜色的标记,0,1,2分别代表三种颜色。

f[i][0]表示油漆前i栋房子,并且i-1(前i栋房子的最后一栋)房子是颜色0的最小花费。

在这里,如果不考虑颜色冲突问题,那么油漆前i栋房子的费用等于油漆前i-1栋房子的费用加上油漆房子i-1的费用。

f[i-1][1]表示油漆前i-1栋房子,并且将房子i-2油漆成颜色1的最小花费

cost[i-1][0]:表示将房子i-1油漆成颜色0的费用。

整个式子的意思为:

油漆前i栋房子并且房子i-1油漆成颜色0的最小费用 = 油漆前i-1栋房子并且房子i-2油漆成颜色1的费用 + 将房子i-1油漆成颜色0的费用。

组成部分三:初始条件和边界情况

设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别为f[i][0]、f[i][1]、f[i][2]

初始条件:f[0][0] = f[0][1] = f[0][2] = 0。即不油漆任何房子的花费。

无边界情况

组成部分四:计算顺序

设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别为f[i][0]、f[i][1]、f[i][2]

初始化f[0][0]、f[0][1]、f[0][2]

计算f[1][0]、f[1][1]、f[1][2]

.。。。。

计算f[N][0]、f[N][1]、f[N][2]

答案是min{f[N][0]、f[N][1]、f[N][2]}, 时间复杂度O(N), 空间复杂度O(N)

在下面代码中设计最小值函数在运行时需要进行重新书写

class solution{
    public:
        int minCost(int C[][])
        {
            int n = sizeof(C) / sizeof(C[0]);
            if(n == 0)
            {
                return 0;
            }
            
        //这里一定要开n+1个空间
        //因为除了存储1,... , N栋房子的花费,还要存储0栋房子时的花费
        int f[][] = new int[n + 1][3];    
        
        //初始化,油漆0栋房子的最小花费
        f[0][0] = 0;
        f[0][1] = 0;
        f[0][2] = 0;
        
        for(int i = 1; i <= n; i++)
        {
            //前i栋房子,最后一栋房子为i
            //第i栋房子要染成j颜色
       //这里计算的是将第i栋房子漆成颜色j的最小费用
for(int j = 0; j < 3; j++) { f[i][j] = 100000; //第i-1栋房子要染成K颜色 //这里的计算顺序是从前向后计算的 //在计算f[i][j]时,前面的元素都已经计算完全。 //所以这里f[i - 1][k]是已知量,是第i-1栋房子被染成颜色k的最小费用。用这个已知量来求f[i][j] //将这个循环套在j的循环之内,是因为求第i栋房子漆成颜色j时的费用,要求i-1的颜色k不能等于j // for(int k = 0; k < 3; k++) { if(j != k ) { f[i][j] = min(f[i][j], f[i - 1][k] + C[i - 1][j]); } } } } return min(f[n][0], f[n][1], f[n][2]); } }

序列型动态规划: ...前i个...最小/方式数/可行性

在设计动态规划过程中,发现需要知道油漆前N-1栋房子的最有策略中,房子N-2的颜色

如果只用f[N-1],将无法区分

解决方法:记录下房子N-2的颜色

 - 在房子N-2是红/蓝/绿的情况下,油漆N-1栋房子的最小花费

序列 + 状态

划分型动态规划

题目:有一段由A-Z组成的字母串信息被加密成数字串

加密方式:A->1, B-> 2, ... , Z-> 26

给定加密后的数字串S[0 ... N-1], 问有所少种方式解密成字母串

例子:

输入:12          输出:2(AB或L)

 组成部分一:确定状态

解密数字串,即划分成若干段数字,每段数字对应一个字母

最后一步:对应一个字母。(在划分型问题中,最后一步对应的是最后一段。无论怎么划分,最后一段数字一定对应一个字母)

-A, B, ... , Z

这个字母加密时对应的数字串为:1, 2, ... , 26

假设这是具有N个数字的数字串。

那么最后一个字母只能对应两个数字或者一个数字,除此之外没有其它的对应关系。

如果最后一个字母对应一位数字,那么在这个问题中就是2.那么字母就是B。假设前N-1个字母共有100种解密方式

如果最后一个字母对应两位数字,那么在这个问题中就是数字12,对应的字母就是L。假设前N-2个字母共有50种解密方式

除此之外,最后一个字母没有其它的可能。那么

子问题

设数字串长度为N

原问题:要求数字串前N个字符的解密方式数

子问题:需要知道数字串前N-1和N-2个字符的解密方式数。

状态:设数字串前i个数字解密成字母串有f[i]种方式。

组成部分二:转移方程

设数字串S前i个数字揭密乘字母串有f[i]种方式,则

组成部分三:初始条件和边界情况

设数字串S前i个数字揭密乘字母串有f[i]种方式

初始条件:f[0] = 1, 即空串有1种方式解密 ,即解密成空串

边界情况:如果i = 1,只看最后一个数字

组成部分四:计算顺序

f[0], f[1], ... , f[N]

原文地址:https://www.cnblogs.com/hxhlrq/p/13284220.html