Pull to refresh

Comments 54

NP-полную задачу можно решить полным перебором, да. В чем суть статьи?

По крайней мере в еще одном и достаточно простом варианте реализации этого перебора, разве нет?

Тем более, что тут не прям ПОЛНЫЙ перебор как минимум за счет сортировки по весу и ее обыгрывания в дальнейшем.

Ну как "не полный"? 100 элементов, вес каждого 1, вместимость рюкзака 10, стоимость не принципиальна. Алгоритм так или иначе переберет все 100! / (10! * 90!) = 17310309456440 возможных комбинаций.

Мне кажется, что вы все же что-то неправильно понимаете...

Ради любопытства написал такой тест с 50 элементами, с сотней запариваться не стал. Цена идет по возрастанию от элемента к элементу. Основная логика перебора - цикл в методе addSelectionResultForIndex. В сумме в нем прошло через 1225 итераций, например. Вся работа по формированию возможных наполнений в нем и выполняется. Это, правда, без учета логики подбора новой ветки.

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

Подумаю, как поправить. За сценарий спасибо)

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

Ну, вместимость 100000, веса от 9901 до 10000, если так нужны разные.

В любом случае думаю, что все не настолько печально)

Сперва поправлю проблему с повторяющимися весами.

Если еще интересно: разные веса, сгенерировано вот таким образом

Set<Item> items = new HashSet<>();
for (int i = 1; i < 1000; i++) {
items.add(new Item(String.valueOf(i), i, i));
}

Вместимость рюкзака - 1999.

Решает за 200-300 мс. 498501 итераций основного цикла. Выдает цену 1999.

И всего веток-комбинаций сгенерировано 493055.

И пофиксить такое, возможно, тоже большого труда не составит. Уже крутится пара идей... Выявлять в отсортированном массиве такие группы повторяющихся весов, запоминать где-нибудь их начало и самую выгодную комбинацию внутри них (за счет сортировки по цене в рамках этой группы, когда вес равен 1 - это вообще ерунда, просто с последнего на вместимость рюкзака фетчим)... когда наткнулись на элемент такой группы - подтягиваем ветку оттуда... нужно будет попробовать. За сумбур извиняюсь)

Все, кажется, проблема решена.

Дополню статью.

Еще раз спасибо за кейс. Без вас, возможно, не обратил бы на это внимание.

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

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

Учту)

Рекурсии, кстати, здесь вроде и нет... Голые циклы)

Итак, первый подвох уже найден) Как чувствовал, что что-то может пойти не так, когда множество элементов с повторяющимися весами.

На 50 элементах весом в 1 и стоимостью от 1 и возрастающей от элемента к элементу - такой кейс дает неправильный результат. На вместимости в 10 должно быть 495. В ближайшее время займусь фиксом этой проблемы. Уже есть несколько идей.

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

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

Проблему с повторяющимися весами решил и, надеюсь, "креститься" не придется.

Дополнил статью и тест-кейсы. Минус один подвох, ждем выявления других)

Зачем использовать UUID для веток? Это не создаёт проблем с производительностью и памятью при больших наборах данных? Не проще ли работать с хэшами комбинаций?

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

Спасибо за идею, действительно, уберу.

И ведь точно)) Большой тест теперь работает в 2 раза быстрее, как убрал ууидки.

А я и не знал, что такая прожорливая вещь.

Еще раз спасибо))

А если решение лежит в области наименее длинных комбинаций? Ну скажем 7 предметов из 1000?

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

P.S. Пока Вы работает с малыми количествами и без лимита по времени, так и сравните ваш результат с полным перебором для 10000 случайных выборок.

Да, вы правы. Надо будет попробовать.

Пока самый большой кейс был таким.

Set<Item> items = new HashSet<>();
for (int i = 1; i < 1000; i++) {
items.add(new Item(String.valueOf(i), i, i));
}

С вместимостью 1999. Решало за 200-300 мс., 498501 итераций основного цикла, всего веток-комбинаций сгенерировано 493055. Показывало наборы с 1999 стоимостью. Конечно, стоит подсчитать сложность алгоритма, но пока лень, пусть остается на десерт.

Поменял вместимость на 10 - за 11 мс. подобрало решение с набором в стоимость 10. При такой генерации, судя по всему, наивысший профит будет равняться размерности. Если не ошибаюсь.

Конечно, алгоритм стоит гонять по разному. С придумыванием кейсов пока проблемы)

А, только сейчас до конца понял, в чем ваш вопрос...

"А если решение лежит в области наименее длинных комбинаций? Ну скажем 7 предметов из 1000?"

Да, по идее должно работать.

UFO landed and left these words here

Алгоритм "Монте-Карло" называется или "Метод Научного Тыка" :) Но действительно работает и широко применяется. А его модификация MCTSU вообще чудеса творит.

UFO landed and left these words here
UFO landed and left these words here

Ну, лично я такое не пробовал. Звучит интересно)

Продолжаю свой крестовый поход по NP-полным задачам

Есть хорошая книга про NP-полные задачи и экспоненциальные алгоритмы: Fedor V. Fomin and Dieter Kratsch, Exact Exponential Algorithms
там есть все, от классических решений и стандартных оптимизаций до последних достижений науки (на 2010 год)

Ссылка в вашем коменте на мой взгляд ценнее статьи. Правда без статьи не было бы и коммента...

Ну, разумеется полноценная книга, посвященная NP в целом будет ценнее неясных пока потуг по решению)

Алгоритм интересный, было бы интересно сравнить по скорости с динамическим программированием, которое работает за O(W*Nпредметов)
Например на задаче

