Pull to refresh
77
4
Владислав @rebuilder

Разработчик

Send message

А ларчик просто открывался!

Прирост скорости от 50% - 100% от базового решения.

Мне только так и не нравится, что по факту три прохода по массиву.

1)     Копирование части массива

2)     Присваивание значимых элементов

3)     Максимизация

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

Как понимаю на C можно в один проход сделать, но мне кажется всё равно медленнее будет чем текущая версия с векторизацией.

import random as rnd
import numpy as np
import time

def find_subset_sum(numbers: list, target: int):
    dp = np.zeros(target + 1, dtype='uint8' if len(numbers) < 256 else 'uint16')    
    dp[0] = 1
    offset = 1

    for i, val in enumerate(numbers):
        offset += val
        if offset > target + 1:
            offset = target + 1
        temp = dp[:offset-val].copy()
        temp[temp > 0] = len(numbers) - i
        np.maximum(dp[val:offset], temp, dp[val:offset])
        if dp[target]:
            break

    if not dp[target]:
        return []

    subset = []
    j = target   
    while j > 0:
        i = numbers[len(numbers) - dp[j]]
        subset.append(i)
        j -= i

    return subset[::-1]


n = 50
rnd.seed(1)
source = [rnd.randint(1000000, 9999999) for i in range(n)]

target_sum = sum(source) // 2
print('source:', source)
print('target_sum:', target_sum)
print()
start = time.time()
result = find_subset_sum(source, target_sum)
end = time.time()
elapsed_time = end - start
print(f"Execution time: {elapsed_time:.6f} seconds")
print('result:', sum(result), result)

С последним вариантом неувязка вышла. Зато новая попытка в среднем 10% выигрыш по времени.

Основная идея в том, что не нужно трогать ту часть массива, которая остаётся неизменной на каждом этапе.

for i, val in enumerate(numbers):
    temp = np.roll(dp, val).view()[val:]
    temp[temp > 0] = len(numbers) - i
    dp_slice = dp[val:]
    np.maximum(dp_slice, temp, dp_slice)
    if dp[target]:
        break

Попробуйте изменить на это:

for i, val in enumerate(numbers):
  k = np.uint8(len(numbers) - i) if len(numbers) < 256 else np.uint16(len(numbers) - i)
  temp = np.roll(dp, val)
  temp[:val] = 0
  np.maximum(dp, temp.astype(bool) * k, dp)
  if dp[target]:
      break

У меня ускорилось в два раза, так как мы убираем лишний проход по массиву.

Всякие максимумы, которые не факт что векторизованы.

np.maximum 100% векторизированна, там внутри должна быть интерпретация к PMAXUB/PMAXUW. А в зависимости от процессора там прирост может быть значительный.

И если вы будете его сравнивать с линейным решателем, то надо делить время на 10. Ведь решатель-то написан на каком-нибудь с++, а из питона вы лишь вызываете библиотеку

Ну это такой аргумент, numpy если я не ошибаюсь написан на том же си и так же использует SIMD векторизацию. Так что разница будет не столь значительна.

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

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

Хотелось бы сравнить алгоритм с вашей реализацией по скорости.

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

Мне не остаётся ничего кроме, как проиндексировать биты dp, тогда восстановление идет всегда правильно. Памяти требует, как в оригинальном решении, при длине входного массива до 256 элементов, и в двое больше в ином случае.

import numpy as np

def find_subset_sum(numbers: list, target: int):
    dp = np.zeros(target + 1, dtype='uint8' if len(numbers) < 256 else 'uint16')
    dp[0] = len(numbers)

    for i, val in enumerate(numbers):
        temp = np.roll(dp, val)
        temp[:val] = 0
        temp[temp > 0] = len(numbers) - i
        dp = np.maximum(dp, temp)
        if dp[target]:
            break        

    if not dp[target]:
        return []

    subset = []
    j = target   
    while j > 0:
        i = numbers[len(numbers) - dp[j]]
        subset.append(i)
        j -= i

    return subset[::-1]


