Home

Blog

Photography

Me

Algorithms 101 - 9 - dynamic-programming

03 May 2018

算法入门笔记,基于《Grokking Algorithms: An illustrated guide for programmers and other curious people》这本书的内容

主要内容


开始看到 ‘dynamic programming’ 以为是某种写程序的动态方法(翻译成中文是动态规划)。后面才发现,指的其实一种解决问题的思路,这种思路会在代码上体现出来。


1 The knapsack problem

在greedy algorithm那章提到了这个问题,当时是为了说明 greedy algorithm 对于某些问题会失效。

这属于一个 NP-complete problem,也就是当n很小的时候,可以用穷尽所有可能的方式找到确切的最佳解,而当n逐步变大时,使用遍历所有可能的方式会使计算量陡增,使得这种方式不可取。

这章对问题作了一点简化,将三种商品 Guitar, Laptop, Stereo 的重量改为了1,3,4 磅,knapsack的容量改为了4磅。

  重量 价值
Guitar 1 1500
Laptop 3 2000
Stereo 4 3000
1.1 simple algorithm

step1: 找出商品的所有可能组合 2^3 = 8
step2: 算出所有组合各自的重量
step3: 找到重量小于等于4磅的所有组合
step4: 从上面一步的结果中找出价值最大的组合,即结果

这里的run time 实际可以算作 2^n + n + n + n = 2^n + 3n

或者可以近似算作 O(2^n)

最后结果是 [Laptop, Guitar] => 价值 3500

1.2 greedy algorithm

step1: 找出能装下的最贵的,那就是 Stereo, 价值3000
step2: 从剩余容量中再选最贵的放进去,没有空间了,于是结果就是 Stereo

这种方法的 run time 是 O(n)

最后结果是 [Stereo] => 3000

1.3 dynamic programming

上面两种方式中,Greedy algorithm 比 simple algorithm 快的多,但只拿到了近似最佳解, miss 掉了实际最佳解。

而 simple algorithm 找到了最佳解,但在 n 很大的时候变得不可取。

另外一种思路即是 dynamic programming

Every dynamic-programming algorithm starts with a grid.

使用 dynamic-programming 算法时通常都会用到表格。

item\Weight bearing 1 2 3 4  
Guitar          
Stereo          
Laptop          

这个表格中横轴 1 2 3 4 是假想的 knapsack 装载重量,从最低的1磅到4磅

纵轴是要装的东西,第一行只能装前1行的items, 第2行可以装前2行中的items, 第3行可以装前3行包含的items,以此类推

表格中的每一格都要填,内容是装的东西以及价值总和

The Guitar row

先填吉他那行

Guitar 的重量是 1 磅, 价值 1500

item\Weight bearing 1 2 3 4  
Guitar G: 1500 G: 1500 G: 1500 G: 1500  
Stereo          
Laptop          

The Stereo row

接下来是音响的那行

Stereo 重4磅, 值 3000, 现在是第2行,那么前两行的items也就是 Guitar 和 Stereo 都可以装

item\Weight bearing 1 2 3 4  
Guitar G: 1500 G: 1500 G: 1500 G: 1500  
Stereo G: 1500 G: 1500 G: 1500 S: 3000  
Laptop          

The Laptop row

Laptop 重 3 磅, 值 2000

现在是第3行了,所以三个items都可选用

item\Weight bearing 1 2 3 4
Guitar G: 1500 G: 1500 G: 1500 G: 1500
Stereo G: 1500 G: 1500 G: 1500 S: 3000
Laptop G: 1500 G: 1500 L: 2000 G & L: 3500

The formula

这里填写每一格的时候实际是有公式的。

整个过程有点像 Dijkstra’s 算法的逐行更新。

针对knapsack这个具体案例,上面那个公式的步骤是

1 i 代表行row,j代表列column, 第2行第3列表示为 Cell[2][3]

2 在当前某一格 Cell[i][j], 会算出两个值

3 把上面拿到的两个值进行比较,如果新算出来那个更大,那么更新,否则,继承。

表格记录在这里的作用是,我们将每一步的结果存储了下来,当我们要去拿一个差值重量的item时,不需要再进行一次搜索, 这会增加 O(n) 的复杂度,当然这个例子中只有3个item一眼就可以看到,但如果当item数量到达100或更多时,这种直接定位到指定位置的方法很高效,类似从 array 中根据索引拿对象。

