C&C++实现算法习题第二部分—动态规划

6 篇文章 0 订阅

动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,以至于最后解决原问题需要耗费指数时间。然而,不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,从而得到多项式时间算法。为了达到此目的,可以用一个表来记录所有已解决的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思想。具体的动态规划算法多种多样,但它们具有相同的填表格式。

动态规划算法适用于解最优化问题。通常可按以下4个步骤设计:
(1)找出最优解的性质,并刻画其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,构造最优解。

步骤(1)~(3)是动态规划算法的基本步骤。在只需要求出最优值的情形,步骤(4)可以省去。若需要求出问题的最优解,则必须执行步骤(4)。此时,在步骤(3)中计算最优值时,通常需记录更多的信息,以便在步骤(4)中,根据所记录的信息,快速构造出一个最优解。

一.矩阵连乘问题

问题描述

给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2,…,n- 1。计算出这n 个矩阵的连乘积A1A2…An。

分析

矩阵乘法满足结合律,因此计算矩阵的连乘积可以具有不同的计算次序。这种计算次序可以用加括号的方式来确定。若一个矩阵连乘积的计算次序完全确定,也就是说该连乘积已完全加括号,则可依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积。

代码
#include <iostream>

using namespace std;
void MatrixChain(int *p,int n,int **m,int **s);
void Traceback(int i, int j, int **s);

int main()
{
    const int n = 6;
    int p[n + 1] = {30,35,15,5,10,20,25 };
    int **m = new int*[n+1];
    int **s = new int*[n+1];
    for (int i = 0; i < n+1; i++)
    {
        m[i] = new int[n+1];
        s[i] = new int[n+1];
    }

    MatrixChain(p,n, m, s );
    Traceback(1, n, s);
    return 0;
}
void MatrixChain(int *p,int n,int **m,int **s)
{    //m是最优值,s是最优值的断开点的索引,n为题目所给的矩阵的个数
    for(int i = 1;i<=n;i++) m[i][i] = 0;          //对角线为0
    for(int r = 2;r<=n;r++){
        for(int i = 1;i<=n-r+1;i++){
            //从第i个矩阵Ai开始,长度为r,则矩阵段为(Ai~Aj)
            int j = i+r-1;//当前矩阵段(Ai~Aj)的起始为Ai,尾为Aj
            //求(Ai~Aj)中最小的,其实k应该从i开始,但先记录第一个值,k从i+1开始,这样也可以。
            //例如对(A2~A4),则i=2,j=4,下面一行先得出m[2][4]=m[3][4]+p[1]*p[2]*p[4],即A2(A3A4)
                 m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j];
                 s[i][j] = i;//记录断开点的索引
            //以下for循环求出(Ai~Aj)中的最小数乘次数
                 for(int k = i+1 ; k<j;k++){
                     //例如对(A2~A4),则k=2,此处for循环计算(A2A3)A4 此次for循环不在包含
                     //A2(A3A4)因为上面已经得出m[2][4]=m[3][4]+p[1]*p[2]*p[4],即A2(A3A4)
            //将矩阵段(Ai~Aj)分成左右2部分(左m[i][k],右m[k+1][j]), 不包含A2(A3A4)再加上左右2部分最后相乘的次数(p[i-1] *p[k]*p[j])
            int t = m[i][k] + m[k+1][j] + p[i-1] *p[k]*p[j];
            if(t<m[i][j]) { m[i][j] = t; s[i][j] = k; //保存最小的断开位置,即最优的结果
                }//if
            }//k
        }//i
    }//r
}
void Traceback(int i, int j, int **s)
{
    if (i == j)
    {
        cout << "A" << i;
        return;
    }
    cout << "(";
    Traceback(i, s[i][j], s);
    Traceback(s[i][j] + 1, j, s);
    cout << ")";
}

运行结果

图1 动态规划

二. 最长公共子序列问题

问题描述

给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。

