前言
对动态规划最初的认识:是能解决很难的问题,动态规划是一个很厉害的方法论。
场景
前天做力扣上做一个题目,等级是困难,理解完问题以后,觉得可以使用动态规划来做,但是自己已经记不清如何使用了。就按照自己能想到的方法开始写代码。
题目如下:
你正在安装一个广告牌,并希望它高度最大。这块广告牌将有两个钢制支架,两边各一个。每个钢支架的高度必须相等。
你有一堆可以焊接在一起的钢筋 rods。举个例子,如果钢筋的长度为 1、2 和 3,则可以将它们焊接在一起形成长度为 6 的支架。
返回广告牌的最大可能安装高度。如果没法安装广告牌,请返回 0。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/tallest-billboard
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
示例 1:
输入:[1,2,3,6]
输出:6
解释:我们有两个不相交的子集 {1,2,3} 和 {6},它们具有相同的和 sum = 6。
示例 2:
输入:[1,2,3,4,5,6]
输出:10
解释:我们有两个不相交的子集 {2,3,5} 和 {4,6},它们具有相同的和 sum = 10。
示例 3:
输入:[1,2]
输出:0
解释:没法安装广告牌,所以返回 0。
提示:
0 <= rods.length <= 20
1 <= rods[i] <= 1000
钢筋的长度总和最多为 5000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/tallest-billboard
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
我的解体思路是假设每次能用完rods里的所有钢筋,那么rods里的钢筋长度加起来需要是偶数total,如果不是偶数,则需要去掉rods里一根钢筋再尝试。如果是偶数,这一堆钢筋里能找到一些钢筋加起来等于total/2,如果不能,再去掉一根钢筋进行尝试。
算法采用了递归。并通过下面几种措施减少算的次数:
1、每次算的时候检查总的高度是否小于已经算出的高度
2、总的高度如果计算过了,就不用再算
3、 寻找total/2的时候,避免重复计算,缓存了计算的值
实现代码如下:
/**
* @param {number[]} rods
* @return {number}
*/
var tallestBillboard = function(rods) {
const sortrods = rods.sort((a, b) => {
if (a > b) {
return 1
} else if (a === b) {
return 0
} else {
return -1
}
})
let height = 0
const halfMap = {}
// 找到rods里的数加起来,可以等于half
const findAggregateToHalf = (rods, half) => {
let key = rods.reduce((total, item) => `${total}${item}`)
key += `${half}`
if (halfMap[key] !== undefined) {
return halfMap[key]
}
if (rods.length === 1) {
halfMap[key] = 0
return 0
}
if (rods.length === 2) {
if (rods[0] === half || rods[1] === half) {
halfMap[key] = half
return half
}
return 0
}
for (let i = rods.length - 1; i >= 0; i--) {
if (rods[i] > half) {
halfMap[key] = 0
return 0
}
const newRods = [].concat(rods)
const left = newRods.splice(i, 1)[0]
if (half < left) {
continue
}
if (half === left) {
halfMap[key] = half
return half
}
if (findAggregateToHalf(newRods, half - left)) {
halfMap[key] = half
return half
}
}
halfMap[key] = 0
return 0
}
let max = 0
const getMaxTall = (rods) => {
if (rods.length < 2) {
return 0
}
if (rods.length === 2) {
if (rods[0] === rods[1]) {
return rods[0]
} else {
return 0
}
}
const total = rods.reduce((total, item) => total + item)
if (halfMap[total]) {
return halfMap[total]
}
if (total < (max * 2)) {
return 0
}
if (total % 2 === 0) {
const half = total / 2
if (findAggregateToHalf(rods, half)) {
if (half > max) {
max = half
}
halfMap[total] = half
return half
} else {
// 去掉偶数相等的值,如果有值大于剩下
while(rods[rods.length - 1] > half) {
rods.pop()
}
for (let i = 0; i < rods.length; i++) {
const newrods = [].concat(rods)
newrods.splice(i, 1)
const result = getMaxTall(newrods)
if (result > max) {
max = result
}
}
halfMap[total] = max
return max
}
} else {
for (let i = 0; i < rods.length; i++) {
const newrods = [].concat(rods)
newrods.splice(i, 1)
const result = getMaxTall(newrods)
if (result > max) {
max = result
}
}
halfMap[total] = max
return max
}
}
let data2 = getMaxTall(sortrods)
return data2
};
虽然通过了测试,但是时间和空间消耗都比较悲催
而且代码可读性也比较查,应该多个地方含有布丁代码。而且这个代码历时5小时才最终通过了所有测试用例。
第二天
看了官方对题解,第一个方法就是动态规划,看第一眼很熟悉,但是仔细看,发现没能看懂官方的方法和状态转移方程。
整天都在思考🤔这个解题思路是咋回事,看了其他人的解题思路后,逐渐有了一点理解,于是开始写自己的版本,官方使用了递归来实现,我没有使用递归,而是循环从最小开始往大了找。
大概的是,在 rods里每增加一根钢筋,有三种情况,第一种是这个钢筋用不上,第二种是这跟钢筋放在左边的支架,第三种是这个钢筋放在右边的支架。那么增加一根钢筋后,最大值就在这三种情况里选了。
这里有一个细节,就是要记录每一次放了钢筋,左边支架和右边支架高度还差多少h,已经钢筋左边的值。我们使用一个map来记录,map的key表示h,map[h]表示钢筋左边的值。这样最后的map['0']就是最后的解。
代码如下:
/**
* @param {number[]} rods
* @return {number}
*/
var tallestBillboard = function(rods) {
const dp = []
let map = {}
for (let i = 0; i < rods.length; i++) {
if (i === 0) {
map['0'] = 0
map[`${-rods[i]}`] = 0
map[`${rods[i]}`] = rods[i]
continue
}
const r = rods[i]
const nMap = {}
for (let key in map) {
nMap[key] = map[key]
}
for (let key in map) {
const value = map[key]
const kn = Number(key)
nMap[`${kn + r}`] = Math.max(value + r, nMap[`${kn + r}`] || 0)
nMap[`${kn - r}`] = Math.max(value, nMap[`${kn - r}`] || 0)
}
map = nMap
}
return map['0'] || 0
};
比第一天的代码清晰了许多,并且更简单,当然依然有一些不足的地方。不过时间和空间复杂度得到了比较好的改善。
然后我重新看了一下wiki百科上对动态规划的描述,总结了两点:
1、动态规划用于解决有重叠子问题和最优子结构性质的问题。
2、动态规划通过把问题分解为子问题,且每个子问题有最优解,通过缓存中间子问题的结果来计算最终的结果,这通常需要推导出状态转移方程。
所以下次遇到算法问题,具有重叠子问题和最优子结构问题的时候,就可以考虑使用动态规划了。
比如下面这个问题:
问题描述
给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。
[2],
[3,4],
[6,5,7],
[4,1,8,3]
从顶到底部的最小路径和为11 ( 2 + 3 + 5 + 1 = 11)。
输入:
[[2], [3,4], [6,5,7], [4,1,8,3]]
输出:
11