Проблема собеседования с комбинаторной оптимизацией Google

Мне задали этот вопрос на собеседовании для google пару недель назад, я не получил ответа, и мне было интересно, может ли кто-нибудь здесь помочь мне.

У вас есть массив с элементами n. Элементами являются либо 0, либо 1. Вы хотите, чтобы разделил массив на k смежных подмассивов. Размер каждого подмассива может варьироваться от ceil (n/2k) до пола (3n/2k). Вы можете предположить, что k < п. После того, как вы разделите массив на k subarrays. Один элемент каждого подмассива будет случайным образом выбран.

Разработать алгоритм максимизации суммы случайно выбранных элементов из 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

Ответ 1

Я не знаю, является ли это еще открытым вопросом или нет, но похоже, что 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) от внутреннего цикла).

Ответ 2

Я думаю, мы сможем решить эту проблему с помощью динамического программирования.

В принципе, мы имеем:

f (i, j) определяется как максимальная сумма всех ожидаемых значений, выбранных из массива размера i, и разбивается на j subarrays. Поэтому решение должно быть f (n, k).

Рекурсивное уравнение:

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

Ответ 3

Я не знаю, хочет ли кто-нибудь еще увидеть решение этой проблемы. Только наткнулся на этот вопрос полчаса назад и подумал о публикации моего решения (Java). Сложностью для этого является O (n * K ^ log10). Доказательство немного свернуто, поэтому я бы предпочел предоставить номера времени выполнения:

n k time (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;
}
 }

Ответ 4

Не уверен, что я понимаю, алгоритм состоит в том, чтобы разбить массив на группы, правильно? Максимальное значение, которое может иметь сумма, - это количество единиц. Так разбивайте массив в "n" группах по 1 элемента каждый, и добавление будет максимально возможным. Но это должно быть что-то еще, и я не понял проблему, это кажется слишком глупым.

Ответ 5

Я думаю, что это можно решить с помощью динамического программирования. В каждом возможном разделенном месте получите максимальную сумму, если вы разделите ее в этом месте и если вы не разделите ее. Полезной может быть рекурсивная функция и таблица для хранения истории.

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)
           }

Это может привести к чему-то...

Ответ 6

Я думаю, что это плохой вопрос интервью, но это также легкая проблема для решения.

Каждое целое число вносит вклад в ожидаемое значение с весом 1/с, где 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
  • Заполните оставшиеся слоты 0

Ответ 7

Как насчет рекурсивной функции:

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) вычисляет ожидаемое значение раскола A, которое переходит от l к m, т.е. (A [l] + A [l + 1] +... A [m] )/(m-l + 1).

BestValue вызывает BestValueHelper после вычисления массива допустимых разделенных размеров между ceil (n/2k) и floor (3n/2k).

Я пропустил обработку ошибок и некоторые конечные условия, но их не следует добавлять слишком сложно.

Ответ 8

Пусть

  • a [] = заданный массив длины n
  • from = инклюзивный индекс массива a
  • k = количество требуемых расщеплений
  • minSize = минимальный размер раскола
  • maxSize = максимальный размер раскола
  • d = maxSize - minSize
  • Ожидание (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)