如果将上述公式写成ruby代码:

 1class Item
 2  attr_accessor :name, :weight, :price
 3end
 4
 5class Knapsack
 6  attr_accessor :items, :capacity, :table
 7
 8  def initialize(capacity)
 9    @items = []
10    @capacity = capacity
11    @table = Hash.new([0, [nil]])
12  end
13
14  def fill_items
15    return "No available items" if items.empty?
16    items.each_with_index do |item, index|
17      row = index + 1
18      (1..capacity).each do |column|
19        previous_max_value = table[[row-1, column]][0]
20        previous_max_name = table[[row-1, column]][1]
21        if column < item.weight
22          table[[row, column]] = table[[row-1, column]]
23        elsif column == item.weight
24          new_value = item.price
25          new_value >= previous_max_value \
26            ? table[[row, column]] = [new_value, [item.name]] \
27            : table[[row, column]] = [previous_max_value, [previous_max_name]]
28        else
29          diff_weight = column - item.weight
30          diff_item_name = table[[row-1, diff_weight]][1]
31          diff_item_value = table[[row-1, diff_weight]][0]
32          new_value = item.price + diff_item_value
33          if diff_item_value == 0
34            table[[row, column]] = [new_value, [item.name]]
35          else
36            table[[row, column]] = [new_value, [diff_item_name[0], item.name]]
37          end
38        end
39      end
40    end
41  end
42
43
44  def find_best_combinations
45    sorted_table = table.sort_by { |k, v| v[0] } # return an array
46    max_combination_price = sorted_table[-1][1][0]
47    p best_combinations = table.select { |k, v| v[0] == max_combination_price }
48  end
49
50
51end
52
53
54guitar = Item.new; guitar.weight = 1; guitar.price = 1500; guitar.name = "guitar"
55laptop = Item.new; laptop.weight = 3; laptop.price = 2000; laptop.name = "laptop"
56stereo = Item.new; stereo.weight = 4; stereo.price = 3000; stereo.name = "stereo"
57
58knapsack = Knapsack.new(4)
59knapsack.items = [stereo,laptop,guitar]
60knapsack.fill_items
61knapsack.find_best_combinations

最后输出结果是 {[3, 4]=>[3500, ["laptop", "guitar"]]}

可以尝试使用其他item进行测试

中间的 fill_items 方法冗长,其中大部分代码是为了正确记录当前格子中的价格是由哪两种商品组成。

为了避免以后自己都看不懂或者重构的可能,注释一下

 1
 2# @table = Hash.new([0, [nil]]) 的作用
 3
 4  # 后面会使用 table[row][column] 这样的格式来拿对应的格子中的信息
 5  # 格子中包含的信息有 1 装下的商品总价, 2 构成这个价格是哪些商品
 6    # 因此,一个格子中的信息单用一个 string 不能灵活的处理
 7    # 所以至少是一个 array, 而商品构成又很可能是多个所以 array 中的第二个element又需要是一个 array
 8  # 在填写空格的时候,会涉及到比较的问题
 9    # 1 当当前column对应的knapsack承重大于当前row商品的重量时
