Какую технику использовать, когда я хочу проверить все возможные комбинации набора? - PullRequest
5 голосов
/ 01 марта 2010

Я прорабатываю вопрос интервью, который звучит так:

Учитывая массив целых чисел и сумму, проверьте, складывается ли какая-либо комбинация к сумме.

Какую технику программирования используют, когда хотят попробовать все возможные комбинации набора?

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

Ответы [ 9 ]

10 голосов
/ 01 марта 2010

Один удобный способ понять, что двоичное представление всех чисел от 0 до (2^N)-1 на самом деле представляет собой набор битовых масок для возможных комбинаций из N отдельных элементов. Например, для N=3 (3 элемента) и, таким образом, (2^3)-1 = 7:

0: 000 = none
1: 001 = third item
2: 010 = second item
3: 011 = second and third items
4: 100 = first item
5: 101 = first and third items
6: 110 = first and second items
7: 111 = all 3 items

Это позволяет очень легко циклически проходить все возможные выборы в установленном порядке (так что невозможно пропустить или дважды посетить любой потенциальный выбор).

3 голосов
/ 01 марта 2010

Существует несколько способов решения этой проблемы. Одним из них является классическое решение DP, которое опубликовали другие. Я собираюсь опубликовать решение, которое использует только O (S) память, где S - это сумма всех целых чисел в массиве (может быть изменено, чтобы обозначить и желаемую сумму), а другое - очень эффективный рандомизированный алгоритм, который может Быть проверенным, чтобы быть очень быстрым даже для сотен тысяч чисел любого размера, и даже рациональных и отрицательных чисел.

Решение DP во времени O (nS) и памяти O (S):

//let F[i] = 1 if we can get sum i and 0 otherwise
F[0] = 1; // we can always make sum 0
for ( int i = 1; i <= n; ++i )
  for ( int j = S; j >= numbers[i]; --j )
    F[j] |= F[j - numbers[i]]; // basically, if F[j - numbers[i]] == 1, then we 
                               // can add numbers[i] to make F[j] 1, otherwise 
                               // we can't. A bitwise or operation will save us 
                               // an if/else structure basically.

Псевдокод для рандомизированного алгоритма: Пусть Используется = список чисел, которые вы суммируете. Пусть Unused = список чисел, которые вы НЕ суммируете. Пусть tmpsum = 0. Пусть S = желаемая сумма, которую вы хотите достичь.

for ( each number x you read )
  toss a coin:
    if it's heads and tmpsum < S
      add x to Used
    else
      add x to Unused

while ( tmpsum != S )
  if tmpsum < S 
    MOVE one random number from Unused to Used
  else
    MOVE one random number from Used to Unused

print the Used list, containing the numbers you need to add to get S

Это будет намного быстрее, чем решение для динамического программирования, особенно для случайных входов. Единственные проблемы состоят в том, что вы не можете надежно определить, когда решения не существует (вы можете позволить алгоритму работать в течение нескольких секунд, а если он не завершится, предположить, что решения не существует) и что вы не можете быть уверены, что получите решение с минимальным количеством выбранных элементов. Опять же, вы можете добавить некоторую логику, чтобы заставить алгоритм продолжать работу и пытаться найти решение с меньшим количеством элементов, пока не будут выполнены определенные условия остановки, но это сделает его медленнее. Однако, если вас интересует только решение, которое работает, и у вас МНОГО чисел, а желаемая сумма может быть ОЧЕНЬ большой, это, вероятно, лучше, чем алгоритм DP.

Еще одним преимуществом этого подхода является то, что он также будет работать для отрицательных и рациональных чисел без изменений, что неверно для решения DP, поскольку решение DP предполагает использование частичных сумм в качестве индексов массивов, а индексы могут быть только естественными номера. Конечно, вы можете, например, использовать хеш-таблицы, но это сделает решение DP еще медленнее.

Чтобы сгенерировать все комбинации, вы должны поискать обратное отслеживание: http://en.wikipedia.org/wiki/Backtracking

Для этой проблемы вам нужно использовать что-то вроде этого:

void back(int k)
{
  if ( k > numElements )
  { 
    // add all the nums[i] for which st[i] == 1 and check
    // if their sum is what you desire, then return;
  }

  for ( int i = 0; i <= 1; ++i )
  {
    st[k] = i;
    back(k + 1);
  }
}

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

2 голосов
/ 01 марта 2010

Это не отвечает на ваш «комбинированный» вопрос, но, вероятно, это оптимальное решение вопроса: P

Это проблема подмножества суммы , где вам нужно искать N сумм.

Сумма подмножества имеет псевдополиномиальный алгоритм с использованием динамического программирования:

psuedocode из этой ссылки

Subset-Sum-Solver[S = w1,w2, . . . ,wn,B]
1 Initialize M[0..n, 0..B] everywhere False apart from M[0, 0] = True
2 for i  from 1 to n
  do
3    for w from  0 to B
     do
4        M[i,w] = M[i − 1,w] _M[i − 1,w − wi]
         (any reference outside the array returns false)
5 Output M[n,B]

где B - сумма, S - набор чисел, n - количество элементов S (количество элементов в S), а M - матрица n по B. Этот алгоритм O (нБ)

В случае вопроса об интервью, сделайте это для каждой суммы, и вы получите алгоритм O (nmB), где m - количество сумм, которые вы должны проверить.

Вопрос немного двусмысленный, является ли массив целых чисел, используемых для получения подмножеств, таким же массивом сумм? т.е. подмножество целых чисел в массиве A также складывается в одно из целых чисел в массиве A? в этом случае алгоритм равен O (n ^ 2B), поскольку n == m

1 голос
/ 01 марта 2010

Здесь нужна некоторая осторожность с терминологией. Комбинации используется для обозначения выбора k элементов из набора n элементов, где порядок элементов k не имеет значения. Связанная концепция выбора k предметов из набора n предметов, где порядок k предметов имеет значение , называется перестановкой .

Однако то, о чем вы изначально говорите:

Учитывая массив целых чисел и сумму, проверьте, суммирует ли любая комбинация сумму.

- это совсем другое - здесь нет фиксированного k: вас интересует любое подмножество размеров оригинальных предметов.

Набор всех подмножеств набора S называется power-set of S, и существует очень простая формула для числа членов, которые он содержит. Я оставлю это как упражнение - после того, как вы его разработаете, должно быть относительно очевидно, как перечислять элементы набора параметров.

(Подсказка: блок питания { 1, 2 } равен { {}, { 1 }, { 2 }, { 1, 2 } })

0 голосов
/ 01 марта 2010

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

В Haskell есть подпоследовательность функций, которая по существу возвращает powerset любого набора в виде списка списков.

Или вы можете написать это сами

powerSet :: [a] -> [[a]]
powerSet [] = [[]]
powerSet x:xs = map (:x) (powerSet xs) ++ (powerSet xs)
0 голосов
/ 01 марта 2010

вижу два варианта:

  1. Вычислить набор мощности входного массива и проверить сумму каждого элемента в наборе мощности (см. http://en.wikipedia.org/wiki/Power_set). Это, вероятно, O (2 ^ N) и не подходит для больших N
  2. Попробуйте что-нибудь с задачей 0-1 Рюкзак (см. http://en.wikipedia.org/wiki/Knapsack_problem).). Это должно либо найти наибольшую сумму, меньшую, чем ваше желаемое значение, сумму, которая является вашим желаемым значением, либо ничего не найти. На основе выходных данных , вы можете ответить на свой первоначальный вопрос. 0-1 Рюкзак - это хорошо, потому что он работает за полиномиальное время O (N ^ c), где c - константа, хотя я не помню, работает ли он для отрицательных чисел.
0 голосов
/ 01 марта 2010

Если у вас есть как положительные, так и отрицательные целые числа, вы столкнетесь с комбинаторным взрывом, где любой алгоритм, который вы выберете, будет замедляться на фиксированный процент при каждом увеличении длины вашего массива. (Если у вас есть только положительные целые числа, вы можете ограничить свой поиск, как только будет превышена целевая сумма.)

Граничный вопрос: разрешено ли вам также повторно использовать целые числа?

Вам следует искать «комбинаторные алгоритмы». Knuths '1005 * to-in-progress может вам очень помочь, если вы захотите углубиться в вопрос.

0 голосов
/ 01 марта 2010

Звучит как классическая проблема рекурсии. Вы начинаете с первого элемента и рассматриваете остальную часть массива; для каждого элемента либо он выбран, либо нет. Базовый случай - это когда начальный индекс больше длины массива. Что-то вроде

public static bool canSum(int start, int[] array, int sum)
{
      if (start >= array.Length)
           return sum == 0;
      return canSum(start + 1, array, sum - array[start]) || canSum(start + 1, array, sum);
}
0 голосов
/ 01 марта 2010

Рекурсивный. Псевдокод будет примерно таким:

function f(set,currentelement,selectedelements,sum,wantedsum)
{
for (thiselement=currentelement+1 to lastelement)
   {
   if (sum+thiselement==wantedsum) print out selectedelements+thiselement;
   if (sum+thiselement<wantedsum)
      {
      f(set,thiselement,selectedelements+thiselement,sum+thiselement,wantedsum);
      }
   }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...