【问题标题】:Dynamic Programming - 4 Keys Keyboard动态编程 - 4 键键盘
【发布时间】:2018-12-27 23:47:50
【问题描述】:

假设您有一个带有以下键的特殊键盘:

键 1:(A):在屏幕上打印一个“A”。

键 2:(Ctrl-A):选择整个屏幕。

键 3:(Ctrl-C):将选择复制到缓冲区。

键 4:(Ctrl-V):在屏幕上打印缓冲区,将其附加在已完成的内容之后 已经打印出来了。

现在,你只能按键盘N次(以上四次 键),找出您可以在屏幕上打印的“A”的最大数量。

示例 1:

输入:N = 3

输出:3

说明:我们最多可以通过按以下键序列在屏幕上获得3个A:A, A, A

示例 2:
输入:N = 7

输出:9

说明:我们最多可以通过按以下键序列在屏幕上获得 9 个 A:A, A, A, Ctrl A, Ctrl C, Ctrl V, Ctrl V

(免责声明:我不想听到不同的解决方案。我只想了解我缺少什么以及如何解决它。)

这是我当前的(不正确的)解决方案(解释如下):

class Solution:
    def maxA(self, N):
        screen = [0] * N
        screen[0] = 1
        applied_clipboard = clipboard = select = 0        
        for i in range(1, N):
            if i < 3:
                screen[i] = screen[i-1] + 1
            else:
                screen[i] = max(screen[i-3] + clipboard, screen[i-1] + 1, screen[i-1] + applied_clipboard)

                if screen[i] == screen[i-3] + clipboard:
                    applied_clipboard = clipboard

            select, clipboard = max(select, screen[i-1]), max(clipboard, select)
        return screen[-1]

如您在上面的代码中所见,我正在跟踪一些状态。状态是:

  1. 我选择了多少个字符?
  2. 我在屏幕上打印了多少个字符?
  3. 我的剪贴板中有多少个字符?
  4. 我实际应用的最新剪贴板是什么?

有了这些状态,我相信我可以在每一步做出最佳决策。

但是,我的代码不正确。对于N = 11的输入,我的代码返回27,正确值为27。但是,对于N=9N=10,正确的值是1620(分别),但我得到1518(分别)。

有人看到我更新状态的方式中的错误吗?

编辑。回应现有答案: 我知道我的状态更新可能不完整,但问题是在哪里。我的想法是保持每个状态最大化,因为有一系列单独的决策来最大化每个状态。然后,使用所有这些信息来更新下一次迭代中的状态。

重写了我的代码以保留每个部分的状态。

class Solution:
    def maxA(self, N):
        screen = [0] * N
        screen[0] = 1
        applied_clipboard, clipboard, select = [[0] * N for _ in range(3)]

        for i in range(1, N):
            if i < 3:
                screen[i] = screen[i-1] + 1
            else:
                screen[i] = max(screen[i-3] + clipboard[i-1], screen[i-1] + 1, screen[i-1] + applied_clipboard[i-1])

                if screen[i] == screen[i-3] + clipboard[i-1]:
                    applied_clipboard[i] = clipboard[i-1]
                else:
                    applied_clipboard[i] = applied_clipboard[i-1]

            select[i] = max(select[i-1], screen[i-1])
            clipboard[i] = max(clipboard[i-1], select[i-1])

        print('screen', screen)
        print('clipboard', clipboard)
        print('applied', applied_clipboard)
        return screen[-1]

我认为我在这里感到困惑的是如何保留已应用的剪贴板值 (applied_clipboard) 与最大剪贴板状态。但我需要区分两者,因为它会影响递归关系。

感谢user3386109 引导我看到:

  • N=9 的正确步骤是AAAASCVVV(总共 16 个 A)
  • 我的代码,对于 N=9,输出 AAASCVVVV(总共 15 个 A)。

其中 s = 选择,c = 复制,v = 粘贴。

【问题讨论】:

  • 您的“免责声明”是什么意思?也许不同的解决方案会更容易阅读并且更不容易出现错误。也许您的解决方案是错误的,因此需要“不同”的正确解决方案。
  • 当你得到解决方案时,请记得给有用的东西投票并接受你最喜欢的答案(即使你必须自己写),这样 Stack Overflow 才能正确存档问题。跨度>