for (int i = 0; i < 1000; i++) {
    items.add(new Item(String.valueOf(i), 9000+i, i%10+1));
}
W := 500000

дин программирование у меня тупит 12 секунд (общая сумма 538). А сколько у Вас проверяемых узлов получится?

Спасибо)
У меня был похожий тест-кейс.
Set<Item> items = new HashSet<>();for (int i = 1; i < 1000; i++) { items.add(new Item(String.valueOf(i), i, i));}

На вместимости 1999 решает за 200-300 мс. с 493055 созданных "веток". Показывало цену 1999. Нужно будет попробовать и на большей вместимости.

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

Будет чем позаниматься завтра, в общем)

Да, ваш тест-кейс тоже хорош. Он успешно сломал мою реализацию branch&bounds - там вместо <= стояло < и она скатывалась в полный перебор если все удельные стоимости одинаковы. Однако после исправления оно находит в нем сумму всего за 169 проверенных узлов.
Но мой тест-кейс ломает и branch&bounds (как я понимаю, стоимость элементов слишком маленькая чтоб эвристика эффективно работала) и только динамическому программированию пофиг на особенности задачи и оно успешно проходит его.

Что ж, надо будет его запомнить) Еще раз спасибо)

А у вас есть ваша реализация на динамическом программировании?

Я просто продолжаю работу над алгоритмом (то, что описано в статье - в целом ерунда и полу-решение, способное дать лишь приближенный к оптимальному результат). Новая версия разруливает все подобранные мной кейсы, кроме, видимо, вашего. На вашем кейсе отрабатывает за 40-50 мс., но результат правда показывает 498, а не 538. Было бы очень полезно и интересно глянуть, как отрабатывает ваш вариант и в чем заключается отличие - возможно, удастся пофиксить проблему.

Да, спасибо! В принципе, я уже нашел, тоже показывает 538.

Ну, пол-дела сделано, будем разбираться)

Ну что ж, полтора месяца работы - и я ПОЧТИ победил этот кейс))

int capacity = 500000;
int h = 1000;
List<Item> items = new ArrayList<>(); for (int i = 0; i < h; i++) { items.add(new Item(String.valueOf(i), 9000 + i, i % 10 + 1)); }

Итого: 1.389.910 итераций основных циклов и 130 мс. моего алгоритма против 500.502.001 (надеюсь, правильно считаю) итераций и почти двух секунд на динамическом. Заветное 538 - есть. А если h=520 (набор дает тот же результат) - то количество итераций вообще 154.820.

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

Все-таки клевый кейс вы дали, он надолго стал для меня "боссом-вертолетом". Спасибо еще раз!)

А вот на этом кейсе, например

Скрытый текст
 List<Item> items = List.of(
         new Item("1", 1, 10),
         new Item("2", 3, 15),
         new Item("3", 5, 21),
         new Item("4", 8, 40),
         new Item("5", 15, 55),
         new Item("6", 24, 61),
         new Item("7", 48, 35),
         new Item("8", 56, 38),
         new Item("9", 72, 72),
         new Item("10", 93, 90),
         new Item("11", 101, 101),
         new Item("12", 125, 425),
         new Item("13", 134, 25),
         new Item("14", 147, 100),
         new Item("15", 153, 240),
         new Item("16", 161, 15),
         new Item("17", 182, 150),
         new Item("18", 250, 181),
         new Item("19", 256, 203),
         new Item("20", 278, 258),
         new Item("21", 301, 111),
         new Item("22", 304, 104),
         new Item("23", 305, 1002),
         new Item("24", 307, 20),
         new Item("25", 310, 310),
         new Item("26", 315, 315),
         new Item("27", 328, 560),
         new Item("28", 334, 770),
         new Item("29", 561, 220),
         new Item("30", 585, 185),
         new Item("31", 604, 215),
         new Item("32", 631, 205),
         new Item("33", 657, 200),
         new Item("34", 688, 150),
         new Item("35", 691, 100),
         new Item("36", 728, 1100),
         new Item("37", 734, 160),
         new Item("38", 905, 305),
         new Item("39", 999, 310),
         new Item("40", 1010, 350),
         new Item("41", 1250, 450),
         new Item("42", 1348, 480)
 );
 int capacity = 2000;

показывает результат чуть хуже д.п.

91.573 основных итераций против 2.000 x 42 = 84.000, результат в обоих случаях 4217 (цена).

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

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

Блин, если бы не пара серьезных (но, возможно, таки решаемых) препятствий - возможно, это даже было бы чем-то прорывным. Но пока так)

Кстати, а вы не пробовали расширять этот кейс? 800.000 и 3000 элементов, например? Пытаюсь на д.п получить результат - но у меня по хипу отваливается) А алгоритм на удивление отрабатывает, емнип 848 показывает.

А чем плох такой вариант?


1) Сортируем по удельной цене по убыванию

2) Наполняем рюкзак. Когда встречаем предмет, который уже не "пролазит" по массе:

3) Берем из рюкзака минимальное n последних предметов так, чтобы:
вес этих предметов + оставшийся свободный вес >= вес текущего предмета

4) Сравниваем цены, чтобы решить, заменять ли их новым предметом

5) Считаем удельную цену замены с учетом оставшегося пустого места и кэшируем

6) Сравниваем удельную цену следующего не пролезающего предмета с кэшированным значением. Если она больше, повторяем с шага 3.

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

UFO landed and left these words here

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

Sign up to leave a comment.

Articles