例如,若X= {A,B,C,B,D,A,B},Y = {B,D,C,A,B,A}则序列{B,C,A}是X和Y的-一个公共子序列,但它不是X和Y的一个最长公共子序列。序列{B,C,B,A}也是X和Y的一个公共子序列,它的长度为4,而且它是X和Y的最长公共子序列,因为X和Y没有长度大于4的公共子序列。

代码
#include <iostream>
using namespace std;
#include <cstring>
const int MAX = 100;

void LCSLength(char *x, char *y, int m, int n, int c[][MAX], int b[][MAX])
{
    int i, j;
    for (i = 0; i <= m; i++)        //从0开始遍历
        c[i][0] = 0;            //当i或者j为0时,最长公共子序列为空
    for (j = 1; j <= n; j++)
        c[0][j] = 0;                //当i或者j为0时,最长公共子序列为空
    for (i = 1; i <= m; i++)
    {
        for (j = 1; j <= n; j++)
        {
            if (x[i - 1] == y[j - 1])       //遍历到两个值相等
            {
                c[i][j] = c[i - 1][j - 1] + 1;              //向左上方遍历
                b[i][j] = 0;                //标记
            }
            else if (c[i - 1][j] >= c[i][j - 1])        //取大的
            {
                c[i][j] = c[i - 1][j];                     //向上
                b[i][j] = 1;
            }
            else
            {
                c[i][j] = c[i][j - 1];                  //向左
                b[i][j] = -1;
            }
        }
    }
}

void PrintLCS(int b[][MAX], char *x, int i, int j)
{
    if (i == 0 || j == 0)
        return;
    if (b[i][j] == 0)
    {
        PrintLCS(b, x, i - 1, j - 1);
        cout << x[i - 1] << " ";
    }
    else if (b[i][j] == 1)
        PrintLCS(b, x, i - 1, j);
    else
        PrintLCS(b, x, i, j - 1);
}

int main()
{
    char x[MAX] = { "abcdefggigk" };
    char y[MAX] = { "acdefhglkm" };
    int b[MAX][MAX];
    int c[MAX][MAX];
    int m, n;

    m = strlen(x);
    n = strlen(y);
    //保存两个序列的长度

    LCSLength(x, y, m, n, c, b);
    PrintLCS(b, x, m, n);

    cout << endl;
    return 0;
}

运行结果

图2 最长公共子序列
图3 最长公共子序列

三. 流水作业调度问题

问题描述

n个作业{1,2,…,n}要在由2台机器M1和M2组成的流水线上完成加工。每个作业加工的顺序都是先在M1上加工,然后在M2上加工。M1和M2加工作业i所需的时间分别为ai和bi,1≤i≤n。流水作业调度问题要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少。

分析

一个最优调度应使机器M1没有空闲时间,且机器M2的空闲时间最少。在一般情况下,机器M2上会有机器空闲和作业积压两种情况。

设全部作业的集合为N = {1,2,…,n}.S⊆N是N的作业子集。在一般情况下,机器M1开始加工S中作业时,机器M2还在加工其他作业,要等时间t后才可利用。这种情况下完成S中作业所需的最短时间记为T(S,t)。流水作业调度问题的最优值为T(N,0)。

代码
//#include "stdafx.h"
#include <iostream>
using namespace std;

const int N = 5;

class Jobtype
{
    public:
        int operator <=(Jobtype a) const
        {
            return(key<=a.key);
        }
        int key,index;
        bool job;
};

int FlowShop(int n,int a[],int b[],int c[]);
void BubbleSort(Jobtype *d,int n);//本例采用冒泡排序

int main()
{
    int a[] = {2,4,3,6,1};
    int b[] = {5,2,3,1,7};          //分别表示在两个机器上的使劲
    int c[N];                       //存放调度的顺序

    int minTime =  FlowShop(N,a,b,c);

    cout<<"完成作业的最短时间为:"<<minTime<<endl;
    cout<<"编号从0开始,作业调度的顺序为:"<<endl;
    for(int i=0; i<N; i++)
    {
        cout<<c[i]<<" ";
    }
    cout<<endl;
    return 0;
}