标签: python algorithm dynamic-programming


【解决方案1】:

错误很简单,就是代码在每一步都在最大化屏幕上的“A”数量。如果 print screen 在函数末尾 N 为 9 时,你会看到:

[1, 2, 3, 4, 5, 6, 9, 12, 15]

请注意,当N 为 6 时,屏幕上的“A”的最大数量为 6。这可以通过三种不同的方式实现:

AAAAAA
AAscvv
AAAscv

其中 s = 选择,c = 复制,v = 粘贴。

但是,当N 是 9 时,正确答案是

AAAAscvvv

如您所见,按 6 次键后,屏幕上的“A”数量只有 4 个,而不是 6 个。因此,在每个步骤中最大化屏幕上的“A”数量并不能给出正确答案。

【讨论】:

  • 我明白了,这也与@Prune 所说的一致,但我不清楚我在哪里做错了。 DP 背后的想法是为(i-1) 步骤独立维护所有状态片段,以便在(ith) 步骤中为每个状态片段做出最佳决策。因为我正在正确更新我的剪贴板状态,所以我在哪里短路了不正确的状态? screen[i] 应该对于 [i] 的任何值都是最佳的,但其他状态应该允许相应地更新。
  • 我的状态更新中一定有一些不正确的地方(或者它只是不完整的,正如 Prune 建议的那样),这就是结果。
  • 对于N=9,这里是每个部分的状态:screen [1, 2, 3, 4, 5, 6, 9, 12, 15] | clipboard [0, 0, 1, 2, 3, 4, 5, 6, 9]| applied [0, 0, 0, 0, 0, 3, 3, 3, 3]|有趣的是,剪贴板 [5] 实际上是 4,正如您所建议的那样
  • @גלעדברקן 我尝试了您的新解决方案,它似乎确实有效。但在我看来,代码中的“早出”需要某种证明。 Prune 的答案中概述的算法也是 O(n),因为具有三个属性,最多有 6 个主导状态。因此,对于每次迭代,代码都会计算 24 个可能的新状态(6 个旧状态 * 4 个可能的动作),并从这 24 个状态中选择最多 6 个主导状态。结果是 O(n) 算法。
  • @גלעדברקן 同意。我想我对此有所了解。我检查了各种键序列的对数效率。这是我发现的。序列scv 需要 k=3 次按键,并且屏幕上的 A 数量加倍,m=2。所以ln(m) / k的对数效率为0.231。 scvv 有 k=4 和 m=3,效率 0.275。 scvvv 是 0.277。 scvvvv 是 0.268。 scvvvvv 是 0.256。因此,可以通过将scvv 提高三倍或将scvvv 提高四倍来实现峰值效率。这意味着要选择的最佳屏幕通常是三个或四个状态。
【解决方案2】:

一种思考方式可能是我们的解决方案必须以paste 结尾,因为显然我们不希望以selectcopy 结尾来浪费印刷机。然后我们可以假设我们要选择粘贴的屏幕状态是最佳的(否则,我们为什么要从它粘贴?)。

f(n) 表示可能输出的As 的最大数量。然后我们可以选择重复应用任何屏幕状态返回三个或更多按键。例如,在第 7 次按下时,我们可以回顾并说:要重复屏幕状态 4(只有 4 次),我们需要 3 次按下,因此从 4 开始重复将提供我们的记录 7, 4 + 4 = 8 As .如果我们尝试从 3 开始重复,我们需要按 3 次重复 1 次,使我们达到 6,我们仍然可以再多 1 次,所以 3 + 2*3 = 9。所以重复应用屏幕状态 3,提供记录对于 7 九 As,这是最好的。对于任何0 &lt; j &lt; i-3,我们可以得到1 + i - (j + 3) + 1i - j - 1 乘以jth 屏幕状态。

JavaScript 代码:

function f(n) {
  const m = [0,1,2,3,4,5,6].concat(
    new Array(Math.max(n-6,0)).fill(0));

  for (let i=7; i<=n; i++)
    for (let j=1; j<=i-3; j++)
      m[i] = Math.max(m[i], m[j]*(i-j-1));

  console.log(JSON.stringify(m));
  return m[n];
}

