动态规划之-Rod Cutting 问题_2

动态规划之-Rod Cutting 问题(2)

上一篇Rod-cutting动态规划问题中,我们提到了《算法导论》中提到的动态规划问题解决方案的CRCC步骤,并介绍动态的两大特征:

  • 最优子结构
  • 重叠子问题

我们在rod cutting问题中对这两点进行了分析和阐述,本文继续利用方法2中提到的递归公式,对top-down和bottom-up两种方法进行说明,同时对记录的保存进行说明。

Top-Down 方式

一般采用递归结构,其典型的思维是“大处着眼,小处着手”,对问题的分析,以总的问题(大处)为根节点,然后不断分裂子树,解决子树问题(小处),从而最后得到总问题的解。值得一提的是,由于利用递归,所以需要输入递归的结束条件,这样才能有效求出问题的解。让我们以Rod-cutting问题为例进行说明,首先我们从顶层出发,求解r(n)的最大化利润,要求得r(n)的最大利润,就需要利用关系式, 不断求解:
r ( n ) = m a x { p ( i ) + r ( n − i ) , 1 ≤ i ≤ n } r(n)=max \left\{p(i)+r(n-i), 1≤i≤n\right\} r(n)=max{p(i)+r(ni),1in}

求解的结构过程类似 深度优先遍历DFS(Depth First Search)的过程,整个遍历路径以Start point为起点,沿着深蓝色路径,一直来到终点,为了展现整个过程,下图中未引入重复子问题查询的基本概念。

在这里插入图片描述

Bottom-up 方式

Bottom-up 解决问题的思路是从小处开始,遵循从底层到顶层的解决问题之道。从某种意义上讲,bottom-up解决问题是利用迭代的思路进行问题解决,迭代之前需要确认底层的起始点(也就是递归的结束点),利用循环语句结果问题。一般情况下,如果子问题的形式只有一个,那么bottom-up采用双重循环即可求得结果;如果子问题有两个,那么至少需要3重循环才能解决问题,用不专业的属于表述,bottom-up至少比递归多一层循环,因为递归的过程,计时函数体内没有循环语句,其过程中也隐含循环的过程。

其实对于动态规划问题的求解,如果我们把每个子问题(sub-problem)抽象为有向图(DG)的结点,那么此有向图一定不存在环,也就是说,它是拓扑有序的,属于DAG图。如果某几个子问题的结点组成的一个环,那么此动态规划问题将无法求解,因为子问题之间不是相互独立的,子问题相互独立是动态规划的重要条件之一。

我们回到rod-cutting问题上来,以长度n=4为例,说明 bottom-up的工作方式,我们以DAG 图为辅助阐释条件。

在这里插入图片描述

在上述有向图中,我们实际上是对Top-down形成的递归树进行压缩,对于相同结点的子问题,我们进行同类问题的合并,对于同类问题,我们不仅合并结点,而且也合并有向边,我们对这个过程进行说明:

a) 对于底层结点#0的值r(0),它属于最bottom结点,在bottom之前,我们将对其进行赋值r(0)=0

b) 接下来我们从下到上,可以遍历各个顶点,接着关注结点#1的r(1),由于r(1)紧紧依附于顶点r(0),顶点r(0)已经求出,再加上某些权值,作为选择的代价;

c) r(0)和r(1)的值明确后,我们开始关注结点2,结点2依附于结点0和结点1,如果要求出结点2的值,我们需要分别关注路径②→①和②→0, 由于r(0)和r(1)已经已知,我们只要加上选择的代价p[i],最终比较两个路径的长度,选择最大的值作为r(2)的值

d) 对于③,④,⑤结点,我们套用同样的逻辑,求得r(3),r(4)和r(5)

值得一提的是,在bottom-up过程中,我们始终坚持先求“小”的子问题,然后再求解“大”的子问题,自然而然的求得最终问题的解。

上面已经介绍完成top-down和bottom-up的执行概念,对于同一个问题,两个概念的时间复杂度基本相同,对于rod-cutting问题,二者的时间复杂度都为:
O ( n 2 ) O(n^2) O(n2)
因为对于二者问题的求解都最终都形成类似一个等差数列,

  • 对于top-down而言,我们可以定义其复杂度为=n+(n-1)+…(n-i)+2+1, 通过求和我们得到:

T ( n ) = ( n + 1 ) ∗ n / 2 = ( n 2 + n ) / 2 ≈ O ( n 2 ) T(n)=(n+1)*n/2=(n^2+n)/2≈O(n^2) T(n)=(n+1)n/2=(n2+n)/2O(n2)

  • 对于bottom-up而言,我们可以定义其复杂度=1+2+3+4…n,通过求和得到相同的结论

接下来,我们要介绍动态规划的实际效果,也就是保存重复子问题的值,通过查询结果直接返回值或引用,这是典型的用空间换取时间的例子,如果用物理中的杠杆原理比例,那就是用合理的空间“ 杠杆” 极大撬动(降低)运行的时间复杂度,从而有效提升程序的运行效率。

查询数组概念

  1. Top-down 方式的查询

我们回到本次rod-cutting 问题本身,从top-down方式开始介绍,如何保存运算结果到数组当中,从而直接进行查询或引用,从而提升程序效率。我们还是用图来说明执行的过程。

