【问题标题】:Smallest n-bit integer c that has k 1-bits and is the sum of two n-bit integers that have g, h bits set to 1(dynamic programming)具有 k 个 1 位的最小 n 位整数 c,它是 g、h 位设置为 1 的两个 n 位整数之和(动态规划)
【发布时间】:2018-03-06 20:58:02
【问题描述】:

我正在尝试解决以下问题:

找到最小的 n 位整数 c,它有 k 个 1 位并且是两个 g、h 位设置为 1 的 n 位整数之和。g, h, k

首先,这里的 n 位整数意味着我们可以使用所有 n 位,即最大值。这样一个整数的值为2^n - 1。所描述的整数可能根本不存在。 很明显,k > g + h 的情况没有解决方案,g + h = k 的答案只是2^k - 1(第一个k 位是1 位,k - n 前面是零)。

关于程序应该做什么的一些例子:

g = h = k = 4, n = 10 :
0000001111 + 0000001111 = 0000011110
15 + 15 = 30 (30 should be the output)


(4, 6, 5, 10):
0000011110 + 0000111111 = 0001011101
30 + 63 = 93

(30, 1, 1, 31):
1 + (2^30 - 1) = 2^30

在我看来,这是一个动态编程问题,我选择了以下方法: 让dp[g][h][k][n][c] 是所描述的整数,c 是用于携带的可选位。我尝试根据最低位重建可能的总和。 所以,dp[g][h][k][n + 1][0]

的最小值
(0, 0):       dp[g][h][k][n][0]
(0, 0): 2^n + dp[g][h][k - 1][n][1]
(1, 0): 2^n + dp[g - 1][h][k - 1][n][0]
(0, 1): 2^n + dp[g][h - 1][k - 1][n][0]

同样,dp[g][h][k][n + 1][1]

的最小值
(1, 1): dp[g - 1][h - 1][k][n][0]
(1, 1): dp[g - 1][h - 1][k - 1][n][1] + 2^n
(1, 0): dp[g - 1][h][k][n][1]
(0, 1): dp[g][h - 1][k][n][1]

这个想法并不难,但我对这类事情并没有真正的经验,而且我的算法即使在最简单的情况下也不起作用。我选择了自上而下的方法。我很难考虑所有极端情况。我真的不知道我是否正确选择了递归基等。我的算法甚至不适用于g = h = k = 1, n = 2 的最基本情况(答案是01 + 01 = 10)。 g = h = k = 1, n = 1 不应该有答案,但算法给出了1(这基本上就是前一个示例输出1 而不是2 的原因)。 所以,这是我糟糕的代码(只有非常基本的 C++):

int solve(int g, int h, int k, int n, int c = 0) {
  if (n <= 0) {
    return 0;
  }
  if (dp[g][h][k][n][c]) {
    return dp[g][h][k][n][c];
  }
  if (!c) {
    if (g + h == k) {
      return dp[g][h][k][n][c] = (1 << k) - 1;
    }
    int min, a1, a2, a3, a4;
    min = a1 = a2 = a3 = a4 = std::numeric_limits<int>::max();
    if (g + h > k && k <= n - 1) {
      a1 = solve(g, h, k, n - 1, 0);
    }
    if (g + h >= k - 1 && k - 1 <= n - 1) {
      a2 = (1 << (n - 1)) + solve(g, h, k - 1, n - 1, 1);
    }
    if (g - 1 + h >= k - 1 && k - 1 <= n - 1) {
      a3 = (1 << (n - 1)) + solve(g - 1, h, k - 1, n - 1, 0);
    }
    if (g + h - 1 >= k - 1 && k - 1 <= n - 1) {
      a4 = (1 << (n - 1)) + solve(g, h - 1, k - 1, n - 1, 0);
    }
    min = std::min({a1, a2, a3, a4});
    return dp[g][h][k][n][c] = min;
  } else {
    int min, a1, a2, a3, a4;
    min = a1 = a2 = a3 = a4 = std::numeric_limits<int>::max();
    if (g - 2 + h >= k && k <= n - 1) {
      a1 = solve(g - 1, h - 1, k, n - 1, 0);
    }
    if (g - 2 + h >= k - 1 && k - 1 <= n - 1) {
      a2 = (1 << (n - 1)) + solve(g - 1, h - 1, k - 1, n - 1, 1);
    }
    if (g - 1 + h >= k && k <= n - 1) {
      a3 = solve(g - 1, h, k, n - 1, 1);
    }
    if (g - 1 + h >= k && k <= n - 1) {
      a4 = solve(g, h - 1, k, n - 1, 1);
    }
    min = std::min({a1, a2, a3, a4});
    return dp[g][h][k][n][c] = min;
  }
}