console.log(4, f(4));
console.log(7, f(7));
console.log(9, f(9));
console.log(10, f(10));
console.log(11, f(11));

检查数据,我发现要从中复制的最佳屏幕状态似乎始终非常接近n(在 6 个状态之内),并且在向后遍历时似乎始终是凸的,这意味着我们可以通过向后遍历和查看来优化向下变化。

如果这种行为是一致的,则意味着该算法具有O(n) 复杂度。

function f(n) {
  const m = [0,1,2,3,4,5,6].concat(
    new Array(Math.max(n-6,0)).fill(0));

  for (let i=7; i<=n; i++){
    let prev = 0;
    for (let j=i-3; j>0; j--){
      let curr = m[j]*(i-j-1);
      if (curr < prev){
        console.log(`Early exit. n: ${n}, i: ${i}, j: ${j}`);
        break;
      }
      prev = curr;
      m[i] = Math.max(m[i], curr);
    }
  }

  console.log(JSON.stringify(m));
  return m[n];
}

console.log(50, f(50));

【讨论】:

    【解决方案3】:

    据我所知(现在我已将代码更改为可运行),您可以正常更新内容。我怀疑问题出在不完整的状态更新上。不幸的是,我认为你的实现是正确的,但是你的算法是错误的。

    认为您的问题在于 screen 更新每次迭代都会使您的长期目标短路。在每次迭代中,您都会处理选择,就好像这次按键必须是您的最后一次一样。因此,您有时会放弃具有更大潜力的剪贴板,并仅使用在此步骤中最能获得As 的内容进行更新。

    你需要为每一步保持多个有用的状态:不仅是最大的屏幕显示,还有一个丰富的剪贴板的潜力。

    更新 我想我们现在的想法是一致的。您需要为每个击键数量存储多个状态 - 部分解决方案。这不是一个简单的递归关系:它更像是一种量子态。

    例如,在 7 次击键时,您至少需要表示两种状态。

    • 由于 AAAASCV,屏幕上有 8 个 As,剪贴板中有 4 个。 (8, 4)
    • 由于 AAASCVV,屏幕上有 9 个As,剪贴板中有 3 个。 (9, 3)

    您当前的算法对于每个击键次数只允许screenclipboard 各有一个值。它只保留具有最佳短期结果的解决方案。它错过了 (8,4) 将成为 9 和 10 击键更好的垫脚石。

    你需要保留每一步没有被支配的所有对。当G(screen) &gt;= H(screen) G(clipboard) &gt;= H(clipboard).

    时,状态G“支配”状态H

    另外请注意,您需要在您的状态中有一个较少使用的元素:当前选择的内容。例如,考虑上述两种状态的情况(用于说明),一笔(粘贴)更远,另一笔“提前计划”:

    scr   clip   sel
    12      4     0          Nothing selected
    12      3     0          Nothing selected
     9      3     9          Stopped at 9 A's and did a select-copy
    

    这第三个状态将立即超过前两个状态另外两个步骤。

    【讨论】:

    • 所以clipboard 状态是我用来建立一个丰富的剪贴板的,并且每次迭代都会更新。只有apply_clipboard 不是,这没关系,因为我应该只在应用时更新apply_clipboard。对吗?
    • screenclipboard 的状态应该独立优化(有一系列单独的决策来优化 screenclipboard)。这就是为什么我有clipboard = max(clipboard, select)。哪里短路了?
    • 您的状态更新不完整,因为屏幕和剪贴板不是独立的。您的完整状态是屏幕和剪贴板的元组;您需要沿着解决方案的路径携带pair
    • 尝试在纸上为各种活动路径构建游戏树;您应该看到 9 和 10 的最佳路径之间的差异,而不是您的程序所做的。您是否了解需要如何链接信息才能回到正确的路径?
    • 是的,我编辑了我的原始问题以包括我的程序为N = 9 输出的步骤与N=9 的正确输出是什么。但我还想指出,正如预期的那样,代码经常选择:max(screen[i-3] + clipboard[i-1]),这意味着它会根据剪贴板是否足够高来重新访问以前的值。所以我仍然对如何解决这个问题感到困惑..
    猜你喜欢
    • 2014-02-26
    • 2011-06-20
    • 1970-01-01
    • 1970-01-01
    • 2011-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-24
    相关资源
    最近更新 更多