动态规划之切棍子问题
Posted 算法的秘密
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划之切棍子问题相关的知识,希望对你有一定的参考价值。
❝今天这道题和无限背包问题的思路是一模一样的。如果还没有看过无限背包问题强烈建议先看一下。
❞
对比思考,大家可以更深刻的体会,这两道题,外表形式看起来不同,但思维内核却完全一样。只有体会到思路上的共同性,掌握思维要领,才能做到举一反三。
问题描述
给定一根长度为n的棍子,我们要将其切割出售,以便获得最大收益。从1到n的不同长度的棍子的价格是已知的。
举例如下:
长度:[1、2、3、4、5]
价格:[2、6、7、10、13 ]
棍长:5
让我们尝试切割棍子:
五件长度1 => 10
两件长度2和一件长度1 => 14
一件长度3和两件长度1 => 11
一件长度3和一件长度2 => 13
一件长度4和一件长度1 => 12
一件长度5 => 13
这表明我们通过将棍子切成两个长度为2和一个长度为1的棍子出售,可以获得最高的价格14。
思路导引
在具体讲解这个题之前,我们先大概聊聊,绝大多数的算法题的基本思路。
首先,绝大多数算法题,从本质上讲,都是「求一个最优的组合」。这个最优有很多具体的形式:
-
最大价格(比如本题)
-
最长公共子串
-
最长窗口(比如滑动窗口问题)
-
最小子集
-
最短路径
-
等等
总之,这些题目的特征是非常的明显的,基本上一眼就能认出来。
对这些问题,有两个基本的解题思路:
-
穷举。遍历所有组合,寻找最优组合,一般会加入一些计算优化
-
分类。将所有组合分类,每类寻最优,各类最优比较得出答案
这两个思想是如此的简单,但是也是如此的深刻和通用。
「动态规划,滑动窗口,双指针,回溯等等都是具体的技巧,但是背后的思路都跳不出上面两个。」
穷举,这个大家都好理解。「难点在于如何做到穷举」。像回溯,递归这些技巧,都是可以帮助我们完成穷举的,关键是运用好这些技巧。
分类,这个思路所站的角度更高一些。使用这个思路时,很多文章都会介绍动态规划的状态转移方程。但是,根据我自己的经验,直接从寻找最优组合的角度进行思考会更自然和直接,这一点,在待会儿的具体解题中大家可以体会的。
当你面对一个问题没有思路时,不妨返璞归真,尝试从穷举和分类两个思路进行思考,也许就会柳暗花明又一村。
自上向下加记忆
「下面的解题过程中,大家可以放空大脑,假设完全没有学过动态规划,没有学过递归,就是一个算法萌新,看看我们是如何从分类这一个基本的思路出发,一点一点解决问题的。」
首先,我们定义,(n, 1)表示原问题,第一个n表示棍子的长度为n,第二个1表示可以将棍子切割成1-n的的长度,也就是一共有n个切割选项。假设最优的组合产生的价格为O(n,1)。S(n)表示长度为n的棍子的价格。
对于本题,我们可以这样思考,棍子有很多种切割的方法,每种方法其实就是一种切割组合。我们要找的是价格最高的那个组合。
那么多组合中,如何寻找最优的组合呢?
组合虽然有很多种,但是,我们可以将其分成两类:
-
组合中 「至少包含一个」长度为1的棍子
当组合中至少包含了一个长度为1的切割时,这类组合中最优的组合产生的价格就相当于 「S(1) + O(n-1, 1)」。n-1表示从原棍子中减去已经切割出来的长度为1的子棍。第二个1表示,仍然有1到n个可选切割。
-
组合中 「不包含」长度为1的棍子
这类组合中的最优组合,相当于子问题(n,2)的最优组合,即「O(n, 2)」。
然后我们只要从上面两个最优解中,选择更优的那个就可以了。
即:
「max(S(1) + O(n-1, 1), O(n, 2))」
我们可以递推一步,
「O(n, 2) = max(S(2)+O(n-2,2), O(n,3))」
本质还是对组合分类。
此时,我们的递推式,已经初具形状。下面,我们就要来考虑一下,边界条件。
边界条件一般分两类:
一种就是正确性条件,其实就是防止用户输入错误的数据。
另一种才是我们关注的真正的base问题条件。
「观察我们上面的递推式」,可以发现O(p,q) 在递推过程中, p在减小,q在增加。很自然的,就有了两个边界条件
p<= 0,棍子已经切割完了。
q > n,表示已经没有可用的切割选项了。
这两种情况下,结果都应该是0。
至此,我们已经将整个解决方案分析出来了,剩下的就是代码实现了。
代码实现
实现的时候,只要再加上一个缓存,就完美了。
代码如下所示:
def solve_rod_cutting(pirces, selects, rod_len):
dp = [[-1 for _ in range(rod_len+1)] for _ in range(len(pirces))]
return solve_rod_cutting_recursive(dp, pirces, selects, rod_len, 0)
def solve_rod_cutting_recursive(dp, prices, selects, rod_len, currentIndex):
n = len(prices)
if rod_len <= 0 or n == 0 or len(selects) != n or currentIndex >= n:
return 0
if dp[currentIndex][rod_len] == -1:
profit1 = 0
if selects[currentIndex] <= rod_len:
profit1 = prices[currentIndex] + solve_rod_cutting_recursive(
dp, prices, selects, rod_len - selects[currentIndex], currentIndex)
profit2 = solve_rod_cutting_recursive(
dp, prices, selects, rod_len, currentIndex + 1)
dp[currentIndex][rod_len] = max(profit1, profit2)
return dp[currentIndex][rod_len]
总结
以上的分析,我们尽可能从自然的思路出发去思考问题,逐步引出解决方案,思路衔接上非常自然。
很多讲动态规划的文章,上来就是最优子结构,好像这个子结构是某个牛叉的人发明的某种神奇的技术。其实,背后的思想就是简单的分类,我们高中数学中四大思想之一,那么问题来了,另外三个思想是什么呢?(手动狗头),加小编好友告诉我吧。
以上是关于动态规划之切棍子问题的主要内容,如果未能解决你的问题,请参考以下文章
(动态规划)1547. 切棍子的最小成本(区间dp)/221. 最大正方形 / 1312. 让字符串成为回文串的最少插入次数(区间dp)
UVA-10003 Cutting Sticks 动态规划 找分界点k的动规