问题 Google Combinatorial Optimization面试问题


几周前我在谷歌的一次采访中被问到这个问题,我没有得到答案,我想知道这里是否有人可以帮助我。

你有一个数组 ñ 元素。元素为0或1。 你想要 将数组拆分为k 邻近的 子阵。每个子阵列的大小可以在ceil(n / 2k)和floor(3n / 2k)之间变化。你可以假设k << n。 将数组拆分为k个子数组后。将随机选择每个子阵列的一个元素。

设计一种算法,用于最大化k个子阵列中随机选择的元素的总和。 基本上意味着我们希望以这种方式拆分数组,使得从每个子阵列中选择的元素的所有期望值的总和最大。

你可以假设n是2的幂。

Example:

Array: [0,0,1,1,0,0,1,1,0,1,1,0]
n = 12
k = 3
Size of subarrays can be: 2,3,4,5,6

Possible subarrays [0,0,1] [1,0,0,1] [1,0,1,1,0]
Expected Value of the sum of the elements randomly selected from the subarrays: 1/3 + 2/4 + 3/5 = 43/30 ~ 1.4333333 

Optimal split: [0,0,1,1,0,0][1,1][0,1,1,0]
Expected value of optimal split: 1/3 + 1 + 1/2 = 11/6 ~ 1.83333333

4281
2017-11-18 21:23


起源

哇我希望面试官解释得比这更好,否则我绝不想采访谷歌。 - Mike Christensen
实际上它非常接近。 - John Smith
是的,我认为这是最佳分区。 - John Smith
显然不是:[0,0,0,0,0,0] [1,1,1] [1,1,1]。 - Stephen Canon
@hatchet:嗯,那更有意思(作为一名数学家,我对“分区”一词的滥用很可怕)。 - Stephen Canon


答案:


我不知道这是否仍然是一个悬而未决的问题,但似乎OP已经设法补充足够的说明,这应该是直截了当的解决。无论如何,如果我理解你所说的话,在面试环境中询问软件开发职位似乎是公平的事情。

这是基本的O(n ^ 2 * k)解,它应该适用于小k(如面试官所指定):

def best_val(arr, K):
  n = len(arr)
  psum = [ 0.0 ]
  for x in arr:
    psum.append(psum[-1] + x)
  tab = [ -100000 for i in range(n) ]
  tab.append(0)
  for k in range(K):
    for s in range(n - (k+1) * ceil(n/(2*K))):
      terms = range(s + ceil(n/(2*K)), min(s + floor((3*n)/(2*K)) + 1, n+1))
      tab[s] = max( [ (psum[t] - psum[s]) / (t - s) + tab[t] for t in terms ])
  return tab[0]

我使用了numpy ceil / floor功能,但你基本上可以理解。这个版本中唯一的“技巧”就是它会将窗口开销减少到O(n)而不是O(n * k),并且预先计算部分和以计算框a的预期值。恒定时间操作(从而从内环中节省O(n)因子)。


3
2017-11-19 05:57



这实际上是正确的。但我对你的代码有点困惑。您(或其他也了解此代码的人)可以进一步解释for循环内部的情况吗? - John Smith
另外,您将如何获得分割数组的实际位置而不是最佳预期值 - John Smith
@JohnSmith:迭代后 k, tab[i] 是期望值的最大总和 arr[i:n] 分成 k+1 连续的子阵列。你可以维护另一个数组 sp 存储产生值的分裂位置 tab[i]:简单地替换 max() 使用循环调用,不仅记录最大值,还记录最佳分裂点 t,并设置 sp[s]=sp[t]+[t]。 - han
@Mikola你能以简单的形式提供解释吗?伪代码也许? - Nitin Garg


我认为我们可以使用动态编程来解决这个问题。

基本上,我们有:

F(I,J) 定义为从大小数组中选择的所有预期值的最大总和 一世  分裂成 Ĵ 子阵。因此解决方案应该是 F(N,k)的

递归方程是:

f(i,j) = f(i-x,j-1) + sum(i-x+1,i)/x where (n/2k) <= x <= (3n/2k)

