【问题标题】:Algorithm on interview面试算法
【发布时间】:2012-10-03 00:31:36
【问题描述】:

最近我被问到以下面试问题: 您有两组长度相同的数字 N,例如 A = [3, 5, 9] 和 B = [7, 5, 1]。接下来,对于 0..N-1 范围内的每个位置 i,您可以选择数字 A[i] 或 B[i],因此最后您将获得另一个长度为 N 的数组 C,其中包含来自 A 和B.如果C中所有元素的总和小于或等于K,那么这样的数组是好的。请编写一个算法,通过给定的数组 A、B 和数字 K 计算出好的数组的总数。

我想出的唯一解决方案是动态编程方法,当我们有一个大小为 NxK 的矩阵时,M[i][j] 表示如果当前总和相等,我们可以对数字 X[i] 有多少组合到 j。但看起来他们希望我想出一个公式。你能帮我解决这个问题吗?至少我应该寻找什么方向?将不胜感激任何帮助。谢谢。

【问题讨论】:

  • 如果 a[i] = b[i],会产生多个解决方案吗?我会说不,但不清楚

标签: algorithm


【解决方案1】:

经过一番考虑,我认为这是一个 NP 完全问题。考虑:

A = [0, 0, 0, ..., 0]
B = [b1, b2, b3, ..., bn]

请注意,第三组C = ( A[i] or B[i] for i = 0..n ) 的每个构造都只是A 的某个子集和B 的某个子集的并集。在这种情况下,由于A 的每个子集总和为0,因此C 的总和与B 的某个子集的总和相同。

现在您的问题是“我们可以用多少种方法构造总和小于 KC?”可以重述为“B 的子集有多少总和小于K?”。解决K = 1K = 0 的这个问题会得到Bsubset sum problem 的解决方案(两个解决方案之间的差异是总和为0 的子集的数量)。

通过类似的论证,即使在A包含非零元素的一般情况下,我们也可以构造一个数组S = [b1-a1, b2-a2, b3-a3, ..., bn-an],问题变成“S的子集有多少总和小于K - sum(A)?”

由于子集和问题是NP完全的,所以这个问题也一定是。所以考虑到这一点,我敢说你提出的动态编程解决方案是你能做的最好的,当然不存在神奇的公式。

【讨论】:

  • This question 似乎也有些相关。
  • 看来你打了我一拳!这种情况(A=0)等价于#Knapsack,即统计一个背包实例的可行解的个数,已知为#P-complete。
  • 你误解了任务:“另一个长度为 N 的数组 C”
  • @KarolyHorvath 不,我没有。您可以通过获取B 的任何子集以及来自A0s 的必要数量来构造C。显然0s 不会改变C 的总和,所以这个问题确实相当于“B 的子集有多少总和小于K?”
  • verdesmarald 所说的进一步示例A = [3, 5, 7]B = [7, 5, 1]K = 14 可以简化为A = [0, 0, 0]B = [4, 0, -6]K = -1
【解决方案2】:

" 请写一个算法来算出好的总数 由给定数组 A、B 和数字 K 组成的数组。"

这不是目标吗?

int A[];
int B[];
int N;
int K;
int Solutions = 0;

void FindSolutons(int Depth, int theSumSoFar) {
    if (theSumSoFar > K) return;
    if (Depth >= N) {
    Solutions++;
    return;
    }

    FindSolutions(Depth+1,theSumSoFar+A[Depth]);
    FindSolutions(Depth+1,theSumSoFar+B[Depth]);
}

调用FindSolutions 并将两个参数都设置为零。返回时,Solutions 将等于好数组的数量;