source = [2, 3, 3, 4, 52, 52, 100, 1000, 1000]
target_sum = 1108
print('source:', source)
print('target_sum:', target_sum)
print()
result = find_subset_sum(source, target_sum)
print('result:', sum(result), result)

Ну вот новый контрпример: ([2, 3, 3, 4, 52, 52, 100, 1000, 1000], 108) ...

Ну вот я добавил пару 1000 в массив и ваша эвристика сломалась.

Где же сломалась? Работает, проверьте исходные данные.

Еще раз, проблема в том, что можно получить ситуацию, что вам надо набрать 8, а у вас остались числа [2, 3, 3, 4]

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

И да вы правы нужно доказать логически, а это не простая задача.

Благодарю Вас в вашем упорстве находить истину и желании помочь другим в этом же.

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

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

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

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

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

import bitstring as bit

def find_subset_sum(numbers: list, target: int):    
    numbers.sort()
     
    dp = bit.BitArray(target + 1)
    dp[0] = 1
    count = -1
    
    for i in numbers:
        temp = dp.copy()
        dp.ror(i)
        dp.set(0, range(i)) 
        dp |= temp
        print('i:', i, dp.bin)
        count += 1
        if dp[target] == 1:
            break

    if not dp[target]:
        return []
    print()

    subset = []
    j = target
    for i in numbers[count::-1]:   
        if j >= i and dp[j - i]:
            print('i:', i, 'j:', j, '- берём')
            subset.append(i)
            j -= i
        else:
            print('i:', i, 'j:', j, '- не берём')
    return subset[::-1]


source = [2, 3, 3, 4, 52, 52, 100]
target_sum = 108

print('source:', source)
print('target_sum:', target_sum)
print()

result = find_subset_sum(source, target_sum)
print('result:', sum(result), result)

Более сложный пример, когда надо половину набрать, как в исходной задаче из статьи: {3, 5, 6, 7, 11}.

Обратите внимание на условие:

for i in numbers:
  temp = dp.copy()
  dp.ror(i)
  dp.set(0, (range(i)))    
  dp |= temp
  if dp[target]:
      break

Оно заранее завершает процесс заполнения dp, и исключает во многом те ситуации, что вы описывали в предыдущем ответе.

Вам надо набрать 8, есть числа {2,3,3,4}. Первым шагом вы возьмете 4, ведь можно же набрать 4 всеми этими числами. И дальше все. Оставшиеся 4 вы никак из {2,3,3} не наберете.

Это не так! Проиллюстрирую:

Так понимаю достаточно сделать: numbers.sort() первым шагом функции.

Поскольку обход цикла идёт с конца списка, проблема полностью устраняется.

source: [2, 2, 2, 2, 4, 4, 6]
target_sum: 8

Subset with sum 8: [2, 6]

Могли бы вы подробнее пояснить, что за идея у вас тут оформлена. Очень хотелось бы разобраться, но боюсь мой скил в плюсах не настолько хорош.

Немного доработал представленный алгоритм динамического программирования для Python.

Теперь он требует в восемь рез меньше памяти и в два раза быстрее, чем через numpy. Но это ожидаемо, так как используется более плотное хранение бит благодаря модулю bitstring.

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

Возможно, кому то пригодится.

import bitstring as bit
import random as rnd
import time

def find_subset_sum(numbers: list, target: int):
    dp = bit.BitArray(target + 1)
    dp[0] = 1

    for i in numbers:
        temp = dp.copy()
        dp.ror(i)
        dp |= temp
        if dp[target] == 1:
            break

    if not dp[target]:
        return []

    subset = []
    j = target
    for i in numbers[::-1]:
        if j >= i and dp[j - i]:
            subset.append(i)
            j -= i

    return subset[::-1]


n = 100
# rnd.seed(0)
source = [rnd.randint(1000000, 9999999) for i in range(n)]
print('source:', source)
target_sum = sum(source) // 2
print('target_sum:', target_sum)

