动态规划总结

原创 2016年06月01日 23:08:12
                                     动态规划总结

小序:
最近一个月被动态规划折磨的不要不要的,然而期限已到,该专题被关闭,所以到了写份总结来复习与小结一下动态规划。总体说来,感觉DP的大部分题目都并不易做,虽然有些题目被人称作水题、入门题,不管怎么说吧,基本上关键的就是找状态转移方程了,这也是题目的难点所在。那好,本博客就来复习一下动态规划的基本内容和几类经典题目。

基础知识:
什么是动态规划呢,简单来说,动态规划是解决多阶段决策问题的一种方法。多阶段决策问题是说如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果。
动态规划的指导思想就是在做每一步决策时,列出各种可能的局部解;依据某种判定条件,舍弃那些肯定不能得到最优解的局部解;以每一步都是最优的来保证全局是最优的。
动态规划问题具有以下基本特征:
问题具有多阶段决策的特征;
每一阶段都有相应的“状态”与之对应,描述状态的量称为“状态变量”;
每一阶段都面临一个决策,选择不同的决策将会导致下一阶段不同的状态;
每一阶段的最优解问题可以递归地归结为下一阶段各个可能状态的最优解问题,各子问题与原问题具有完全相同的结构。
动态规划问题的一般解题步骤
1、判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
2、把问题分成若干个子问题(分阶段)。
3、建立状态转移方程(递推公式)。
4、找出边界条件。
5、将已知边界值带入方程。
6、递推求解。

题目分类:
最长上升子序列问题
问题描述:
一个数的序列bi,当b1 < b2 < … < bS 的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < …

int b[MAX_N + 10];
int aMaxLen[MAX_N + 10];
int main()
{
    int i, j, N;
    scanf("%d", & N);
    for( i = 1;i <= N;i ++ )
        scanf("%d", & b[i]);
    aMaxLen[1] = 1;
 for( i = 2; i <= N; i ++ ) 
    { //求以第i 个数为终点的最长上升子序列的长度
        int nTmp = 0; //记录第i 个数左边子序列最大长度
        for( j = 1; j < i; j ++ ) 
        { //搜索以第i 个数左边数为终点的最长上升子序列长度
            if( b[i] > b[j] ) 
            {
                if( nTmp < aMaxLen[j] )
                    nTmp = aMaxLen[j];
            }
        }
        aMaxLen[i] = nTmp + 1;
    }
    int nMax = -1;
    for( i = 1;i <= N;i ++ )
        if( nMax < aMaxLen[i])
            nMax = aMaxLen[i];
    printf("%d\n", nMax);
    return 0;
}

最大字段和问题
题目简介
描述:给定 N (1 <= N <= 100000) 个绝对值不大于 1000 的整数(可能为负数)组成的序列a[1],a[2],a[3],…,a[N],求从该序列中取出连续一个子段,使这个子段的和最大。如果某子序列全是负数则定义该子段和为 0。求 max{0,a[i]+a[i+1]+…+a[j]}, 1 <= i <= j <= N
输入:有一个正整数 N,后面紧跟 N 个绝对值不大于 1000 的整数
输出:最大子段和
样例输入:
5 6 -1 5 4 -7
样例输出:
14
样例解释
6 -1 5 4 -7
最大子段和:14
问题分析
状态设计:
dp[i] (1 <= i <= N) 表示以 a[i] 结尾的最大连续子段和。
显然,dp[i] >= 0 (1 <= i <= N)
求解目标:
max{dp[i]} (1 <= i <= N)
状态转移方程:
dp[i] = max{a[i],0} (i = 1)
dp[i] = max{dp[i-1] + a[i], 0} (2 <= i <= N)
简要代码:

int maxSubSum = 0;
dp[1] = Max(a[1], 0);
int i;
for (i = 2; i <= N; ++i)
    dp[i] = Max(dp[i - 1] + a[i], 0);
for (i = 1; i <= N; ++i) {
    if (maxSubSum < dp[i])
        maxSubSum = dp[i];
}

楼梯问题
题目简介
描述:(1)(NUAA 1350)某学校跳楼塔从底到顶只有一个楼梯,共 n 阶,一同学在第 k 次收到 CET-4 <= 250 的证书后欲爬上该塔自行了断,已知该同学每次可以上 1 阶或 2 阶,则他从塔底走到塔顶共有多少种方法?(2)当他爬到塔顶早已累得没有跳下去的勇气了,只想早点回到宿舍去上校内,此时他回城心切,可以每次下 1 阶、 2 阶或 3 阶,则他从塔顶走到塔底共有多少种方法?
输入:只有一个整数 n (1 <= n <= 30)
输出:只有 1 行,包含 2 个整数,中间用空格分开,分别为走上和走下 n 阶楼梯所对应的方法总数
样例输入:3
样例输出:3 4
样例解释
上楼时:
0 1 2 3
0 1 - 3
0 - 2 3
一共 3 种方法
下楼时:
3 2 1 0
3 2 - 0
3 - 1 0
3 - - 0
一共 4 种方法
问题分析
(1)上楼梯时
状态设计:f(i) 表示上 i 阶楼梯所对应的方法总数
求解目标:f(N)
状态转移方程:
f(i) = f(i-1) + f(i-2) 2 <= i <= N
f(i) = 1 0 <= i <= 1
(2)下楼梯时
状态设计:g(i) 表示下 i 阶楼梯所对应的方法总数
求解目标:g(N)
状态转移方程:
g(i) = g(i-1) + g(i-2) + g(i-3) 3 <= i <= N
g(i) = 2 i = 2
g(i) = 1 i = 1 或 i = 0
简要代码:

// 初始化:
int n, i, f[31], g[31];
scanf("%d", &n);
//上楼梯时:
f[1] = f[0] = 1;
for (i = 2; i <= n; ++i)
    f[i] = f[i - 1] + f[i - 2];
//下楼梯时:
g[1] = g[0] = 1;
g[2] = 2;
for (i = 3; i <= n; ++i)
    g[i] = g[i - 1] + g[i - 2] + g[i - 3];

花束摆放问题
1.问题描述
现在有F束不同品种的花束,同时有至少同样数量的花瓶被按顺序摆成一行,其位置固定于架子上,并从1至V按从左到右顺序编号,V是花瓶的数目(F≤V)。花束可以移动,并且每束花用1至F的整数唯一标识。标识花束的整数决定了花束在花瓶中排列的顺序,如果i

#include <iostream>
using namespace std;

int dp[105][105];
int a[105][105];

int max(int a,int b)
{
    return a>b?a:b;
}
int main()
{
    int n,m;
    cin>>n>>m;
    int i,j;
    for(i=1;i<=n;i++)
        for (j=1;j<=m;j++)
            cin>>a[i][j];
        for(i=1;i<=n;i++)
            for (j=i;j<=m;j++)
            {
                dp[i][j]=dp[i-1][j-1]+wei[i][j];
                if(j>i)
                    dp[i][j]=max(dp[i][j],dp[i][j-1]);
            }
            cout<<dp[n][m]<<endl;
            return 0;
}

背包问题:
01背包:
题目:
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
基本思路:
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
分析:
f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
例题:
poj3624 Charm Bracelet
有N件物品和一个容量为V的背包。第i件物品的费用是c,价值是w。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

空间优化:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
初始化细节:
若要求恰好装满背包,初始化时除了f[0]为0其它f[1..V]均设为-∞
若没有要求必须把背包装满,初始化时将f[0..V]全部设为0
代码:

#include<iostream>
using namespace std;
#define MAX_N 3500
#define MAX_V 20000
struct good
{
    int c;
    int w;
}goods[MAX_N];
int main()
{
    int i,j;
    int n,v;
    int f[MAX_V];
    while(cin>>n>>v)
    {
        for(i=0;i<=v;i++)f[i]=0;
        for(i=0;i<n;i++)cin>>goods[i].c>>goods[i].w;
        for(i=0;i<n;i++)
        {
            for(j=v;j>=goods[i].c;j--)
            {
                   if(f[j]<f[j-goods[i].c]+goods[i].w)
                   f[j]=f[j-goods[i].c]+goods[i].w;
            }
        }
        cout<<f[v]<<endl;
    }
    return 0;
}

完全背包:

题目 :
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
简单优化:
1)若两件物品i、j满足c[i]<=c[j] 并且w[i]>=w[j],则将物品j去掉,不用考虑 .
2)将费用大于V的物品去掉 .
转化为01背包:
考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题
伪码:
for i=1..N
for v=0..V
f[v]=max{f[v],f[v-cost]+weight}
注意:此处v的循环次序和01背包的循环次序不同!!!
假如合理的交换v,n的循环次序,貌似可以稍稍的提高效率.
下面分析为何伪码可以成立。最原始的代码应该是:
for i=1..N
for v=0..V
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0=

#include <stdio.h>
#include <string.h>
const int INF = 1000000000;
int  f[10010];
int main()
{
    int E, F, W, T, n, p, w,i , j;
    scanf("%d\n",&T);
    while(T--)
    {
        scanf("%d%d",&E,&F); W = F - E;
        scanf("%d",&n);
        for(i=1; i<=W; i++) f[i] = INF;
        f[0] = 0;
        for(i=0; i<n; i++)
        {
            scanf("%d%d",&p, &w);
                        for(j=w; j<=W; j++)
            if(f[j]>f[j-w]+p)
                f[j] = f[j-w]+p;
        }
        if(f[W]!=INF)
        printf("The minimum amount of money in the piggy-bank is %d.\n",f[W]);
        else
            printf("This is impossible.\n");
    }
    return 0;
}

