我正在重新阅读Skiena的算法设计手册,以了解自学校以来我忘记的一些东西,我对他对动态编程的描述感到有些困惑。我在维基百科和其他各种网站上查了一下,虽然描述都很有意义,但我自己也难以找出具体的问题。目前,我正在从Skiena的书中解决问题3-5。 (给定n个实数的数组,在输入的任何连续子向量中找到最大总和。)我有一个O(n ^ 2)解,如this answer中所述。但我坚持使用动态编程的O(N)解决方案。我不清楚复发关系应该是什么。
我看到子序列形成一组总和,如下:
S = {a,b,c,d}
a a+b a+b+c a+b+c+d
b b+c b+c+d
c c+d
d
我没有得到的是如何选择哪一个在线性时间内最大。到目前为止,我已经尝试过跟踪最大值的事情,如果当前值为正,则将其添加到总和中。但是当你有更大的序列时,这就成了问题,因为可能有一些负数会减少总和,但是后来的大正数可能会使它恢复到最大值。
我也想起了总计区域表。你可以只使用累积和来计算所有的总和:a,a + b,a + b + c,a + b + c + d等等(例如,如果你需要b + c,它只是(a +) b + c) - (a)。)但是没有看到O(N)的方法来获得它。
有人可以向我解释一下这个特定问题的O(N)动态编程解决方案是什么吗?我觉得我几乎得到了它,但我错过了一些东西。
答案 0 :(得分:11)
答案 1 :(得分:1)
有一个解决方案,首先将数组排序到某个辅助内存中,然后将最长公共子序列方法应用于原始数组和排序数组,其中包含公共子序列的总和(不是长度) 2个数组作为表的入口(Memoization)。这也可以解决问题
总运行时间是O(nlogn)+ O(n ^ 2)=>为O(n ^ 2) 空间是O(n)+ O(n ^ 2)=>为O(n ^ 2)
当内存进入画面时,这不是一个好的解决方案。这只是为了让人们了解如何将问题相互减少。
答案 2 :(得分:0)
我对DP的理解是关于“制作一张桌子”。事实上,DP中原来的“编程”意思就是制作表格。
关键是找出要放在表格中的内容,或现代术语:要跟踪的状态,或者DAG中的顶点键/值是什么(如果它们听起来很奇怪,请忽略这些术语你)。
如何选择dp[i]
表是数组索引 i 的最大总和,例如,数组为[5,15,-30,10]
第二个重要的关键是“最佳子结构”,即“假设”dp[i-1]
已经存储了以索引 i-1 结尾的子序列的最大总和,这就是为什么 i 的唯一步骤是决定是否将a[i]
包含在子序列中
dp[i] = max(dp[i-1], dp[i-1] + a[i])
max
中的第一项是“不包括[i]”,第二项是“包括[i]”。请注意,如果我们不包含a[i]
,则到目前为止最大的总和仍然是dp[i-1]
,它来自“最佳子结构”参数。
所以整个程序看起来像这样(在Python中):
a = [5,15,-30,10]
dp = [0]*len(a)
dp[0] = max(0,a[0]) # include a[0] or not
for i in range(1,len(a)):
dp[i] = max(dp[i-1], dp[i-1]+a[i]) # for sub-sequence, choose to add or not
print(dp, max(dp))
结果:在dp
遍历数组i
之后,最大的子序列总和应该是a
表中的最大项。但仔细看看dp
,它掌握了所有信息。
因为它只遍历数组a
中的项目,所以它是一个O(n)算法。
这个问题看起来很愚蠢,因为只要a[i]
为正,我们就应该总是把它包含在子序列中,因为它只会增加总和。这种直觉与代码匹配
dp[i] = max(dp[i-1], dp[i-1] + a[i])
所以最大。子序列问题的总和很容易,根本不需要DP。简单地说,
sum = 0
for v in a:
if v >0
sum += v
然而,“连续子阵列”问题的最大总和呢?我们需要改变的只是一行代码
dp[i] = max(dp[i-1]+a[i], a[i])
第一项是“在连续子阵列中包含[i]”,第二项是决定开始一个新的子阵列,从[i]开始。
在这种情况下,dp[i]
是最大值。求和以index-i结尾的连续子数组。
这肯定比天真的方法O(n ^ 2)* O(n),i-loop内的for j in range(0,i):
和sum
所有可能的子阵列都要好。
一个小警告,因为设置dp[0]
的方式,如果a
中的所有项都是否定的,我们就不会选择任何项。因此,对于max sum连续子阵列,我们将其更改为
dp[0] = a[0]