<算法学习>动态规划练习题

本篇文章为初学动态规划时的练习题。参考优质博客学习后根据伪代码描述完成代码。记录一下用于以后复习。

一、数字三角形问题

问题描述

给定一个有n行数字组成的数字三角形. 试设计一个算法, 计算出从三角形的顶至底的一条路径, 使该路径经过的数字和最大.

算法设计: 对于给定的n行数字组成的三角形, 计算从三角形顶至底的路径经过的数字和的最大值.

数据输入: 第1行数字三角形的行数n, 1<=n<=100. 接下来n行是数字三角形各行中的数字. 所有数字在0~99之间.

结果输出: 第1行中的数是计算出的最大值.

测试输入期待的输出时间限制内存限制额外进程
测试用例 1以文本方式显示
  1. 5↵
  2. 7↵
  3. 3 8↵
  4. 8 1 0 ↵
  5. 2 7 4 4↵
  6. 4 5 2 6 5↵
以文本方式显示
  1. 30↵
1秒64M0

 问题分析

学习这篇文章     入门DP | 1:数字三角形问题

代码练习

#include <iostream>
#include <algorithm>
#define N 1000
using namespace std;
int a[N][N];
int dp[N][N];
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<=i;j++)
        {
            cin>>a[i][j];
            getchar();
        }
    }
    for(int i=n-1;i>=0;i--)   //自底向上计算
    {
        for(int j=0;j<=i;j++)
        {
            if(i==n-1)   //最后一层直接加
            {
                dp[i][j]=a[i][j];
            }
            else   //上层要考虑本身值加上下层左右孩子中最大的
            {
                dp[i][j]=a[i][j]+max(dp[i+1][j],dp[i+1][j+1]);
            }
        }
    }
    cout<<dp[0][0]<<endl;
    return 0;
}

二、矩阵链乘问题

问题描述

输入:

共两行

第一行 N ( 1<=N<=100 ),代表矩阵个数。

第二行有 N+1 个数,分别为 A1 、 A2 ...... An+1 ( 1<=Ak<=2000 ), Ak 和 Ak+1 代表第 k 个矩阵是个 Ak X Ak+1 形的。

输出:

共两行

第一行 M ,为最优代价。注:测试用例中 M 值保证小于 2^31

第二行为最优顺序。如 (A1((A2A3)A4)) ,最外层也加括号。

注意:测试用例已经保证了输出结果唯一,所以没有AAA的情况.

测试输入期待的输出时间限制内存限制额外进程
测试用例 1以文本方式显示
  1. 6↵
  2. 30 35 15 5 10 20 25↵
以文本方式显示
  1. 15125↵
  2. ((A1(A2A3))((A4A5)A6))↵
1秒64M0

算法分析 

 学习这篇文章  区间DP | 1:矩阵链乘问题(含优化) —— 例题:矩阵链乘、合并石子

代码练习

#include <iostream>
#include <limits.h>
using namespace std;
#define N 110
int dp[N][N];
int part[N][N];
int a[N];
void Matrix_multiply(int n)
{
    for(int l=2;l<=n;l++)  //l表示从2链矩阵开始求最优值,一直讨论到n链矩阵
    {
        for(int i=1;i<=n-(l-1);i++)  //i是从1到n-(l-1)的,因为2链矩阵组数会吞掉一个矩阵,3链会吞掉2个,以此类推
        {
            int j=i+l-1;   //j表示该组矩阵链中最后一个矩阵的列数下标
            dp[i][j]=INT_MAX;  //初始全设为无穷大
            for(int k=i;k<j;k++)  //在i到j的范围内找最优划分
            {
                int t=dp[i][k]+dp[k+1][j]+a[i-1]*a[k]*a[j];  //注意第i个矩阵的行列大小是a[i-1]*a[i]
                if(t<dp[i][j])  //如果找到优于最优划分情况的,就更新最优乘法次数和最优划分位置
                {
                    dp[i][j]=t;
                    part[i][j]=k;
                }
            }
        }
    }
}
void PrintPart(int i,int j)  //输出划分结果
{
    if(i==j) cout<<"A"<<i;  //i=j表示此时只有一个矩阵相乘,直接输出
    else    
    {
        int k=part[i][j];   //否则需要递归输出划分结果
        cout<<"(";
        PrintPart(i,k);
        PrintPart(k+1,j);
        cout<<")";
    }
}
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<=n;i++)
    {
        cin>>a[i];
    }
    Matrix_multiply(n);
    cout<<dp[1][n]<<endl;  //不管几个矩阵都要先输出最少乘法次数
    if(n==1)   //单独讨论只有一个矩阵的情况,别忘了加()
    {
        cout<<"(A1)"<<endl;
        return 0;
    }
    PrintPart(1,n);
    cout<<endl;
    return 0;
}

三、石子合并问题

问题描述