【讨论】:

    【解决方案3】:

    这就是我尝试解决问题的方法 (对不起,如果它很愚蠢)

    想想数组

    A=[3,5,9,8,2]
    B=[7,5,1,8,2]
    

    如果 元素 0..N-1

    选择数量

    2^N

    C1=0,C2=0
    
    for all A[i]=B[i] 
    {
        C1++
        C2+=A[i]+B[i]
    }
    

    然后创建新的两个数组,如

    A1=[3,5,9]
    B1=[7,5,1]
    

    现在 C2 也是 10

    现在所有选项的数量减少到 2^(N-C1)

    现在计算所有好的数字

    使用“K”作为 K=K-C2

    不幸的是 不管你用什么方法,你都有 计算总和 2^(N-C1) 次

    【讨论】:

      【解决方案4】:

      所以有 2^N 选择,因为在每一点你要么从 A 中选择,要么从 B 中选择。在具体的例子中,你给出的 N 恰好是 3,有 8 个。为了讨论,你可以将每组决策描述为位模式。

      所以作为蛮力方法会尝试每一个位模式。

      但是应该很明显的是,如果前几位产生一个太大的数字,那么每个后续可能的尾位组也会产生一个太大的数字。因此,更好的建模方法可能是一棵树,您不必费心沿着已经长出极限的四肢走下去。

      您还可以计算从每个位到表末尾可以达到的最大总数。如果在任何时候你的运行总数加上你可以从这里获得的最大值小于 K,那么你所在的每个子树都是可以接受的,不需要遍历。正如 cmets 中所讨论的,每个组合都可以接受的情况是这种观察的一个特例。

      正如下面 Serge 所指出的,一个相关的观察对我们来说是最小值,并使用相反的逻辑来取消整个子树而无需遍历。

      一个潜在的进一步优化依赖于观察,只要我们以相同的方式洗牌,改变 A 和 B 的顺序就没有效果,因为加法是可交换的。因此,您可以努力确保最大值尽可能快地增长或最小值尽可能慢地增长,以尽可能早地退出遍历。在实践中,您可能希望应用启发式方法将绝对最大值和最小值(无论如何您都已经计算过)与 K 进行比较。

      既然如此,递归实现是最简单的,例如(在 C 中)

      /* assume A, B and N are known globals */
      
      unsigned int numberOfGoodArraysFromBit(
                 unsigned int bit,
                 unsigned int runningTotal,
                 unsigned int limit)
      {
          // have we ended up in an unacceptable subtree?
          if(runningTotal > limit) return 0;
      
          // have we reached the leaf node without at any
          // point finding this subtree to be unacceptable?
          if(bit >= N) return 1;
      
          // maybe every subtree is acceptable?
          if(runningTotal + MAXV[bit] <= limit)
          {
              return 1 << (N - bit);
          }
      
          // maybe no subtrees are acceptable?
          if(runningTotal + MINV[bit] > limit)
          {
              return 0;
          }
      
          // if we can't prima facie judge the subtreees,
          // we'll need specifically to evaluate them
          return
             numberOfGoodArraysFromBit(bit+1, runningTotal+A[bit], limit) +
             numberOfGoodArraysFromBit(bit+1, runningTotal+B[bit], limit);
      }
      
      // work out the minimum and maximum values at each position
      for(int i = 0; i < N; i++)
      {
          MAXV[i] = MAX(A[i], B[i]);
          MINV[i] = MIN(A[i], B[i]);
      }
      
      // hence work out the cumulative totals from right to left
      for(int i = N-2; i >= 0; i--)
      {
          MAXV[i] += MAXV[i+1];
          MINV[i] += MINV[i+1];
      }
      
      // to kick it off
      printf("Total valid combinations is %u", numberOfGoodArraysFromBit(0, 0, K));
      

      我只是临时思考;可能存在更好的解决方案。

      【讨论】:

      • 我不认为这是一个很好的方法,在每个子集都很好的情况下,您将遍历整个树,给 O(2^n) 运行时间。
      • 是的,我同意,如果我们首先计算 max(A[i],B[I]) 数组的总和,那么我们可以过滤掉这种唯一的情况。也许我们可以找到一种方法使其最坏的情况是 O(2^(n-1))?
      • 在这种情况下,最好的优化是生成长度为 N 的 C,其中 C[i] = MAX(A[i], B[i]) 和 D,其中 D[i] =所有 C[j] 的总和,j >= i。如果在任何时候运行总数 + D[i] 小于限制,那么您可以将整个子树添加到可接受的集合中。我已经修改了我的答案(尽管 C 的含义与中间结果和作业的编程世界略有不同)。
      • 还有一项优化——我们并行构建具有最小值的相同数组。一旦我们得到的运行总数加上我们面前的已知最小值超过了 K,我们就可以取消整个子树
      • @Serge 是的。好点子。我将进行编辑,并将其标记为社区 wiki,因为它现在是我们的答案的组合,而不仅仅是我的答案。
      猜你喜欢
      • 2015-06-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多