动态规划

一. 原理:

动态规划:

每个阶段的最优状态可以从之前某个阶段某个某些状态直接得到而不管之前这个状态如何得到的。

每个阶段最优状态可以从之前某个阶段某个某些状态直接得到

这个性质叫做最优子结构

不管之前这个状态是如何得到

这个性质叫做无后效性

什么是动态规划?动态规划的意义是什么?

二. 举个🌰:

假设你是一个小偷,背着一个可装4磅东西的背包,你可盗窃的商品有如下3件

image.png

为了让盗窃的商品价值最高,你该如何选择商品

1. 背包问题网格如下:

背包问题的网格.png

网格各行商品各列不同容量(1-4磅)背包,所有的这些列你都需要,因为它将帮助你计算子背包价值

2. 吉他行

意味着你尝试将吉他装入背包。在每个单元格,都需要做简单的决定:偷不偷吉他?然后找出价值最高商品集合第一个单元格表示背包的重量1磅吉他重量也是1磅,所以能装入背包以此类推吉他行单元格如下所示:

吉他行.png

3. 音响行

音响行处于第二行,表示现在可偷的商品吉他音响。在每一行可偷的商品都为当前行商品以及之前各行商品
我们先来看第一个单元格,他表示容量1磅背包,在此之前,可装入1磅背包的商品的最大价值1500美元。因为音响重4磅,所以第一个单元格没法装音响,只能装吉他
以此类推,当背包容量2磅3磅时,也只能装吉他,偷不来音响

背包容量4磅时,能装入音响,因为音响价值3000美元,高于吉他1500美元,所以为了价值最高,当然是偷音响
音响行单元格如下所示:

音响行.png

4. 笔记本电脑行

同样的方式处理笔记本电脑,因为笔记本电脑重达到3磅,没法装入容量1磅2磅的背包,因此:前两个单元格最大价值还是1500美元

但对于容量3磅的背包,原来最大价值为1500美元,但现在你可以选择盗窃价值2000美元笔记本电脑而不是吉他,这样第三个单元格最大价值为2000美元

对于容量4磅的背包,当前最大的价值3000美元,但是你可以选择不偷音响,而偷笔记本电脑和吉他笔记本电脑2000美元吉他1500美元,总价值3500美元

笔记本电脑行单元格如下:

笔记本电脑.png

所以将吉他笔记本电脑装入背包时价值最高,为3500美元

从上面单元格的图中可以看出:在计算每个单元格价值时,使用的公式都相同,公式如下:

计算公式.png

5. 代码展示:

typedef  struct StructGood {
    // 商品 重量
    int goodWeight;
    // 商品 价值
    int goodValue;
} Good;

int main(int argc, const char * argv[]) {
    // 背包 最大 磅数
    int maxCapacity = 4;
    // 吉他
    Good guitarGood = {1, 1500};
    // 音响
    Good soundGood = {4, 3000};
    // 笔记本 电脑
    Good computerGood = {3, 2000};
    // 商品 数组
    Good goodArray[3] = {guitarGood, soundGood, computerGood};
    // 单元格 数组
    int tableCellArray[3][4] = {0};
    
    // 商品
    for (int i = 0; i < 3; i++) {
        // 背包 容量
        for (int j = 0; j < maxCapacity; j++) {
            // 当前商品
            Good tmpGood = goodArray[i];
            // 真正 背包容量
            int realCapacity = j + 1;
            // 当多余1个商品
            if (i > 0) {
                // 如果 背包 容量 大于 商品 重量
                if (realCapacity >  tmpGood.goodWeight) {
                    int tmpMaxGoodValue = tmpGood.goodValue + tableCellArray[i - 1][realCapacity - tmpGood.goodWeight];
                    if (tmpMaxGoodValue < tableCellArray[i - 1][j]) {
                        tmpMaxGoodValue = tableCellArray[i - 1][j];
                    }
                    tableCellArray[i][j] = tmpMaxGoodValue;
                }
                // 如果 背包 容量  等于 商品 重量
                else if(realCapacity == tmpGood.goodWeight){
                    int tmpMaxGoodValue = tmpGood.goodValue;
                    if (tmpMaxGoodValue < tableCellArray[i - 1][j]) {
                        tmpMaxGoodValue = tableCellArray[i - 1][j];
                    }
                     tableCellArray[i][j] = tmpGood.goodValue;
                }
                // 如果 背包 容量 小于 商品 重量
                else {
                     tableCellArray[i][j] = tableCellArray[i - 1][j];
                }
            }
            // 当 只有 一个 商品
            else {
                if (tmpGood.goodWeight <= realCapacity) {
                    tableCellArray[i][j] = tmpGood.goodValue;
                }
            }
        }
    }
    
    // 商品
    for (int i = 0; i < 3; i++) {
        // 背包 容量
        for (int j = 0; j < maxCapacity; j++) {
             printf("%d ", tableCellArray[i][j]);
        }
         printf("\n");
    }


    return 0;
}

