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\n",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\n",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\n",i,dep[i],size[i]);
    }
    return 0;
}

  五.动态规划的意义:

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

好好理解动态规划吧!

优质内容筛选与推荐>>
1、Cuckoo for Hashing
2、CS3402 Integrity Constraints
3、年薪10w和年薪100w的人,差在哪里?
4、ASP.NET在本地服务器上创建目录并在该目录下写文件(转)
5、hexo建站报错解决记录


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号





    联系我们

    欢迎来到TinyMind。

    关于TinyMind的内容或商务合作、网站建议,举报不良信息等均可联系我们。

    TinyMind客服邮箱:support@tinymind.net.cn