【问题标题】:Optimal choice algorithm最优选择算法
【发布时间】:2019-06-04 12:38:51
【问题描述】:

我有一个今天到期的大学约会,我开始感到紧张。我们最近讨论了算法优化的动态规划,现在我们将自己实现一个使用动态规划的算法。

任务

所以我们有一个简单的游戏,我们将为其编写一个算法来找到最佳策略以获得最佳分数(假设两个玩家都进行了优化)。

我们有一排像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

在第一个任务中,我们应该简单地实现“一种简单的递归方法来解决这个问题”,其中我只使用了一个极小极大算法,这对于自动化测试来说似乎很好。然而,在第二个任务中,我被困住了,因为我们现在将使用动态编程技术。我发现的唯一提示是在任务本身中提到了matrix

到目前为止我所知道的

我们有一个单词转换问题的示例,其中使用了这样的矩阵,它被称为Edit distance,由两个单词组成,这意味着有多少变化(Insertions,Deletions, Ssubstitutions) 是否需要将一个单词变为另一个单词。这两个单词以表格或矩阵的形式排序,并且对于单词的每个组合都会计算距离。

例子:

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 次编辑:替换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

但我真的不知道如何从中得出结论,我真的卡住了。

所以我真的很想知道我是否走在正确的道路上,或者我是否过度考虑了这一点。这是解决这个问题的正确方法吗?

【问题讨论】:

  • 通常,要开始使用动态编程,您需要一个递归关系。这将是以下内容:X(i, j) = max(Arr[i] + X(i+1, j), X(i, j-1) + Arr[j] 如果j-i == 0 % 2X(i, j) = min(Arr[i] + X(i+1, j), X(i, j-1) + Arr[j] 如果j-i == 1 % 2X(i, j) 代表玩家 1 可以预期的最佳得分,具有最佳策略。从这个关系(和琐碎的终止案例),您可以通过在计算时存储X 的所有值来构建动态规划算法。
  • @m.raynal 我认为这是我没有真正理解的部分。所以我需要创建一个递归关系,但如何处理呢?你的例子有点难以阅读(我认为他们也错过了一些括号)。所以我需要以某种方式找到这种关系,然后将值存储到矩阵中?然后从该表中搜索最佳路径?

标签: algorithm dynamic-programming


【解决方案1】:

在开始动态规划算法时,您应该尝试写下的第一件事是递归关系。
让我们首先简化一点问题。我们会考虑卡的数量是偶数,并且我们想为第一个玩家设计一个最佳策略。一旦我们设法解决了这个版本的问题,其他的(奇数张牌,为第二个玩家优化策略)就很简单了。

所以,首先,一个递归关系。当剩余的牌从i^thj^th 时,让X(i, j) 成为玩家 1 可以预期的最佳分数(当玩家 2 也发挥最佳时)。然后,玩家 1 在玩游戏时可以预期的最高分数将由X(1, n) 表示。
我们有:
X(i, j) = max(Arr[i] + X(i+1, j), X(i, j-1) + Arr[j]) if 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
}

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

【讨论】:

  • 感谢您的回答!我只是不明白第二个例子中的X 是什么。一次它被用作数组,另一次被用作函数。而且我认为我必须小心选择关于卡片奇数的最小值和最大值,因为你可能会从奇数开始,然后玩家 1 会最小化而不是最大化。我猜 X 是提到的数组或矩阵,应该用作数组?
  • 关于X:您的编辑是正确的,它不应该出现在第一个示例中。它是一个数组(二维数组),用于存储中间值。因为如果我们不存储X(2, 3),我们会计算很多次(来自X(1, 3),来自X(1, 4) ....),实际上是指数倍。 X 实际上是唯一真正使此代码成为 DP 代码的东西。关于最小/最大、顺序、界限:是的,写这些部分不出错有点费力。
  • 其他:当我为我的示例 4 7 2 3 手动模拟上述算法时,结果是 16。我觉得问题在于,当达到匹配i == j 时,我可能需要返回匹配的最终结果而不是单个元素得分。所以我的回报不会是Arr[i],而是endscorep1 - endscorep2,我可以完全按照我对minimax函数所做的那样做。我看对了吗,我可以通过向数组/表中添加查找来复制我的极小值?
  • 好吧,我又迷路了,我试图复制我的 minimax 函数并添加表查找,但它当然没有用。我发现了明显的问题,但我仍然不知道如何使用上述功能。返回的值是什么意思?
【解决方案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 的游戏。以此类推。

【讨论】:

  • 但是你如何区分玩家呢?另外我的问题是返回的值是什么意思?我只是没有得到“其他方式”的部分。例如,极小极大在叶子中具有所有可能的结果,然后您一直工作到获得最终结果的顶部。它如何与 DP 一起工作?我可以存储什么值?例如,如果我有 4 7 2 3 并且 p1 选择 4 并且 p2 选择 3 下一步有一个决定 (2,2) 我存储该值。但是如果 p1 选择 3 和 p2 4 那么我也有 (2,2) 但值不同。
  • @Yastanub 哪个玩家是隐含在作为子游戏一部分的伸展长度中。 Evens是玩家A,赔率是玩家B。至于与minimax相比,minimax可以通过知道结束时的数字和两个较小游戏的分数来计算。自上而下避免了多次计算这些分数。在自下而上,我会更好。我可以通过知道长度为i 的游戏的数量和分数来计算长度为i+1 的游戏。然后,一旦我拥有了所有长度为 i+1 的游戏,我再也不需要查看长度为 i 的游戏,并且可以丢弃这些数据。
  • 好吧,我明天再深入研究一下,我想我不会很遗憾地赶不上作业的最后期限,但也许我至少可以理解这一点。
猜你喜欢
  • 1970-01-01
  • 2011-02-28
  • 1970-01-01
  • 2021-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-19
相关资源
最近更新 更多