三. 常见使用:

1. 斐波那契数列

详见:漫画:什么是动态规划?(整合版)

有一座高度10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。

A. 问题建模:

假设你只差最后一步就走到第10级台阶,因为每一步只能走1级或者2级这时候会出现两种情况:

  • 第一种是从9级直接走到10级

  • 第二种是从8级走到10级

也就是说要想走到第10级,最后一步必然是从8级或者9级开始。

那如果:我们已经知道从0级走到9级的走法有X种,从0级8级的走法有Y种,为了方便表达我们把10级台阶走法的数量写为F(10),那么0级10级的走法就是F(10) = X + Y = F(9) + F(8)

因此很容易递推出,F(9) = F(8) + F(7), F(8) = F(7) + F(6)

再考虑下如果只有1级台阶和2级台阶的时候,很显然1级台阶只有1种走法,2级台阶有2种走法。F(1) = 1; F(2) = 2;

所以我们可以归纳出如下的公式:

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

动态规划中包含三个重要的概念:

- 最优子结构
- 边界
- 状态转移公式

刚才我们分析出的:F(10) = F(9) + F(8),因此F(9)F(8)F(10)最优子结构

当只有1级台阶或2级台阶的时候,我们可以得出结果,无需转换,因此我们称F(1)F(2) 是问题的边界。如果一个问题没有边界就永远无法得到有限的结果。

F(n) = F(n - 1) + F(n - 2) (n >= 3);阶段阶段之间的状态转移方程。这是动态规划的核心,决定了问题的每一个阶段下一个阶段的关系。

这是我们完成了: 问题建模,接下来我们进行:求解问题

B. 求解问题

a. 解法1:递归求解

首先很容易想到递归求解:

递归求解.png

接下来我们分析下递归求解时间复杂度:

要计算出F(N), 就要得到F(N - 1)F(N - 2)的值,要计算F(N - 1),就得计算出F(N - 2)F(N - 3)的值,以此类推,可以归纳出下图:

image.png

不难看出,这是一个二叉树,高度为N - 1, 节点个数接近2N-1次方。所以方法的时间复杂度可以近似看做O(2^N);

显然时间复杂度指数级别,这是不可接受的。

b. 解法二:备忘录算法

二叉树图中可以很明显的看出,很多相同的参数重复计算了,所以很容易想到将需要重复计算的值,用哈希表先进行存储,当遇到相同参数时,再直接从哈希表取出,就不用重复计算。这种暂时存储计算结果的方式叫做备忘录算法

image.png

以上代码中集合map是一个备忘录。当每次需要计算F(N)的时候,会首先从map中寻找匹配元素。如果map中存在,就直接返回结果,如果map中不存在,就计算出结果,存入备忘录中。

从中我们不难分析出,从F(1)F(N)一共有N个不同的输入,在哈希表中存储了N-2个结果,所以时间复杂度空间复杂度都是O(N);

c. 解法三: 动态规划求解