6
2017-11-18 23:15



我觉得这不行。你重复使用输入数组在哪里?你试过一些小例子吗? - Mikola
@Mikola:再次阅读问题,发现我们无法对数组进行排序... = 0 =抱歉... - derekhh
@Mikola:修改过 - derekhh
只是补充说这个DP方法的复杂性也是O(K * N ^ 2) - KFL


我不知道是否有人仍然有兴趣看到这个问题的解决方案。半小时前偶然发现了这个问题并考虑发布我的解决方案(Java)。其复杂性为O(n * K ^ log10)。证明有点复杂,所以我宁愿提供运行时编号:

n k时间(ms)
48 4 25
48 8 265
24 4 20
24 8 33
96 4 51
192 4 143
192 8 343919

解决方案是同一个旧的递归方法,给定一个数组,选择第一个大小为ceil的分区(n / 2k)并以其他方式递归找到最佳解决方案,其中分区数= k -1,然后取ceil(n / 2k) )+ 1等等。

码:

public class PartitionOptimization {
public static void main(String[] args) {
    PartitionOptimization p = new PartitionOptimization();
    int[] input = { 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0};
    int splitNum = 3;
    int lowerLim = (int) Math.ceil(input.length / (2.0 * splitNum));        
    int upperLim = (int) Math.floor((3.0 * input.length) / (2.0 * splitNum));
    System.out.println(input.length + " " + lowerLim + " " + upperLim + " " +
            splitNum);
    Date currDate = new Date();
    System.out.println(currDate);       
    System.out.println(p.getMaxPartExpt(input, lowerLim, upperLim,
            splitNum, 0));
    System.out.println(new Date().getTime() - currDate.getTime());
}

public double getMaxPartExpt(int[] input, int lowerLim, int upperLim,
        int splitNum, int startIndex) {
    if (splitNum <= 1 && startIndex<=(input.length -lowerLim+1)){
        double expt = findExpectation(input, startIndex, input.length-1);           
        return expt;
    }
    if (!((input.length - startIndex) / lowerLim >= splitNum))
        return -1;
    double maxExpt = 0;
    double curMax = 0;
    int bestI=0;
    for (int i = startIndex + lowerLim - 1; i < Math.min(startIndex
            + upperLim, input.length); i++) {
        double curExpect = findExpectation(input, startIndex, i);           
        double splitExpect = getMaxPartExpt(input, lowerLim, upperLim,
                splitNum - 1, i + 1);
        if (splitExpect>=0 && (curExpect + splitExpect > maxExpt)){
            bestI = i;
            curMax = curExpect;
            maxExpt = curExpect + splitExpect;
        }
    }
    return maxExpt;
}

public double findExpectation(int[] input, int startIndex, int endIndex) {
    double expectation = 0;
    for (int i = startIndex; i <= endIndex; i++) {
        expectation = expectation + input[i];
    }
    expectation = (expectation / (endIndex - startIndex + 1));
    return expectation;
}
 }

1
2018-02-18 08:33





不确定我理解,算法是将数组分组,对吧?总和可以具有的最大值是1的数量。因此,将数组拆分为每个1个元素的“n”组,并且加法将是可能的最大值。但它必须是别的东西,我不明白这个问题,这似乎太傻了。


0
2017-11-18 21:35



“你想把数组分成k个分区。” - Karoly Horvath
每个分区中的最小和最大元素数有限制。我们的想法是以这样一种方式进行分区,即你的分区捕获1和0的自然聚类,所以你最终会得到很多分区,其中包含大部分1和几个包含大部分0的分区。然后,当从每个分区中随机选择单个值时,总和最大化。 - hatchet
原始邮件已被编辑。据我所知,我的回复在原始邮件的限制范围内有效。 - DPM
@hatchet:这不一定能最大化总和。实际上你不想聚集1,因为它们不会增加预期值。 - Karoly Horvath
我的意思是你想要尽可能多的分区,其中出现的平均值高于平均值,还有一些大的分区将0分组。 - hatchet