在这里插入图片描述

当用top-down递归方式遍历到最左边的0号节点时候,递归将要退出,我们在递归退出之前,利用数组r[0]记录求出的值,紧接着退出①号节点,这是我们r[1]记录节点1的值;接下来就是见证奇迹的时刻,当遍历②号节点的右子树O的时候,由于我们已经求得r[0],此时便直接返回r[0]的值,而无需进行任何递归运算,如果说这是一道小菜,那么接下来回退到②号节点,我们保存r[2]的结果,对于rod-cutting问题,自此以后,我们无需利用递归对节点进行任何遍历,只需要利用已经存在的记录直接返回值即可,圈内空白表示直接返回值,无需递归计算。

在这里插入图片描述

那么程序上如何实现呢?每次递归运算之前,我们需要对数组的值进行判断,确认此节点之前是否已经有运算值。所以数组内的值需要有两类状态(未填充 和 已赋值),未填充可以用INT_MIN或某个特殊数表示,原则上需要避免和后面的赋值不冲突,所以NULL, INT_MIN或INT_MAX都是不错的选择。具体Top-down的实现程序如下:

我们对函数进行说明:

  • 主函数,int cut_rod(int *p, int n), p表示不同长度钢棒的出售价格,n表示未切割前的钢棒长度,函数返回最大化收益。在函数里面定义r[N]数组,用来记录已经求得的子问题的最优解,通过初始化赋值未INT_MIN.

  • 辅助函数,int cut_rod_aux(int *p, int *r, int n) 先判断目前长度n的钢棒的最优解是否存在r数组中,如果存在数组中,那么就是直接返回至原来递归处;如果不存在数组中,那么就进行常规的递归运算,关键的一点是,对于n长度的钢棒求出最优解之后,我们需要利用r数组对其结果进行保存,以便下次查询使用。

/**
 * @file cut_rod_recursive_memo.c
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2023-02-19
 * 
 * @copyright Copyright (c) 2023
 * 
 */

#ifndef CUT_ROD_RECURSIVE_MEMO_C
#define CUT_ROD_RECURSIVE_MEMO_C
#include "cut_rod_recursive_memo.h"

int cut_rod(int *p, int n)
{
    int r[N];

    for(int i=0;i<=n;i++)
    {
        *(r+i)=INT_MIN;
    }

    return cut_rod_aux(p,r,n);
}

int cut_rod_aux(int *p, int *r, int n)
{
    int q;
    int i;
	//Look up the optimal solution if it is available in the table(array)
    //By checking if is more than zero to make the decision
    if(*(r+n)>=0)
    {
        return *(r+n);
    }

    if(n==0)
    {
        q=0;
    }
    else
    {
        // The program will check the cut point from the right-hand side
        // instead of left-hand side
        q = INT_MIN;
        for (i = 1; i <= n; i++) //p={0,1, 5, 8, 9, 10, 17, 17, 20, 24, 30};
        {
            q = max_revenue(q, p[i] + cut_rod_aux(p, r, n - i));
        }
    }

    //Record the optimal solution in order for looking up in the beginning of program
    r[n]=q; 
    return q;
}

int max_revenue(int r1, int r2)
{
    return(r1>r2?r1:r2);
}

#endif

  1. Bottom-up 方式的查询

一般情况下,采用Bottom-up方式对最优解(optimal solution)进行保存,形式上会更为优雅,但是理解与程序实施上会比较困难,一则是需要找到最底层节点的位置,二则循环层数至少比top-down多1层。但是,许多程序仍然选择bottom-up形式,此行可以节省递归调用过程中,对函数栈管理和调用的额外开销,减少不必要的栈管理麻烦。bottom-up方式仅需要一个函数即可实现程序目的,程序简洁而紧凑。

/**
 * @file cut_rod_bottomup.c
 * @author your name (you@domain.com)
 * @brief
 * @version 0.1
 * @date 2023-02-19
 *
 * @copyright Copyright (c) 2023
 *
 */

#ifndef CUT_ROD_BOTTOMUP_C
#define CUT_ROD_BOTTOMUP_C
#include "cut_rod_bottomup.h"

void cut_rod(int *p, int *r,int n)
{
    //from smaller subproblem to the bigger original problem solution
    int j;
    int i;
    int q;

    r[0]=0; //bottom value defintion, all the iterative will be based on this number

    for(j=1;j<=n;j++) //Listing the rod from small to big with regard to rod length
    {
        q=INT_MIN;
        for(i=1;i<=j;i++) // it should include j index(right hand rule)
        {
            q=max_revenue(q,p[i]+r[j-i]);
        }

        r[j]=q;
    }

    return;
}

int max_revenue(int r1, int r2)
{
    return(r1>r2?r1:r2);
}

#endif

再次就不赘述函数的实现过程,请读者对比代码自行理解。

总结:

本文对rod-cutting 问题的动态规划的两类实现方式进行探索,加深对动态规划的理解,由于初学动态规划,难免存在理解瑕疵和表述不足,希望多多指正。

参考文献:

  1. 《Introduciton to Algorithm, 4ed, Edition》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值