备忘录算法中,我们用到哈希表存储重复计算的值,这样空间复杂度O(1)扩大到O(N);有没有更好的方法,时间复杂度还是O(N)空间复杂度依然为O(1)的。

我们上面两种解法都是采用自顶向下来求解的,换个思路,采用自底向上来推导试试。

我们知道F(1) = 1; F(2) = 2; F(N) = F(N - 1) + F(N - 2) (n >= 3);
所以我们很容易求出F(3) = F(1) + F(2) = 1 + 2 = 3;这里F(3)只依赖于F(1)F(2);

同样的F(4) = F(3) + F(2) = 3 + 2 = 5;这里F(4)只依赖于F(3)F(2);

由此可见,每一次迭代过程中,只要保留之前的两个状态,就可以推导出新的状态,而不需要保留全部子状态

新的状态只与之前的两个状态有关,跟最开始其他状态无关,这也是动态规划无后效性的体现。

image.png

程序i=3开始迭代,一直到i=n结束。每一次迭代,都会计算出多一级台阶的走法数量。迭代过程中只需保留两个临时变量a和b,分别代表了上一次上上次迭代的结果。 为了便于理解,我引入了temp变量temp代表了当前迭代的结果值。

从结果可以看出,采用自底向上递推方式,实现了时间O(N)空间O(1)最优化。这就是动态归划

再回过头去看背包问题,我们会发现背包问题的:

  • 最优子结构:

    最优子结构.png

  • 边界:
    当只有一件商品时,商品磅数如果大于等于背包磅数,就是当前商品的价值,否则为0

  • 状态转移方程:

    状态转移方程.png

2. 最长公共子串

假如你做一款翻译器,当用户拼错单词时,你必须猜测他原来输入的单词,例如,Alex想查单词fish,但不小心输入了hish.这时你需要找出最类似的单词,呈现给用户,这时你查找发现fishvista这两个单词类似,你需要比较,找到最类似的单词,呈现给用户并翻译。

a. 绘制网格
  • 单元格中的值是什么?

在这个问题里,要找出两个单词最长公共子串hishfish都包含的最长子串是什么?有多少个hishvista都包含的最长子串是什么?有多少个?通过比较两者之间最长子串的总个数,来判别出,fishvista两者和输入的hish类似程度

所以单元格的值就是两个字符串都包含的最长子串长度

  • 如何将这个问题划分为子问题

你可以需要比较子串:不是比较hish和fish,而是先比较his和fis。每个单元格都将包含这两个子串的最长公共子串的长度。

  • 网格坐标轴是什么?

因为单元格包含着这两个子串的最长公共子串长度,所以坐标轴,应该分别为输入的字符串和类似的子串

image.png

最终hishfish的网格填充如下:

image.png

hishvista的网格填充如下:

image.png

从上面单元格的值,可以总结出状态转移方程为:

状态转移方程.png

实现代码:

// 最长 重复 子串
int maxSubStringLength(char *fristStr, int fristStrLength,  char *secondStr, int secondStrLength) {
    if (fristStr == NULL || secondStr == NULL) {
        return 0;
    }
    
    if (fristStrLength == 0 || secondStrLength == 0) {
        return 0;
    }
    
    int maxSubStrLength = 0;
    
    // 开辟 存储 网格
    int **postionArray = (int **)malloc(sizeof(int *) * (fristStrLength + 1));
    
    for (int i = 0; i <= fristStrLength; i ++) {
        postionArray[i] = (int *)malloc(sizeof(int) * (secondStrLength + 1));
    }
    // 初始化 存储 网格
    for (int i = 0; i <= fristStrLength; i++) {
        for (int j = 0; j <= secondStrLength; j++) {
            postionArray[i][j] = 0;
        }
    }
    
    // 遍历 网格
    for (int i = 0; i < fristStrLength; i++) {
        for (int j = 0; j < secondStrLength; j++) {
            if (i == 0 || j == 0) {
                // 如果 两个 字符 相同
                if (fristStr[i] == secondStr[j]) {
                    postionArray[i][j] = 1;
                }
                else {
                    postionArray[i][j] = 0;
                }
            }
            else {
                // 如果 两个 字符 相同
                if (fristStr[i] == secondStr[j]) {
                    postionArray[i][j] = postionArray[i - 1][j - 1] + 1;
                    if (maxSubStrLength < postionArray[i][j]) {
                        maxSubStrLength = postionArray[i][j];
                    }
                }
                else {
                    postionArray[i][j] = 0;
                }
            }
        }
    }

    return maxSubStrLength;
}

