需要帮助理解“平衡0-1矩阵”的动态规划方法?

时间:2016-05-16 11:51:24

标签: algorithm dynamic-programming

问题:我正在努力理解/可视化动态编程方法“动态编程中的一种平衡0-1矩阵 - 维基百科文章。”

维基百科链接:https://en.wikipedia.org/wiki/Dynamic_programming#A_type_of_balanced_0.E2.80.931_matrix

在处理多维数组时,我无法理解memoization的工作原理。例如,在尝试使用DP解决Fibonacci系列时,使用数组来存储先前的状态结果很容易,因为数组的索引值存储该状态的解决方案。

有人可以用更简单的方式解释DP方法的“0-1平衡矩阵”吗?

2 个答案:

答案 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 = 30 1 0 1, k = 4。但我们可以从不同的分配集合中找到相同的状态((1, 1) (1, 1) (1, 1) (1, 1)), k = 2,例如:0 1 0 1, k = 31 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