Day3-part1
Longest Common Subsequence 最长公共子序列问题
问题描述
这里主要想解决的问题是,确定两个字符串所包含的公共序列,允许字符之间存在间隙,但是不允许改变字符之间的顺序,例如:
序列1: GATTACA
序列2: TACTGTC
最长公共子序列显然为: ATTC, 但是如何确定公共子序列最大长度,以及具体是什么呢?
公共子序列解决思路
[1 ] 确定dp数组(dp table)以及下标的含义
[2 ] 确定递推公式
[3 ] dp数组如何初始化
[4 ] 确定遍历顺序
[5 ] 举例推导dp数组
我们解决这个问题,如果分别从两个序列的头部向后看,比如现在:
S_source=GATTACA
S_target=TACTGTC
从target第一个元素开始看起
GATTACT
TAC
此时LCS=3,因为S_target后续元素T,在source中找不到了,最大的长度就为3!
同理,如果反过来看呢:
TACTGTC
GATTACA
此时LCS=3,因为后续元素找不到了,最大的长度就为3!
因此,很自然我们可以想到,想获取最长公共子序列,需要两个字符串序列,分别去对方的序列中寻找,那么dp数组应该是一个二维数组。dp数组每个位置的含义是什么呢?
dp[i][j] 对应的是 S_target[0:j] 在序列 S_source[0:i] 中, 两者对应的最大公共子序列。
比如: dp[0][5]对应的是 ‘G’ 在序列 ‘TACTGT’ 中的公共子序列长度, 显然为1
比如: dp[3][5]对应的是 ‘GATT’ 在序列 ‘TACTGT’ 中的公共子序列长度, 显然为3
s_source='GATTACA'
s_target='TACTGTC'
dp=[[0 for i in range(len(s_target))] for j in range(len(s_source))]
下图为对应生成的二维数组,并且全部由0充满
那么,我们应该对dp数组进行初始化。根据如上的分析,很容易对第一行和第一列进行初始化
for i in range(len(s_target)):
if s_source[0]==s_target[i]:
for j in range(i,len(s_traget)):
result[0][j]=1
break
for i in range(len(s_source)):
if s_target[0]==s_source[i]:
for j in range(i,len(s_source)):
result[j][0]=1
break
递推公式的确定
动态规划的核心是确定递推公式,针对很多问题应该首先确定递推公式,随后确定dp数组初始化,但是在这里的话,为了针对分析的便利显示,我们首先将图绘出,进行分析。
针对问号点的数值我们应该如何确定呢?
针对点dp[ i ] [ j ],如果满足s_source[ i ] == s_target[ j ]的话,那公共子序列的长度肯定就会+1
是针对谁+1呢? 显然是针对dp[ i-1 ] [ j-1 ]的数值+1, 即针对上一个状态+1
if s_source[ i ] == s_target[ j ] → dp[ i ] [ j ]=dp[ i-1 ] [ j-1 ]+1
当时当 s_source[ i ] != s_target[ j ],现在应该执行什么操作呢?
想一想~
那现在肯定不能是前一个状态+1了,甚至说,都不应该考虑+1的事情了!
但是现在还是想知道公共子序列的最长长度,那么现在dp[ i ] [ j ] 应该就是对应未进入i,j状态的最大值了!
dp[ i ] [ j ] =max(dp[ i-1 ] [ j ],dp[ i ] [ j-1 ])
if s_source[ i ] != s_target[ j ] → dp[ i ] [ j ]=max(dp[ i-1 ] [ j ],dp[ i ] [ j-1 ])
for i in range(1,len(s_source)):
for j in range(1,len(s_target)):
if s_source[i]==s_target[j]:
dp[i][j]=dp[i-1][j-1]+1
else:
dp[i][j]=max(dp[i-1][j],dp[i][j-1])
至此,可以获得完全的dp矩阵,最大子序列长度应当出现在dp[ len(s_source)-1 ][ len(s_target)-1 ]
即右下角的点的值!
获取序列信息
如何获得子序列信息分别是什么呢?
需要进行的操作是从右下角开始寻找,逐步往上或者往左移动,标记dp[ i ][ j ] >dp[ i-1 ][ j ] 同时dp[ i ][ j ] >dp[ i ][ j-1 ]的点,对应的字符即为我们想知道的子序列
common_string=[]
i=len(s_source)-1
j=len(s_target)-1
while(i>=1 and j>=1):
if dp[i][j]>dp[i-1][j] and dp[i][j]>dp[i][j-1]:
common_string.append((s_source[i]))
i-=1
j-=1
elif dp[i][j]==dp[i-1][j]:
i-=1
elif dp[i][j]==dp[i][j-1]:
j-=1
return common_string
Day3-part2
The 0/1 knapsack Problem 0/1背包问题
问题描述
你有一个背包需要填充,背包的体积容量有要求。针对每个物品,你只可以选择0件/1件。每个物品有自己的体积(重量),和他对应的价值,现在你要做的事情:合理选择物品获得背包最大价值!
物品(items):( wi , valuei ) 物品重量、价值
(3,9);(1,7);(12,18);(5,3);(2,11)
背包容量:10 最大价值:9+7+11=27 选择物品:3+1+2
穷举的话,肯定是可以举出来的,但是这是否有点太暴力了。
我们在这里想一下用动态规划怎么来做呢?
0/1背包解决思路
[1 ] 确定dp数组(dp table)以及下标的含义
[2 ] 确定递推公式
[3 ] dp数组如何初始化
[4 ] 确定遍历顺序
[5 ] 举例推导dp数组
第一步,确定dp数组,这里的dp数组我们首先想到的是,延续part1的思路,我们构建一个二维数组。
整个二维数组,row=len(items), column=weight+1。
行代表是否放入某个物品进行考虑,列代表针对不同容量的背包从0容量开始考虑。
dp[ i ][ j ] 代表目前情况下背包内能放下的最大价值。
第二步,确定递推公式。
针对任意的 i,j。i 代表是否要放入物品 i ,j 代表的是当前的背包容量。 很直观我们想到的是双重循环,先遍历 i 再遍历 j。针对dp[ i ][ j ],肯定要取当前能取到的最大值,才能确保背包含有的价值是最大值。
dp[ i ][ j ]的取值无非是两种情况:
- 不放入items[ i ],那么dp[ i ][ j ]=dp[ i-1 ][ j ] ,也就是说和不放入的情况一模一样喽!
- 如果放入items[ i ],那么dp[ i ][ j ]=dp[ i-1 ][ j-items[ i ][ 0 ] ]+items[ i ][ 1 ] (这个不懂看图)
现在看我画圈圈的地方,现在是不是针对背包容量为5,考虑item(1,7)的情况!
如果不放入item的话,那么dp[ i ][ j ]=9
如果放入item的话,那么就相当于上一行背包容量为4的时候的值**(黄色点)**,再加上item(1,7),dp[ i ][ j ]=9+7=16,这两个取最大值就是这个红色圈圈应该放进去的值,对吧!
dp[ i ][ j ]=max(dp[ i-1 ][ j ], dp[ i-1 ][ j-items[ i ][0]] +items[ i ][ 1 ])
第三步,dp数组初始化
这一步也很简单的对吧!因为最开始肯定是空空荡荡的背包喽,每个点的价值都会是0!
dp[ i ][ j ]=0,千真万确。
第四步,确定遍历顺序
遍历顺序:先行后列,前面有说过,那么现在通过我们的代码实现吧!
value=[
[3,9],
[1,7],
[12,18],
[5,3],
[2,11]
] #前项代表重量,后项代表价值
def get_max_value(weight):
dp=[[0 for i in range(weight+1)] for j in range(len(value)+1)]
for i in range(1,len(value)+1):
for j in range(1,weight+1):
if j >=value[i-1][0]:
dp[i][j]=max(dp[i-1][j],dp[i][j-value[i-1][0]]+value[i-1][1])
else:
dp[i][j]=dp[i-1][j]
return dp[len(value)][weight]
def get_max_value(weight):
dp=[[0 for i in range(weight+1)]for j in range(len(value))]
for i in range(value[0][0],weight+1):
dp[0][i]=value[0][1]
for i in range(1,len(value)):
for j in range(0,weight+1):
if j>=value[i][0]:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-value[i][0]]+value[i][1])
else:
dp[i][j]=dp[i-1][j]
return dp[len(value)-1][weight]
这就是执行完的样子了,右下角就是最大价值了,自己动手实现一下吧!
找到具体放入的物品
那么,我们怎么才能知道具体放入了什么物品呢?
因为在dp数组中,最后一列代表着背包容量为weight的最大价值,那么在这里对最后一列进行分析。
27-16=11 筛选出item(2,11)
16-9=7 筛选出item(1,7)
9-0=9 筛选出item(3,9)
最终得到结果 item(2,1,3)
这里理解怎么出来的就好,基本上不会让你去实现的,fine!
现在pdf上面的内容就实现完了!Day3你已经成功拿下了!
一种可行的解决方案
现在我们的dp数组是一个二维数组对吧!这肯定会占用很大的存储空间,我们用一维数组能不能处理这个事情呢,可以的,是可以的。
数组递推方法:
dp[ i ]=max(dp[ i ],dp[ i-items[ i ][ 0 ] ]+items[ i ][ 1 ] )
还是考虑两种情况,是否放入items[i]
如果不放入: dp[ i ]=dp[ i ]
如果放入: dp[ i ]=dp[ i-items[ i ][ 0 ] ]+items[ i ][ 1 ] 想一下为什么呢?其实和二维是一样的,就是预留容量价值 + 物品价值!
两者取最大值是不是就完事了!
数组初始化:
初始均为0
def max_value(weight):
dp=[0]*(weight+1)
for i in range(len(value)):
for j in range(weight,value[i][0]-1,-1):
dp[j]=max(dp[j],dp[j-value[i][0]]+value[i][1])
return dp[weight]
注意仔细阅读两层循环嵌套,第二层循环嵌套,是不是逆向进行的? 为什么要逆向呢?
自己想一下,如果是正向的话,如果重量满足情况,在不断更新中,是不是将这个物体重复放入了,这样就不是 0/1背包 问题了!
自己动手实现吧,辛苦了!
海客谈瀛洲,烟涛微茫信难求。
越人语天姥,云霞明灭或可睹。
天姥连天向天横,势拔五岳掩赤城。
天台一万八千丈,对此欲倒东南倾。
我欲因之梦吴越,一夜飞度镜湖月。