寻找解决这个动态规划问题的提示

Posted

技术标签:

【中文标题】寻找解决这个动态规划问题的提示【英文标题】:Looking for hints to solve this dynamic programming problem 【发布时间】:2020-05-13 12:23:43 【问题描述】:

我正在努力提高我在编程面试中解决问题的能力,并正在努力解决this problem。我感觉可以使用动态编程来解决,但递归关系对我来说并不明显。

要选择前三位合唱歌手,我只是使用蛮力。因为只有20 Choose 3 = 1140 可以选择它们。起初我以为dp[a][b][c]可以代表三个合唱团歌手剩余呼吸的最短歌曲a, b, c。如果我可以使用dp[a][b][c] = 1 + dp[a - 1][b - 1][c - 1] 计算这个值,但是当任何索引等于0 时应该做什么,应该替换哪个合唱团歌手。此外,我们不能重用dp 数组,因为在一个例子中我们从有呼吸的合唱团歌手开始a, b, c,然后是d, e, f。一旦计算了第一个实例并填充了 dp 数组;第二个实例可能需要使用第一个实例计算的dp[i][j][k]。由于此值取决于第一个实例中可用的合唱团歌手,并且两个实例中可用的歌手不一样,dp[i][j][k] 在第二个实例中可能是不可能的。这是因为最短的歌曲长度dp[i][j][k] 可能使用合唱团歌手,而合唱团歌手在第二种情况下已经被使用。

我没有解决这个问题的想法,也没有任何解决方案。有人可以给我一些提示来解决它吗?

问题陈述

我们有N歌手,每个人都有一定的时间可以唱歌,一旦气喘吁吁需要1秒恢复。他们能唱的最低限度的歌曲是多少,三位歌手一直在唱,三位歌手同时唱完的歌曲是多少?

输入:

输入 3

【问题讨论】:

我在答案中添加了 Python 代码(以及添加了检查以避免歌手重复,这是我之前忽略的)。 @TomFinet 看起来很相似,我不确定。关于不止一位歌手被禁止重复的好点 - 我必须多考虑一下,特别是因为如果他们只是交换“位置”,他们可以继续。排除歌手的方法是否会限制某些州? 你的意思是生成初始状态的代码正在减少一些状态吗? @גלעדברקן @bingbong 我的意思是 countV 但也许那个也是,我不知道。 好吧,要检查一个歌手是否可用,我会计算该歌手在已使用的歌手向量中出现的次数(countV 正在做什么),以及它是否大于或等于歌手的数量那个呼吸长度,必须使用那个歌手。你认为我在countV中数错了吗? @גלעדברקן 【参考方案1】:

这就是想法。

在演唱的每一点,当前状态可以通过歌手是谁、他们演唱了多长时间以及哪些人目前已经气喘吁吁来代表。而从每一个状态我们都需要过渡到一个新的状态,也就是每一个上气不接下气的歌手都准备好再唱一次,每一个歌手唱的都好少转一圈,可能会选择新的歌手。

天真地完成了,最多有 20 个选择 3 个歌手,每个歌手可以处于 10 个当前状态,再加上最多 2 个气喘吁吁的人。这是您可以处于的 175560000 个组合状态。这太多了,我们需要更聪明才能完成这项工作。

更聪明的是,我们确实没有有 20 位可区分的歌手。根据歌手能唱多长时间,我们有 10 位歌手。如果一个歌手可以唱 7 圈,那么他们不可能处于 10 种状态,而现在只能唱 7 圈。我们确实关心两个人能否唱 7 圈转在 4 和 3 左转或 3 和 4,它们是相同的。这引入了很多对称性。一旦我们处理好所有的对称性,就会将我们可能处于的可能状态的数量从数亿减少到(通常)数万。

现在我们的 DP 有一个状态转换,从 dp[state1]dp[state2]。挑战在于生成利用这些对称性的状态表示,您可以将其用作数据结构的键。

更新:

代码的主循环看起来像这样 Python:

while not finished:
    song_length += 1
    next_states = set()
    for state in current_states:
        for next_state in transitions(state):
            if is_finished(next_state):
                finished = True # Could break out of loops here
            else:
                next_states.add(next_state)
    current_states = next_states

大部分挑战是很好地表示一个状态,以及你的transitions 函数。

【讨论】:

所以对于N的歌手来说,与其看N Choose 3的首发歌手,如果有两位歌手的气息相同,我只看其中一位歌手和另外两位不同的歌手使所有具有不同呼吸的歌手三重奏。然后,我查看两位歌手呼吸相同的所有三重奏,然后查看所有歌手呼吸相同的所有三重奏。这避免了冗余地进行相同的计算。我不确定这是否是您的意思:“我们不在乎这两个是否可以唱 7 圈是在 4 和 3 转左还是 3 和 4,它们是一样的”? 但也许你的意思不是:如果我们从歌手7 5 3 开始,3 秒后我们将 3 替换为 7,那么新组合是 4 2 7,但歌手被使用完整的呼吸是7 5 7,所以如果我们做同样的事情,但先使用其他 7 位歌手并不重要,因为这两个 7 位歌手是无法区分的。这不是@btilly 的意思吗? 这确实是我的意思。因此,您的表示可能在 JSON 中看起来像 7: [4, 3], 6: [6], "out": [2],这意味着可以持续 7 秒的 2 个歌手正在唱歌,剩下 4 个和 3 个,可以持续 6 秒的 1 个歌手正在唱歌,并且还剩 6 个转角和一个可以最后2个气喘吁吁。此状态必须转换为7: [3, 2], 6: [5], "out": []。担心利用所有可用的对称性比使用表示的效率更重要。最终你会想要两者。 所以使用对称性我可以通过忽略冗余状态来减少所需的计算。但我不明白这对找到两个州之间的关系有何帮助。 @bingbong 在给定的时刻,你有一组可能的状态,你可以进入。从每个状态,我们可以计算出你接下来可能进入的状态。(任何人都喘不过气来恢复,任何唱歌的人都会减少 1 的时长。如果有人结束唱歌,他们会气喘吁吁,而其他人开始唱歌。)这些状态转换应该是你的大部分逻辑。当我们第一次找到下一个状态有 3 位歌手屏住呼吸的时间点时,歌曲就结束了。我将添加伪代码。【参考方案2】:

在记忆方面的状态似乎与自开始以来经过的时间无关。采取任何起始位置,

a, b, c

其中a, b, c 是选定的幅度(每个歌手可以屏住呼吸的时间),a 是最小的幅度。我们有

a, b, c
t = 0

和它一样:

0, b - a, c - a
t = a

所以让我们定义具有最小幅度的初始状态a 为:

b, c, ba, ca
  where ba = b - a
        ca = c - a
t = a

从这里开始,状态的每一次转换都是相似的:

new_a <- x
  where x is a magnitude in
  the list that can be available
  together with b and c. (We only
  need to try each such unique
  magnitude once during this
  iteration. We must also prevent
  a singer from repeating.)

  let m = min(new_a, ba, ca)

  then the new state is:
    u, v, um, vm
    t = t + m
      where u and v are from the
      elements of [new_a, b, c] that
      aren't associated with m, and um
      and vm are their pairs from
      [new_a, ba, ca] that aren't m,
      subtracted by m.

已访问组合的记忆状态只能是:

[(b, ba), (c, ca)] sorted by
the tuples' first element

如果到达的关联t 等于或高于该状态下看到的最小值,我们可以使用它修剪搜索中的分支。

例子:

2 4 7 6 5

解决方案(自上而下阅读):

4 5 6
7 4 5
  2

州:

u v um vm
5 6 1 2
t = 4

new_a = 7
m = min(7, 1, 2) = 1 (associated with 5)
7 6 6 1
t = 5

new_a = 4
m = min(4, 6, 1) = 1 (associated with 6)
4 7 3 5
t = 6

new_a = 5
m = min(5, 3, 5) = 3 (associated with 4)
5 7 2 2
t = 9

new_a = 2
m = min(2, 2, 2) = 2 (associated with 2)
5 7 0 0
t = 11

Python 代码:

import heapq
from itertools import combinations

def f(A):
  mag_counts = 

  for x in A:
    if x in mag_counts:
      mag_counts[x] = mag_counts[x] + 1
    else:
      mag_counts[x] = 1

  q = []

  seen = set()

  # Initialise the queue with unique starting combinations
  for comb in combinations(A, 3):
    sorted_comb = tuple(sorted(comb))
    if not sorted_comb in seen:
      (a, b, c) = sorted_comb
      heapq.heappush(q, (a, (b-a, b), (c-a, c), a))
    seen.add(sorted_comb)

  while q:
    (t, (ba, b), (ca, c), prev) = heapq.heappop(q)

    if ba == 0 and ca == 0:
      return t

    for mag in mag_counts.keys():
      # Check that the magnitude is available
      # and the same singer is not repeating.
      [three, two] = [3, 2] if mag != prev else [4, 3]
      if mag == b == c and mag_counts[mag] < three:
        continue
      elif mag == b and mag_counts[mag] < two:
        continue
      elif mag == c and mag_counts[mag] < two:
        continue
      elif mag == prev and mag_counts[mag] < 2:
        continue

      m = min(mag, ba, ca)

      if m == mag:
        heapq.heappush(q, (t + m, (ba-m, b), (ca-m, c), m))
      elif m == ba:
        heapq.heappush(q, (t + m, (mag-m, mag), (ca-m, c), b))
      else:
        heapq.heappush(q, (t + m, (mag-m, mag), (ba-m, b), c))

  return float('inf')

As = [
  [3, 2, 3, 3], # 3
  [1, 2, 3, 2, 4], # 3
  [2, 4, 7, 6, 5] # 11
]

for A in As:
  print A, f(A)

【讨论】:

@TomFinet btilly 在我们稳步增加歌曲长度时更新状态可能会有所帮助。这限制了无休止的搜索分支,并保证第一个终止是最优的。 如果我们去 1 秒而不是你的建议,为什么它会限制无休止的搜索? @גלעדברקן @bingbong depth-first 可能会追求在歌手之间持续保持一些差异的分支,因此永远不会终止。如果保证问题有一个(合理的)解决方案,广度优先应该找到它并且可以停止搜索。 所以我们需要广度优先搜索。歌曲结束的第一个实例就是答案?这是否意味着我们不需要记忆? @TomFinet 是的,我也是这么理解的。该代码不包含“已访问”状态缓存。尽管我不确定,但它可能对一些输入有所帮助。

以上是关于寻找解决这个动态规划问题的提示的主要内容,如果未能解决你的问题,请参考以下文章

算法之动态规划

《数据结构与算法之美》28——动态规划理论

floyd算法 是动态规划的思想吗

寻找最小阶梯成本的动态规划问题的错误答案

01背包动态规划

动态规划问题之矩阵路径问题