动态规划算法经典案例

转载自:https://blog.csdn.net/uestclr/article/details/50760563 

动态规划算法是从暴力搜索算法优化过来的,如果我们不清楚暴力搜索的过程,就难以理解动态规划的实现,当我们了解了动态规划算法的基本原理的文字概述,实现条件之后,这时可能并不是太理解这种思想,去面对实际问题的时候也是无从下手,这个时候我们不能停留在文字层面上,而应该去学习经典动态规划算法的实现,然后倒回来看这些概念,便会恍然大悟。

动态规划算法的难点在于 从实际问题中抽象出动态规划表dp,dp一般是一个数组,可能是一维的也可能是二维的,也可能是其他的数据结构。


动态规划的关键点:

1、最优化原理,也就是最有子结构性质。这指的是一个最优化策略具有这样的性质,无论过去状态和决策如何,对前面的决策所形成的状态而言,余下的决策必须构成最优策略,简单来说就是一个最优化策略的子策略总是最优的,如果一个问题满足最优化原理,就称其有最优子结构性质。

2、无后效性,指的是某个状态下的决策的收益,只与状态和决策相关,与达到该状态的方式无关。

3、子问题的重叠性,动态规划将原来指数级的暴力搜索算法改进到了具有多项式时间复杂度的算法,其中的关键在于解决了荣誉,重复计算的问题,这是动态规划算法的根本目的。

4、总体来说,动态规划算法就是一系列以空间换取时间的算法。

案例一:

有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。

分析:动态规划的实现的关键在于能不能准确合理的用动态规划表来抽象出 实际问题。在这个问题上,我们让f(n)表示走上n级台阶的方法数。

那么当n为1时,f(n) = 1,n为2时,f(n) =2,就是说当台阶只有一级的时候,方法数是一种,台阶有两级的时候,方法数为2。那么当我们要走上n级台阶,必然是从n-1级台阶迈一步或者是从n-2级台阶迈两步,所以到达n级台阶的方法数必然是到达n-1级台阶的方法数加上到达n-2级台阶的方法数之和。即f(n) = f(n-1)+f(n-2),我们用dp[n]来表示动态规划表,dp[i],i>0,i<=n,表示到达i级台阶的方法数。

/*dp是全局数组,大小为n,全部初始化为0,是题目中的动态规划表*/
int fun(int n){
    if (n==1||n==2)
        return n;
    /*判断n-1的状态有没有被计算过*/
    if (!dp[n-1])
        dp[n-1] = fun(n-1);
    if(!dp[n-2])
        dp[n-2]=fun(n-2);
    return dp[n-1]+dp[n-2];
}

案例2:
给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,那么路径1,3,1,0,6,1,0就是最小路径和,返回12.

1 3 5 9

8 1 3 4

5 0 6 1

8 8 4 0

分析:对于这个题目,假设m是m行n列的矩阵,那么我们用dp[m][n]来抽象这个问题,dp[i][j]表示的是从原点到i,j位置的最短路径和。我们首先计算第一行和第一列,直接累加即可,那么对于其他位置,要么是从它左边的位置达到,要么是从上边的位置达到,我们取左边和上边的较小值,然后加上当前的路径值,就是达到当前点的最短路径。然后从左到右,从上到下依次计算即可。

#include <iostream>
#include <algorithm>
using namespace std;
int dp[4][4] = {};
int main(){
    int arr[4][4] = {1,3,5,9,8,1,3,4,5,0,6,1,8,8,4,0};
    //cout << fun(arr,4,4) << endl;
     const int oo = ~0U>>2;
     for (int i = 0;i<4;i++)
         for (int j = 0; j < 4;j++)
             dp[i][j] = oo;
     //dp[0][0] = oo;
     for (int i = 0; i < 4;i++){
         for (int j = 0; j<4;j++){
             if (dp[i][j] == oo){
                if (i==0&&j==0)
                    dp[i][j] = arr[i][j];
                else if (i==0&&j!=0)
                    dp[i][j] = arr[i][j] + dp[i][j-1];
                else if(i!=0&&j==0)
                    dp[i][j] = arr[i][j] + dp[i-1][j];
                else{
                    dp[i][j] = arr[i][j]+min(dp[i-1][j],dp[i][j-1]);
                }
             }
         }
     }
    // cout << dp[3][3] << endl;
     for (int i = 0; i< 4;i++){
         for (int j = 0; j<4;j++){
            cout << dp[i][j] << "  ";
         }
         cout << endl;
     }
}