int main(int argc, const char * argv[]) {

    char firstStr[4] = {'h','i','s','h'};
    char secondStr[4] = {'f','i','s','h'};
    char threeStr[5] = {'v','i','s','t','a'};
    
   int firstMax = maxSubStringLength(firstStr, 4,  secondStr, 4);
    
   int secondMax = maxSubStringLength(firstStr, 4, threeStr, 5);
    
    if (firstMax > secondMax) {
        printf("hish 和 fish最类似, 最长公共子串长度为:%d\n", firstMax);
    }
    else {
         printf("hish 和 vista 最类似, 最长公共子串长度为:%d\n", secondMax);
    }
    return 0;
}

综上所述:

  • 最优子结构:
    i > 0 && j > 0
    如果两个字符相同postionArray[i][j] = postionArray[i - 1][j - 1] + 1;
    如果两个字符不同: postionArray[i][j] = 0;
 // 如果 两个 字符 相同
  if (fristStr[i] == secondStr[j]) {
        postionArray[i][j] = postionArray[i - 1][j - 1] + 1;
    }
     else {
         postionArray[i][j] = 0;
      }
  • 边界:
    i== 0或者j == 0
 if (i == 0 || j == 0) {
    // 如果 两个 字符 相同
       if (fristStr[i] == secondStr[j]) {
             postionArray[i][j] = 1;
        }
        else {
            postionArray[i][j] = 0;
       }
}
  • 状态转移方程:
 // 如果 两个 字符 相同
  if (fristStr[i] == secondStr[j]) {
        postionArray[i][j] = postionArray[i - 1][j - 1] + 1;
    }
     else {
         postionArray[i][j] = 0;
      }

3.最长公共子序列

假设Alex不小心输入了fosh,那他原本想输入的是fish还是fort
因为foshfish2个最长公共子串shfoshfort也有2个最长公共子串fo

所以这里应该比较最长公共子序列两个单词中都有的序列包含的字母数

所以这里单元格的值应该为当前子序列中包含的相同字母个数

最终网格如下:

image.png

填写网格所用公式:

状态转移方程式.png

实现代码如下:

// 最长 重复 子序列
int maxSubSequenceLength(char *fristStr, int fristStrLength,  char *secondStr, int secondStrLength) {
    if (fristStr == NULL || secondStr == NULL) {
        return 0;
    }
    
    if (fristStrLength == 0 || secondStrLength == 0) {
        return 0;
    }
    
    int maxSubStrLength = 0;
    
    // 开辟 存储 网格
    int **postionArray = (int **)malloc(sizeof(int *) * (fristStrLength + 1));
    
    for (int i = 0; i <= fristStrLength; i ++) {
        postionArray[i] = (int *)malloc(sizeof(int) * (secondStrLength + 1));
    }
    // 初始化 存储 网格
    for (int i = 0; i <= fristStrLength; i++) {
        for (int j = 0; j <= secondStrLength; j++) {
            postionArray[i][j] = 0;
        }
    }
    
    // 遍历 网格
    for (int i = 0; i < fristStrLength; i++) {
        for (int j = 0; j < secondStrLength; j++) {
            if (i == 0 || j == 0) {
                // 如果 两个 字符 相同
                if (fristStr[i] == secondStr[j]) {
                    postionArray[i][j] = 1;
                }
                else {
                    if (i == 0 && j > 0) {
                        postionArray[i][j] = postionArray[i][j - 1];
                    }
                    else if(j == 0 && i > 0){
                          postionArray[i][j] = postionArray[i - 1][j];
                    }
                    else {
                        postionArray[i][j] = 0;
                    }
                }
            }
            else {
                // 如果 两个 字符 相同
                if (fristStr[i] == secondStr[j]) {
                    postionArray[i][j] = postionArray[i - 1][j - 1] + 1;
                    if (maxSubStrLength < postionArray[i][j]) {
                        maxSubStrLength = postionArray[i][j];
                    }
                }
                else {
                    postionArray[i][j] = postionArray[i - 1][j] > postionArray[i][j - 1] ? postionArray[i - 1][j] :  postionArray[i][j - 1];
                }
            }
        }
    }
    
    return maxSubStrLength;
}

