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堆石子。现要将石子有次序地合并成一堆。

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

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

输入格式

第一行有一个正整数n(n<=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块水晶中任取M(1≤M≤N)块来搭建。但是他不知道能否使两座塔有同样的高度,也不知道如果能搭建成一座双塔,这座双塔的最大高度是多少。所以他来请你帮忙。

  给定水晶的数量NN(1≤N≤100)和每块水晶的高度Hi(N块水晶高度的总和不超过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[i−1][j])f[i][j]=max(f[i−1][j]) . 这块水晶被丢掉了。

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

f[i[][j]=max(f[i−1][j−h[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[i−1][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;
}

  五.动态规划的意义:

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

好好理解动态规划吧!

原文地址:https://www.cnblogs.com/smilke/p/10679345.html

时间: 2024-10-11 04:09:21

DP(动态规划)学习心得的相关文章

ACM 学习心得

ACM 学习心得 STL:完美的艺术品 STL 由四大部分组成:算法.容器.迭代器.仿函数. 算法(algorithm) 算法定义了一组与实现无关的操作,也是 ACM 学习的核心.C++ 算法库的内容全都是一些比较基本的算法,包括移动.转换.遍历.删除.过滤等等.C++ 算法库本身是基于抽象的,在迭代器的抽象下,使得这些算法可以在不同结构的容器中重用.一个比较坑的地方就是我高中的时候学完 C++ 之后报名了 NOIP.那一年刚刚允许用 STL(之前一直不准用),然后我对于标准库的依赖很严重,连快

(转)dp动态规划分类详解

dp动态规划分类详解 转自:http://blog.csdn.NET/cc_again/article/details/25866971 动态规划一直是ACM竞赛中的重点,同时又是难点,因为该算法时间效率高,代码量少,多元性强,主要考察思维能力.建模抽象能力.灵活度. ****************************************************************************************** 动态规划(英语:Dynamic programm

Linux系统理解以及Linux系统学习心得

原创作品转载请注明出处  <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 作者:严哲璟 说一下我对Linux系统的理解 1.加载Linux内核准备:在加载基本输入输出模块(BIOS)之后,从磁盘的引导扇区读入操作系统的代码文件块到内存中,之后开始整个系统的初始化. 2.main.c的start_kernel函数是整个操作系统的入口,这也与Linux是基于C语言的特性相符,start_kernel具体做的动作很多

我的MYSQL学习心得(八)

我的MYSQL学习心得(八) 我的MYSQL学习心得(一) 我的MYSQL学习心得(二) 我的MYSQL学习心得(三) 我的MYSQL学习心得(四) 我的MYSQL学习心得(五) 我的MYSQL学习心得(六) 我的MYSQL学习心得(七) 这一篇<我的MYSQL学习心得(七)>将会讲解MYSQL的插入.更新和删除语句 同样的,只会讲解跟SQLSERVER不同的地方 插入 将多行查询结果插入到表中 语法 INSERT INTO table_name1(column_list1) SELECT (

我的MYSQL学习心得(一)

我的MYSQL学习心得(一) 使用MYSQL有一段时间了,由于公司使用SQLSERVER和MYSQL,而且服务器数量和数据库数量都比较多 管理起来比较吃力,在学习MYSQL期间我一直跟SQLSERVER进行对比 第一期主要是学习MYSQL的基本语法,陆续还有第二.第三.第四期,大家敬请期待o(∩_∩)o 语法的差异 我这里主要说语法的不同 1.默认约束 区别:mysql里面DEFAULT关键字后面是不用加括号的 --sqlserver CREATE TABLE emp ( id INT DEFA

在马哥linux运维学院学习心得

题目:在马哥linux运维学院学习心得 姓名:谭龙 班级:M18 学号:26 时间:2016-02-29--2016-06-02(正常毕业时间预计在7月中上旬)   正文: 个人基本情况: 我是一名在校的即将毕业的大四学生,毕业时间为2016.7.专业为矿物加工工程专业,纯正的四川-广安人(邓小平故居就在那).因找不到工作,加上自己也不知道干什么,在堂弟的推荐下,来参加了马哥linux运维学院的学习:怀揣着一颗对计算机懵懂的心,开始涉足从未接触过了linux. 个人收获与心理变化: 在一开始接触

第一篇大数据学习心得

之前未习惯发布学习心德博文,后续会采用这种方式发布学习心得,希望能够很好的督促自己. 计划会按scala,Hadoop,Spark的顺序去学习. 刚学scala的时候,眼前一亮,这语法跟python,java很像啊,刚好两者很熟悉,偷笑,后面果然学的得心应手.今天就不发表具体的技术内容.反正王学林老师的视屏讲解很好,声音非常富有感染力,想开小差都比较难,呵呵,话语精炼,个人较喜欢的风格,这里说下这段时间学习scala的小心得?,视频学完一章紧接着进行敲代码,调试,最后记笔记,对,记笔记,不一定是

spring核心知识(学习心得)

直接进入主题,主要分为两大部分:框架学习心得和spring框架的核心知识. 学习心得 1.学习框架的时候,一定要弄清楚的几个问题: a. 这是一个什么框架 轻量级还是重量级, 侵入式还是非侵入式,是解决单个问题还是整体的解决方案. b. 框架的设计理念是什么(为了解决什么问题而出现) c. 框架的优缺点 d. 框架的架构是怎样的 e. 框架的核心是什么 f. 框架能实现哪些功能 在学习一个框架的时候如果都不知道它能够提供哪些功能,就更加不用谈功能实现和充分利用框架了 2. 在学习多个框架以后,如

C++用法的学习心得

c++这门课,在我刚进入大学的就已经开始接触了.因为自己的专业就是计算机科学,因此c++嘛,对于我来说还是比较重要的.不同于其他专业,一开始我接触就是c++了,跳过了c语言一类的课.就我自己认为,c++这课学起来还是很有难度的.大一上课的时候,老师就说过这课在生活中的应用很广泛.处于初学者的我,开始给我的感觉就是很是乏味枯燥,提不起兴趣.不过仔细想想自己的专业就是和它有关,就算将来自己不从事这个行业,还是很有学习它的必要.因为多一门技术总归是不会吃亏的. 作为男生嘛,自己没有少玩游戏.很多人玩游