案例3:
给定数组arr,返回arr的最长递增子序列的长度,比如arr=[2,1,5,3,6,4,8,9,7],最长递增子序列为[1,3,4,8,9]返回其长度为5.

分析:

首先生成dp[n]的数组,dp[i]表示以必须arr[i]这个数结束的情况下产生的最大递增子序列的长度。对于第一个数来说,很明显dp[0]为1,当我们计算dp[i]的时候,我们去考察i位置之前的所有位置,找到i位置之前的最大的dp值,记为dp[j](0=<j<i),dp[j]代表以arr[j]结尾的最长递增序列,而dp[j]又是之前计算过的最大的那个值,我们在来判断arr[i]是否大于arr[j],如果大于dp[i]=dp[j]+1.计算完dp之后,我们找出dp中的最大值,即为这个串的最长递增序列。

#include <iostream>
#include <algorithm>
using namespace std;
/*动态规划表*/
int dp[5] = {};
int main(){
    int arr[5] = {2,4,5,3,1};
    dp[0] = 1;
    const int oo = 0;
    for (int i = 1;i<5;i++){
        int _max = oo;
        for (int j=0;j<i;j++)
            if(dp[j]>_max&&arr[i]>arr[j])
                _max = dp[j];
        dp[i] = _max+1;
    }
    int maxlist=0;
    for (int i = 0; i < 5;i++)
        if (dp[i] > maxlist)
            maxlist=dp[i];
    cout << maxlist << endl;
}


案例四:

给定两个字符串str1和str2,返回两个字符串的最长公共子序列,例如:str1="1A2C3D4B56",str2="B1D23CA45B6A","123456"和"12C4B6"都是最长公共子序列,返回哪一个都行。

分析:本题是非常经典的动态规划问题,假设str1的长度为M,str2的长度为N,则生成M*N的二维数组dp,dp[i][j]的含义是str1[0..i]与str2[0..j]的最长公共子序列的长度。

dp值的求法如下:

dp[i][j]的值必然和dp[i-1][j],dp[i][j-1],dp[i-1][j-1]相关,结合下面的代码来看,我们实际上是从第1行和第1列开始计算的,而把第0行和第0列都初始化为0,这是为了后面的取最大值在代码实现上的方便,dp[i][j]取三者之间的最大值。


int findLCS(string A, int n, string B, int m) {
        // n表示字符串A的长度,m表示字符串B的长度
        int dp[500][500] = {};
        for (int i = 0;i < n;i++)
        {
            for (int j = 0; j<m;j++)
            {
                if (A[i]==B[j])
                    dp[i+1][j+1] = dp[i][j]+1;
                else
                    dp[i+1][j+1] = max(dp[i+1][j],dp[i][j+1]);
            }
        }
        return dp[n][m];
    }

案例5:

背包问题,动态规划经典问题,一个背包有滴定的承重W,有N件物品,每件物品都有自己的价值,记录在数组V中,也都有自己的重量,记录在数组W中,每件物品只能选择要装入还是不装入背包,要求在不超过背包承重的前提下,选出的物品总价值最大。

分析:假设物品编号从1到n,一件一件的考虑是否加入背包,假设dp[x][y]表示前x件物品,不超过重量y的时候的最大价值,枚举一下第x件物品的情况:

情况1:如果选择了第x件物品,则前x-1件物品得到的重量不能超过y-w[x]。

情况2:如果不选择第x件物品,则前x-1件物品得到的重量不超过y。

