动态规划入门

本文是有关动态规划的个人性总结,不保证治疗效果


动态规划,英文:Dynamic Programming,并不能简单的描述成一种算法,相对于BFS,DFS等看得见摸得着,容易理解的算法来说,DP要更玄学(真)一点。

基础的思想可以解释为,通过一个或多个最优状态,导出下一个最优状态从而得到最优解的过程。而从另外一个方面来说,DP其实是一种有计划的搜索,相比直接爆搜能够有效减少时间复杂度。

有几个基础概念需要解释一下,状态状态转移方程最优子结构无后效性以及记忆化搜索

状态大概是指你当前算到哪了,或者说对后面问题的求解唯一有影响的那些关键变量之间的组合。在DAG中应当是点的编号。

状态转移方程是指通过几个状态导出下一层状态的递推式。在DAG求最短路径中一个状态(点)的值等于所有能够只经过一条边就可以到达该点的点的值加上该边的权值求最小值。

最优子结构是指导出下一个最优状态的上一个状态必定也是最优的的这种性质,为DP所必须。

无后效性是指后面所做的决策不会对前面做的决策产生影响的性质,同样为DP所必须。例如后面做的最优决策不会导致前面的最优决策变得不是最优。

记忆化搜索是记录当前状态的最优值以避免重复计算增加时间复杂度的一种方法,这也是DP相比直接爆搜所做的一个重大优化,直接导致了其较优的时间复杂度(还有其他原因,例如它不会对已经不是最优的决策分支进行拓展)。

这些是基本概念,DP的精髓就在于状态与状态转移方程,找到了找对了什么都好说。可以以这样的思路思考:把对求解有影响的变量列出来,共n个,其中1个是其他条件特定时该条件能达到的最优解。寻找这两个东西是部分DP题的难点。

结合例题讲解,数字三角形,嵌套矩形以及0-1背包问题是最基础也最经典的DP问题,但是仅仅掌握了这些是不够的,对于初次接触动态规划的人来说过于单薄,需要主动接触有关问题。

给定一个数塔,要求从顶层走到底层,若每一步只能走到正下方的点或者右下方的结点,则经过的结点的数字之和最大是多少?

输入
第一行是一个整数N,表示数塔的高度,接下来用N行数字表示数塔,其中第i行有个i个整数。
输出
输出一个整数,表示路径之和的最大值。

样例输入

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5  

样例输出

30

提示

对于50%的数据,n的范围[1,10];
对于100%的数据,n的范围[1,100];
数字三角形中的数字范围,[-100,100];

这个问题,与后面求解有关的变量有1. 层数 2. 该层内的列数 3. 以该点作为起点能够达到的路径之和。

那么我们可以简单地定义该问题的状态为以该点作为起点能够达到的最大路径之和。

本题是个简单的DAG上最长路径问题(DAG:有向无环图)每一个点都指向它对应的左孩子和右孩子。之前说过,DAG上求最短路径的状态转移方程是所有能够一步到达该点的点的状态加上该边的权值求最小值,求最大路径类似,表现为所有能够一步到达该点的状态加上该边的权值求最大值。本题中表现为某点的状态等于左孩子的状态和右孩子的状态求最小值加上该点的值。即:

\[zhuangtai[ i ][ j ]=value[ i ][ j ]+min(zhuangtai[ i+1 ][ j ] , zhuangtai[ i+1 ][ j+1 ]); \]

由此我们便可以求解,但是如果直接递归,它的时间复杂是\(O(2^n)\),等于直接爆搜,一旦数据稍微大一点就会直接TLE或者更可怕地爆栈。原因在于,我们做了重复的计算,某点(子问题)的值已经被计算过,而在再次调用时却需要再次计算,通过记忆化搜索优化这点我们能够使时间复杂度降到\(O(n^2)\),这是一个巨大的优化!(但是空间复杂度也会增至\(O(n^2)\)

具体方法是用一个二维数组记录各点的状态,如果该点已经被计算过就直接返回数组里的值。或者直接递推。

最底层的状态我们已经得知,就是最底层点的值。而我们直接递推至顶层即可。伪代码如下:

for(层数n~1)
    for(列数1~i)
        状态[i][j]=值[i][j]+两个孩子的状态求最小值;(就是上面那个公式)

基于动态规划上的时间复杂度应该已经无法缩小,然而空间复杂度却能利用滚动数组优化为\(O(n)\)。我们在递推上一层状态时只用到了这一层状态,而之前的状态全部都是无用的。所以我们只需要用一条数组记录当前的状态,再在这条数组上覆盖下一层状态。(只当上一次的状态不会对下一次的覆盖产生影响的时候,如果担心上一次状态的遗留产生影响可以用两条数组)伪代码如下:

for(层数n~1)
    for(列数1~i)
        状态[j]=值[i][j]+状态[j]与状态[j+1]求最小值;

用两条数组的方法如下:

for(层数n~1){
    清空状态[p]中的元素;
    for(列数1~i)
        状态[p][j]=值[i][j]+状态[!p][j]与状态[!p][j+1]求最小值;
    p=!p;
    }

有些麻烦,但至少杜绝了隐患。
下个例题:嵌套矩形:

有n个矩形,每个矩形可以用a,b来描述,表示长和宽。

矩形\(X(a,b)\)可以嵌套在矩形\(Y(c,d)\)中当且仅当\(a<c,b<d\)或者\(b<c,a<d\)(相当于旋转X90度)。例如(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)中。

你的任务是选出尽可能多的矩形排成一行,使得除最后一个外,每一个矩形都可以嵌套在下一个矩形内。

首先需要建图,这题不像数字三角形那样呈现出明显的满二叉树结构,需要预处理出状态间的关系,用邻接矩阵储存。以下为代码:

#include<bits/stdc++.h>
using namespace std;
struct node{
    int l,w;
}brk[1005];//每个矩形
int n,dp[1005],ans;
bool Map[1005][1005];//邻接矩阵
int rep(int p){
    if(dp[p])
        return dp[p];
    dp[p]=1;
    for(int i=1;i<=n;i++)
        if(Map[p][i])
            dp[p]=max(dp[p],rep(i)+1);//求解该点最大值
    return dp[p];
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>brk[i].l>>brk[i].w;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(i!=j&&(brk[i].l<=brk[j].l&&brk[i].w<=brk[j].w||brk[i].l<=brk[j].w&&brk[i].w<=brk[j].l))
                Map[i][j]=1;//建图
    for(int i=1;i<=n;i++)
        ans=max(ans,rep(i));
    cout<<ans;
}

0-1背包问题也类似,不过状态变成了已经占用的体积和利用前n个物品能够达到的最大价值。状态转移方程为:

状态[ i ][ j ]=max(状态[ i-1 ][ j ] , 状态[ i-1 ][ j-体积[ i ] ]+值[ i ])

代码如下:

#include<bits/stdc++.h>
#define max(a,b) a>b?a:b
using namespace std;
int v,n,val[1005],c[1005],dp[20005];
int main(){
    cin>>v>>n;
    for(int i=1;i<=n;i++)
        scanf("%d%d",&c[i],&val[i]);
    for(int i=1;i<=n;i++)
        for(int j=v;j>=c[i];j--)
            dp[j]=max(dp[j-c[i]]+val[i],dp[j]);
    cout<<dp[v];
}

I'm Schwarzkopf Henkal.

posted @ 2019-10-18 16:19  Schwarzkopf_Henkal  阅读(141)  评论(0编辑  收藏  举报