10    # 会取重量差值,然后去上一行找对应的 knapsack 对应的该重量的商品价值,尝试与当前item价格相加
11      # 这里又可能有3种情况 1 上一行中没有差值对应的column, 2 找到了但是重量是0,也就是什么也装不下, 3 有价格,有商品
12      # 当上一行中不存在这样一个对应column时,如果不用Hash.new([0, [nil]])让不存在的格子填入一个默认值,那么就会报错
13      # 当然可以再使用条件式去处理这种报错,但会更繁杂
14
15    # 2 填完当前格之后,会需要去跟同column的上一行比较,也就是上一次记录的最大值
16      # 这里要考虑的是填第一行的时候,上一行是不存在的
17      # 所以其实每一次比较都是用 Hash.new的默认值array中的第一个 0 去比较的
18      # 不然去拿不存在的格子会返回一个 nil, 用nil去比较会报错
19  # 因此,中间可能出现这么多可能的状况,那还不如直接一开始就固定好格式,都是一个嵌套array
20  # 第一个元素是总价,第二个是商品构成
21  # 并且设好默认值,不能存在的格子的价格都设为0, 商品构成中写 nil
22
23def fill_items
24  return "No available items" if items.empty?
25  items.each_with_index do |item, index|
26    row = index + 1      # row 其实就是 item 的 index + 1
27    (1..capacity).each do |column|
28      previous_max_value = table[[row-1, column]][0] # 在处理每一格的时候,先准备好要用作比较的上一次记录的最大值和其商品构成
29      previous_max_name = table[[row-1, column]][1]  # 方便后面添加
30      # 这里拆分三种比较情况主要是为了方便 table 中商品构成那部分的处理
31      if column < item.weight                              
32        table[[row, column]] = table[[row-1, column]]          # 当当前column容量装不下当前item的时候,直接继承上一行中的最大值
33      elsif column == item.weight                              # 当刚好可以装下时,就需要比较
34        new_value >= previous_max_value \                        # 如果当前item价格大于等于之前记录的最高价,将当前item信息作为当前格子信息
35          ? table[[row, column]] = [new_value, [item.name]] \    # 如果当前item价格小于之前记录的最高价,那么就要继承之前的总价和商品构成
36          : table[[row, column]] = [previous_max_value, [previous_max_name]]
37      else                                                     # 最后是column承重大于item重量时,就需要求差值,然后去找上一行对应承重的格子中的信息
38        diff_weight = column - item.weight   # 前把可能存在的差值格子中的信息提取出来,先算差值
39        diff_item_name = table[[row-1, diff_weight]][1] # 再拿商品名称
40        diff_item_value = table[[row-1, diff_weight]][0]  # 再拿总价
41        new_value = item.price + diff_item_value # 算差值格子中总价与当前item价格的和。这里没有再写代码去处理差值格子不存在的情况,这种情况下拿到的价格会是 0 , 具体说明看前面对 table 默认值设定的说明
42        if diff_item_value == 0 # 当差值不存在对应的格子时,将当前item信息作为当前格子信息
43          table[[row, column]] = [new_value, [item.name]]
44        else  # 当差值存在时,说明新总价是由两部分价格构成,所以要汇总两个格子的信息
45          table[[row, column]] = [new_value, [diff_item_name[0], item.name]]
46        end
47      end
48    end
49  end
50end
1.4 关于knapsack的dynamic programming 解法的说明

1.4.1 三个item的处理顺序是否必须从重量最轻的到最重的?

不需要,把上面 knapsack.items = [stereo,laptop,guitar] 里面item的顺序打乱,然后多试几次看看是否得到相同的结果。

knapsack.items = [stereo,laptop,guitar].shuffle

偶尔会出现 {[2, 4]=>[3500, ["laptop", "guitar"]], [3, 4]=>[3500, [["laptop", "guitar"]]]}

这样的结果,这其实就是从轻到重放置item出现的结果,在第二行其实就出现了最佳组合,直到最后一个cell [3,4] 也没有出现更大的组合,所以 [3,4] 直接继承了这个结果。

1.4.2 增加 item 会不会影响结果正确性?

如果代码逻辑正确,是不会出现问题的,可以测试一下,加两个item,Headphone和necklace

+++

1headphone = Item.new; headphone.weight = 2; headphone.price = 500; headphone.name = "headphone"
2necklace = Item.new; necklace.weight = 1; necklace.price = 2500; necklace.name = "necklace"
3
4knapsack.items = [stereo,laptop,guitar,headphone,necklace].shuffle

有趣的是,每运行一次,结果都可能不同,虽然最高总价相同,但可能会有不同的组合。

1{[3, 4]=>[4500, ["laptop", "necklace"]], [4, 4]=>[4500, ["headphone", "guitar"]], [5, 4]=>[4500, [["headphone", "guitar"]]]}
2
3{[5, 4]=>[4500, ["headphone", "necklace"]]}
4
5{[3, 4]=>[4500, ["necklace", "laptop"]], [5, 4]=>[4500, ["necklace", "guitar"]]}

上面是3次执行的结果,由于item排列方式的不同,最后拿到的组合也可能不同。在写的算法中,如果后面出现了新组合与前面最高价相同,是会更新此cell中的商品构成的。由结果也可以看出算法存在一个缺点是,只保证每次至少找出一个符合要求的最佳组合,但无法保证找出所有可能的最佳组合。

1.4.3 如果item或knapsack的重量包含小数会有影响吗?

从操作逻辑上讲是不会的,但是,注意我们写的算法中

(1..capacity).each do |column|

