DP解题之剪带子

522 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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

  • 根据算法中复杂度的定义,这个算法的时间复杂度应该是O(N)O(N), 还是比较好的。

  • 计算过程中,我们用到一个数字来存储各个长度的答案,所以空间复杂度也是O(N)O(N)

示例代码(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即可。