动态规划

状态转移方程

从之前某个阶段的某个或某些状态状态到下一个状态之间的关系式,就叫做状态转移方程。

最优子结构

每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到的性质叫做最优子结构。
最优子结构的产生往往是对状态转换方程结果的挑选。

无后效性

如果一个问题被划分各个阶段之后,阶段I中的状态只能由阶段i-1中(或多个有限历史阶段)的状态通过状态转移方程得来,与其它状态没有关系,特别是与未发生的状态没有关系。

上述三项都在讲同一个问题:该问题拥有递推式。该递推式就是状态转移方程,该递推式的等号右边下标都不会大于等号左边。

重叠子问题

动态规划将问题分解为子问题(一般两个)。然后子问题又会分解出子子问题,这些子问题的解都被存储起来,相同子问题的解将被重复利用。
这些子问题解的的时候从多个备选结果中选择最优的那个(比如最小,最大,之类的)。

备忘录

应该是所有动态规划为提中必备的一项。
该项是子问题,也就是存储了每个状态。

  1. 钢条切割:使用一维容器存储了不同长度钢条的利润
  2. 矩阵链乘法:每个子问题都有一个开始位置和结束位置,所以需要使用二维容器来存储。
  3. 最长公共子序列:每个字问题:第一个序列的结束位置,第二个序列的结束位置,所以也使用二维容器来存储。

大白话

动态规划是一种寻找问题最优解的方法。
这种方法将一个问题从状态的角度进行定义,同时定义前一个状态到后一个状态的状态转移方程,并存储每一个状态的最优解。以便后续状态使用。

上面这句话的意思其实就是:

保持问题给出的条件不变,但是改变问题的问法:

  1. 在钢条切割问题中,不同长度钢条的售价不变,但是将问题改为当钢条1米的时候,怎样利润最大,2米的时候曾阳利润最大,3米,4米……一直到问题所问的米数。
  2. 矩阵链乘法中,各个矩阵大小不变,但问题改为其中连续两个矩阵相乘时计算歩数最小是多少,连续三个,连续四个,连续五个……一直到问题所问个数。
  3. 在最长公共子序列中,两个子序列不变,将问题改为是否存在长度为1,2,3,4,5…的子序列长度。
    改变问题的问法,同时也改变了解题的思路。从之前的状态推后来的状态。

在自底而上的方式中(递推)
这种方式的实现符合上述。
在自上而下的方式中(递归)
这种方式的实现也仍然是求的最开始的状态然后推后来的状态。

钢条切割问题

一家公司购买长钢条,将其切割成短钢条出售,切割本身没有成本,长度为i的短钢条的价格为Pi。那给定一段长度为n的钢条和一个价格表Pi,求钢条的切割方案使得收益Rn最大。如一个Pi如下:
长度i 1 2 3 4 5 6 7 8 9 10
价格Pi 1 5 8 9 10 17 17 20 24 30

采用上述来分析:
钢条长度为:1 不切割,2 不切割, 3不切割 ,4 切割2+2,5 切割2+3……

同时应该存储每个状态(钢条长度)的最大值:
自底而上

int cal(const std::vector<int> lp, int len)
{
    lpLen = lp.size() - 1;
    int num(0);                             //记录循环次数
    std::vector<int> p(len+1);              //建立备忘
    for(int i=1; i <= len; i++)             //i代表长度,从1开始建立备忘
    {
        tmp = 0;
        if ( i <= lpLen )                      //让i<lpLen的时候,备忘录还不可用
        {
            for(int j = 0 ; j <= i;j++)
            {
                num++;
                tmp  = tmp > lp[j] + lp[i-j] ? tmp : lp[j] + lp[i-j];
            }
        }else{                              //长度>lpLen以后备忘录已经可用了。
            for(int j = 1 ; j < i/2 +1;j++)
            {
                num++;
                tmp  = tmp > p[j] + p[i-j] ? tmp : p[j] + p[i-j];
            }
        }
        p[i] = tmp;                         //当确定找到了最大值,才添加
    }
    std::cout<<"num : "<<num<<std::endl;
    return p[len];
}

在一边存一边读取备忘录的时候,备忘录什么时候可用是一个坑:

  1. 刚开始,备忘录中全是0,根本不能用。
  2. 只有当基本的方式都全了以后才可用。
    也就是lp中可能的切割方式。
  3. 当长度>能够卖出的最长长度时,不存在不切割这种情况,所以j从1开始。
    但是,当长度小于等于卖出的最长长度时,是可以不切割的。

同时,切割的长度只需要从开始到总长度的一半即可,因为切割是对称的。

另外,代码中的lpLen使用其实是错的:
代码中lpLen假设的是基本的切割方式组合。但是实际中可能并不是和lpLen相等。
代码中的lpLen应该换成基本切割方式。比如如果将本题中5对应的利润改为16,那么就会有11中基本切割方式。此时代码的结果是错的。
如果我们不能知道问题的输入(也就是给定的条件)那么一般不能使用该方法。

自上而下

//子上而下
int cut(const std::vector<int> &lp, std::vector<int> &p ,int len)
{
    int tmp = 0;    
    if(p[len] > 0) return p[len];       //使用备忘录
    if(len == 0) return 0;              //0的时候出点

    if(len <= 10)
    {
        for(int i = 0;i <= len; i++)
        {
            tmp = std::max(tmp, lp[len-i]+lp[i]);
        }
    }else{
        for(int i = 1 ;i <= len/2 +1 ; i++)
        {                               //这里两个都必须是cut*****
            tmp = std::max(tmp,cut(lp,p,i)+cut(lp,p,len-i) );
        }
    }
    p[len]=tmp;
    return p[len];
}
int calT(const std::vector<int> lp , int len)
{
    std::vector<int> p(len+1);
    return cut(lp,p,len);
}