这里是一种简化的情况,每一列对应一个整数的承重磅数

这也是我们后面可以简化的使用差值去对应 cell 拿信息的原因。这里有几种情况

差值定位能正确工作,是我们能正确更新cell信息的基础之一,所以上面的算法其实是很简化的版本。如果要处理小数的问题,需要重新考虑如何正确定位cell的问题。

和给array排序这类处理简单对象的算法不同,对于dynamic programming对待具体问题需要重新思考算法怎么写。

2 另一个使用 dynamic programming 方式解决问题的例子-Optimizing your travel itinerary

2.1 问题描述

假设你现在要计划一次旅行,你的可选目的地有 A B C D E 五个,每个目的地消耗的游览时间从半天到两天不等,而你对这几个目的地都有一个评分。你的时间有限,现在你想弄清楚选择哪几个目的地才可以最大化这次旅行的总体评分。(书中给的目的地是具体地名,这里简化为字母)

现在来看看具体的情况

attraction time costs(day) rating
A 0.5 7
B 0.5 6
C 1 9
D 2 9
E 0.5 8

现在你只有2天游览时间

2.2 规划表格

首先找到限制条件,那就是游览时间,这是一个固定值,可以将这个值作为 column 的组成

那么rows对应的就是每一个目的地,每一格中包含的信息就是 当前的评分总和,以及目的地构成

其实与前面 knapsack 的例子很像。

attraction\time 0.5 1 1.5 2  
A          
B          
C          
D          
E          
2.3 代码实现

有几个关键点

代码实现

 1class Destination
 2  attr_accessor :name, :time_cost, :weight
 3  def initialize(name,time_cost,weight)
 4    @name = name
 5    @time_cost = time_cost
 6    @weight = weight
 7  end
 8end
 9
10class Schedule
11  attr_accessor :limitation, :destinations, :table, :subtimes
12
13  def initialize(limitation)
14    @limitation = limitation
15    @destinations = []
16    @subtimes = make_subtimes
17    @table = Hash.new([0, [nil]])
18  end
19
20  def make_subtimes
21    t = 0.5
22    @subtimes = []
23    while t <= limitation
24      @subtimes << t
25      t += 0.5
26    end
27    @subtimes
28  end
29
30  def fill_destinations
31    destinations.each.with_index do |destination, index|
32      row = index + 1
33      subtimes.each.with_index do |subtime, index|
34        column = index + 1
35        previous_max_weight = table[[row-1, column]][0]
36        previous_max_name = table[[row-1, column]][1][0]
37        if subtime < destination.time_cost
38          table[[row, column]] = table[[row-1, column]]
39        elsif subtime == destination.time_cost
40          current_weight = destination.weight
41          current_weight >= previous_max_weight \
42          ? table[[row, column]] = [current_weight, [destination.name]] \
43          : table[[row, column]] = table[[row-1, column]]
44        else
45          diff_time = subtime - destination.time_cost
46          offset_column = (diff_time/0.5).to_i
47          diff_weight = table[[row-1, offset_column]][0]
48          diff_destinations = table[[row-1, offset_column]][1]
49          if diff_weight == 0
50            table[[row, column]] = table[[row-1, column]]
51          else
52            new_weight = diff_weight + destination.weight
53            new_destinations = [destination.name] + diff_destinations
54            table[[row, column]] = [new_weight, new_destinations]
55          end
56        end
57      end
58    end
59  end
60
61  def find_best_combinations
62    sorted_table = table.sort_by { |k, v| v[0] }
63    best_combinations_weight = sorted_table[-1][1][0]
64    p best_combinations = table.select { |k,v| v[0] == best_combinations_weight }
65  end
66
67end
68
69a = Destination.new("A",0.5,7)
70b = Destination.new("B",0.5,6)
71c = Destination.new("C",1,9)
72d = Destination.new("D",2,9)
73e = Destination.new("E",0.5,8)
74
75schedule = Schedule.new(2)
76schedule.destinations = [a,b,c,d,e].shuffle
77schedule.fill_destinations
78schedule.find_best_combinations
2.4 Dynamic programming 只是一种近似解法

上面的例子中,每次执行前 destinations 都被打乱,最后结果可能有两种,一种最终得分是24, 另一种是 23

{[5, 4]=>[23, ["E", "B", "C"]]}{[5, 4]=>[24, ["C", "E", "A"]]}

