 |
от vesso(30-08-2005)
рейтинг (23)
[ добре ]
[ зле ]
Вариант за отпечатване
Въведение в динамичната оптимизация
Задача: Имаме определен брой монети от различни
деноминации. Имаме и сума която трябва да платим. Да се
напише функция която да изчисли минималния брой монети
необходими за плащане на сумата. Ако не можем да формираме
сумата с наличните монети функцията трябва да върне число
по-голямо от броя на монетите като индикация за грешка.
Първи опит: Осланяме се на инстинктите си. Знаем че колкото
повече едри монети ползваме толкова по-малък брой монети ще
са необходими. Затова взимаме най-голямата монета (която все
пак е по-малка от сумата), намаляваме сумата със стойността
на монетата и извикваме същата функция с намалена сума но и
с една монета по-малко. Когато сумата стигне до нула връщаме
0 монети. Когато броя монети стигне до 0 връщаме 1 (грешка).
На питон функцията изглежда така:
def coins1(purse, amount):
if amount == 0:
return 0
if amount < 0 or len(purse) == 0:
return len(purse) + 1
purse.sort()
for index in range(len(purse) - 1, -1, -1):
if purse[index] <= amount:
new_purse = purse[:index] + purse[index+1:]
new_amount = amount - purse[index]
return 1 + coins1(new_purse, new_amount)
return len(purse) + 1
Тестваме набързо:
print coins1([50], 20), ' -> >1'
print coins1([50], 50), ' -> 1'
print coins1([10, 20, 50], 20), ' -> 1'
print coins1([5, 5, 10, 10, 10, 20, 50], 25), ' ->
2'
print coins1([5, 5, 10, 10, 10, 50], 25), ' -> 3'
print coins1([5, 5, 10, 20, 20, 20, 50, 50], 60), ' ->
2'
Нещата изглеждат добре ... докато не пробваме:
print coins1([2, 2, 2, 5], 6), ' -> 3'
Очакваме резултат 3 (3 монети по 2 ст) но функцията връща
5, т.е. не може да направи сумата от зададените монети. А не
може защото веднъж решила да ползва монетата от 5 ст няма
как да направи остатъка от 1 ст.
Този алгоритъм може да се усложни така че ако не може да
направи new_amount да опита с по-малка монета - тогава
винаги ще успява и извикването coins1([2, 2, 2, 5], 6) ще
връща коректна стойност. Обаче извикване coins1([1, 1, 1, 1,
1, 1, 5, 20, 20, 20, 50], 60) ще връща 7 (50 + 5 + 1 + 1 + 1
+ 1 + 1) вместо коректното 3 (20 + 20 + 20).
Описаният алгоритъм в опит 1 е алчен (greedy) защото винаги
започва от най-голямото кандидат-решение. Тези алгоритми са
лесни за писане и работят бързо но не винаги работят
коректно.
Втори опит: Явно настояването да ползваме най-голямата
монета не винаги е удачно. Затова за всяка монета ще опитаме
какво ще стане (1) ако монетата участва и (2) ако монетата
не участва в сумата. След това ще вземем по-доброто решение.
Отново се оформя рекурсивна функция:
def coins2(purse, amount):
if amount == 0:
return 0
if amount < 0 or len(purse) == 0:
return len(purse) + 1
without = coins2(purse[1:], amount)
if without >= len(purse):
without = len(purse) + 1
with = coins2(purse[1:], amount - purse[0])
if with >= len(purse):
with = len(purse) + 1
return min([without, with + 1])
Пускаме горните тестове и всичко върви добре, включително и
тези примери които затрудняваха първата функция ... докато
не опитаме:
print coins2([1, 1, 1, 1, 1, 1, 2, 2, 5, 5, 5, 10, 10, 10
,10, 20, 20, 20, 20, 50, 50, 50], 275), ' -> 12'
Функцията работи вярно но неприемливо дълго време.
Причината е че ако има 1 монета функцията че обработи 2
случая (2^1) - единия със а другия без монетата. Ако има 2
монети функцията ще обработи 4 случая (2^2) и съответно ако
има N монети функцията ще трябва да обработи 2^N случая. Или
записано в т.нар. О нотация сложността на функцията е
О(2^N). Такава функция, дори написана на асемблер, ще работи
години наред да сметне сума от стотина монети.
Трети опит: решение с динамична оптимизация.
Името на понятието "динамична оптимизация" идва
от поведението на програмите които динамично изграждат
таблици с междинни резултати. Ползвайки тези междинни
резултати лесно и бързо се стига до крайния резултат. Ето и
функцията:
def coins3(purse, amount):
big = len(purse) + 1
current_row = [0]
for amount_idx in range(amount):
current_row.append(big)
all_rows = [current_row]
for coin in purse:
prev_row = current_row
current_row = []
for amount_idx in range(amount + 1):
if amount_idx < coin:
current_row.append(prev_row[amount_idx])
else:
without = prev_row[amount_idx]
with = 1 + prev_row[amount_idx - coin]
current_row.append(min([with, without]))
all_rows.append(current_row)
return current_row[amount]
Обяснение:
Нека извикаме функцията с coins3([2, 2, 2, 2, 5, 5, 10],
11). Функцията си избира произволен код за "невъзможна
комбинация", по-голям от броя на монетите. След това си
създава един списък (едномерен масив) със следното
съдържание:
[0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8]
Списъкът има толкова елемента колкото е сумата която ни
трябва (11) плюс едно (за сума 0). Смисълът на така
полученият ред е че с никакви монети (защото още никакви
монети не са обработени) можем да постигнем сума 0 със 0
монети (първия елемент) и не можем да направим никакви други
суми (8 = код за невалидна сума). Индексът в този списък е
сумата която искаме да направим, а стойностите - броя монети
необходими за постигането на сумата.
След това за всяка монета си създаваме нов списък, в
които:
- Ако индексът (сумата) е по-малък от монетата просто
копираме стойностите от предишния ред.
- в противен случай сравняваме броя монети от същия индекс
на предишния ред (т.е. броя монети без текущата) с едно плюс
броя монети от предишния ред, необходим за постигане на сума
намалена със стойността на текущата монета. Добавяме
по-малката от тези стойности към текущия ред.
След обработване на първата монета (2 ст) списъкът изглежда
така:
[0, 8, 1, 8, 8, 8, 8, 8, 8, 8, 8, 8],
Смисълът на това е че с 0 монети можем да постигнем сума 0,
с 1 монета можем да постигнем сума 2 (индекс 2) а останалите
суми не могат да бъдат направени с 1 монета от 2 ст.
Когато програмата завърши списъкът all_rows съдържа
следното:
[
[0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8],
[0, 8, 1, 8, 8, 8, 8, 8, 8, 8, 8, 8],
[0, 8, 1, 8, 2, 8, 8, 8, 8, 8, 8, 8],
[0, 8, 1, 8, 2, 8, 3, 8, 8, 8, 8, 8],
[0, 8, 1, 8, 2, 8, 3, 8, 4, 8, 8, 8],
[0, 8, 1, 8, 2, 1, 3, 2, 4, 3, 8, 4],
[0, 8, 1, 8, 2, 1, 3, 2, 4, 3, 2, 4],
[0, 8, 1, 8, 2, 1, 3, 2, 4, 3, 1, 4]]
Последният ред съдържа броя монети необходими за сума равна
на индекса в реда. Можем да постигнем:
- с 0 монети: сума 0
- с 1 монета: сума 2, 5 и 10 ст (с такива монети
разполагаме)
- с 2 монети: суми 4 и 7 ст.
- с 3 монети: суми 6 и 9 ст.
- с 4 монети: суми 8 и 11 ст.
- не можем да направим суми 1 и 3 ст.
Отговорът на нашата задача е в последния ред, последната
колона.
Парадокс е че намерихме решенията на всички суми по-малки
от търсената, при това - поне в някои случаи - по-ефективно
отколкото намирането само на едно решение от втория ни
опит.
По-внимателните може би са забелязали че към списъкът
all_rows само се добавя но от него не се чете. Програмата
може да работи чудесно и без него. Такъв списък е необходим
когато искаме да видим кои монети участват в решението.
Правилото е:
1. Тръгваме от долния десен ъгъл и от последната монета
2. Сравняваме числото над текущото:
2.1. Ако е същото значи текущата монета не участва в
резултата. Запазваме същия индекс.
2.2. Ако е различно значи текущата монета участва в
резултата. Намаляваме индекса със стойността на текущата
монета.
3. Минаваме на по-горния ред и на предишната монета.
4. Ако сме стигнали до 0 сме готови, в противен случай
отиваме на т. 2.
Задачи за упражнение:
1. Преправете функцията coins3 така че да използва само
един списък (едномерен масив)
2. Пешо има да дава на Гошо дадена сума. Знаем какви монети
имат и Пешо и Гошо. Да се напише програма която да изчисли
минималния брой монети сменяващи собственика си така че Пешо
да се издължи. Знаем че е допустимо Гошо да връща ресто.
Функция:
def exer2(pesho_coins, gosho_coins, amount):
Извикване:
print exer2([10, 10, 20, 50], [5, 10, 20], 40)
резултат: 2 - Пешо дава една монета от 50 и получава ресто
една монета от 10
3. Можем да носим максимум X кг. Има N предмета с тегло Gi
kg и стойност Vi. Каква е максималната стойност на
предметите които можем да вземем. Функция:
def exer3(MaxWeight, Weights, Values):
Извикване:
print exer3(50, [24, 25, 45], [5, 6, 10])
резултат: 11 - първия и втория предмет
(Подсказка: Google "knapsack problem")
24 Август 2005
Веселин Костадинов
Редакция от 31 Август 2005: Вместо термина "динамична
оптимизация" статията ползваше "динамично
програмиране" (буквален превод от английския термин
"dynamic programming"). Благодаря на
d.vasilev.
<< Крайна цел: FreeBSD | Програмиране под Линукс. Въведение за начинаещи. >>
|
 |