int FlowShop(int n,int a[],int b[],int c[])
{
    Jobtype *d = new Jobtype[n];
    for(int i=0; i<n; i++)
    {
        d[i].key = a[i]>b[i]?b[i]:a[i];
        //按Johnson法则分别取对应的b[i]或a[i]值作为关键字
        d[i].job = a[i]<=b[i];
        //给符合条件a[i]<b[i]的放入到N1子集标记为true
        d[i].index = i;
    }

    BubbleSort(d,n);//对数组d按关键字升序进行排序

    int j = 0,k = n-1;

    for(int i=0; i<n; i++)
    {
        if(d[i].job)
        {
            c[j++] = d[i].index;
            //将排过序的数组d,取其中作业序号属于N1的从前面进入
        }
        else
        {
            c[k--] = d[i].index;
            //属于N2的从后面进入,从而实现N1的非减序排序,N2的非增序排序
        }
    }

    j = a[c[0]];
    k = j+b[c[0]];
    for(int i=1; i<n; i++)
    {
        j += a[c[i]];//M1在执行c[i]作业的同时,M2在执行c[i-1]号作业,最短执行时间取决于M1与M2谁后执行完
        k = j<k?k+b[c[i]]:j+b[c[i]];//计算最优加工时间
    }

    delete d;
    return k;
}

//冒泡排序
void BubbleSort(Jobtype *d,int n)
{
    int i,j,flag;
    Jobtype temp;

    for(i=0;i<n;i++){
        flag = 0;
        for(j=n-1;j>i;j--){
            //如果前一个数大于后一个数,则交换
            if(d[j]<=d[j-1]){
                temp = d[j];
                d[j] = d[j-1];
                d[j-1] = temp;
                flag = 1;
            }
        }
        //如果本次排序没有进行一次交换,则break,减少了执行之间。
        if(flag == 0){
            break;
        }
    }
}

运行结果

图4 流水作业调度
图5 流水作业调度

四. 大币找零钱问题

问题描述

有多种不同方式的硬币,需要对用户进行找零服务,例如,用户需要找零18元,指定三种买呢的硬币,分别是5元,2元,1元,在此情况下,用户得到的最优找零数为5张(三枚五元,一枚两元,一枚一元)

代码
#include<iostream>
using namespace std;
int main() {
    //硬币面值的数组
    int values[100];
    //要找零的数
    int money,size;
    //保存每个面值对应的最小值 因为0号位置要舍弃 因此要加1


    //输入总共有几种面值 和从小到大输入每种面值的大小
    cout<<"请输入要找零的金额";
    cin>>money;
    cout<<"请输入要找回面值的分类数";
    cin>>size;
    cout<<"请输入要找回面值的种类";
    for(int i = 0; i < size; i++)
        cin >> values[i];
    int *coinsUsed = new int[money + 1];
    coinsUsed[0] = 0;
    //总共的钱数的遍历
    int mincoin = 0;
    for(int i = 1; i <= money; i++){
        mincoin = i;
        //总共有几种面值的遍历
        for (int j = 0; j < size; j++) {
            //如果当前第j中面值小于总共的钱数
            if(values[j] <= i){
                //用了这一面值 前提是用这张纸币的数量比不用这张纸币的数量小采用

                    mincoin = min(mincoin,coinsUsed[i - values[j]] + 1);

            }
        }
        //当前金额所需的最小纸张数
        coinsUsed[i] = mincoin;


    }
    cout<<"总共需要面币张数为"<<coinsUsed[money]<<endl;
    return 0;
}

运行结果

图6 大币找零钱

参考文献 《计算机算法设计与分析(第四版)》 王晓东 编著

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值