问题:我正在努力理解/可视化动态编程方法“动态编程中的一种平衡0-1矩阵 - 维基百科文章。”
维基百科链接:https://en.wikipedia.org/wiki/Dynamic_programming#A_type_of_balanced_0.E2.80.931_matrix
在处理多维数组时,我无法理解memoization的工作原理。例如,在尝试使用DP解决Fibonacci系列时,使用数组来存储先前的状态结果很容易,因为数组的索引值存储该状态的解决方案。
有人可以用更简单的方式解释DP方法的“0-1平衡矩阵”吗?
答案 0 :(得分:4)
维基百科提供了一个糟糕的解释和一个不理想的算法。但是让我们将它作为一个起点。
首先让我们采用回溯算法。不是把矩阵的单元格按“某种顺序”放置,而是让第一行中的所有内容,第二行中的所有内容,然后是第三行中的所有内容,依此类推。显然这将起作用。
现在让我们稍微修改回溯算法。我们不是一个接一个地逐个进行,而是逐行进行。所以我们列出了n choose n/2
可能的行,它们是一半0和一半1.然后有一个递归函数看起来像这样:
def count_0_1_matrices(n, filled_rows=None):
if filled_rows is None:
filled_rows = []
if some_column_exceeds_threshold(n, filled_rows):
# Cannot have more than n/2 0s or 1s in any column
return 0
else:
answer = 0
for row in possible_rows(n):
answer = answer + count_0_1_matrices(n, filled_rows + [row])
return answer
这是我们之前的回溯算法。我们一次只做整行,而不是细胞。
但请注意,我们传递的信息超出了我们的需要。没有必要传递行的确切排列。我们需要知道的是每个剩余列中需要多少1个。所以我们可以让算法看起来更像这样:
def count_0_1_matrices(n, still_needed=None):
if still_needed is None:
still_needed = [int(n/2) for _ in range(n)]
# Did we overrun any column?
for i in still_needed:
if i < 0:
return 0
# Did we reach the end of our matrix?
if 0 == sum(still_needed):
return 1
# Calculate the answer by recursion.
answer = 0
for row in possible_rows(n):
next_still_needed = [still_needed[i] - row[i] for i in range(n)]
answer = answer + count_0_1_matrices(n, next_still_needed)
return answer
这个版本几乎是维基百科版本中的递归函数。主要区别在于我们的基本情况是,在每一行完成之后,我们什么都不需要,而维基百科会让我们编写基本案例代码,以便在完成其他任务后检查最后一行。
要从此到自上而下的DP,您只需要记住该功能。在Python中,您可以通过定义然后添加@memoize
装饰器来完成。像这样:
from functools import wraps
def memoize(func):
cache = {}
@wraps(func)
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
但请记住,我批评了维基百科的算法?让我们开始改进吧!第一个重大改进就是这个。您是否注意到still_needed
元素的顺序无关紧要,只是它们的值?因此,仅对元素进行排序将阻止您针对每个排列单独进行计算。 (可以有很多排列!)
@memoize
def count_0_1_matrices(n, still_needed=None):
if still_needed is None:
still_needed = [int(n/2) for _ in range(n)]
# Did we overrun any column?
for i in still_needed:
if i < 0:
return 0
# Did we reach the end of our matrix?
if 0 == sum(still_needed):
return 1
# Calculate the answer by recursion.
answer = 0
for row in possible_rows(n):
next_still_needed = [still_needed[i] - row[i] for i in range(n)]
answer = answer + count_0_1_matrices(n, sorted(next_still_needed))
return answer
那个无害的sorted
看起来并不重要,但它可以节省大量的工作!现在我们知道still_needed
总是排序,我们可以简化检查是否已完成,以及是否有任何结果为负。另外,我们可以添加一个简单的检查来过滤掉列中有太多0的情况。
@memoize
def count_0_1_matrices(n, still_needed=None):
if still_needed is None:
still_needed = [int(n/2) for _ in range(n)]
# Did we overrun any column?
if still_needed[-1] < 0:
return 0
total = sum(still_needed)
if 0 == total:
# We reached the end of our matrix.
return 1
elif total*2/n < still_needed[0]:
# We have total*2/n rows left, but won't get enough 1s for a
# column.
return 0
# Calculate the answer by recursion.
answer = 0
for row in possible_rows(n):
next_still_needed = [still_needed[i] - row[i] for i in range(n)]
answer = answer + count_0_1_matrices(n, sorted(next_still_needed))
return answer
并且,假设您实施possible_rows
,这应该比维基百科提供的更有效并且效率更高。
=====
这是一个完整的工作实现。在我的机器上,它在4秒内计算了第6个术语。
#! /usr/bin/env python
from sys import argv
from functools import wraps
def memoize(func):
cache = {}
@wraps(func)
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
@memoize
def count_0_1_matrices(n, still_needed=None):
if 0 == n:
return 1
if still_needed is None:
still_needed = [int(n/2) for _ in range(n)]
# Did we overrun any column?
if still_needed[0] < 0:
return 0
total = sum(still_needed)
if 0 == total:
# We reached the end of our matrix.
return 1
elif total*2/n < still_needed[-1]:
# We have total*2/n rows left, but won't get enough 1s for a
# column.
return 0
# Calculate the answer by recursion.
answer = 0
for row in possible_rows(n):
next_still_needed = [still_needed[i] - row[i] for i in range(n)]
answer = answer + count_0_1_matrices(n, tuple(sorted(next_still_needed)))
return answer
@memoize
def possible_rows(n):
return [row for row in _possible_rows(n, n/2)]
def _possible_rows(n, k):
if 0 == n:
yield tuple()
else:
if k < n:
for row in _possible_rows(n-1, k):
yield tuple(row + (0,))
if 0 < k:
for row in _possible_rows(n-1, k-1):
yield tuple(row + (1,))
n = 2
if 1 < len(argv):
n = int(argv[1])
print(count_0_1_matrices(2*n)))
答案 1 :(得分:1)
您正在记忆可能会重复的状态。在这种情况下需要记住的状态是向量(k
是隐式的)。让我们看看你linked给出的一个例子。向量参数中的每一对(长度为n
)表示&#34;尚未放置在该列中的零和1的数量。&#34;
以左侧为例,向量为((1, 1) (1, 1) (1, 1) (1, 1)), when k = 2
,其前导的分配为1 0 1 0, k = 3
和0 1 0 1, k = 4
。但我们可以从不同的分配集合中找到相同的状态((1, 1) (1, 1) (1, 1) (1, 1)), k = 2
,例如:0 1 0 1, k = 3
和1 0 1 0, k = 4
。如果我们要记住状态((1, 1) (1, 1) (1, 1) (1, 1))
的结果,我们可以避免再次重新计算该分支的递归。
如果有什么我可以更好地澄清的话,请告诉我。
进一步阐述以回应您的评论:
维基百科的例子似乎是备忘录的蛮力。该算法似乎试图枚举所有矩阵,但使用memoization从重复状态提前退出。我们如何列举所有可能性?以n = 4
为例,我们从向量[(2,2),(2,2),(2,2),(2,2)]
开始,其中零和1尚未放置。 (由于向量中每个元组的总和为k
,因此我们可以使用更简单的向量,其中k
并保留1或0的计数。)
在每个阶段k
,在递归中,我们枚举下一个向量的所有可能配置。如果状态存在于我们的哈希中,我们只返回该键的值。否则,我们将向量分配为散列中的新键(在这种情况下,此递归分支将继续)。
例如:
Vector [(2,2),(2,2),(2,2),(2,2)]
Possible assignments of 1's: [1 1 0 0], [1 0 1 0], [1 0 0 1] ... etc.
First branch: [(2,1),(2,1),(1,2),(1,2)]
is this vector a key in the hash?
if yes, return value lookup
else, assign this vector as a key in the hash where the value is the sum
of the function calls with the next possible vectors as their arguments