在这个简单的例子中,item数量很少,24是实际最佳解,23虽然不是,但也很接近了。但这也提醒了一点,Dynamic programming 只是一种求近似答案的思路。

wikipedia中approximation algorithm的页面中也提到了 Dynamic programming 是一种近似算法的范式。

对于 NP-complete problems ,使用遍历算法不可取,所以才会 approximation algorithms 来平衡 run time 和 solution。 对于这个例子, Dynamic programming 方法得到的solution可能并不稳定,就如上面出现的不同情况。

dynamic programming 需要把大问题拆分成大小不同的子问题,然后解决并存储每一个子问题的解法,利用已有的子问题解法组合出更大一点的子问题解法,最后拿到整个问题的近似解。

整个过程中可变的是如何拆分子问题。比如一个问题的整体尺度是10,那么可以选择拆成1,2,3...10, 也可以拆成 2,4,6...10, 或者 5..10。拆的越细,run time 就越大,但可能就越接近最佳解,拆得粗则反之。 但这不是说拆的越细就越好,需要综合考虑。

比如上面 destination 的例子

3 中场小结

目前为止看到关于 Dynamic programming 的关键点

对于一个现实问题,识别他是否可以使用 dynamic programming 方式求解,以及规划出适合的解决方法是相对困难的,下面是一些建议:

4 Longest common substring 与 Longest common subsequence

两个问题名字接近但很不同。

Longest common substring

Longest common substring 求的是两个string之间的最大连续字母集, 翻译成中文是 最长公共子字串。比如

OPQRSQRSOP

的Longest common substring 就是 QRS

Longest common subsequence

Longest common subsequence 翻译成中文是 最长公共子序列

这里不好理解子序列和子集的区别

比如

CDEF

BCDXF

他们的 longest common substring 是 CD

而他们的 longest common subsequence 是 CDF , 中间把D和F隔开的X并不影响F被包含进去,因为在顺序上两个字串中的 F 都出现在 D 之后

Longest common subsequence 常被用于生物领域,比如对比基因序列的相似度等。其他方面也有重要的应用:

The longest common subsequence problem is a classic computer science problem, the basis of data comparison programs such as the diff utility, and has applications in bioinformatics. It is also widely used by revision control systems such as Git for reconciling multiple changes made to a revision-controlled collection of files.

wikipedia

4.1 code of Longest common substring

可以把字串看做很多字母的集合,然后求最大交集

思路是两个单词的字母分别作为 row 和 column

逐行比较,如果出现相同字母,则基于 table[[row-1, column-1]] 的值+1, 如果出现不匹配,填0

这样中间有断开不匹配的字母就会归零

下面是如何填表的伪代码

 1s1 = "publish"
 2s2 = "fisher"
 3
 4s1.each_char.with_index do |a, i|
 5  row = i + 1
 6  s2.each_char.with_index do |b, n|
 7    column = n + 1
 8    if a == b
 9      table[[row, column]] = 1 + table[[row-1, column-1]]
10    else
11      table[[row, column]] = 0
12    end
13end
4.2 code of Longest common subsequence

书中给出的例子只是计数,也就是算最长的common subsequence的长度,而没有记录具体是什么样的 subsequence。

表的划分还是和上面一样,使用单个字母

先看下书中给出的用于填表的伪代码

1if word_a[i] == word_b[j]:
2  cell[i][j] = cellp[i-1][j-1] + 1
3else:
4  cell[i][j] = max(cell[i-1][j], cell[i][j-1])

匹配到字母时的操作和substring一样,从取左上方的值+1

不匹配时的操作是取左边一格和上边一格两个值中较大的那个

公式给的很简单,但没有具体说明公式是如何得出的,以及为什么在表格中这样操作可以成功。

下图是两个字串 CGATCAACTGTA 寻找 longest common subsequence 的过程

下面是用ruby写的找出两个字串之间的所有 longest common subsequence 的代码

 1s1 = "CGATCT"
 2s2 = "ACTGTA"
 3
 4all_css = []
 5
 6s1.length.times do |n|
 7# starting with different letter, we may get different subsequences
 8  subsequence = ""
 9  start = 0