在一个圆形操场的四周摆放着n堆石子. 现在要将石子有次序地合并成一堆. 规定每次只能选相邻的2堆石子合并成一堆, 并将新的一堆石子数记为该次合并的得分. 试设计一个算法, 计算出将n堆石子合并成一堆的最小得分和最大得分.

算法设计: 对于给定n堆石子, 计算合并成一堆的最小得分和最大得分.

数据输入: 第1行是正整数n, 1<=n<=100, 表示有n堆石子. 第2行有n个数, 分别表示n堆石子的个数.

结果输出: 第1行是最小得分, 第2行是最大得分.

测试输入期待的输出时间限制内存限制额外进程
测试用例 1以文本方式显示
  1. 36↵
  2. 53 49 2 9 9 30 2 35 1 46 39 46 42 33 13 41 35 57 38 59 15 40 18 6 46 30 53 31 34 57 41 20 1 42 59 46 ↵
以文本方式显示
  1. 5913↵
  2. 24595↵
1秒64M0

算法分析

如果是线性石子堆,其实跟矩阵链乘是非常类似的。

矩阵链乘是相邻的两个可乘,需要由底向上乘起来,先乘2个的,再乘3个的,直到n个。

石子合并是相邻的两堆可加,需要由底向上加起来,先加两堆,再加三堆,直到n堆。

所以对于线性石子合并问题来说,与矩阵链乘的代码并无太大差异。唯一不同的是更新dp数组的值时,除了划分的两块加数外,新加的当前这一项不再是a[i-1]*a[k]*a[j],而是从 i 到 j 的石子堆中石子总数。(因为每次合并都要把当前合并的石子数再加一遍)(其实这个运算跟矩阵链乘也有异曲同工之处)

代码如下

#include <iostream>
#include <algorithm>
#include <limits.h>
using namespace std;
#define N 110
int dp_min[N][N];
int dp_max[N][N];
int a[N];
void Stones(int n)
{
    for(int l=1;l<=n;l++)  //与矩阵链乘相似,由底向上,此循环代表几堆石子合并(从1堆开始)
    {
        for(int i=1;i<=n-(l-1);i++)  
        {
            int j=i+l-1,sum=0;
            dp_min[i][j]=INT_MAX; //初始化dp
            dp_max[i][j]=INT_MIN;
            if(i==j)  //一堆石子不合并
            {
                dp_min[i][j]=0;
                dp_max[i][j]=0;
                continue;
            }
            for(int m=i;m<=j;m++)  //计算从当前一次合并需要加的数值(从第i到j堆的石子数之和)
            {
                sum+=a[m];
            }
            for(int k=i;k<j;k++) //更新dp
            {
                int t1=dp_min[i][k]+dp_min[k+1][j]+sum; //试图找更优的划分
                int t2=dp_max[i][k]+dp_max[k+1][j]+sum;
                if(t1<dp_min[i][j])  //尝试更新最小值和最大值
                {
                    dp_min[i][j]=t1;
                }
                if(t2>dp_max[i][j])
                {
                    dp_max[i][j]=t2;
                }
            }
        }
    }
}
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    Stones(n);
    int resmin=dp_min[1][n];  
    int resmax=dp_max[1][n]; 
    cout<<resmin<<endl;
    cout<<resmax<<endl;
    return 0;
}

下面我们讨论一下如果是环形石子堆应该怎么办。

我们可以把环形石子断开,将其展成一条链,这样就跟线性石子合并求法相同了。只是对于从哪里断开,我们有多种选择。比如4堆石子:1234这个序列,我们可以展成1234,2341,3412,4123四种链条。对于每一条链,我们都需要求出其线性石子合并的最小值(和最大值),最后再对这四条链作比较选出最小的最小值(和最大的最大值)

所以我们可以把链条长度延长到2n,0处空着,从1开始放第一堆石子。还是以1234为例,延展后长为2n的石子链条数组变为  空1234123  ,则第一种链是1234,往后移一位得到第二种链2341,第三种和第四种链依次再往后移一位。所以这条延长链构成的dp数组中会保存四种链的石子合并值,最终四种链条的石子合并值会分别保留在dp[1][4],dp[2][5],dp[3][6],dp[4][7]中。

由此,我们可以写出如下代码

代码练习