int main(int argc, const char * argv[]) {

    char firstStr[4] = {'f','o','s','h'};
    char secondStr[4] = {'f','i','s','h'};
    char threeStr[4] = {'f','o','r','t'};
    
   int firstMax = maxSubSequenceLength(firstStr, 4,  secondStr, 4);
    
   int secondMax = maxSubSequenceLength(firstStr, 4, threeStr, 5);
    
    if (firstMax > secondMax) {
        printf("fosh 和 fish最类似, 最长公共子串长度为:%d\n", firstMax);
    }
    else {
         printf("fosh 和 fort 最类似, 最长公共子串长度为:%d\n", secondMax);
    }
    return 0;
}

综上所述:

  • 最优子结构:
    i > 0 && j > 0
    如果两个字符相同postionArray[i][j] = postionArray[i - 1][j - 1] + 1;
    如果两个字符不同: postionArray[i][j] = postionArray[i - 1][j] > postionArray[i][j - 1] ? postionArray[i - 1][j] : postionArray[i][j - 1];
 // 如果 两个 字符 相同
  if (fristStr[i] == secondStr[j]) {
        postionArray[i][j] = postionArray[i - 1][j - 1] + 1;
    }
     else {
         postionArray[i][j] = postionArray[i - 1][j] > postionArray[i][j - 1] ? postionArray[i - 1][j] :  postionArray[i][j - 1];
      }
  • 边界:
    i== 0或者j == 0
if (i == 0 || j == 0) {
     // 如果 两个 字符 相同
      if (fristStr[i] == secondStr[j]) {
          postionArray[i][j] = 1;
       }
       else {
          if (i == 0 && j > 0) {
              postionArray[i][j] = postionArray[i][j - 1];
           }
          else if(j == 0 && i > 0){
                postionArray[i][j] = postionArray[i - 1][j];
           }
          else {
              postionArray[i][j] = 0;
            }
      }
}
  • 状态转移方程:
 // 如果 两个 字符 相同
  if (fristStr[i] == secondStr[j]) {
        postionArray[i][j] = postionArray[i - 1][j - 1] + 1;
    }
     else {
         postionArray[i][j] = postionArray[i - 1][j] > postionArray[i][j - 1] ? postionArray[i - 1][j] :  postionArray[i][j - 1];
      }

四:最后:

宁静.jpeg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容

  • 树形动态规划,顾名思义就是树+DP,先分别回顾一下基本内容吧:动态规划:问题可以分解成若干相互联系的阶段,在每一个...
    Mr_chong阅读 1,475评论 0 2
  • 回溯算法 回溯法:也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并...
    fredal阅读 13,632评论 0 89
  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,268评论 0 18
  • 分治方法 将问题划分成互不相交的子问题 递归地求解子问题 将子问题的解组合起来 动态规划(两个要素:最优子结构、子...
    superlj666阅读 497评论 0 0
  • 关键词:传扬神的名,神的荣耀,生命的兴盛 既然活在私欲当中就是死,那我们该怎么办呢?神打乱了当中建造巴别塔人们的语...
    Naomisky阅读 223评论 0 0