zoukankan      html  css  js  c++  java
  • DP(动态规划)学习心得

     

                  动态规划学习心得

      说实话吧,动态规划(DP)确实是一个比较难的知识点,对于初学者来说,是一个难过的坎(笔者的脸呢?开玩笑。)。动态规划就是我从初学开始遇到的最神奇的解法,它不同于暴力搜索,也不同于一般的贪心,能够以出乎人意料的时间复杂度(近似于O(n^2))解决一些难题,算法远远优于一般的深搜(O(2^n))。不过,动态规划的思维性比较强,必须会设好状态,正确写出状态转移方程,并且能够准确判断有无最优子结构。

      其实有点像贪心,但是它有局部最优解推导向整体最优解的过程,形象一点说,动态规划的“眼光”比贪心更长远,有一个更新最优解的过程,发现问题了可以“反悔”。它还有一点分治的味道,通过对问题划分各个阶段,对各个阶段分别求解,最后推向整体的过程。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

      学习动态规划是一个比较漫长的过程,需要慢慢领悟,去体会动态规划的奥义。显然,多做题,多思考是必需的,坚持下去,慢慢就能学会了。

      下面详细地描述一下:

      一.动态规划的表示方法:

      一般地,动态规划有两种表示方法,分别是:1.递推  2.记忆化搜索。

       这两种方法各有优缺点,递推的效率更高,可以降维节省空间,能使用滚动数组,但思维性强,难度高。而记忆化搜索更好写,更便于理解,不容易出错,但容易超空间。有时候状态数目多,记忆化搜索就不行了,会超空间。但是递推是绝对没有问题的,只要会滚动数组或者降维。所以,我比较推荐递推的方法,更能够锻炼我们的算法能力。所以我们一般用递推的方法解决动态规划的问题。

        例如:下面的代码就是一种递推:

    f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];

       至于记忆化搜索,也上一波代码:

    int dfs(int k,int j) 
    {
    	if(f[k][j]) return f[k][j];
    	if(k == n) return a[k][j];
    	temp1 = dfs(k+1 , j);
    	temp2 = dfs(k+1 , j+1);
    	f[k][j] = max(temp1 , temp2)+a[k][j];
    }
    

      (以上代码以题目数字三角形为例)。

      二.动态规划的条件:

      动态规划有两个必要条件:

      1.无后效性.

      2.最优子结构.

      无后效性:

      标准定义是这样的:将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。

      最优子结构:

      标准定义是这样的:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。

      三.动态规划的分类及解决过程:

        分类:

      动态规划分为:

      1.基本线性动规: 比较基础的DP入门

      2.背包动规  背包问题,见某大佬著作《背包九讲》

      3.区间动规  区间型的动态规划

      4.双进程动规  分为两个进程,一般只需要增加一个维度表示状态就可以了。

      5.树状动规      在树上做动态规划,较为高级。

      6.各种优化...............等等

       解决过程:

       第一步:读懂题意,看看题目是否可以满足动态规划的条件(及是否可以用动态规划解决)。

       第二步:根据题目所给的条件划分阶段,可以是题目给定的顺序,或者是贪心的顺序,或者是特殊的顺序。

       第三步:根据阶段设置状态,一般用f数组表示,最基本规则:求什么设什么,必须满足无后效性 。当感觉是dp,但是当前状态不满足必要条件的时候,状态+维。

       第四步:推出状态转移方程式,能够表示当前最优值和前面最优值的关系。

       第五步:代码实现,检查前面的步骤是否正确。  

      四.动态规划经典例题详解:

      1.基本线性动规:

        hloj#402护卫队

         见链接:https://www.cnblogs.com/smilke/p/10502784.html(本蒟蒻的一篇博客题解,如有不当之处欢迎指出)

      2.背包动规:                     

                    【背包】采药

    题目描述

    宁智贤是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。

    医师为了判断他的资质,给他出了一个难题。

    医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。

    如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

      如果你是宁智贤,你能完成这个任务吗?

    输入格式

    输入的第一行有两个整数T(1 <= T <= 1000)和M(1 <= M <= 100),用一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

    输出格式

    输出包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。

    样例数据

    input

    70 3
    71 100
    69 1
    1 2

    output

    3

    数据规模与约定

    时间限制:1s

    空间限制:256MB

    ----------------------------------------我是美美的分割线-------------------------------------------------------

       这道题就是最基本的01背包,对于每件物品,我们有取和不取两种选择.

      首先定义状态f[i][j]以j为容量为放入前i个物品(按i从小到大的顺序)的最大价值,那么i=1的时候,放入的是物品1,这时候肯定是最优的.

      由此,我们推出状态转移方程:f[i][j] = max(f[i-1][j-w[i]])+v[i],f[i-1][j]);

       其实,这道题还有一个滚动数组优化,可以优化第一维的空间。

      优化后的状态转移方程:f[j]=max(f[j-w[i]]+v[i],f[j]);

           下面是代码:

      

    #include<bits/stdc++.h>
    using namespace std;
    int t,m;
    int w[100010],v[100010];
    int f[100010];
    int main()
    {
        freopen("input.in","r",stdin);
        freopen("output.out","w",stdout);
        cin>>m>>t;
        for(int i=1;i<=t;i++)
            cin>>w[i]>>v[i];
        for(int i=1;i<=m;i++)
            for(int j=m;j>=w[i];j--)
                {
                    f[j]=max(f[j],f[j-w[i]]+v[i]);
                }
        cout<<f[m];
        return 0;
    }

      3.区间动规:

                                 【区间动规】石子合并

    题目描述

    在操场上沿一直线排列着n堆石子。现要将石子有次序地合并成一堆。

    规定每次只能选相邻的两堆石子合并成新的一堆,并将新的一堆石子数计为该次合并的得分。

    我们希望这n1次合并后得到的得分总和最小。

    输入格式

    第一行有一个正整数nn<=300),表示石子的堆数; 第二行有n个正整数,表示每一堆石子的石子数,每两个数之间用一个空格隔开。它们都不大于10000

    输出格式

    一行,一个整数,表示答案。

    样例数据

    input

    3
    1 2 9

    output

    15

    数据规模与约定

    区间dp第一题

    时间限制:1s

    空间限制:256MB

    --------------------------------------------我是华丽的分割线-----------------------------------------------------

       

      这样我们可以定义状态f[i][j],表示i到j合并后的最大得分。其中1<=i<=j<=N。

      既然这样,我们就需要将这一圈石子分割。很显然,我们需要枚举一个k,来作为这一圈石子的分割线。

      这样我们就能得到状态转移方程:

      f[i][j] = max(f[i][k] + f[k+1][j] + d(i,j));    

      其中,1<=i<=<=k<j<=N。d(i,j)表示从i到j石子个数的和。

      下面是代码:

      

    #include<bits/stdc++.h>
    
    #define din(a) (scanf("%d",&a));
    #define dout(a) (printf("%d
    ",a));
    #define ll long long
    
    using namespace std;
    int m,k;
    int n;
    int a[101000];
    int f[1001][1001];
    int sumn[1001];
    int cost[1001][1001];
    void work_cost()//计算合并的代价/得分.
    {
        for(int i=1;i<=n;i++)
            for(int j=i;j<=n;j++)
                cost[i][j]=sumn[j]-sumn[i-1];
    } 
    void init()
    {
        din(n);//初始化
        memset(f,0,sizeof(f));
        memset(sumn,0,sizeof(sumn));
        sumn[0]=0;//计算石子总数,方便累加得分.
        for(int i=1;i<=n;i++){
            din(a[i]);
            sumn[i]=sumn[i-1]+a[i];
        }
        work_cost(); 
    }
    void work() //区间动规
    {
        for(int p=1;p<=n;p++)
            for(int i=1;i<=n;i++){
                int j=i+p-1;
                if(j>n) break;
                for(int k=i;k<j;k++)
                    if((f[i][j]>f[i][k]+f[k+1][j]+cost[i][j]||(f[i][j]==0)))
                        f[i][j]=f[i][k]+f[k+1][j]+cost[i][j];
            }
    } 
    int main()
    {
        freopen("Stone.in","r",stdin);
        freopen("Stone.out","w",stdout);
        init();
        work();
        dout(f[1][n]); 
        return 0;
    }

      4.双进程DP

                                    构建双塔

    题目描述

      2001年9月11日,一场突发的灾难将纽约世界贸易中心大厦夷为平地,Mr. F曾亲眼目睹了这次灾难。为了纪念“911”事件,Mr. F决定自己用水晶来搭建一座双塔。

      Mr. F有N块水晶,每块水晶有一个高度,他想用这N块水晶搭建两座有同样高度的塔,使他们成为一座双塔,Mr. F可以从这N块水晶中任取M1MN)块来搭建。但是他不知道能否使两座塔有同样的高度,也不知道如果能搭建成一座双塔,这座双塔的最大高度是多少。所以他来请你帮忙。

      给定水晶的数量NN(1N100)和每块水晶的高度HiN块水晶高度的总和不超过2000),你的任务是判断Mr. F能否用这些水晶搭建成一座双塔(两座塔有同样的高度),如果能,则输出所能搭建的双塔的最大高度,否则输出“ImpossibleImpossible”。

    输入格式

    输入的第一行为一个数N,表示水晶的数量。

    第二行为N个数,第i个数表示第i个水晶的高度。

    输出格式

     输出仅包含一行,如果能搭成一座双塔,则输出双塔的最大高度,否则输出一个字符串“Impossible”。

    样例数据

    input

    5
    1 3 4 5 2

    output

    7

    数据规模与约定

    时间限制:1s

    空间限制:256MB

     

    -------------------我还是华丽的分割线-------------------------------

     

    水晶放置在任意一座塔上都会对另一座塔产生影响,故属于双进程问题。

    f[i][j]表示取前i块水晶、两塔差为j时较高塔的最大高度。

    注意,这里的f[i][j]都是从上一阶段推得的。我们在面对第i块水晶时,它可能是从以下四种决策得来的:

    f[i][j]=max(f[i1][j])f[i][j]=max(f[i−1][j]) . 这块水晶被丢掉了。

    f[i][j]=max(f[i1][j+h[i]])f[i][j]=max(f[i−1][j+h[i]]) . 这块水晶被给了上一个状态中较低的那座塔,且它未超过较高的塔,由图可知较高塔的最大高度是不变的。 

    f[i[][j]=max(f[i1][jh[i]]+h[i])f[i[][j]=max(f[i−1][j−h[i]]+h[i]) .这块水晶被给了上一个状态中较高的塔,由图可知,较高塔的值增加了h[i]h[i]。

    当然,此时我们要保证j>h[i]j>h[i]。f[i][j]=max(f[i1][h[i]j]+j)f[i][j]=max(f[i−1][h[i]−j]+j) .这块水晶被给了上一阶段较低的塔,且它超过了较高塔。由图可知,较高塔的值增加了jj。

    (感谢hh大佬提供思路)。

    以下为代码

    #include<bits/stdc++.h>
    using namespace std;
    int h[10010];
    int f[1010][1010];
    int n,sum=0;
    int main()
    {
        freopen("input.in","r",stdin);
        freopen("output.out","w",stdout);
        memset(f,-10,sizeof(f));
        f[0][0]=0;
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&h[i]);
            sum+=h[i];
        }
        for(int i=1;i<=n;i++)
            for(int j=0;j<=sum;j++)//f[i][j]表示前i个水晶选择完后,落差为j时的最优值 
            {
                f[i][j]=max(f[i-1][j],f[i-1][j+h[i]]);//不要水晶和要水晶的最优。
                if(j>=h[i]) f[i][j]=max(f[i-1][j-h[i]]+h[i],f[i][j]);
                else 
                    f[i][j]=max(f[i-1][h[i]-j]+j,f[i][j]);//状态转移 
            }
        if(f[n][0]) printf("%d
    ",f[n][0]);
        else
            printf("Impossible");
        return 0;
    }

      5.树形动规

                      树形DP例题1

    题目描述

    给定一棵n个点的无权树,问树中每个节点的深度和每个子树的大小? (以1号点为根节且深度为0)

    输入格式

    第1行:n。

    第2~n行:每行两个数x,y,表示x,y之间有一条边。

    输出格式

    n行,每行输出格式为:#节点编号 deep:深度 count:子树节点数(详见样例)

    样例数据

    input

    7
    1 2
    2 3
    1 4
    3 5
    1 6
    3 7

    output

    #1 deep:0 count:7
    #2 deep:1 count:4
    #3 deep:2 count:3
    #4 deep:1 count:1
    #5 deep:3 count:1
    #6 deep:1 count:1
    #7 deep:3 count:1

    数据规模与约定

    15% n<=10;

    40% n<=1000;

    100% n<=100000;

    ------------------------------我又是美美的分割线-------------------------------

      基本的树形,建立一个领接表就OK了。

      话不多说,直接上代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,p; 
    int head[1001000],size[1001000],dep[1001000];
    int cnt=0;
    int x,y;
    struct node
    {
        int to,next;
    }e[1001000];
    void add(int x,int y)
    {
        cnt++; 
        e[cnt].to=y;
        e[cnt].next=head[x];
        head[x]=cnt;
    }
    void dfs(int x,int fa,int depth)
    {
        size[x]=1;
        dep[x]=depth;
        for(int i=head[x];i;i=e[i].next)
        {
            int v=e[i].to;
            if(v==fa) continue;
            dfs(v,x,depth+1);
            size[x]+=size[v];
        }
    }
    int main()
    {
        freopen("tree.in","r",stdin);
        freopen("tree.out","w",stdout);
        memset(dep,0,sizeof(dep)); 
        scanf("%d",&n);
        for(int i=1;i<=n-1;i++)
        {
            scanf("%d%d",&x,&y);
            add(x,y);
            add(y,x);
        }
        dfs(1,0,0);
        for(int i=1;i<=n;i++)
        {
            printf("#%d deep:%d count:%d
    ",i,dep[i],size[i]);
        }
        return 0;
    }

      五.动态规划的意义:

      动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。

    好好理解动态规划吧!

     

     

  • 相关阅读:
    【BZOJ3218】 a+b Problem
    【BZOJ3993】 星际战争
    【BZOJ3991】 寻宝游戏
    【BZOJ2959】长跑 (LCT+并查集)
    NOIP2017记
    【CF 678F】Lena and Queries
    【Learning】多项式乘法与快速傅里叶变换(FFT)
    Subsequence Count (线段树)
    【留言板】
    【bzoj 4173】数学
  • 原文地址:https://www.cnblogs.com/smilke/p/10679345.html
Copyright © 2011-2022 走看看