算法设计与分析——动态规划算法设计

前言

NEFU,东林,计算机与控制工程学院,基于C/C++的算法设计与分析课程

实验二  动态规划算法设计

环境

操作系统:Windows 10
IDE:Visual Studio Code、Dev C++ 5.11、Code::Blocks

说明

 “实验二  动态规划算法设计” 包含以下问题

  1. 数字三角问题
  2. 最长公共子序列问题
  3. 日常购物

其他联系方式:

Gitee:@不太聪明的椰羊

B站:@不太聪明的椰羊

一、实验目的

        掌握动态规划算法的基本思想及适用条件,掌握动态规划算法的设计步骤和具体实现。

二、实验原理

        算法总体思想:动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题但是经分解得到的子问题往往不是互相独立的。不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。

动态规划算法设计步骤:

        (1)找出最优解的性质,并刻划其结构特征。

        (2)递归地定义最优值。

        (3)以自底向上的方式计算出最优值。

        (4)根据计算最优值时得到的信息,构造最优解。


三、实验内容

1、数字三角问题

问题描述:给定一个由n行数字组成的数字三角形,如下所示

          7

       3   8

     8   1   0

  2    7   4   4

4    5   2   6   5

        试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大。如上图最大值为 30=7+3+8+7+5

        从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到想一个和,和最大的路径称为最佳路径。 


注意:路径上的每一步只能从一个数走到下一层上和它最近的左边的数或者右边的数。

​1.1 分析

        抽象的方法思考问题:把当前的位置(i,j)看成一个状态,然后定义状态(i,j)的指标函数d(i,j)为从格子(i,j)出发时能得到的最大和(包括格子(i,j)本身的值)。在这个状态定义下,原问题的解是d(1,1)。


        下面看看不同状态之间是如何转移的。

        从格子(i,j)出发有两种决策。如果往左走,则走到(i+1,j)后需要求从(i+1,j)出发后能得到的最大和这一问题,即d(i+1,j)。类似的,往右走之后需要求解d(i+1,j+1)。由于可以在这两个决策中自由选择,所以应选择d(i+1,j)和d(i+1,j+1)中较大的一个,换句话说,得到了所谓的状态转移方程:

d(i, j) = a(i, j) + max{ d(i+1, j) , d(i+1, j+1) }

        如果往左走,那么最好情况等于(i,j)格子里的值a(i,j)与从(i+1,j)出发的最大总和之和,此时需注意这里的最大二字。如果连从(i+1,j)出发走到底部这部分的和都不是最大的,加上a(i,j)之后肯定也不是最大的。这个性质称为最优子结构,也可以描述成全局最优解包含局部最优解。不管怎样,状态和状态转移方程一起完整的描述了具体的算法。

        首先定义一个二维数组 d[N][N] 用来表示指标函数 d(i, j),其中 (i, j) 表示三角形中的格子位置,d(i, j) 表示从格子 (i, j) 出发时能得到的最大和(包括格子 (i, j) 本身的值)。
        用户输入三角形的行数 n,然后依次输入每个格子的值存储在二维数组 a[N][N] 中,并初始化指标函数 d(i, j) 的初值为格子本身的值。
        从倒数第二行开始,利用动态规划的思想逐行计算指标函数值 d(i, j)。对于每个格子 (i, j),更新它的值为当前值加上从下一行相邻两个格子中选择较大的值。
        最终输出 d[1][1],即为整个三角形中从顶部到底部能够得到的最大路径和。
        

        这是一个经典的动态规划问题,通过自底向上的方式逐步计算最优解,避免了重复计算,从而有效地解决了三角形最大路径和问题。

1.2 代码

#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <math.h>
using namespace std;