我认为这可以通过动态编程来解决。在每个可能的拆分位置,如果您在该位置拆分,并且在该点没有拆分,则获取最大总和。递归函数和存储历史的表可能很有用。

sum_i = max{ NumOnesNewPart/NumZerosNewPart * sum(NewPart) + sum(A_i+1, A_end),
                sum(A_0,A_i+1) + sum(A_i+1, A_end)
           }

这可能导致一些事情......


0
2017-11-18 21:36



DP是一种解决方案,但表格比您描述的更复杂。 - Per
你能详细说明Per吗? - John Smith
这种复发对我来说似乎不对。你可以尝试一些小例子吗? - Mikola


我认为这是一个糟糕的面试问题,但它也是一个容易解决的问题。

每个整数都会产生重量为1 / s的预期值,其中s是放置它的集合的大小。因此,如果您猜测分区中集合的大小,则只需要从最小集合开始填充集合,然后用零填充剩余的最大集合。

你可以很容易地看到,如果你有一个分区,如上所示填充,其中集合的大小是S_1,...,S_k,你进行转换,你从集合S_i中删除一个项目并将其移动到设置S_i + 1,您有以下情况:

  • S_i和S_i + 1都填充了一个;那么期望值不会改变
  • 他们都充满了零;那么期望值不会改变
  • S_i包含1和0,S_i + 1仅包含零;将0移至S_i + 1会增加预期值,因为S_i的预期值会增加
  • S_i包含1,S_i + 1包含1和0;将1移至S_i + 1会增加预期值,因为S_i + 1的预期值增加且S_i保持不变

在所有这些情况下,您可以将元素从S_i移位到S_i + 1,保持填充最小集合的填充规则为1,以便预期值增加。这导致了简单的算法:

  1. 创建一个分区,其中包含最大数量的最大大小数组和最小数量的最小大小数组
  2. 从1的最小值开始填充数组
  3. 用0表示填充剩余的插槽

0
2017-11-19 01:41



我不确定我理解算法。你能澄清你对Words分区,Set和Array的使用吗?因为在某一点上你谈论分区内的集合然后你提到数组。谢谢您的帮助! - John Smith
他们都意味着相同...... - Antti Huima
@ antti.huima我认为你已经错过了S_i和S_i + 1同时拥有1和0的情况。 - Nitin Garg
@Nitin没有这种情况,因为分区是从左到右的顺序开始填充的。如果S_i为0,则1已经用完,S_i + 1不能再为1。 - Antti Huima
然后可能是您将数组分区为子集,而不是OP指定的连续子数组。 - Nitin Garg


递归函数怎么样:

int BestValue(Array A, int numSplits)
// Returns the best value that would be obtained by splitting 
// into numSplits partitions.

这又使用了一个帮手:

// The additional argument is an array of the valid split sizes which 
// is the same for each call.
int BestValueHelper(Array A, int numSplits, Array splitSizes)
{
    int result = 0;
    for splitSize in splitSizes
        int splitResult = ExpectedValue(A, 0, splitSize) + 
                          BestValueHelper(A+splitSize, numSplits-1, splitSizes);
        if splitResult > result
            result = splitResult;
}

ExpectedValue(Array A,int l,int m)计算从1到m的A的分裂的期望值,即(A [1] + A [l + 1] + ... A [m])/( M-L + 1)。

在计算ceil(n / 2k)和floor(3n / 2k)之间的有效分割大小数组后,BestValue调用BestValueHelper。

我省略了错误处理和一些结束条件,但这些不应该太难添加。


0
2018-01-04 09:11





  • a [] =给定长度为n的数组
  • from =包含数组a的索引
  • k =所需拆分的数量
  • minSize =拆分的最小尺寸
  • maxSize =拆分的最大大小
  • d = maxSize - minSize
  • expectation(a,from,to)=数组a的所有元素从“from”到“to”的平均值

    Optimal(a[], from, k) = MAX[ for(j>=minSize-1 to <=maxSize-1) { expectation(a, from, from+j) + Optimal(a, j+1, k-1)} ]
    

运行时(假设memoization或dp)= O(n * k * d)


0
2017-07-22 21:15