一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情。
DP 概念
这是一种用来解决复杂算法问题的常用办法,其实是一种思想:
- 如果这个问题很复杂,那么肯定是问题本身涉及的数值太大,所以比较复杂。
- 既然涉及到的数值太大,我们就将数值缩小
- 然后找到缩小的数值和原数值之间的关系
- 进而得到最终的答案
问题之剪带子
这是一个OJ网站上的题目,大意如下:
小明有一根带子,长度是N,他想剪掉这根带子,但是小明规定剪完之后的长度只能是a, b, c这三种(a,b,c可能相等)。
小明可以随便怎么剪都行,只要满足上述的限制。
小明想问:该如何剪,才能让最终的带子的数量是最大的?
一个具体例子
比如说,这个带子的原本长度是7, 小明规定只能剪成 5, 5, 2 这几种长度。
通过人脑都能得知,只有一种剪法:5, 2, 最终的数量是2 根。
数值增加影响复杂度
上面的例子很简单,那是因为长度为7,人脑计算很快。
那么试试长度为700?
复杂度上升了不少,接下来是重点,DP的思路:
-
长度7既然算出来了,那么在这个基础上,往上加长度 -
比如说
长度8,长度8第一剪子,能有两种剪法:5 或者 2 -
也就分成两个方向,
剪5变成长度3 + 长度5: 问题成为 计算长度3然后加 1剪2变成长度6 + 长度2: 问题成为 计算长度6然后加 1
-
此时,如果我们已经知道
长度3或者长度6的答案,那么长度8的答案也就出来了。 -
就是将上面两种剪法的答案比较一下,取大的就行。
-
重点来了,计算
长度8这个复杂的问题转变成了 复杂度较小的长度3和长度6的问题。 -
那么
长度3可以继续转化成更小的 -
长度6也是如此
这个例子的具体计算步骤
首先声明一个结果数组,用来存储,各个长度的结果:
var res []int = make([]int, N+1)
-
计算
长度1:不合法,丢弃 :res[1] = -1, 这里存储-1是为了讲述方便,就说明这个长度没法剪。 -
计算
长度2: 结果是1,存储:res[2] = 1 -
计算
长度3: 根据小明的规定:- 第一刀剪5:不合法
- 第一道剪2:变成
长度1+ 1, 而长度1上面已经计算过不合法了,所以也丢弃这种,这就是为什么要存储不合法的长度1 - 基于上面两种剪法,
长度3不合法,丢弃:res[2] = -1
-
一直这样算下去,一直算到
长度700 -
根据算法中复杂度的定义,这个算法的时间复杂度应该是, 还是比较好的。
-
计算过程中,我们用到一个数字来存储
各个长度的答案,所以空间复杂度也是
示例代码(Golang)
func cutRibbon(N, a, b, c int) int {
res := make([]int, N+1)
cons := []int{a, b, c}
//
three := []int{0, 0, 0}
for idx := 1; idx <= N; idx++ {
three[0], three[1], three[2] = 0, 0, 0 // 清空
for jdx, onecon := range cons { // 遍历第一剪的三种情况
if idx < onecon { // 剪不了
continue
}
if idx == onecon { // 刚好相等
three[jdx] = 1
continue
}
if res[idx-onecon] == 0 { // 不合法的剪法
continue
}
three[jdx] = res[idx-onecon] + 1
}
// 三种剪法最终比较一下,取大的
sort.Ints(three)
res[idx] = three[2]
}
return res[N]
}
注意,某一个长度,如果完全不能减的话,我没有置成-1, 直接用默认值0即可。