print()
start = time.time()
result = find_subset_sum(source, target_sum)
end = time.time()
elapsed_time = end - start
print(f"Execution time: {elapsed_time:.6f} seconds")
print(f"Subset with sum {target_sum}: {result}")

Не утверждаю, что решение с ЦЛП - серебряная пуля.

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

Алгоритм ДП, что я рассматривал, очень неэффективен по сравнению с тем, что предоставили Вы.

Беру свои слова назад, в такой реализации ДП на многих наборах обходит мою версию ЛП.

Любопытно, на некоторых наборах сильно лидирует ЛП, но если проигрывает, то часто значительно, тогда как ДП более стабилен. Как вариант интересно устраивать гонку алгоритмов, какой финиширует первым.

Однако меня огорчил тот факт, что на больших наборах входных данных, памяти ДП всё же не хватает, и он не выполняется с ошибкой:

MemoryError: Unable to allocate 14.2 GiB for an array with shape (15207959671,) and data type bool.

n = 200
t = 15207959670
source = [118034063, 176397250, 108470054, 134234785, 115826780, 166496171, 160329669, 163383683, 187455328, 150951092, 128179657, 112597620, 165479012, 103804733, 152319252, 158085012, 181528947, 100282669, 193393106, 159778857, 135746282, 196843463, 130703945, 179343270, 113720696, 142604684, 104105718, 102996023, 103415285, 187180606, 172667152, 101235465, 151164366, 192138303, 129071478, 156655527, 197422287, 103897788, 170817221, 129754951, 158772277, 166546792, 174203556, 131284065, 146399124, 130986382, 190845073, 129364293, 161686932, 138893829, 102884299, 155858725, 174686034, 186207290, 113421809, 124951916, 184470316, 197125183, 139780845, 116225575, 199743456, 144653591, 196835997, 195454543, 167216197, 156654242, 168144655, 189966890, 125481199, 140717432, 138139224, 178863733, 167023240, 167818046, 152795029, 179054544, 104633978, 164454973, 132580007, 199821838, 154262629, 155608283, 189220365, 123220660, 149274526, 173658522, 194360533, 190527955, 199081602, 150291788, 111605483, 158916432, 189088064, 168239848, 114486288, 121971213, 169919170, 152781805, 149730710, 165725551, 198350162, 103969484, 162991083, 105836765, 141410118, 194406345, 182518497, 179615772, 177601456, 152828055, 186859830, 122863882, 122628343, 167409318, 130459014, 101651090, 126778633, 172426227, 173596743, 131162152, 154285013, 168957265, 146147529, 177550306, 147415655, 161623617, 136142079, 188478314, 173550819, 181731190, 197898435, 100766266, 151497950, 199388685, 168786576, 117347564, 169615820, 175344177, 127579764, 157188922, 107532741, 164572392, 148954043, 176504015, 174410468, 126821992, 167742434, 155485614, 165085546, 147887538, 155623117, 146449792, 100212701, 172273400, 172492277, 183683337, 182201978, 144444516, 161491422, 180511200, 103754738, 130817065, 185278064, 123784892, 173921254, 178445010, 124264416, 112294532, 173957878, 134264986, 104356590, 190343768, 109456105, 111171496, 102240178, 160800468, 101954206, 137741579, 133495272, 136056483, 114695314, 183859516, 124777959, 146227654, 138961302, 109330196, 122477484, 121424575, 134254527, 170783798, 122568032, 188134944, 136629955, 187000307, 195507983, 139526151, 161029019, 194304805, 143218345, 166638250]