【问题讨论】:

  • 好问题。但我相信你跳得太快得出了错误的结论(使用动态编程)。我会寻找一种更具建设性的方法。通过手动解决一些情况,例如 1 2^(n + 1) - 1” - 实际上是2^n - 1
  • @Gassa 。如果您暗示 g + h = k + 1 的解决方案是 10 + k 个,对于 g + h = k + 2 它是 110 + k 个等等,那么您就错了。首先,g + h - k> k 有一个更复杂的模式。其次,他们都有反例!
  • 抱歉,我没有具体的模式。相反,一种通用的调查方法可以揭示这种模式——或者它不存在,如果确实如此的话。

标签: c++ algorithm dynamic-programming top-down


【解决方案1】:

我不太相信动态编程方法。如果我理解正确,您将需要根据先前的计算定义如何转到dp[g + 1][h][k][n]dp[g][h + 1][k][n]dp[g][h][k + 1][n]dp[g][h][k][n + 1],有和没有进位位,我不确定所有这些的正确规则是什么。

我认为一个更简单的方法来考虑这个问题是A* search 树,其中每个节点包含两个要添加的部分候选数字,我们称它们为 G 和 H。你从一个 G = 0 和 H 的节点开始= 0 在级别 m = 0 时,工作如下:

  1. 如果 G + H 有 n 或更少的位和 k 1 位,这就是解决方案,你找到了!
  2. 否则,如果
    n - m 丢弃节点(无法解决)。
  3. 否则,如果
    (g + h) - (G 中 1 的位数 + H 中的 1 位数) 丢弃节点(不可行的候选者)。
  4. 否则,将节点分支到新级别。通常,每个节点最多可构成四个子节点,分别在 G 和 H 前面加上 0 和 0、0 和 1、1 和 0 或 1 和 1。然而:
    • 如果 G 中 1 的位数少于 g,则只能在 G 前面加上 1,H 和 h 也是如此。
    • 在级别 m(G 和 H 有 m 位),您只能在 G 前面加上 0 if
      n - m > g - G 中 1 的位数
      H 和 h 也是如此。
    • 如果 G == H 和 g == h,您可以跳过 0 和 1 以及 1 和 0 之一,因为它们会导致相同的子树。
  5. 继续下一个节点并重复,直到找到解决方案或没有更多节点可访问。

访问节点的顺序很重要。您应该将节点存储在优先级队列/堆中,以便下一个节点始终是可能导致最佳解决方案的第一个节点。这实际上很简单,您只需要为每个节点 G + H 取前缀,并在其前面加上必要数量的 1 位即可达到 k;这是最好的解决方案。

可能有更好的规则来丢弃无效节点(第 2 步和第 3 步),但算法的思想是相同的。

