本期任务:介绍算法中关于动态规划思想的几个经典问题
一、问题描述
"""
剪绳子问题
给你一根长度为N的绳子,请把绳子剪成M段(m,n都是整数),每段绳子的
长度记为k[0],k[1],k[2]…. 请问如何剪绳子使得k[0],k[1],k[2]
的乘积最大
例如 绳子长度8 最大乘积18 = 2*3*3
输入:n=8
输出:18
"""
二、算法思路
本题的解法与硬币找零问题大同小异,可以阅读【算法】【动态规划篇】第4节:硬币找零问题,加深对此类问题的理解。
1. 策略选择
一个模型:
- 剪绳子问题是典型的“多阶段决策求最优解”问题,每一次决策有最多有n种选择,直到目标值n为0时结束;最优解是各段长度乘积的最大值。
三个特征:
-
重复子问题:
- 不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
- 本题中,前两段长度分别为 ( 2 , 2 ) (2,2) (2,2)和 ( 1 , 3 ) (1,3) (1,3)都能进入目标值为4的状态
-
无后效性:
-
最优子结构:
- 后面阶段的状态可以通过前面阶段的状态推导出。
- 本题中,目标值为8的状态可以由目标值为1-7的状态来推导。
综上所述,本问题满足一个模型、三个特征,所以可以使用动态规划来求解。
当然,凡是能用动态规划解决的问题,都可以用回溯思想来暴力求解,具体实现代码在文末给出,更多关于回溯思想的应用,可以参照:【算法】【回溯篇】第7节:0-1背包问题
三、Python代码实现
1. 动态规划解法
class Solution():
def dp(self, n):
"""
使用动态规划求解剪绳子问题,思路:维护一个长为n的数组,自底向上求解最优值!!!
:param n: 绳子长度
:return: 最大乘积值
"""
if n < 2: # 长度为1,无法剪
return 0
if n == 2: # 长度为2,只能剪成1*1
return 1
if n == 3: # 长度为3,剪成2*1 > 1*1*1
return 2
res = [-1] * (n + 1) # 初始化数组
res[0] = 1 # 约定f(0)=1
for i in range(1, n + 1): # 依次计算f(1)--f(8)
res[i] = max([(j + 1) * res[i - j - 1] for j in range(i)])
return res[-1]
def main():
n = 8
client = Solution()
print(client.dp(n))
if __name__ == '__main__':
main()
运行结果:
18
2. 回溯解法
class Solution():
def trackback(self, n):
"""
使用回溯法求解剪绳子问题
:param n: 绳子长度
:return: 最大乘积值
"""
if n < 2: # 长度为1,无法剪
return 0
if n == 2: # 长度为2,只能剪成1*1
return 1
if n == 3: # 长度为3,剪成2*1 > 1*1*1
return 2
self.max_v = n # n大于0,所以初值设为n
self.helper(n, 1) # 回溯暴力求解
return self.max_v
def helper(self, n, v):
"""一次遍历完所有可能"""
if n == 0: # 当绳长为0时进行结算
if self.max_v < v:
self.max_v = v
return
for i in range(1, n + 1): # 每次分段的长度介于[1, n]
self.helper(n - i, v * i)
def main():
n = 8
client = Solution()
print(client.trackback(n))
if __name__ == '__main__':
main()
运行结果:
18