【基础算法】切割钢管与动态规划

  尽管排序算法还有很多没有说,但因为这篇文章是已经现成有的,就先上这个,回头再把排序补一下。

  好的开始~BigMoyan有一个好基友叫zou先生,zou先生除了是BigMoyan在学校的社团老大外,还是一家专门为夜总会提供钢管的公司的区域经理。最近,zou经理发现这样一个事情,夜总会需要各种长度的钢管用作各种用途,然而每种长度的钢管的价格却不一样,总而言之如下表。

  从前,zou经理总是傻乎乎的把总公司发来的10m长钢管以25块的价格卖出去,但是某天他跟学校的打菜阿姨聊起人生和事业的时候,打菜阿姨却来了一句:

   “明明每根钢管能卖27块钱,你只卖25,学过C++吗?”

  这深深伤害了zou经理的心灵,于是他向室友Jie求救。

   以下是BigMoyan转述Jie的分析:

  首先我们正式的表达一下这个问题,有一段长度为i的钢管,整段出售的价格为Pi,求适当的切割钢条方案使得获利最大。为了便于zou理解问题,jie先用一个例子做了一下简单说明。

  比如你现在有一段长为4m的钢管,如果整段卖的话,卖9块。然而你也可以切成两段2m的钢管分别卖,这样一共可以卖5*2=10块,所以并不是整段卖就一定获益最大的。假设对于长为i的钢管,获益最大的售价为Ri,我们试着穷举一下。

  长为1的钢管,i=1,只能整段出售,R1=1

  长为2的钢管,i=2,很容易发现整段出售获益最高,R2=2

  …

  长为4的钢管,i=4,上面分析了应该切成两段,R4=10=5+5

  长为5的钢管,整段出售是10块,分成1+4,因为R4=10,所以可以卖11块,分成2+3,则可卖13块,因此R5=2+3=13

  …

  由于jie看了BigMoyan关于快速排序的介绍,受到分治策略的影响,于是jie刚开始自然而然的认为这是一个可以递归解决的问题,对于长为i的钢管,其最大获益为:

Ri= max{Pn, R1+Rn-1,R2+Rn-2,…Ri-1+R1}

  这个意思是说,为了求Ri,我们可以把钢管分为和为i的两段,而这两段各自又是一个求大获益的问题,正如BigMoyan曾经说过的那样,想要理解递归,首先,你要理解递归。然后在各种分法中找出最大的那个,自然就是结果。注意上式中第一个参数Pn为整段出售的价格。

  Jie把思路告诉了BigMoyan,BigMoyan很快为zou写了这样一段程序(没错我就是这样一个仗义的人),用C++写大概是下面这个样子(BigMoyan用的最多的依然是Python,可惜Python没有做尾递归优化,只能暂时用一把C++了):

#include<iostream>
using namespace std;

int Max(int i, int j){
    return i>j?i:j;
} 

int cut_rod(int p[],int n){
    if(n==0)
        return 0;
    int f=0;
    int q=-1;
    for(int i=1;i!=n;i++){
        f=Max(p[n-1],cut_rod(p,i)+cut_rod(p,n-i));
        q=Max(q,f);
    }
    return q;
}

int main(){
    int p[10]={1, 5, 8, 9, 10, 17, 17, 20, 24, 25};
    int f=cut_rod(p,10);
    cout<<f;
    return 1;

} 

  老规矩,碰到代码不要慌,拿个实例稍微分析一下,例如求长为5的钢管的最大收益。调用函数cut_rod(p,5)

  进去以后, if测试失败,进入for循环

i=1:

  f=Max(p[4],cut_rod(p,1)+cut_rod(p,4));

  找出了p[4]和cut_rod(p,1)+cut_rod(p,4)的大者,前者为5m钢管整段出售的价格(下标从0开始所以是5-1=4),后者将5m分为1m和4m,将两段的最大收益加起来作为进行1+4分割的最大收益。

  q=Max(q,f);

  第一趟的时候q=-1,所以此时q=f保存1+4分割时的最大收益。

