【算法笔记】动态规划,三个例题(解题思路与C++代码)

写在前面,我想发个感慨:

      当年大学时代ACM的时候,动态规划算法对鄙人来说一直算得上魔障,有时能敲出来代码,有时候狗咬刺猬无从下嘴。

      以至于一直认为动态规划是ACM的代表算法,没有掌握DP,就压根算不上ACMer。

      自从2013年退役之后,别说DP了,就连算法都没有怎么摸过。最近心血来潮,下决心把DP算法从原理到模型,系统的整理一遍,才有了这个笔记


正文开始:

      这个笔记参考了《算法导论》第二版和第三版两版里,关于动态规划的基本模型范例:“装配线调度”,“切割钢条”,再加上一个比较简单的ACM入门题“拦截导弹”,来分析一下动态规划的具体内容。

      从个人角度来说,这三个模型范例的难度依次是:拦截导弹<切割钢条<装配线调度;


《算法导论》是怎么解释“动态规划”?

      《算法导论》第三版解释如下:动态规划(dynamic programming)与分治法相思,都是通过组合子问题的解来求原问题(在这里,"programming"指的是一种表格法,并非编写的计算机程序)。……分治法将问题划分为互不相干的子问题,递归地求解子问题,再将他们的解组合起来,求出原问题的解。与之相反的,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。在这种情况下,分治法会做许多不必要的工作,它会反复地求解那些公共的子子问题。动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了这种不必要的计算工作。


个人是如何解释“动态规划”?

        鄙人在学习代码、算法道路上,我总喜欢用类比的方式来解释理解一个问题。

        所以,个人抛出了一个“回家”的问题模型,来理解动态规划。

        模型:假如你刚到一个新的城市,租了一间不错的公寓HOME,但是你对这座城市甚至你所在的小区Q一无所知。那么如何从这个城市的某个地方S,最快的回到你的公寓呢HOME?


解决方案:

        假设你当前处于问题的某个阶段:

        原问题:从城市S点,回到公寓HOME

        子问题:从所在小区Q里的某个位置R,回到公寓HOME

        子子问题:熟悉小区Q的布局。

        当前阶段,你经过了一段时间的生活,虽然没有熟悉城市的路线,但是你已经知道了小区Q的布局,你可以保证无论在小区中的任何一个位置,都可以找到最快的回家HOME路线。

        那么这时候,你就无需再考虑小区Q内的路线了,你只需要找到从城市内的S点到小区Q的最快路线。

        这个阶段,就是动态规划进行问题处理的一个阶段。而这个小区Q,其实是一个概念,它实际上代表你可以快速找到回家路线的区域Q。

        所以,经过长时间的熟悉,你所熟悉的区域Q会越来越大,而你不熟悉的区域会越来越小,当问题经过逐渐的推演,这个区域Q终将覆盖城市C,此时,你就找到了从S点到HOME的最短区域。

