最佳选择算法

时间:2019-06-04 12:38:51

标签: algorithm dynamic-programming

我要去上大学,这是今天的约会,我开始变得紧张。我们最近讨论了用于算法优化的动态规划,现在我们将使用动态规划自己实现一个算法。

任务

因此,我们有一个简单的游戏,我们将为此编写一种算法,以寻找最佳策略来获得最佳分数(假设两个玩家的游戏均处于最佳状态)。

我们有一排类似4 7 2 3的数字(请注意,根据任务描述,不能保证它始终是相等的数字)。现在,每个玩家轮流从后面或前面获得一个数字。当选择了最后一个数字时,将对每个玩家的数字求和,并将每个玩家的得分相减。结果就是玩家1的得分。因此,上述数字的最佳顺序将是

P1:3-> p2:4-> p1:7-> p2:2

因此,p1将拥有3, 7,p2将拥有4, 2,这将导致玩家1的最终得分为(3 + 7) - (4 + 2) = 4

在第一个任务中,我们应该简单地实现“一种简单的递归方法来解决此问题”,其中我只是使用了minimax算法,该算法对于自动测试似乎很好。但是在第二项任务中,我陷入了困境,因为我们现在将使用动态编程技术。我发现的唯一提示是,在任务本身中提到了matrix

到目前为止我所知道的

我们有一个单词转换问题的示例,其中使用了这样的矩阵,它被称为两个单词的Edit distance,这意味着要进行多少次更改( I 插入, D < / strong>元素, S 个字母)是否可以将一个单词转换为另一个单词。在那里,这两个单词按表或矩阵排序,并且将针对该单词的每种组合计算距离。

示例:

W    H    A         T
     | D       | I
     v         v
W         A    N    T

编辑距离将为2。您有一个表格,其中每个子字符串的每个编辑距离都显示如下:

   ""    W    H    A    T
         1    2    3    4

   W 1   0    1    2    3

   A 2   1    1    2    3

   N 3   2    2    2    3

   T 4   3    3    3    2

例如,从WHAWAN将进行2次编辑:插入N并删除H,从WHWAN也将进行2次编辑:substitude H- > A并插入N,依此类推。这些值是通过“ OPT”函数计算得出的,我认为这代表了优化。 我还研究了自下而上和自上而下的递归方案,但不确定如何将其附加到我的问题上。

我的想法

提醒一下,我使用数字4 7 2 3

我从上面了解到,我应该尝试创建一个表,在该表中显示每个可能的结果(例如minimax,因为它之前将被保存)。然后,我创建了一个简单的表格,尝试在表格中包含可以进行的绘制(我认为这是我的OPT函数):

         4    7    2    3
       ------------------
a.  4 |  0   -3    2    1
      |
b.  7 |  3    0    5    4
      |
c.  2 | -2   -5    0   -1
      |
d.  3 | -1   -4    1    0

玩家1绘制左列标记,玩家2绘制上一行标记,然后每个数字代表numberP1 - numberP2。从该表中,我至少可以阅读上述3-> 4-> 7-> 2(-1 + 5)的最佳策略,因此请确保该表应包含所有可能的结果,但是我现在不确定如何从中得出结果。我的想法是开始遍历所有行,并选择其中编号最高的行,并将其标记为p1的行(但无论如何还是贪婪的)。然后,p2将在该行中搜索最低编号,然后选择该特定条目,该条目将变成转弯。

示例:

p1选择行a。 7 | 3 0 5 4,因为5是表中的最高值。 P2现在从该行中选择3,因为它是最低的(0是无效抽奖,因为它是相同的数字,您不能两次选择),所以第一回合将是7-> 4,但随后我注意到此抽奖由于从一开始就无法访问7,因此无法执行。因此,对于每个回合,您只有4种可能性:表格的外部编号和直接位于它们之后/之前的编号,因为在绘制之后可以访问这些外部编号。因此,对于第一轮,我只有a行。或d。然后p1可以选择:

4使p2保留7或3。或者p1取3,使p2保留4或2

但是我真的不知道如何得出结论,我真的很困惑。

因此,我真的很想知道即时通讯是否以正确的方式进行,或者即时通讯是否对此进行了过多思考。这是解决这个问题的正确方法吗?

2 个答案:

答案 0 :(得分:2)

启动动态编程算法时,您应该尝试写下的第一件事是递归关系。
让我们首先简化一下问题。我们将认为纸牌数是偶数,并且我们想为第一个玩者设计最佳策略。一旦我们设法解决了这个问题,其他步骤(奇数张牌,针对第二张牌手的优化策略)就会随之而来。