【讨论】:

  • 谢谢,这个主意很棒。但是,我不太了解如何在这里实现“A*-ness”。优先级队列的键应该是什么,即边权重(所有边可能 = 1)和启发式权重。以所需的 1 位数量作为前缀会给出该数字,但不能保证这是两个整数与 g、h 1 位的总和。此外,这种前缀对优先级队列/堆的作用似乎很模糊
  • @Atin 最小堆的权重就是那个前缀值。就像您说的那样,不能保证您能够从给定节点达到该数字,但您可以保证您不可能达到更好的解决方案(即,它是一个乐观启发式),这就是A* 需要工作(显然,乐观启发式越准确越好)。
  • 哦,我明白了。但是,这种方法忽略了边缘权重,使其成为贪婪的最佳优先搜索而不是 A* 搜索。边权重是隐含的还是确实是最佳优先搜索?只是确保我正确理解了所有内容。 @jdehesa
  • @Atin 老实说,我不太确定您所说的“边权重”是什么意思...您应该只需要按乐观估计排序的节点的优先级队列并选择最好的每次迭代的下一个节点(不需要是前一个访问节点的子节点)。树更多地是考虑算法的抽象。第一个结果总是最好的,但这并不意味着它是贪婪的;该算法将到达没有可能解决方案的节点,然后从队列中选择另一个可能的节点(有点回溯,尽管不是按预定义的顺序)。
【解决方案2】:

您可以根据位数 g、h 和 k 构造最小的和,而无需进行任何动态规划。假设 g ≥ h(否则切换它们)这些是规则:

k ≤ h ≤ g

      11111111  <-  g ones  
  111100000111  <-  h-k ones + g-k zeros + k ones
 1000000000110  <-  n must be at least h+g-k+1

h ≤ k ≤ g

    1111111111  <-  g ones  
      11111100  <-  h ones + k-h zeros
    1011111011  <-  n must be at least g+1

h ≤ g ≤ k

 1111111100000  <-  g ones + k-g ones  
 1100000011111  <-  g+h-k ones, k-h zeros, k-g ones
11011111111111  <-  n must be at least k+1, or k if g+h=k

示例:n=10、g=6 和 h=4 时的所有 k 值:

k=1           k=2           k=3           k=4       
0000111111    0000111111    0000111111    0000111111
0111000001    0011000011    0001000111    0000001111
----------    ----------    ----------    ----------
1000000000    0100000010    0010000110    0001001110
k=4           k=5           k=6
0000111111    0000111111    0000111111
0000001111    0000011110    0000111100
----------    ----------    ----------
0001001110    0001011101    0001111011
k=6           k=7           k=8           k=9           k=10
0000111111    0001111110    0011111100    0111111000    1111110000
0000111100    0001110001    0011000011    0100000111    0000001111
----------    ----------    ----------    ----------    ----------
0001111011    0011101111    0110111111    1011111111    1111111111

或者,不先计算a和b,直接取c的值:

k ≤ h ≤ g

c = (1 << (g + h - k)) + ((1 << k) - 2)

h ≤ k ≤ g

c = (1 << g) + ((1 << k) - 1) - (1 << (k - h))

h ≤ g ≤ k

c = ((1 << (k + 1)) - 1) - (1 << ((g - h) + 2 * (k - g)))

h + g = k

c = (1 << k) - 1

这导致了这个令人失望的平凡代码:

int smallest_sum(unsigned n, unsigned g, unsigned h, unsigned k) {
    if (g < h) {unsigned swap = g; g = h; h = swap;}
    if (k == 0) return (g > 0 || h > 0 || n < 1) ? -1 : 0;
    if (h == 0) return (g != k || n < k) ? -1 : (1 << k) - 1;
    if (k <= h) return (n <= h + g - k) ? -1 : (1 << (g + h - k)) + ((1 << k) - 2);
    if (k <= g) return (n <= g) ? -1 : (1 << g) + ((1 << k) - 1) - (1 << (k - h));
    if (k < g + h) return (n <= k) ? -1 : (1 << (k + 1)) - 1 - (1 << (2 * k - g - h));
    if (k == g + h) return (n < k) ? -1 : (1 << k) - 1;
    return -1;
}

一些示例结果:

n=31, g=15, h=25, k=10  ->  1,073,742,846  (1000000000000000000001111111110)
n=31, g=15, h=25, k=20  ->     34,602,975  (0000010000011111111111111011111)
n=31, g=15, h=25, k=30  ->  2,146,435,071  (1111111111011111111111111111111)

(我将 n、g、h 和 k 从 0 到 20 的每个值与暴力算法的结果进行了比较,以检查正确性,没有发现差异。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多