多重背包:
题目:
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大 。

例题:
题意大致是给出一个数CASH,再给出N种钱,dk价值的钱nk个。要你求小于CASH的情况下最多可以支付多少钱。
因为每种货币的面值及数量已知,可以将其转化为多重背包,背包的容量即为cash
dp数组记录i价值的钱数是否可以拼凑成功。
dp[ i ] =dp[ i ] && dp[ i-dk*x ];
而x的大小有限,需要用use数组记录dk在凑成 i 价值时使用次数。
最后只需在dp[] 里面找出最大的i是dp[i]=1就行。

简要代码:

int main(){
    // freopen ("g.txt","r",stdin);
    int cash;
    int N;
    int nk[1010],dk[1010];
    int dp[100010],use[100010];
    while ( ~ scanf ("%d%d",&cash,&N)){
        memset(dp,0,sizeof(dp));
        for (int i=0;i<N;i++){
            cin>>dk[i]>>nk[i];
        }

    return 0;
}
        dp[0]=1;
        for (int i=0;i<N;i++){
            memset(use,0,sizeof(use));
            for (int j=dk[i];j<=cash;j++){
            if (!dp[j]&&dp[j-dk[i]]&&use[j-dk[i]]<nk[i]){
                    dp[j]=1;
                    use[j]=use[j-dk[i]]+1;
                }
            }
        }
        for (int i=cash;i>=0;i--){
            if (dp[i]){
                cout<<i<<endl;
                break;
            }}}
    return 0;
}

区间DP

有一根长度为L的木棍,木棍上面有M个切割点,每一次切割都要付出当前木棍长度的代价,问怎样切割有最小代价
分析:
1 区间DP
2 区间动态规划问题一般都是考虑,对于每段区间,他们的最优值都是由几段更小区间的最优值得到。将一个区间问题不断划分为更小的区间直至一个元素组成的区间,枚举他们的组合,求合并后的最优值。
3 设F[i,j](1<=i<=j<=n)表示区间[i,j]内的数字相加的最小代价 , 最小区间F[i,i]=0(一个数字无法合并,∴代价为0)每次用变量k(i<=k<=j-1)将区间分为[i,k]和[k+1,j]两段

4区间DP模板,代码

for(intp = 1 ; p <= n ; p++){//p是区间的长度,作为阶段
for(int i = 1 ; i <= n ; i++)
    {//i是穷举区间的起点
        int j = i+p-1;//j为区间的终点
        for(int k = i ; k < j ; k++)//状态转移
        dp[i][j] = min{dp[i][k]+dp[k+1][j]+w[i][j]};
    //这个是看题目意思,有的是要从k开始不是k+1
dp[i][j]= max{dp[i][k]+dp[k+1][j]+w[i][j]};

   }
 }

5 对于这一题,如果我们只对最左边的切割点到最右边的切割点进行DP,那么得到的答案肯定是错的,因为不是整个区间,所以我们必须在木棍的做左边和左右边分别增加一个点,那么得到的就是整个区间,再对这个区间进行DP求解即可

代码:

#define MAXN 55  
int len , m; 
int dp[MAXN][MAXN]; 
int cut[MAXN];//记录切割点 

void solve(){ 
    int i , j , k , p , tmp , min; 
    cut[0] = 0 ; cut[m+1] = len;//增加切点,那么切点有0-m+1 
    memset(dp , 0 , sizeof(dp)); 
    for(p = 1 ; p <= m+1 ; p++){//区间长度0-m+1 
        for(i = 0 ; i <= m+1-p ; i++){//区间起点0-m+1-p 
            j = i+p ; min = 999999999;//j为终点 
            for(k = i+1 ; k < j ; k++){//枚举k 
                tmp = dp[i][k]+dp[k][j]+cut[j]-cut[i];//这个地方注意不是从k+1开始 
                if(tmp < min) min = tmp;//更新min 
            } 

    if(min != 999999999) dp[i][j] = min;
    //如果min有更新过则赋值给dp[i][j] 
        } 
    } 
    printf("The minimum cutting is %d.\n" , dp[0][m+1]);
    //答案就是0-m+1这个区间 
} 

int main(){ 
    //freopen("input.txt" , "r" , stdin); 
    while(scanf("%d" , &len) && len){ 
        scanf("%d" , &m); 
        for(int i = 1 ; i <= m ; i++) scanf("%d" , &cut[i]); 
        solve(); 
    } 
    return 0; 
} 

所有内容复习至此,但是DP内容真是又难又多,所以,少年还需努力消化。

版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

NOIP算法总结——关于简单 线性动态规划

动态规划,显然是一个很让人头疼的地方,也没有个固定的算法,最多就是有一些模板(比如背包啊),要是想要增大做出来的机率,也就只好多做做题找找感觉了~线性动态规划可以说是DP中最简单的类型了,当然里面很多...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)