#include <iostream>
#include <algorithm>
#include <limits.h>
using namespace std;
#define N 110
int dp_min[2*N][2*N];
int dp_max[2*N][2*N];
int a[2*N];
void Stones(int n)
{
    for(int l=1;l<=n;l++)  //与矩阵链乘相似,由底向上,此循环代表几堆石子合并(从1堆开始)
    {
        for(int i=1;i<=2*n-(l-1);i++)  //对于每一条链,延展开变成2n长度
        {
            int j=i+l-1,sum=0;
            dp_min[i][j]=INT_MAX; //初始化dp
            dp_max[i][j]=INT_MIN;
            if(i==j)  //一堆石子不合并
            {
                dp_min[i][j]=0;
                dp_max[i][j]=0;
                continue;
            }
            for(int m=i;m<=j;m++)  //计算从当前一次合并需要加的数值(从第i到j堆的石子数之和)
            {
                sum+=a[m];
            }
            for(int k=i;k<j;k++) //更新dp
            {
                int t1=dp_min[i][k]+dp_min[k+1][j]+sum; //试图找更优的划分
                int t2=dp_max[i][k]+dp_max[k+1][j]+sum;
                if(t1<dp_min[i][j])  //尝试更新最小值和最大值
                {
                    dp_min[i][j]=t1;
                }
                if(t2>dp_max[i][j])
                {
                    dp_max[i][j]=t2;
                }
            }
        }
    }
}
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        a[i+n]=a[i];  //因链条延展了,所以输入数据时要往后copy一份
    }
    Stones(n);
    int resmin=INT_MAX;  //在n种链条中找合并石子所得最小值中最小的那一种
    for(int i=1;i<=n;i++)
    {
        if(resmin>dp_min[i][i+n-1])  //注意j的下标是i+n-1,用1234为例写一下看看,如第一种链的 
                                                                   //结果在dp_min[1][4]中
        {
            resmin=dp_min[i][i+n-1];
        }
    }
    int resmax=INT_MIN;  //在n种链条中找合并石子所得最大值中最大的那一种
    for(int i=1;i<=n;i++)
    {
        if(resmax<dp_max[i][i+n-1])
        {
            resmax=dp_max[i][i+n-1];
        }
    }
    cout<<resmin<<endl;
    cout<<resmax<<endl;
    return 0;
}

四、租用游艇问题

问题描述

问题描述: 长江游艇俱乐部在长江上设置了n个游艇出租站1,2,…,n. 游客可在这些游艇出租站租用游艇, 并在下游的任何一个游艇出租站归还游艇. 游艇出租站i到出租站j之间的租金为r(i,j), 1<=i<j<=n. 试设计一个算法, 计算出从游艇出租站1到游艇出租站n所需的最少租金, 并分析算法的计算复杂性.

算法设计: 对于给定的游艇出租站i到游艇出租站j的租金r(i,j), 1<=i<j<=n. 计算出租站1到n所需的最少租金.

数据输入: 第1行有一个正整数n, n<=200, 表示有n个游艇出租站. 接下来n-1行是r(i,j), 1<=i<j<=n.

结果输出: 游艇出租站1到n最少租金.

测试输入期待的输出时间限制内存限制额外进程
测试用例 1以文本方式显示
  1. 3↵
  2. 5 15↵
  3. 7↵
以文本方式显示
  1. 12↵
1秒64M0

算法分析

这道题跟矩阵链乘和石子合并其实有点像。

题意不太好懂,解释一下。输入的是第行号个出租站到第列号+1个出租站的费用。比如测试用例1,第一行3代表有三个出租站,第二行5 15分别为第一个出租站到第二个出租站的费用和第一个出租站到第三个出租站的费用。第三行7代表第二个出租站到第三个出租站的费用。

我们可以看出输入的数据其实是一个二维的上三角矩阵(也可以理解为无向完全图,即每两个点之间有一条边,每条边只保存一次)。

所以我们可以把原始数据保存在一个二维数组中,按照上述形式存储它。

dp[][]数组由此也设为二维数组。dp[i][j]表示从第i个出租站到第j个出租站所需的最小费用。

状态转移方程为dp[i][j]=min(dp[i][k]+dp[k][j],dp[i][j])

跟矩阵链乘和石子合并问题不同的是,如果需要更新dp的值,只需加划分后的两部分即可,无需额外加本次合并总体所需的什么数值。

而且划分后的两部分是dp[i][k]+dp[k][j],而不是dp[i][k]+dp[k+1][j]。因为从第i个出租站到第i个出租站本身不需要单独费用,但石子合并和矩阵链乘中每一项都有一个数值需要考虑加和。

代码练习

#include <iostream>
#include <algorithm>
#include <limits.h>
using namespace std;
#define N 220
int dp[N][N];
int a[N][N];
void Rent_Yacht(int n)
{
    for(int l=2;l<=n;l++) //类似合并石子,从两个出租站开始合并
    {
        for(int i=1;i<=n-(l-1);i++)
        {
            int j=i+l-1;
            dp[i][j]=a[i][j];
            for(int k=i;k<j;k++)  //试图寻找更优划分
            {
                int temp=dp[i][k]+dp[k][j]; //计算划分后的费用值
                if(dp[i][j]>temp && temp!=0) dp[i][j]=temp; //尝试更新
            }
        }
    }
}
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        for(int j=i+1;j<=n;j++)
        {
            cin>>a[i][j];
            getchar();
        }
    }
    Rent_Yacht(n);
    cout<<dp[1][n]<<endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值