具有k个1位且是两个n位整数之和的最小n位整数c,其中这两个整数各有g、h位被设为1(动态规划)。

7
我正在尝试解决以下问题:
找到具有k个1位且是两个n位整数之和的最小n位整数c,这些整数具有g、h位设置为1。 g、h、k <= n。
这里的n位整数意味着我们可以使用所有n位,即这样一个整数的最大值为2 ^ n-1。所描述的整数可能根本不存在。显然,情况k> g + h没有解决方案,对于g + h = k,答案只是2 ^ k-1(前面有k-n个0)。
至于程序应该执行的一些示例:
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;
  }
}

4
不错的问题。但我认为你过于草率地得出了错误的结论(使用动态规划)。我会寻找更具建设性的方法。通过手动解决一些情况,比如1 <= k、g、h <= 3,你可能会注意到某种模式。挑剔的一点是: "这样的整数的最大值是2^(n + 1) - 1" - 实际上应该是 2^n - 1 - Gassa
如果你暗示对于 g + h = k + 1 的解决方案是 10 + k 个一,对于 g + h = k + 2 的解决方案是 110 + k 个一等等,那么你是错误的。首先,g + h - k > k 有一个更复杂的模式。其次,它们都有反例! - Y N
1
抱歉,我没有具体的模式想法。相反,我有一种通用的调查方法,可以揭示模式 - 或者如果确实如此的话,它的不存在。 - Gassa
2个回答

4
您可以根据位数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" 可以翻译为“h小于等于k小于等于g”。
    1111111111  <-  g ones  
      11111100  <-  h ones + k-h zeros
    1011111011  <-  n must be at least g+1

"h ≤ g ≤ k" 可以翻译为 "h小于等于g,且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

或者,直接求出 c 的值,而不需要先计算 a 和 b:
k ≤ h ≤ g
c = (1 << (g + h - k)) + ((1 << k) - 2)

"h ≤ k ≤ g" 可以翻译为 "h小于等于k,且k小于等于g"。
c = (1 << g) + ((1 << k) - 1) - (1 << (k - h))

"h ≤ g ≤ k" 可以翻译为 "h小于等于g,且g小于等于k"。
c = ((1 << (k + 1)) - 1) - (1 << ((g - h) + 2 * (k - g)))

"h + g = k" 可以翻译为 "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进行比较,以检查正确性,并发现没有任何差异。

2
我对动态规划方法并不是很确信。如果我理解正确,你需要定义如何到达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 *搜索树,每个节点包含两个部分候选加数,我们称其为G和H。您从m = 0级别开始,拥有G = 0和H = 0的节点,并按以下方式工作:
  1. 如果G + H具有n或更少的位数且具有k个1位,那就是解决方案,您已找到它!
  2. 否则,如果
    n-m < G + H中1的位数 - k
    则丢弃该节点(无解可能)。
  3. 否则,如果
    (g + h) - (G和H中1的位数之和) < k - G + H中1的位数
    则丢弃该节点(不可行候选者)。
  4. 否则,将该节点分支到新的级别。通常情况下,您为每个节点创建最多四个子节点,前缀G和H为0和0、0和1、1和0或1和1。但是:
    • 如果G中1的位数少于g,则只能在G之前加上1,并且对于H和h也是如此。
    • 在m级别(G和H具有m位)时,仅当
      n - m > G中1的位数 + g
      并且对于H和h也是如此,才能在G之前加上0。
    • 如果G == H且g == h,则可以跳过0和1以及1和0中的一个,因为它们将导致相同的子树。
  5. 继续到下一个节点并重复,直到找到解决方案或没有更多可访问的节点。
访问节点的顺序很重要。你应该在优先队列/堆中存储节点,以便下一个节点始终是可能导致最佳解决方案的第一个节点。实际上这很容易,您只需要为每个节点G + H添加必要数量的1位前缀,以达到k;从那里开始,这是最好的可能解决方案。
可能有更好的规则可用于丢弃无效节点(步骤2和3),但算法的思想是相同的。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接