PS:对算法有过研究的,对此模型肯定十分熟悉,这就是“最短单源路径”迪杰斯特拉算法(Dijkstra's Algorithm),虽然迪杰斯特拉算法被归类为广度搜索或者是贪新算法,但是拿来解释动态规划也是非常合适的,至少我这么觉得。



范例1:拦截导弹

       A国有一套导弹防御系统,在敌国向国内发射导弹时,该系统回依次发射拦截弹,将飞来的导弹拦截下来。但是这个系统有一个缺陷,它的第n+1发拦截弹会比第n发拦截弹的高度要低。假设此时敌国发射来N发导弹,高度不一,那么一套拦截系统,最多可以拦截多少枚导弹?

      问题实质:寻常最长递减子序列。

      假设N枚导弹飞来次序与高度h[1-8]如下:389,207,155,300,299,170,158,65

解决思路:

      原问题:最多拦截多少枚导弹

      子问题:从第1枚导弹开始,到第t枚导弹之间,最多能拦截多少枚导弹

      子子问题:从第t枚导弹开始,到最后一枚导弹,最多能拦截多少枚导弹。

解析:

      我们先从第8个导弹(高度65)开始考虑,第8个导弹之后有多少颗导弹比它高度低?很明显,没有导弹。那么引入一个记录表note[8]=1,表示从第8枚导弹之后(包括第8个导弹),只有1个导弹满足条件;

      再思考第7个导弹,第7个导弹的高度是158,比对之后的导弹高度,发现之后只有第8个导弹且高度比之低,引入记录表note[8]=1,则note[7]=1+note[8]=2;说明第7个导弹开始(包括第7个),之后最多可拦截2个导弹;

      继续第6个导弹,发现第6个导弹的高度比第7、8个导弹高度都要高,但是note[7]>note[8],所以note[6]=note[7]+1=3;说明第6个导弹之后(包括第6个)可以最多拦截3枚导弹;

      继续第5,4个导弹,……,note[5]=note[6]+1=4;note[4]=note[5]+1=5;

      当分析到第3个导弹的时候,高度155,我们发现第3个导弹比第4、5、6、7个导弹低,只比第8个导弹高,那么note[3]=note[8]+1=2,即表示如果需要拦截第3个导弹,则之后最多可拦截2个导弹(包括第3个)

      重复上述步骤,直到分析到第1个导弹,我们就得到了完整的记录表note[1..8];

      其中note中,数值最大的,就是这一连串导弹中我们可以最多拦截的数量

代码:

void intercept()
{
    vector missile = { 389, 207, 155, 300, 299, 170, 158, 65};
    int len = missile.size();
    vector note(len);
    for (int i = 0; i < len; i++)
        note[i] = 0;//note表初始化

    for (int i = len - 1; i >= 0; i--)//从最后一个导弹开始向前分析
    {
        int max = 0,flag=i;//max表示向后查询记录表note时,当前查询到的最多可拦截导弹数量,flag表示当前查到的最大数量导弹的下标
        cout << "i=" << i << endl;
        for (int j = i + 1; j < len; j++)
        {
            if (missile[i] > missile[j] && note[j] > max)//当当前导弹高度大于之后第j个导弹高度,且第j个导弹之后可拦截最多数量大于目前已知最大数量时候,更新flag和max;
            {
                flag = j;
                max = note[j];
            }
        }
        note[i] = note[flag] + 1;
        for (auto x : note)
            cout << x << " ";
        cout << endl;
    }

    return;
}


范例2:切割钢条——算法导论 第三版

       某公司购买钢条,并将其切割成若干段,将之出售。已知不同长度钢条的售价如下


      给定一个长度为n的钢条,该钢条经过有效切割,最多能卖出多少钱?

分析步骤:

      在最初分析本题局部最优解的时候,陷入了误区,一直在考虑局部最优解是以分割1、2、3……段来达到最后结果,后来发现这样分析走入了误区。

      本题的局部最优解:长度为i的钢条最多可卖多少钱ri

      长度为n的钢条的最大收益:

--情况1:不切割,一整条出售的价格pn;

--情况2:将钢条分割成长度i j两段的最大收益,ri+rj;

      而我们的局部最优就是根据情况2进行递归分析


代码:

#include 
#include 
#include 
using namespace std;

int main()
{
    vector table = { 0,1,5,8,9,10,17,17,20,24,30 };//方便理解,数组下标从1开始,0补位 C++11新标准vector初始化方式
    int n;
    cin >> n;//输入钢条长度
    vector r(n+1,0);//方便理解,数组下标从1开始,初始化11位数组
    for (int i = 1; i <= n; i++)
    {
        int max = -1;
        for (int j = 1; j <= i; j++)
        {
            if (max < table[j] + r[i - j])
                max = table[j] + r[i - j];
        }
        r[i] = max;
    }
    for (auto x : r)
        cout << x << " ";
    cout << endl;
    return 0;
}


范例3:装配线调度

【算法笔记】动态规划,三个例题(解题思路与C++代码)_第1张图片

某车间有两条调度线1、2,产品从底盘进入其中一条装配线,共需要经过6个装配站才能完成出厂。假设在同一条装配线上相邻装配站传输不需要时间,只有产品在装配站加工、更换装配线调度时需要花费时间,那么我们该怎么调度产品流水线,使装配一个产品的总时间最短。

       分析问题:

原问题:使产品经过全部装配站到达终点的总时间最短;

子问题:从第1个装配站到第t个装配站,所用时间最短;

子子问题:从第t个装配站到终点时间最短;


代码:

#include 
using namespace std;
/*
装配站操作时长
S 0 1 2 3 4 5 6
0 0 7 9 3 4 8 4
1 0 8 5 6 4 5 7

调度线搬运时长
T 0 1 2 3 4 5 6
0 2 2 3 1 3 4 3
1 4 2 1 2 2 1 2
*/
#define max 1000;
int main()
{
    int s[2][7]={{0,7,9,3,4,8,4},{0,8,5,6,4,5,7}};
    int t[2][7]={{2,2,3,1,3,4,3},{4,2,1,2,2,1,2}};

    int f[2][7];
    for(int i=0;i<2;i++)
    for(int j=0;j<7;j++)
        f[i][j]=max;
    int sum;
    for(int site=6;site>=0;site--)
    {
        if(site==6)
        {
        f[0][6]=s[0][6]+t[0][6];
        f[1][6]=s[1][6]+t[1][6];
        continue;
        }
        if(site==0)
        {
            if(t[0][0]+f[0][0]


你可能感兴趣的:(算法)