所以dp[x][y]可能等于dp[x-1][y],也就是不取第x件物品的时候,价值和之前一样,也可能是dp[x-1][y-w[x]]+v[x],也就是拿第x件物品的时候,当然会获得第x件物品的价值。两种可能的选择中,应该选择价值较大的那个,也就是:

                            dp[x][y] = max{dp[x-1][y],dp[x-1][y-w[x]]+v[x]}

因此,对于dp矩阵来说,行数是物品的数量n,列数是背包的重量w,从左到右,从上到下,依次计算出dp值即可。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
    /*物品数量*/
    int n = 4;
    /*背包承重*/
    int cap = 10;
    int v[4] = {42,12,40,25};
    int w[4] = {7,3,4,5};
    /*二维动态规划表*/
    vector<int> p(cap+1,0);
    vector<vector<int>> dp(n+1,p);
 
    for (int i = 1;i <=n;i++){/*枚举物品*/
        for (int j = 1;j<cap+1;j++){/*枚举重量*/
            /*判断枚举的重量和当前选择的物品重量的关系
            如果枚举的和总量大于等于选择物品,则需要判断是否选择当前物品*/
            if (j-w[i-1]>=0)
                dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]);
            else
                /*如果枚举的重量还没有当前选择物品的重量大,那就只能是不取当前物品*/
                dp[i][j] = dp[i-1][j];
        }
    }
    cout << dp[n][cap] << endl;
}

案例6:

给定两个字符串str1,str2,在给定三个整数ic,dc,rc,分别代表插入,删除和替换一个字符的代价。返回将str1

编辑成str2的代价,比如,str1="abc",str2="adc",ic=5,dc=3,rc=2,从str1到str2,将'b'换成'd'代价最小,所以返回2.

分析:

在构建出动态规划表的时候,关键是搞清楚每个位置上数值的来源。首先我们生成dp[M+1][N+1]的动态规划表,M代表str1的长度,N代表str2的长度,那么dp[i][j]就是str1[0..i-1]变成str2[0...j-1]的最小代价,则dp[i][j]的来源分别来自以下四种情况:

a、首先将str1[i-1]删除,变成str1[0...i-2],然后将str1[0...i-2]变成str2[0...j-1],那么dp[i-1][j]就代表从str1[0..i-2]到str2[0...j-1]的最小代价,所以:dp[i][j] = dp[i-1][j]+dc;

b、同理也可以是从str1[0...i-1]变成str2[0...j-2],然后在插入str2[j-1],dp[i][j-1]就代表从str1[0...i-1]变成str2[0...j-2]的最小大家,所以:dp[i][j] = dp[i][j-1]+ic;

c、如果str[i-1] == str2[j-1],则只需要将str1[0...i-2]变成str2[0...j-2],此时dp[i][j] = dp[i-1][j-1];

d、如果str1[i-1]!=str2[j-1],则我们只需要将str1[i-1]替换成str2[j-1],此时dp[i][j] = dp[i-1][j-1]+rc;

在这四种情况当中,我们选取最小的一个,即为最小代价。

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
using namespace std;
 
int main(){
    string str1 = "ab12cd3";
    string str2 = "abcdf";
    //cin>>str1;
    //cin>>str2;
    const int M = str1.length();
    const int N = str2.length();
    //vector<int> p(M+1,0);
    //vector<vector<int>> dp(N+1,p);
    int dp[10][10] = {};
    int ic=5,dc=3,rc=2;
    //int ic = 1,dc=1,rc=1;
    dp[0][0] = 0;
    for (int i = 1;i<N+1;i++)
        dp[0][i] = ic*i;
    for (int i = 1;i<M+1;i++)
        dp[i][0] = dc*i;
 
    for (int i=0;i<M;i++){
        for (int j = 0;j<N;j++){
            int x = min(dc+dp[i+1][j],dp[i][j+1]+ic);
            if (str1[i]!=str2[j])
                dp[i+1][j+1] = min(dp[i][j] + rc,x);
            else 
                dp[i+1][j+1] = min(dp[i][j],x);
        }
    }
    cout << dp[M][N] << endl;
}

 

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值