solution optimized = [176397250, 166496171, 160329669, 163383683, 187455328, 165479012, 181528947, 193393106, 159778857, 179343270, 187180606, 172667152, 192138303, 197422287, 170817221, 166546792, 174203556, 190845073, 161686932, 174686034, 186207290, 184470316, 197125183, 199743456, 196835997, 195454543, 167216197, 189966890, 178863733, 167023240, 179054544, 164454973, 199821838, 189220365, 173658522, 194360533, 190527955, 199081602, 158916432, 189088064, 169919170, 165725551, 198350162, 194406345, 182518497, 179615772, 177601456, 186859830, 167409318, 172426227, 173596743, 168957265, 177550306, 188478314, 173550819, 181731190, 197898435, 199388685, 168786576, 169615820, 175344177, 164572392, 176504015, 174410468, 155485614, 172273400, 172492277, 183683337, 182201978, 180511200, 185278064, 173921254, 178445010, 173957878, 190343768, 160800468, 183859516, 170783798, 188134944, 187000307, 195507983, 161029019, 194304805, 143218345, 166638250]

И ещё проблема, что решение должно быть точным, а любая микро-погрешность не найдёт решение Xi in {0,1}, посчитается какой-нибудь Xi=0.99999 и всё, вариант отсечётся. То есть, я думаю, тщательное тестирование на очень сложных случаях покажет, что этот метод не всегда даёт правильный результат.

Это зависит от качества решателя. Если под капотом используются float64, так и бывает у большинства решателей.

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

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

Возможно алгоритм что не хранит всё кэше и вывезет. Но если предположить, что по скорости алгоритм с меморизацией и без равны, то навряд ли сравняется с ЛП.

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

n = 100
val = 77019193
source = [1885440, 1403958, 1794772, 1933488, 1441001, 1042450, 1271493, 1536110, 1509532, 1424604, 1962838, 1821872, 1870163, 1318046, 1499748, 1375441, 1611720, 1934973, 1952225, 1229053, 1529202, 1146039, 1295528, 1146534, 1792518, 1099437, 1648406, 1838234, 1262674, 1953938, 1558433, 1739426, 1849574, 1631140, 1945989, 1154100, 1325213, 1103560, 1765284, 1077324, 1942500, 1891786, 1717209, 1346236, 1495077, 1587007, 1105592, 1370977, 1455262, 1331556, 1640561, 1671532, 1957361, 1214410, 1579363, 1500181, 1464197, 1907343, 1546678, 1273145, 1065304, 1844132, 1963080, 1575352, 1960489, 1014723, 1097802, 1754665, 1880899, 1418196, 1744754, 1864912, 1823182, 1700609, 1655638, 1001198, 1641620, 1517553, 1868287, 1909747, 1349317, 1255759, 1765752, 1341001, 1737822, 1912755, 1066043, 1200348, 1961564, 1595078, 1232473, 1250206, 1842368, 1149416, 1842194, 1569366, 1469730, 1095646, 1084353, 1335601]

solution optimized = [1794772, 1933488, 1536110, 1509532, 1962838, 1821872, 1870163, 1611720, 1934973, 1952225, 1792518, 1648406, 1838234, 1953938, 1558433, 1631140, 1945989, 1942500, 1891786, 1717209, 1640561, 1671532, 1957361, 1579363, 1907343, 1546678, 1844132, 1963080, 1960489, 1880899, 1864912, 1823182, 1700609, 1641620, 1517553, 1868287, 1909747, 1765752, 1912755, 1961564, 1842368, 1842194, 1569366]
time = 4.474821

Динамическое программирование требует очень много память на больших N и P. Линейное в этом смысле сильно выигрывает, но у него плохо со стабильностью результатов, уже больно оно зависит от набора входных данных. На некоторых наборах выигрыш феноменален, не некоторых экспонента всё портит. Но по моим наблюдениям в среднем разы рвёт ДП.

На случайных наборах результат очень хороший, особенно когда допустимых решений много.

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

То, что задача NP - полна коллега вроде и не оспаривал. Видимо он хотел указать, что сложность задачи зависит не только от числа элементов (N), но и от точности (P) и это не имеет отношение к NP-полноте. Я не верно понял?

Вы правы. Мне не хотелось излишне усложнять текст.

Так как для чисел порядка миллиарда, число двоичных разрядов практически не имеет значения, то я несколько упростил повествование.

1
23 ...

Information

Rating
871-st
Location
Краснодарский край, Россия
Registered
Activity