#define N 1024
int d[N][N]; // 指标函数d(i,j)为从格子(i,j)出发时能得到的最大和(包括格子(i,j)本身的值)
// 本题的解为d[1][1]
int a[N][N];
int main()
{
	int n, i, j;
	cin >> n;
	for (i = 1; i <= n; i++) // 1-n行
	{
		for (j = 1; j <= i; j++) // 1-i列
		{
			cin >> a[i][j];	   // 输入三角形各个位置的值
			d[i][j] = a[i][j]; // 指标函数d(i,j)赋初值
		}
	}
	for (i = n - 1; i >= 1; i--) // 从n-1行开始计算指标函数d(i,j)值
	{
		for (j = 1; j <= i; j++)
		{
			d[i][j] += max(d[i + 1][j], d[i + 1][j + 1]);
		}
	}
	cout << d[1][1];
	return 0;
}

1.3 测试

2、最长公共子序列问题

问题描述:给定两个序列X={x1,x2,...,xm}和Y={y1,y2,...,yn},找出X和Y的最长公共子序列。

输入:

第1行:两个子序列的长度,m n

第2行:第1个子序列的各个元素(序列下标从1开始)

第3行:第2个子序列的各个元素(序列下标从1开始)

输出:

最长公共子序列

实例:

输入:

第1行:

4 5            //m和n的值

第2行

abad        //输入4个字符,下标从1开始

第3行

baade      //输入5个字符,下标从1开始

输出:

aad

2.1 分析

        由最长公共子序列问题的最优子结构性质建立子问题最优值的递归关系。用c[i][j]记录序列和的最长公共子序列的长度。其中, Xi={x1,x2,…,xi};Yj={y1,y2,…,yj}。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列。故此时C[i][j]=0。其它情况下,由最优子结构性质可建立递归关系如下:

        定义两个二维数组 c[N][N] 和 b[N][N],分别用来存储最优值和标记函数。其中 c[i][j] 表示序列 x 的前 i 个元素与序列 y 的前 j 个元素的最长公共子序列长度,b[i][j] 用于追踪最优解的方向。

        LCSLength 函数用于计算最长公共子序列的长度。通过动态规划的方法填充二维数组 c 和 b。具体步骤包括初始化边界条件为0,然后根据当前元素是否相等来更新最优值 c[i][j] 和标记函数 b[i][j]。

        LCS 函数用于根据标记函数构造最优解。从右下角开始,根据标记函数的指示逆向输出构成最长公共子序列的元素。

2.2 代码

#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <math.h>
using namespace std;
#define N 1024
int c[N][N];//最优值
int b[N][N];//标记函
void LCSLength(int m, int n, char *x, char *y, int c[][N], int b[][N]) // 计算最优值
{                                                                // m=序列x长度,n=序列y长度。c:最长公共子序列长度;b:标记函数,追踪最优解
    int i, j;
    for (i = 1; i <= m; i++)
        c[i][0] = 0;
    for (i = 1; i <= n; i++)
        c[0][i] = 0;
    for (i = 1; i <= m; i++)
        for (j = 1; j <= n; j++)
        {
            if (x[i-1] == y[j-1])//x序列从x[0]开始,y序列从y[0]开始
            {
                c[i][j] = c[i - 1][j - 1] + 1;
                b[i][j] = 1;
            }
            else if (c[i - 1][j] >= c[i][j - 1]) // 上方的大
            {
                c[i][j] = c[i - 1][j];
                b[i][j] = 2;
            }
            else // 左方的大
            {
                c[i][j] = c[i][j - 1];
                b[i][j] = 3;
            }
        }
}

void LCS(int i, int j, char *x, int b[][N]) // 根据最优值构造最优解
{                                        // b:标记函数,追踪最优解
    if (i == 0 || j == 0)
        return;
    if (b[i][j] == 1) // 向左上
    {
        LCS(i - 1, j - 1, x, b);
        cout << x[i-1];//x序列从x[0]开始
    }
    else if (b[i][j] == 2) // 向上
        LCS(i - 1, j, x, b);
    else // 向左
        LCS(i, j - 1, x, b);
}

int main()
{
    int m, n;
    char x[N];
    char y[N];
    cout << "输入序列x:";
    cin >> x;
    cout << "输入序列y:";
    cin >> y;
    m = strlen(x);
    n = strlen(y);
    LCSLength(m, n, x, y, c, b);
    LCS(m, n, x, b);
    return 0;
}

2.3 测试