i=2:

  f=Max(p[4],cut_rod(p,1)+cut_rod(p,4));

  如上,此次找的是整段出售和2+3分割的最大收益之间的大者。

  q=Max(q,f);

  q在上次保存了f,这次将新f和上一次的值(q)比较,拿出最大的作为最大收益

  后面以此类推

  然而在测试代码的时候BigMoyan发现,这代码执行效率太他喵的低了!n比较大的时候,基本上n增加1,代码运行时间就要加两倍,我要敢测试一个500m长的钢管的最大收益(假设数据都有),估计zou经理都要毕业了。

  稍加分析发现,执行效率低的原因在于子问题一直在重复计算!计算10m钢管收益的时候要把1~9m的收益全算一次,而为了计算9m钢管的收益,我又要把1~8m的都算一次。

  心!好!累!

  此时聪明的jie也已经察觉到了问题所在,这个问题看起来是递归,其实却与递归略有不同。递归的子问题互相不相交,而这个东西的子问题是相交的,相同的子问题被一遍一遍的计算,才导致了效率的低下。

  Jie略一思忖,计上心来,既然子问题被一遍遍的计算,我们何不以空间换时间,把已经被计算好的子问题的结果保存起来,当需要的时候首先查询,如果这个问题已经算好了,直接拿来用就是,若没算好再进行计算。

  BigMoyan拍案叫绝,再次改写了代码,于是就有了下面这段C++ code:

nt cut_rod(int p[],int n){
    int *r=new int[n];
    for(int i=0;i!=n;i++)
        r[i]=-1;
    return cut_rod_memo(p,n,r);
}

  调用函数名保持一致方便理解,首先建立一个备忘录r,r用来保存已经被计算好的子问题的答案。将它初始化为-1。我们还是以计算5m钢管为例,那么r被初始化为[-1,-1,-1,-1,-1].

  接着调用cut_rod_memo来计算最大收益,那么cut_rod_memo是什么呢?

nt cut_rod_memo(int p[], int n, int r[]){
    if(r[n-1]>=0)
        return r[n-1];
    if(n==0)
        int q=0;
    else{
        int f=0;
        int q=-1;
        for(int i=1;i!=n;i++){
            f=Max(p[n-1],cut_rod_memo(p,i,r)+cut_rod_memo(p,n-i,r));
            q=Max(q,f);
        }
    r[n-1]=q;
    return q;
    }
}

  好的,不要头大,BigMoyan慢慢分析。

  进入cut_rod_memo后,首先查询5m的结果是不是已经算好了,答案是当然没有,r[4]=-1。于是进入else语句,这里的语句跟之前递归的版本一模一样,区别只在于递归调用的是cut_rod_memo,因为该函数里第一个语句就是判断子问题答案有没有做好,所以避免了多次计算相同的子问题,下面展示这一过程。

i=1:

  由之前的无脑递归的版本可知,for循环解决了i=1时的最大收益,在for循环完成后,将其保存在r[0]中,此时r[0]=1

i=2:

  在递归调用cut_rod_memo(p,1,r)时,因为已经算好r[0]=1,故cut_rod_memo的第一个if判断成功,返回r[0],此时的返回值由已经计算过的结果直接拿出,没有重新计算。

  其他情况同理。

  BigMoyan与jie两人相对而笑,笑容中充满着对对方的赞赏。他们一起来到zou经理折戟的银桦食堂,与打菜阿姨如此这般一讲,本指望收到阿姨赞美的眼光,没想到阿姨把勺子里的菜抖掉一些,淡然说道:

   “唔?这不就是动态规划吗,什么新鲜东西,值得来浪,刷卡!”

   Ps.动态规划问题当然不止这一类,但其基本思想是一致的,就是不要重复解决已经解决过的问题,感兴趣的同学可以百度一下动态规划

  PPS:下面是全部C++代码

 1 #include<iostream>
 2 using namespace std;
 3
 4 int Max(int i, int j){
 5     return i>j?i:j;
 6 }
 7
 8 int cut_rod_memo(int p[], int n, int r[]){
 9     if(r[n-1]>=0)
10         return r[n-1];
11     if(n==0)
12         int q=0;
13     else{
14         int f=0;
15         int q=-1;
16         for(int i=1;i!=n;i++){
17             f=Max(p[n-1],cut_rod_memo(p,i,r)+cut_rod_memo(p,n-i,r));
18             q=Max(q,f);
19         }
20     r[n-1]=q;
21     return q;
22     }
23 }
24
25 int cut_rod(int p[],int n){
26     int *r=new int[n];
27     for(int i=0;i!=n;i++)
28         r[i]=-1;
29     return cut_rod_memo(p,n,r);
30 }
31
32 int main(){
33     int p[10]={1, 5, 8, 9, 10, 17, 17, 20, 24, 25};
34     int f=cut_rod(p,10);
35     cout<<f;
36     return 1;
37 }
时间: 2024-11-02 23:38:11

