动态规划问题

动态规划(英语:Dynamic programming,简称DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划常常适用于有重叠子问题最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

动态规划的基本思想:若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。

通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。


背包问题(Knapsack problem)
一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。

关于各种背包问题的讲解详见:背包问题九讲

这里给出01背包问题完全背包问题的JavaScript实现
另外,“换零钱问题”也是背包问题中的一种。

01背包问题
有N件物品和一个容量为V的背包。放入第i件物品耗费的空间是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。

这是最基础的背包问题。
特点:每种物品仅有一件,可以选择放或不放。

用子问题定义状态:即F [i, v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:



【“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只和前i − 1件物品相关的问题。如果不放第i件物品,那么问题就转化为“前i − 1件物品放入容量为v的背包中”,价值为F [i − 1, v];如果放第i件物品,那么问题就转化为“前i − 1件物品放入剩下的容量为v − Ci的背包中”,此时能获得的最大价值就是F [i − 1, v − Ci]再加上通过放入第i件物品获得的价值Wi。】


let dp = new Array()
for (let i = 0; i < 1000; i++) {
  dp[i] = new Array()
  for (let j = 0; j < 1000; j++) {
    dp[i][j] = 0
  }
}

function pack(n, capacity, costs, values) {
  if (n < 0 || capacity < 0) return -1
  if (n === 0 || capacity === 0) return 0
  
  for (let i = 0; i <= n; ++i) {
    for (let j = 0; j <= capacity; ++j) {
      if (i > 0 && j >= costs[i - 1]) {
        dp[i][j] = Math.max(dp[i - 1][j], 
                    dp[i - 1][j - costs[i - 1]] + values[i - 1])
      }
    }
  }

  return dp[n][capacity]
}
console.log(pack(4, 10, [1, 3, 4, 5], 
                        [3, 6, 2, 8]))
// 输出
17

完全背包问题
有N种物品和一个容量为V 的背包,每种物品都有无限件可用。放入第i种物品的耗费的空间是Ci,得到的价值是Wi。求解:将哪些物品装入背包,可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……直至取⌊V /Ci⌋件等很多种。

如果仍然按照解01背包时的思路,令F [i, v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:


这跟01背包问题一样有O(V N)个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态F [i, v]的时间是O(v / Ci),总的复杂度可以认为是O(NV Σ(v / Ci)),是比较大的。
将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是要试图改进这个复杂度。





最少换零钱问题
如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够22元?求出最少硬币数。

let dp = [1]

function MinRCIter(aim, faceValueArr) {
  if (aim < 0) { return 100000000 }
  if (aim === 0) { return 0 }

  if (dp[aim]) {
    return dp[aim]
  }

  let localMin = 100000000

  for (let i = 0; i < faceValueArr.length; i++) {
    if (aim === faceValueArr[i]) {
      return 1
    }

    let rest = MinRCIter(aim - faceValueArr[i], faceValueArr)
    localMin = Math.min(localMin, rest + 1)
  }

  dp[aim] = localMin
  return dp[aim]
}

function MinReplaceChange(aim, arr) {
  console.log('Least: ' + MinRCIter(aim, arr))
}
// 测试
MinReplaceChange(22, [1, 3, 5])
// 输出
Least: 6

最长递增子序列longest increasing subsequence)问题
在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能地大。最长递增子序列中的元素在原序列中不一定是连续的。

给定一个长度为n的数组a[0], a[1], a[2]..., a[n-1],找出一个最长的单调递增子序列(注:递增的意思是对于任意的i < j,都满足a[i] < a[j],此外子序列的意思是不要求连续,顺序不乱即可)。
例如:给定一个长度为6的数组: [5, 6, 7, 1, 2, 8],则其最长的单调递增子序列为[5,6,7,8],长度为4。

用dp[i]表示以i结尾的子序列中LIS的长度。然后用dp[j] (0 <= j < i)来表示在i之前的LIS的长度。然后我们可以看到,只有当a[i] > a[j]的时候,我们需要进行判断,是否将a[i]加入到dp[j]当中。

为了保证我们每次加入都是得到一个最优的LIS,有两点需要注意:(1)每一次,a[i]都应当加入最大的那个dp[j],保证局部性质最优,也就是我们需要找到max(dp[j] (0 <= j < i));(2)每一次加入之后,我们都应当更新dp[j]的值,显然,dp[i] = dp[j] + 1。
如果写成递推公式,我们可以得到dp[i] = max(dp[j] (0 <= j < i)) + (a[i] > a[j] ? 1 : 0)

JavaScript实现
【时间复杂度:O(n ^ 2)】

let dp = [1]
let pre = [null]

function lisIter(endWith, listArr) {
  if (dp[endWith]) { 
    return dp[endWith] 
  }

  let localMaxLen = 1
  for (let i = 0; i < endWith; i++) {
    if (listArr[i] < listArr[endWith]) {
      if (localMaxLen < lisIter(i, listArr) + 1) {
        localMaxLen = lisIter(i, listArr) + 1
        pre[endWith] = i
      }
    }
  }
  
  dp[endWith] = localMaxLen
  return dp[endWith]
}


function LIS(arr) {
  for (let i = 0; i < arr.length; i++) {
    lisIter(i, arr)
  }

  let answer = -1
  let lastNode = -1
  for (let i = 0; i < dp.length; i++) {
    if (answer < dp[i]) {
      answer = dp[i]
      lastNode = i
    }
  }

  const seq = []
  do {
    seq.unshift(arr[lastNode])
    lastNode = pre[lastNode]
  } while(lastNode !== null)

  console.log('length: ' + answer)
  console.log('list: ' + seq)
}
// 测试
LIS([3, 5, 8, 2, 9, 10, 4])
// 输出
length: 5
list: 3,5,8,9,10

斐波那契数列
详见上一篇《斐波那契数列及其优化》


汉诺塔问题
详见《汉诺塔问题》


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 回溯算法 回溯法:也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并...
    fredal阅读 13,632评论 0 89
  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,268评论 0 18
  • 树形动态规划,顾名思义就是树+DP,先分别回顾一下基本内容吧:动态规划:问题可以分解成若干相互联系的阶段,在每一个...
    Mr_chong阅读 1,477评论 0 2
  • 1. (和)最大子序列(连续) 这是一道非常经典的动态规划的题目,用到的思路我们在别的动态规划题目中也很常用,以后...
    yangqi916阅读 2,896评论 0 0
  • NO.1 抓拍影子 拍摄要点 1.确认太阳的位置和影子的方向及长度。 2.尽可能选择简单、洁净的地面,将小饰物放在...
    明先森吖阅读 8,497评论 1 5