3、 日常购物

        问题描述:小明今天很开心,因为在家买的新房子即将拿到钥匙。新房里面有一间他自己专用的、非常宽敞的房间。让他更高兴的是,他的母亲昨天对他说:“你的房间需要购买什么物品?怎么布置,你说了算,只要他们的价格总和不超过N元钱”。小明今天早上开始预算,但他想买太多的东西,肯定会超过母亲的N元限额。因此,他把对每件物品的渴望程度,分为5等级:用整数1->5表示,第5等表示最想要。他还从互联网上找到了每件商品(所有整数)的价格。他希望在不超过N元(可能等于N元)的情况下,将每件商品的价格与效益度的乘积的总和最大化.

        设第j件物品的价格为p[j],重要度为w[j],其选中的k件商品,编号依次为j1,j2,……,jk,则所求的总和为:

p[j1]×w[j1]+p[j2]×w[j2]+ …+p[jk]×w[jk]。

请帮小明设计一个符合要求的购物清单。

其中N=2000,K=6

p[1]=200 w[1]=2

p[2]=300 w[2]=2

p[3]=600 w[3]=1

p[4]=400 w[4]=3

p[5]=1000 w[5]=4

p[6]=800 w[6]=5

3.1 分析

① 问题分析:
        根据这个题目,我们需要在金额一定的情况下求解舒适度最高的购物单,可见这是一个最优化问题。又因为小明在做出每一个选择都跟前面已经做出的选择有关,这种子问题具有重叠性的最优化问题求解,动态规划占有很大的优势。
② 模型建立:
        为了设计一个动态规划算法,需要推导出一个递推关系。考虑一个由前i[1≤i≤K]个商品定义的实例。
        设商品的价格依次为P1,P2,...Pi,商品对应的效益度分别为v1,v2,...vi,用户的预算为N元。
        dp[i][N]表示:在前i个物品中选择购买物品,在不超过N元(可以等于N元)的前提下,使每件物品的价格与效益度的乘积的最大化的购物清单。
        可以把在N元预算下,前i个物品的购买清单子集分成两个类别:包含第i个物品的子集和不包含第i个物品的子集。然后就有如下的结论:
(1) 根据定义,在不包含第i个物品的子集中,最优子集中每件物品的价格与效益度的乘积的总和为dp[i-1,N];
(2) 在包含第i个物品的子集中[因此,N-Pi≥0],最优子集是由该物品和前i-1个物品的购买清单的最优子集组成。这种最优子集中每件物品的价格与效益度的乘积的总和为{pi×vi+d[pi-1,N-pi]};
(3) 因此,在前i个物品中最优子集的价格与效益度的乘积的总和等于这两个总和中的最大值。当然,如果当预算不足以购买第i个物品时,前i个物品中最优子集的价格与效益度的乘积的总和等于前i-1个物品中最优子集的价格与效益度的乘积的总和。这个结果导致了如下的递推式:

Max{dp[i-1,N],pi×vi+dp[i-1,N-pi]}

即所得购物单为:J1,J5,J6。

总结:当通过分析得到相应的动态规划状态方程时,用其求解问题的效率会优于普通的理论计算。

该问题可以转化为0-1背包问题

对应关系如下:

效益度

约束

约束上限

物品数量

最优值

最优解

0-1背包

价值v[n]

重量w[n]

背包承重c

n

m[i][j]

m[1][c]

购物问题

渴望度v[n]

价格p[n]

预算N

n

dp[i][j]

dp[1][N]

        子问题的最优值为dp[i][j],即dp[i][j]是预算为容量为j,可选择物品为i,i+1,…,n时购物问题的最优值。由购物问题的最优子结构性质,可以建立计算dp[i][j]的递归式如下:

dp[n][j] = 0;// 0 <= j <= p[n]-1时
dp[n][j] = p[n] * v[n];// p[n] <= j <= n时

dp[i][j] = dp[i + 1][j];// 0 <= j <= p[i]-1时
dp[i][j] = max( dp[i + 1][j] , (dp[i + 1][j - p[i]] + p[i] * v[i]) );// p[i] <= j <= n时

        注意:这里dp[i][j]存的值应该是“预算为j,能买的物品为i~n时,所能达到的最大(价格与效益度的乘积 )的和,即p[i] * v[i]”。