因此,首先是递归关系。假设X(i, j)是玩家1可以期望的最高得分(当玩家2也在最佳玩法时),此时剩余的牌数从i^thj^th。然后,玩家1在玩游戏时可以期望的最高得分将由X(1, n)表示。
我们有:
X(i, j) = max(Arr[i] + X(i+1, j), X(i, j-1) + Arr[j])如果为j-i % 2 == 1,则意味着玩家可以期望的最高分数是在左边拿卡和在右边拿卡之间的最佳成绩。
在其他情况下,其他玩家正在玩,因此他将尝试最小化: X(i, j) = min(Arr[i] + X(i+1, j), X(i, j-1) + Arr[j]),如果j-i % 2 == 0

终端的情况很琐碎:X(i, i) = Arr[i],这意味着当只有一张卡时,我们就选择它,仅此而已。

现在该算法没有动态编程,这里我们仅将递归关系写为递归算法:

function get_value(Arr, i, j) {
    if i == j {
        return Arr[i]
    } else if j - i % 2 == 0 {
        return max(
            Arr[i] + get_value(i+1, j),
            get_value(i, j-1) + Arr[j]
        )
    } else {
        return min(
            Arr[i] + get_value(i+1, j),
            get_value(i, j-1) + Arr[j]
        )
    }
}

此函数的问题在于,对于某些给定的i, j,将有许多X(i, j)的冗余计算。动态编程的本质是存储中间结果,以防止重复计算。

具有动态编程的算法(X到处都用+ inf初始化。

function get_value(Arr, X, i, j) {
    if X[i][j] != +inf {
        return X[i][j]
    } else if i == j {
        result = Arr[i]
    } else if j - i % 2 == 0 {
        result = max(
            Arr[i] + get_value(i+1, j),
            get_value(i, j-1) + Arr[j]
        )
    } else {
        result = min(
            Arr[i] + get_value(i+1, j),
            get_value(i, j-1) + Arr[j]
        )
    }
    X[i][j] = result
    return result
}

您可以看到与上述算法的唯一区别是我们现在使用2D数组X来存储中间结果。时间复杂度的后果是巨大的,因为第一种算法在O(2^n)中运行,而第二种算法在O(n²)中运行。

答案 1 :(得分:2)

动态编程问题通常可以自上而下和自下而上两种方式解决。

自下而上要求构建从最简单到最复杂情况的数据结构。这很难编写,但是提供了丢弃一些您不再需要的数据的选项。自上而下要求编写一个递归函数,然后编写memoizing。因此,自下而上可以更有效,自上而下通常更容易编写。

我将同时显示两者。天真的方法可以是:

def best_game(numbers):
    if 0 == len(numbers):
        return 0
    else:
        score_l = numbers[0] - best_game(numbers[1:])
        score_r = numbers[-1] - best_game(numbers[0:-1])
        return max(score_l, score_r)

但是我们传递了很多冗余数据。因此,让我们对其进行重新组织。

def best_game(numbers):
    def _best_game(i, j):
        if j <= i:
            return 0
        else:
            score_l = numbers[i] - _best_game(i+1, j)
            score_r = numbers[j-1] - _best_game(i, j-1)
            return max(score_l, score_r)

    return _best_game(0, len(numbers))

现在我们可以添加一个缓存层来对其进行记忆:

def best_game(numbers):
    seen = {}
    def _best_game(i, j):
        if j <= i:
            return 0
        elif (i, j) not in seen:
            score_l = numbers[i] - _best_game(i+1, j)
            score_r = numbers[j-1] - _best_game(i, j-1)
            seen[(i, j)] = max(score_l, score_r)
        return seen[(i, j)]

    return _best_game(0, len(numbers))

这种方法将是记忆和时间O(n^2)

现在自下而上。

def best_game(numbers):
    # We start with scores for each 0 length game
    # before, after, and between every pair of numbers.
    # There are len(numbers)+1 of these, and all scores
    # are 0.
    scores = [0] * (len(numbers) + 1)
    for i in range(len(numbers)):
        # We will compute scores for all games of length i+1.
        new_scores = []
        for j in range(len(numbers) - i):
            score_l = numbers[j] - scores[j+1]
            score_r = numbers[j+i] - scores[j]
            new_scores.append(max(score_l, score_r))
        # And now we replace scores by new_scores.
        scores = new_scores
    return scores[0]

这又是O(n^2)时间,但只有O(n)个空格。因为计算完长度为1的游戏后,我可以扔掉长度为0的游戏。长度为2的游戏中,我可以扔掉长度为1的游戏。依此类推。