动态规划 — 钢条切割问题

动态规划:

什么是动态规划?

动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

适用情况

能采用动态规划求解的问题的一般要具有3个性质:

(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

(2)无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

动态规划 - 钢条切割问题

问题:

假定我们知道sering公司出售一段长度为I英寸的钢条的价格为pi(i=1,2,3….)钢条长度为整英寸如图给出价格表的描述(任意长度的钢条价格都有),先给我们一段长度为n的钢条,问怎么切割,获得的收益最大 rn?

长度数组:int[] i = { 0, 1, 2, 3, 4 ,5, 6, 7, 8, 9, 10};

价格数组 : int[] p = { 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30};

如以上价格表:当钢条长度 n = 4 有  种切割方案 (总共有三个切口,每个切口有两种方案切或者不切,所以有 2 * 2 * 2 种可能)

假设 一个最优解 把长度为 n 的钢条,切个成了 k 段 (1<= k <=n),那么最优切割方案为:

(总长度n = 第一段长度 + 第二段长度+.....+第k段长度)———> 

那么最大收益为:

(总收益 = 第一段收益 + 第二段收益 + ......+ 第k段收益) ———>

将切割方案分成下面几种:

1、不切割情况 收益为

2、将钢条切割成两段,切割成两段的情况有 :(1,n-1) , (2,n-2) ........ (n-2,2) , (n-1,1)       (第一段的长度,第二段的长度)

然后再对这两半分别求最优解(这两段有分别可以再分成两段  这里就用到了递归的解决方法),最优解的和就是当前情况的最优解。

分析到这里我们看一下 这分成两段的情况 (1,n-1) , (2,n-2) ........ (n-2,2) , (n-1,1) 发现两段出现了同样的情况只是一段再左边一段再右边而已,这个时候我们就可以优化了,方法很简单就是 :切割成两段后  ,只对右边剩下长度为n-i的一段继续进行切割求最优方案,对左边的不再切割,那么第一段的价格就定下来了:p[i], 所以总收益公式就为:

代码实现 - 自顶向下递归实现:

    class Program
    {
        static void Main(string[] args)
        {
            // 价格数组  索引 i(长度)对应的价格为 p[i]
            int[] p = { 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30, 35, 38, 43, 45, 50, 62, 65, 66, 69, 70, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80 };

            Console.WriteLine("长度为  1 的最大切割收益为:" + UpDown(1, p));
            Console.WriteLine("长度为  2 的最大切割收益为:" + UpDown(2, p));
            Console.WriteLine("长度为  3 的最大切割收益为:" + UpDown(3, p));
            Console.WriteLine("长度为  4 的最大切割收益为:" + UpDown(4, p));
            Console.WriteLine("长度为  5 的最大切割收益为:" + UpDown(5, p));
            Console.WriteLine("长度为  6 的最大切割收益为:" + UpDown(6, p));
            Console.WriteLine("长度为  7 的最大切割收益为:" + UpDown(7, p));
            Console.WriteLine("长度为  8 的最大切割收益为:" + UpDown(8, p));
            Console.WriteLine("长度为  9 的最大切割收益为:" + UpDown(9, p));
            Console.WriteLine("长度为 10 的最大切割收益为:" + UpDown(10, p));
            Console.WriteLine("长度为 30 的最大切割收益为:" + UpDown(30, p));
            Console.ReadKey();
        }

        private static int UpDown(int n,int[] p)
        {
            if (n == 0) return 0;
            int maxPrice = 0;
            // i 代表第一段的长度 当 i==n 时那么第一段长度就为i 第二段长度就为0 ,所以当n为0时则直接return 0
            for (int i = 1; i <= n; i++)
            {
                int tmpPrice = p[i] + UpDown(n-i,p);   // 总价格 =  第一段价格 + (第二段最优价格)
                if (tmpPrice > maxPrice)
                {
                    maxPrice = tmpPrice;
                }
            }
            return maxPrice;
        }
    }

运行结果   运行后我们会发现,输出 "长度为 30 的最大切割收益为:108" 需要等待十几秒的时间(如果电脑差的话需要等待更久),也就是说 cpu 再执行 UpDown(30, p);进行了无数次的计算,所以该方法性能很差。

我们分析一下 UpDown(30, p) 的运行步骤

第一次for 循环时 i = 1:

 tmpPrice = p[1] + UpDown(29,p);

求UpDown(29,p) 的时候 又会去求 UpDown(28,p)

以此类推

i = 1 时 会执行  UpDown(29,p) UpDown(28,p)............UpDown(2,p) UpDown(1,p) UpDown(0,p)

i = 2 时 会执行  UpDown(28,p) UpDown(27,p)............UpDown(2,p) UpDown(1,p) UpDown(0,p)

i = 3 时 会执行  UpDown(27,p) UpDown(26,p)............UpDown(2,p) UpDown(1,p) UpDown(0,p)

.........

i = 28 时 会执行  UpDown(2,p) UpDown(1,p) UpDown(0,p)

i = 29 时 会执行  UpDown(1,p) UpDown(0,p)

i = 30 时 会执行  UpDown(0,p)

我们发现 递归的过程中一直重复的求已经求过的子问题(动态规划适用条件的第三点:有重叠子问题),那么如何优化能很显然我们只需要将已经求过的子问题最优解保存起来再次遇到的时候直接使用而不是再次去计算,接下来我们用代码来实现:

动态规划的方法进行求解:

 class Program
    {
        static void Main(string[] args)
        {
            // 价格数组  索引 i(长度)对应的价格为 p[i]
            int[] p = { 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30, 35, 38, 43, 45, 50, 62, 65, 66, 69, 70, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80 };
            // 用于存放子问题的最优解  有多少长度的钢条就有几种子问题
            int[] optimumSolution = new int[p.Length];

            Console.WriteLine("长度为  1 的最大切割收益为:" + UpDown(1, p, optimumSolution));
            Console.WriteLine("长度为  2 的最大切割收益为:" + UpDown(2, p, optimumSolution));
            Console.WriteLine("长度为  3 的最大切割收益为:" + UpDown(3, p, optimumSolution));
            Console.WriteLine("长度为  4 的最大切割收益为:" + UpDown(4, p, optimumSolution));
            Console.WriteLine("长度为  5 的最大切割收益为:" + UpDown(5, p, optimumSolution));
            Console.WriteLine("长度为  6 的最大切割收益为:" + UpDown(6, p, optimumSolution));
            Console.WriteLine("长度为  7 的最大切割收益为:" + UpDown(7, p, optimumSolution));
            Console.WriteLine("长度为  8 的最大切割收益为:" + UpDown(8, p, optimumSolution));
            Console.WriteLine("长度为  9 的最大切割收益为:" + UpDown(9, p, optimumSolution));
            Console.WriteLine("长度为 10 的最大切割收益为:" + UpDown(10, p, optimumSolution));
            Console.WriteLine("长度为 30 的最大切割收益为:" + UpDown(30, p, optimumSolution));
            Console.ReadKey();
        }

        private static int UpDown(int n, int[] p, int[] optimumSolution)
        {
            if (n == 0) return 0;
            // 判断是否已经求过了
            if (optimumSolution[n] != 0)
            {
                return optimumSolution[n];
            }
            int maxPrice = 0;
            // i 代表第一段的长度 当 i==n 时那么第一段长度就为i 第二段长度就为0 ,所以当n为0时则直接return 0
            for (int i = 1; i <= n; i++)
            {
                int tmpPrice = p[i] + UpDown(n - i, p, optimumSolution);   // 总价格 =  第一段价格 + (第二段最优价格)
                if (tmpPrice > maxPrice)
                {
                    maxPrice = tmpPrice;
                }
            }
            // 保存子问题的最优解
            optimumSolution[n] = maxPrice;

            return maxPrice;
        }
    }

运行后  "长度为 30 的最大切割收益为:108" 就马上就出来了。

总结:(自顶向下法)此方法依然是按照自然的递归形式编写过程,但过程中会保存每个子问题的解(通常保存在一个数组中)。当需要计算一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;如果没有保存过此解,按照正常方式计算这个子问题。我们称这个递归过程是带备忘的。

第二中动态规划 : 自底向上法

原理:

首先恰当的定义子问题的规模,使得任何问题的求解都只依赖于更小的子问题的解,因而我们将子问题按照规划排序,按从小到大的顺序求解。当求解某个问题的时候,它所依赖的更小的问题都已经求解完毕,结果已经保存。

代码实现:

       class Program
    {
        static void Main(string[] args)
        {
            // 价格数组  索引 i(长度)对应的价格为 p[i]
            int[] p = { 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30, 35, 38, 43, 45, 50, 62, 65, 66, 69, 70, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80 };
            // 用于存放子问题的最优解  有多少长度的钢条就有几种子问题
            int[] optimumSolution = new int[p.Length];

            Console.WriteLine("长度为  1 的最大切割收益为:" + ButtomUp(1, p, optimumSolution));
            Console.WriteLine("长度为  2 的最大切割收益为:" + ButtomUp(2, p, optimumSolution));
            Console.WriteLine("长度为  3 的最大切割收益为:" + ButtomUp(3, p, optimumSolution));
            Console.WriteLine("长度为  4 的最大切割收益为:" + ButtomUp(4, p, optimumSolution));
            Console.WriteLine("长度为  5 的最大切割收益为:" + ButtomUp(5, p, optimumSolution));
            Console.WriteLine("长度为  6 的最大切割收益为:" + ButtomUp(6, p, optimumSolution));
            Console.WriteLine("长度为  7 的最大切割收益为:" + ButtomUp(7, p, optimumSolution));
            Console.WriteLine("长度为  8 的最大切割收益为:" + ButtomUp(8, p, optimumSolution));
            Console.WriteLine("长度为  9 的最大切割收益为:" + ButtomUp(9, p, optimumSolution));
            Console.WriteLine("长度为 10 的最大切割收益为:" + ButtomUp(10, p, optimumSolution));
            Console.WriteLine("长度为 30 的最大切割收益为:" + ButtomUp(30, p, optimumSolution));
            Console.ReadKey();
        }

        // 自底向上
        private static int ButtomUp(int n, int[] p, int[] optimumSolution)
        { 
            int maxPrice = 0;
            int tmpPrice = 0;
            for (int i = 1; i <= n; i++)
            {
                for (int j = 1; j <= i; j++)
                {
                    tmpPrice = 0;
                    tmpPrice = p[j] + optimumSolution[i - j];
                    if (tmpPrice > maxPrice)
                    {
                        maxPrice = tmpPrice;
                    }
                }
                optimumSolution[i] = maxPrice;
            }
            return maxPrice;
        }

    }

 

总结:自底向上法要比自顶向下法性能稍微要好点

好在哪里呢?

自顶向下要频繁的对函数的调用,对函数的调用引用需要消一定的耗性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值