其中标*****的:
必须使用cut的原因是:此时备忘录还没建好,不能使用备忘录。所以必须使用cut。

对比这两种方式

  1. 两种方式都需要先建立备忘录。
  2. 子上而下的递归,并不需要去假设总共可能的基本切割方式。
    但是自底而上的递推需要提前计算出基本的切割方式。

钢条切割问题的最优子结构和状态转换方程:

//i表示未切割部分长度,j表示切割的长度
f[i]=f[i-j]+max(f[j])

子问题重叠:
比如,固定米数钢条的最大利润是一样的。

矩阵链乘法

在该问题中,状态和子问题为:从在给定的矩阵链中,取连续的1,2,3,4,5个矩阵相乘的歩数。
比如,矩阵为:1015 1530 305 515 15*20
上述五个矩阵相乘。

当取1个的时候相乘的计算歩数 为0 。
2个时,一共有4中情况
3个时,一共有3中情况
4个时,一共两种情况
5个时,就一种情况

在3个矩阵相乘时,每一种情况都会用到2个矩阵相的结果,并从中选取最少歩数的结果。

#include <limits.h>
#include <iostream>
#include <algorithm>
#include <vector>

int mulM(const std::vector<int> vec)
{
    int len = vec.size();
    int tmp = 0;
    int vecNum = len - 1;                       //实际矩阵个数
    std::vector<std::vector<int>> memo(len);    //备忘录

    for(int i = 0 ;i < memo.size() ; i++)
    {
            memo[i].resize(len);
    }

    for(int len = 2 ;len <=  vecNum  ; len++)   //状态:长度从2开始。
    {                                           //长为2的矩阵链在原矩阵链中的开始位置
        for(int start = 1 ; start <= vecNum - len + 1 ;start++ )
        {
            tmp=INT_MAX;
            int end = start + len -1;

            for(int k =  start ; k <= end -1  ; k++)    //括号的位置
            {
                int memo1=memo[start][k];
                int memo2=memo[k+1][end];
                tmp = std::min(tmp,vec[start-1] * vec[k] * vec[start+len-1] + memo1 + memo2);
                memo[start][end] = tmp;
            }
        }
    }
    for(int i = 0 ;i <= memo.size()-1 ; i++)
    {
        std::cout<<std::endl;
        for(int j = 0 ; j < memo[i].size(); j++)
        {
            std::cout<<memo[i][j]<<" ";
        }
        std::cout<<std::endl;
    }
    return memo[1][vecNum];
}

代码讲解

备忘录中存储的是memo[start][end]在原有矩阵链中从startend位置的子矩阵链相乘的最短歩数。

最长公共子序列

该问题也有用最优子结构:在某个公共子序列的基础上推更长的最优子结构。
这个的状态转换公式貌似不好写吧?

状态和子问题就变为:当在两个序列长度从1开始慢慢增长,其公共子序列最大是多少?
那备忘录也就是要记录两个公共子序列的长度,所以需要一个二维容器。
备忘录中memo[i][j],当第一个序列长i,第二个序列长j情况下的最长公共子系列。

这里其实也并不需要一个二外的容器。遍历仍然可以获得。
这样我们可以得到两个序列的最大公共子序列的长度,但是无法获得其值,所以有需要一个容器来存储相等的部分。

#include <algorithm>
#include <iostream>
#include <vector>
#include <string>

std::string lcs(const std::string &l,const std::string &r)
{

    int ll = l.size();
    int rl = r.size();

    std::vector<std::vector<int> > memo(ll+1,std::vector<int>(rl+1));
    std::vector<std::vector<int> > memoRebuild(ll+1,std::vector<int>(rl+1));

    for(int inL = 1; inL <= ll ;inL ++)
    {
        for(int inR = 1; inR <= rl ;inR ++)
        {
            if(l[inL-1] == r[inR-1])
            {
                memo[inL][inR] = memo[inL-1][inR-1]+1;
                memoRebuild[inL][inR]=3;
            }else if(memo[inL][inR-1] > memo[inL-1][inR]){
                memo[inL][inR]=memo[inL][inR-1];
               memoRebuild[inL][inR]=2;
            }else{
                memo[inL][inR]=memo[inL-1][inR];
                memoRebuild[inL][inR]=1;
            }
       }
   }
    int reL = memoRebuild.size()-1;
    int reR = memoRebuild[0].size()-1;
    std::string re;
    while(reL >= 0 &&  reR >= 0)
    {
        if(memoRebuild[reL][reR]==3)
        {
            re.push_back(l[reL-1]);
            reL--; reR--;
        }else if(memoRebuild[reL][reR] == 1)
        {
            reL--;
        }else{
            reR--;
        }
    }
    std::cout<<"result ; "<<re<<std::endl;
    return re;
}

从上面的代码中,不难发现,其实构造好备忘录才是本质

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

推荐阅读更多精彩内容

  • 《算法导论》这门课的老师是黄刘生和张曙,两位都是老人家了,代课很慢很没有激情,不过这一章非常有意思。更多见:iii...
    mmmwhy阅读 5,243评论 5 31
  • 动态规划应用于子问题重叠的情况。对于公共子问题,分治算法会做很多不必要的工作,它会反复求解公共子问题。而动态规划算...
    LRC_cheng阅读 424评论 0 1
  • 目录 动态规划与分治法 2.动态规划求解的最优化问题应该具备的两个要素2.1 最优子结构2.2 子问题重叠 动态规...
    王侦阅读 1,353评论 0 1
  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,237评论 0 18
  • 《梅雨来时》 文/知心 致命邂逅 于冰火两重天 必然会升起一柄利剑 来斩断潸然落下的思念 用等来守候冷酷到底吧 用...
    知心js阅读 232评论 0 1