【基础算法】切割钢管与动态规划的相关文章

基础算法(七)——动态规划【0/1背包问题】

首先,对于动态规划,我来做一个简短的介绍,相信各位都看得懂.动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法.先给一道最简单的例题(小学奥数水平): 这就是一个模拟的动态规划法! 好了,现在开始认识一下最简单的动态规划实例:0/1背包. [问题描述]有一位旅行者要出远门,到商店里去筹备东西.在商店里,摆放了n样物品,每种物品有各自的体积,但是每种物品只有一件.这个旅行家有一个限制装V个体积的物品的背包,但是这

九章算法 基础算法 强化算法 系统设计 大数据 安卓 leetcode 高清视频

leetcode 直播视频讲座录像 九章算法视频录像,PPT 算法班,算法强化班,Java入门与基础算法班,big data项目实战班,Andriod项目实战班 九章算法下载 九章算法面试 九章算法leetcode 九章算法答案 九章算法mitbbs 九章算法班 九章算法ppt 九章算法录像 九章算法培训 九章算法微博 leetcode 视频 九章算法偷录 算法培训 算法班课程大纲: 1 从strStr谈面试技巧与Coding Style(免费试听) 2 二分搜索与旋转排序数组 Binary S

[转]五大常用算法:分治、动态规划、贪心、回溯和分支界定

Referred from http://blog.csdn.net/yapian8/article/details/28240973 分治算法 一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并.这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)…… 任何一个可以用计算机求

算法导论——lec 11 动态规划及应用

和分治法一样,动态规划也是通过组合子问题的解而解决整个问题的.分治法是指将问题划分为一个一个独立的子问题,递归地求解各个子问题然后合并子问题的解而得到原问题的解.与此不同,动态规划适用于子问题不是相互独立的情况,即各个子问题包含公共的子子问题.在这种情况下,如果用分治法会多做许多不必要的工作,重复求解相同的子子问题.而动态规划将每个子问题的解求解的结果放在一张表中,避免了重复求解. 一. 动态规划介绍 1. 动态规划方法介绍: 动态规划主要应用于最优化问题, 而这些问题通常有很多可行解,而我们希

五大常用算法之二:动态规划算法

转自:http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741374.html 一.基本概念 动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移.一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划. 二.基本思想与策略 基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息.在求解任一子问题时,列出各种

(转)五大常用算法之二:动态规划算法

一.基本概念 动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移.一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划. 二.基本思想与策略 基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息.在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解.依次解决各子问题,最后一个子问题就是初始问题的解. 由于动态规划解决

基础算法总结

位运算 算术位运算 包括:按位与(&).按位或(|).按位异或(^).按位取反(~).按位左移(<<).按位右移(>>) 1 &(and) 对两个数进行操作,然后返回一个新的数,这个数的每个位都需要两个输入数的(同一位)都为1时才为1 举个例子: 1 1 1 1 1 0 0 0 1 1 1 0 =0 0 1 1 1 0 2 |(or) 比较两个数,然后返回一个新的数,这个数的每一位设置1的条件是两个输入数的同一位都不为0(即任意一个为1,或都为1) 举个例子: 1

基础算法之排序--快速排序

1 /************************************************************************************** 2 * Function : 快速排序 3 * Create Date : 2014/04/21 4 * Author : NTSK13 5 * Email : [email protected] 6 * Copyright : 欢迎大家和我一起交流学习,转载请保持源文件的完整性. 7 * 任何单位和个人不经本人允许不

算法录 之 基础算法

这一篇文章主要说说一些基础算法,这些算是很多其他算法的基石. 主要包括  模拟,暴力,枚举,递归,贪心,分治. 1,模拟: 顾名思义,模拟就是...饿,模拟,有一些题目就是给你一些步骤,然后你写代码一步步去模拟那些步骤就行.这类题目主要属于基础题,但是当然如果需要模拟的步骤足够恶心的话,还是比较麻烦的... 具体模拟的例子在之后的练习中会遇到的,按照题目要求一步步做就行,一般算法难度比较小. 2,暴力: 顾名思义,暴力就是...饿,暴力.比如说题目让从平面上的10个点中取三个点,让他们形成的三角