10  s1[n..-1].each_char do |a|
11    if i = s2[start..-1] =~ Regexp.new(a) # 不能直接用 /a/ !!!
12      subsequence << a
13      start = start + i + 1
14    end
15  end
16
17  all_css << subsequence
18end
19
20p all_css
21longest_length =  all_css.map(&:length).max
22p all_lcss = all_css.select{ |e| e.length == longest_length }
23

输出结果是

1["CGA", "GA", "ATT", "TT", "CT", "T"]
2["CGA", "ATT"]

其实这个算法有遗漏,后面会提到。

4.3 关于表格处理的公式推导问题

公式很直白,很清楚的知道每一个表格怎么填。但花了大量时间也没能彻底搞清楚这个公式是怎么一步步构建出来的。

初看这个问题的时候觉得不会太复杂,也就是两个字串之间的比较,总共就那么多个字母,能有多复杂。但慢下来去思考每一个不能想清楚的细节,发现里面包含的知识点太多,这也不是一个简单的问题。

wikipedia中还有这么一段

Notice that the LCS is not necessarily unique; for example the LCS of “ABC” and “ACB” is both “AB” and “AC”. Indeed, the LCS problem is often defined to be finding all common subsequences of a maximum length. This problem inherently has higher complexity, as the number of such subsequences is exponential in the worst case, even for only two input strings.

两个字串的LCSs不一定只有一个(比如上面代码中的例子)。实际上,LCSs问题也通常是为了找到所有最长的公共子序列。这个问题本身就具有很高的复杂度,最坏的情况中这样的公共子序列有很多,即使只是针对两个字串。

4.4 ruby中查找longest common subsequence的另一种笨办法

首先降一级思考什么是 一个字串的 subsequnece 其实就是 一个单词中所有字母的 有顺序的组合,长度从1个到整个单词的长度,在ruby中可以用 combination 方法找到

比如 s1 = "CGATCT" 的所有 subsequence

 1def cal_subsequences(string)
 2  string_letters = string.split("")
 3  subsequences = []
 4  n = 1
 5  while n < string.length
 6    combinations = string_letters.combination(n).to_a
 7    combinations.map! { |c| c.join }
 8    subsequences += combinations
 9    n += 1
10  end
11  subsequences
12end

可以算出

1["C", "G", "A", "T", "C", "T", "CG", "CA", "CT", "CC", "CT", "GA", "GT", "GC", "GT", "AT", "AC", "AT", "TC", "TT", "CT", "CGA", "CGT", "CGC", "CGT", "CAT", "CAC", "CAT", "CTC", "CTT", "CCT", "GAT", "GAC", "GAT", "GTC", "GTT", "GCT", "ATC", "ATT", "ACT", "TCT", "CGAT", "CGAC", "CGAT", "CGTC", "CGTT", "CGCT", "CATC", "CATT", "CACT", "CTCT", "GATC", "GATT", "GACT", "GTCT", "ATCT", "CGATC", "CGATT", "CGACT", "CGTCT", "CATCT", "GATCT"]
262

总共有62个subsequences。

如果把两个字串的 subsequneces 都算出来,然后求交集,就可以拿到他们共同的 subsequences, 接着在找到其中最长的。

 1s1 = "CGATCT"
 2s2 = "ACTGTA"
 3
 4def cal_subsequences(string)
 5  string_letters = string.split("")
 6  subsequences = []
 7  n = 1
 8  while n < string.length
 9    combinations = string_letters.combination(n).to_a
10    combinations.map! { |c| c.join }
11    subsequences += combinations
12    n += 1
13  end
14  subsequences
15end
16
17p common_sequences = cal_subsequences(s1) & cal_subsequences(s2)
18p longest = common_sequences.map(&:length).max
19p common_sequences.select { |s| s.length == longest }

结果是

1["C", "G", "A", "T", "CG", "CA", "CT", "GA", "GT", "AT", "AC", "TT", "CGA", "CGT", "CTT", "ATT", "ACT"]
23
3["CGA", "CGT", "CTT", "ATT", "ACT"]

最后结果比之前手工演算的又多出了3个,找到了所有的 longest common subsequence, 这里的关键是要保持顺序,所以不能用 permutation。

这个方法的run time很高,尤其字串很长的时候,但他能找出所有的子序列。

这里需要再提一次 Dynamic programming 只是一种近似解法 , 例子中用的表格可以算出 longest common subsequence 的长度,也可以找出所有的 longest common subsequence

5 dynamic programming 动态规划的其他用途


recap