Генерировать все суммы подмножеств в диапазоне быстрее, чем O ((k + N) * 2 ^ (N / 2))? - PullRequest
4 голосов
/ 25 июня 2010

Есть ли способ сгенерировать все сумм подмножества s 1 , s 2 , ..., s k которые попадают в диапазон [A, B] быстрее, чем O ((k + N) * 2 N / 2 ), где k - это количество сумм в [A, B]? Обратите внимание, что k известен только после того, как мы перечислили все суммы подмножеств в [A, B].

В настоящее время я использую модифицированный Горовиц-Сахни алгоритм. Например, я сначала вызываю его для наименьшей суммы, большей или равной A, давая мне s 1 . Затем я снова вызываю ее для следующей наименьшей суммы, превышающей s 1 , давая мне s 2 . Повторяйте это до тех пор, пока мы не найдем сумму s k + 1 больше, чем B. Между каждой итерацией повторяется много вычислений, даже без восстановления начальных двух списков 2 N / 2 так есть ли способ сделать лучше?

В моей задаче N около 15, а величина чисел порядка миллионов, поэтому я не рассматривал маршрут динамического программирования.

Ответы [ 3 ]

3 голосов
/ 26 июня 2010

Проверьте сумму подмножества в Википедии. Насколько я знаю, это самый быстрый из известных алгоритмов, который работает за O (2 ^ (N / 2)) времени.

Edit: Если вы ищете несколько возможных сумм, вместо просто 0, вы можете сохранить конечные массивы и просто повторить их снова (что примерно равно операции O (2 ^ (n / 2))) и сохранить их для повторного вычисления. Значение всех возможных подмножеств не меняется в зависимости от цели.

Изменить еще раз: Я не совсем уверен, что вы хотите. Выполняем ли мы поиск K по одному независимому значению каждый или ищем какое-либо подмножество, которое имеет значение в определенном диапазоне, который имеет ширину K? Или вы пытаетесь приблизить второе с помощью первого?

Изменить в ответ: Да, вы получаете много дубликатов, даже не восстанавливая список. Но если вы не перестроите список, это не O (k * N * 2 ^ (N / 2)). Построение списка O (N * 2 ^ (N / 2)).

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

Больше редактировать: У вас есть диапазон сумм, от A до B. Во-первых, вы решаете проблему с подмножеством сумм для A. Затем вы просто продолжаете повторять и сохранять результаты, пока не найдете решение для B, после чего остановитесь. Теперь у вас есть каждая сумма между A и B за один прогон, и это будет стоить вам только одного решения задачи с суммой подмножества плюс операций K для значений K в диапазоне от A до B, которые являются линейными, хорошими и быстрыми.

 s = *i + *j; if s > B then ++i; else if s < A then ++j; else { print s; ... what_goes_here? ... }

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

Извините за использование авто. Компилятор C ++ 0x.

std::vector<int> sums;
std::vector<int> firstlist;
std::vector<int> secondlist;
// Fill in first/secondlist.
std::sort(firstlist.begin(), firstlist.end());
std::sort(secondlist.begin(), secondlist.end());
auto firstit = firstlist.begin();
auto secondit = secondlist.begin();
// Since we want all in a range, rather than just the first, we need to check all combinations. Horowitz/Sahni is only designed to find one.
for(; firstit != firstlist.end(); firstit++) {
    for(; secondit = secondlist.end(); secondit++) {
        int sum = *firstit + *secondit;
        if (sum > A && sum < B)
            sums.push_back(sum);
    }
}

Это все еще не здорово. Но это может быть оптимизировано, если вы заранее знаете, что N очень велико, например, отображает или суммирует хеш-сопоставления с итераторами, так что любой данный firstit может найти подходящих партнеров во secondit, что сокращает время выполнения.

2 голосов
/ 26 июня 2010

Это можно сделать в O (N * 2 ^ (N / 2)), используя идеи, аналогичные Horowitz Sahni, но мы пытаемся сделать некоторые оптимизации, чтобы уменьшить константы в BigOh.

Мы делаем следующее

  • Step 1: разбить на наборы N / 2 и сгенерировать все возможные 2 ^ (N / 2) набора для каждого разбиения.Назовите их S1 и S2.Это мы можем сделать в O (2 ^ (N / 2)) (примечание: здесь отсутствует фактор N из-за оптимизации, которую мы можем сделать).

  • Step 2:Затем отсортируйте большее из S1 и S2 (скажем, S1) за время O (N * 2 ^ (N / 2)) (здесь мы оптимизируем, не сортируя оба).

  • Step 3: Найти суммы подмножеств в диапазоне [A, B] в S1 с помощью бинарного поиска (в порядке сортировки).

  • Step 4: Далее, для каждой суммы в S2 найдите с помощьюбинарный поиск в множествах S1, объединение которых дает сумму в диапазоне [A, B].Это O (N * 2 ^ (N / 2)).В то же время найдите, находится ли соответствующий набор в S2 в диапазоне [A, B].Оптимизация здесь заключается в объединении циклов.Примечание: это дает вам представление наборов (в терминах двух индексов в S2), а не самих наборов.Если вам нужны все наборы, это становится O (K + N * 2 ^ (N / 2)), где K - количество наборов.

Возможна дальнейшая оптимизация,например, когда сумма из S2 отрицательна, мы не считаем суммы

Поскольку шаги 2,3,4 должны быть достаточно ясными, я подробнее расскажу, как выполнить Шаг 1 вВремя O (2 ^ (N / 2)).

Для этого мы используем концепцию

Серых кодов .Серые коды представляют собой последовательность двоичных битовых комбинаций, в которой каждый шаблон отличается от предыдущего шаблона ровно на один бит.Пример: 00 -> 01 -> 11 -> 10 - это серый код с 2 битами.