3.2 代码

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <math.h>
using namespace std;
#define M 2048
int x[M]={0};  // 最优解
int dp[M][M]; // 最优值

// 转化为0-1背包问题
//           效益度     约束     最优值   最优解
//0-1背包    价值v      重量w      m        x
//购物     渴望程度v    价格p      dp       X

/*
dp[n][j] = 0;// 0 <= j <= p[n]-1时
dp[n][j] = p[n] * v[n];// p[n] <= j <= n时

dp[i][j] = dp[i + 1][j];// 0 <= j <= p[i]-1时
dp[i][j] = max( dp[i + 1][j] , (dp[i + 1][j - p[i]] + p[i] * v[i]) );// p[i] <= j <= n时
*/


// 自底向上计算最优值
void Knapsack(int *v, int *p, int N, int n, int dp[][M])
{   //v是渴望程度,p是价格,N是预算,n是商品个数
    //dp[i][j]是在 预算为j,能买的物品为i~n时,所能达到的 最大(价格与效益度的乘积 )的和
    int jMax, j, i;

    // 计算仅第n个物品可买时,dp[n][j]的值。
    jMax = (p[n] - 1) < N ? (p[n] - 1) : N;
    for (j = 0; j <= jMax; j++)//预算0 a~ p[n]-1时,dp为0
        dp[n][j] = 0;
    for (j = p[n]; j <= N; j++)//预算p[n] ~ N时,dp为v[n]
        dp[n][j] = p[n] * v[n];

    for (i = n - 1; i > 1; i--)//自底向上开始求解最优值
    {
        jMax = (p[i] - 1) < N ? (p[i] - 1) : N;
        for (j = 0; j <= jMax; j++)//预算0 ~ p[i]-1时
            dp[i][j] = dp[i + 1][j];
        for (j = p[i]; j <= N; j++)//预算p[i] ~ N时
            dp[i][j] = dp[i + 1][j] > (dp[i + 1][j - p[i]] + p[i] * v[i]) ? dp[i + 1][j] : (dp[i + 1][j - p[i]] + p[i] * v[i]);
    }
    dp[1][N] = dp[2][N];
    if (N >= p[1])
        dp[1][N] = dp[1][N] > (dp[2][N - p[1]] + p[1] * v[1]) ? dp[1][N] : (dp[2][N - p[1]] + p[1] * v[1]);
}

void Traceback(int dp[][M], int *p, int N, int n, int *x)
{  //p是价格,N是预算,n是商品个数, x是商品是否购买的0/1序列
   //dp[i][j]是在 预算为j,能买的物品为i~n时,所能达到的最大渴望程度和
    int i;
    for (i = 1; i < n; i++)
        if (dp[i][N] == dp[i + 1][N])//说明物品i没有购买(放入背包)
            x[i] = 0;
        else
        {
            x[i] = 1;
            N = N - p[i];
        }
    x[n] = (dp[n][N]) ? 1 : 0;//单独判断最后一个物品n是否购买
}

int main()
{
    int N, K, j;
    int p[M],v[M];//p为价格,v为渴望程度

    N = 2000;//预算
    K = 6;//商品个数
    p[1] = 200;v[1] = 2;
    p[2] = 300;v[2] = 2;
    p[3] = 600;v[3] = 1;
    p[4] = 400;v[4] = 3;
    p[5] = 1000;v[5] = 4;
    p[6] = 800;v[6] = 5;
    Knapsack(v, p, N, K, dp);
    Traceback(dp, p, N, K, x);
    cout << endl;
    cout << "价格与效益度的乘积的总和最大为:";
    cout << dp[1][N] << endl;//dp[1][N]指 能买的物品为1~n所以的商品,预算为N时的价格与效益度的乘积的总和最大值,即为本问题的最优解
    cout << "购物单为:";
    for (j = 1; j <= K; j++)
    {
        if(x[j]) {
            cout << "J" << j << " ";
        }
    }
    return 0;
}

3.3 测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值