Существуют серые коды, которые проходят через все возможные номера N / 2 битов, и их можно генерировать итеративно (см. Вики-страницу, на которую я ссылался), вO (1) время для каждого шага (всего O (2 ^ (N / 2)) шагов), учитывая предыдущую битовую комбинацию, то есть, учитывая текущую битовую комбинацию, мы можем сгенерировать следующую битовую комбинацию за O (1).

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

0 голосов
/ 30 июня 2010

Если вы правильно измените алгоритм Горовица-Сахни, то он вряд ли будет медленнее, чем оригинальный Горовиц-Сахни.Напомним, что Горовиц-Сахни работает с двумя списками сумм подмножеств: суммы подмножеств в левой половине исходного списка и суммы подмножеств в правой половине.Вызовите эти два списка сумм L и R. Чтобы получить подмножества, которые суммируют с некоторым фиксированным значением A, вы можете отсортировать R, а затем найти число в R, соответствующее каждому числу в L, используя двоичный поиск.Однако алгоритм асимметричен только для сохранения постоянного фактора в пространстве и времени.Для этой проблемы неплохо бы отсортировать и L, и R.

. В моем коде ниже я также обращаюсь к L. Затем вы можете сохранить два указателя на R, обновленные для каждой записи в L: указатель на последнийслишком низкая запись в R, а указатель на первую запись в R слишком высокий.Когда вы переходите к следующей записи в L, каждый указатель может либо двигаться вперед, либо оставаться на месте, но им не придется двигаться назад.Таким образом, второй этап алгоритма Горовица-Сахни занимает только линейное время в данных, сгенерированных на первом этапе, плюс линейное время в длине вывода.Вплоть до постоянного фактора, вы не можете добиться большего успеха, чем это (как только вы посвятите себя этому алгоритму встречи посередине).

Вот код Python с примером ввода:

# Input
terms = [29371, 108810, 124019, 267363, 298330, 368607,
    438140, 453243, 515250, 575143, 695146, 840979, 868052, 999760]
(A,B) = (500000,600000)

# Subset iterator stolen from Sage
def subsets(X):
    yield []; pairs = []
    for x in X:
        pairs.append((2**len(pairs),x))
        for w in xrange(2**(len(pairs)-1), 2**(len(pairs))):
            yield [x for m, x in pairs if m & w]

# Modified Horowitz-Sahni with toolow and toohigh indices
L = sorted([(sum(S),S) for S in subsets(terms[:len(terms)/2])])
R = sorted([(sum(S),S) for S in subsets(terms[len(terms)/2:])])
(toolow,toohigh) = (-1,0)
for (Lsum,S) in reversed(L):
    while R[toolow+1][0] < A-Lsum and toolow < len(R)-1: toolow += 1
    while R[toohigh][0] <= B-Lsum and toohigh < len(R): toohigh += 1
    for n in xrange(toolow+1,toohigh):
        print '+'.join(map(str,S+R[n][1])),'=',sum(S+R[n][1])

«Идиот» (думаю, ему следует сменить имя пользователя) поднимает разумную проблему оптимизации алгоритма, пропуская один из видов.На самом деле, поскольку каждый список L и R является списком размеров подмножеств, вы можете выполнить комбинированную генерацию и сортировку каждого из них за линейное время!(То есть, линейные по длинам списков.) L - это объединение двух списков сумм, тех, которые включают первый член, член [0], и тех, которые не имеют.Так что на самом деле вы должны просто сделать одну из этих половин в отсортированном виде, добавить константу, а затем выполнить слияние двух отсортированных списков.Если вы примените эту идею рекурсивно, вы сохраните логарифмический фактор во времени, чтобы создать отсортированный L, то есть фактор N в исходной переменной задачи.Это дает хорошую причину для сортировки обоих списков по мере их создания.Если вы сортируете только один список, у вас есть несколько бинарных поисков, которые могут снова ввести этот коэффициент N;в лучшем случае вы должны как-то оптимизировать их.

На первый взгляд, коэффициент O (N) все еще может быть там по другой причине: если вы хотите не просто сумму подмножества, а подмножество, которое делаетсуммируя, тогда выглядит как O (N) время и пространство для хранения каждого подмножества в L и в R. Тем не менее, существует прием обмена данными, который также избавляет от этого фактора O (N).Первым шагом хитрости является сохранение каждого подмножества левой или правой половины в виде связанного списка битов (1, если термин включен, 0, если он не включен).Затем, когда список L удваивается по размеру, как в предыдущем абзаце, два связанных списка для подмножества и его партнера могут совместно использоваться, за исключением заголовка:

     0
     |
     v
1 -> 1 -> 0 -> ...

На самом деле этот трюк со связанным спискомявляется артефактом модели затрат и никогда по-настоящему не помогает.Потому что для того, чтобы указатели в архитектуре ОЗУ стоили O (1), вы должны определить слова данных с битами O (log (memory)).Но если у вас есть слова данных такого размера, вы можете также хранить каждое слово как один битовый вектор, а не с этой структурой указателя.То есть, если вам нужно меньше гигаворда памяти, вы можете хранить каждое подмножество в 32-битном слове.Если вам нужно больше, чем гигаворд, у вас есть 64-битная архитектура или ее эмуляция (или, может быть, 48 бит), и вы все равно можете хранить каждое подмножество в одном слове.Если вы исправите модель стоимости ОЗУ, чтобы учесть размер слова, то этот фактор N так или иначе никогда не существовал.

Итак, что интересно, сложность времени для оригинального алгоритма Горовица-Сахни не равна O (N * 2 ^ (N / 2)), это O (2 ^ (N / 2)).Аналогично, сложность по времени для этой задачи составляет O (K + 2 ^ (N / 2)), где K - длина вывода.

...