Vypracoval Michal Turek, cvičení Po 12:45-14:15 (Petr Fišer)
Navrhněte a implementujte heuristiku řešící zobecněný problém dvou kýblů. Heuristiku otestujte na všech následujících příkladech a srovnejte s prohledáváním stavového prostoru do šířky (BFS). Volitelně srovnejte i s prohledáváním do hloubky (DFS). Zvolenou heuristiku popište ve zprávě.
Základní problém je definován takto: K dispozici jsou dva kýble (předem daných obecně rozdílných objemů), vodovodní kohoutek a kanál. Na počátku jsou oba kýble prázdné. Vaším úkolem je docílit toho, aby v jednom kýblu byla voda o předem stanoveném objemu, přičemž můžete pouze naplnit plný kýbl z kohoutku, vylít celý kýbl do kanálu a přelít jeden kýbl do druhého tak, aby druhý kýbl nepřetekl. Problém lze zobecnit tím, že připustíme užití většího počtu kýblů, aby na počátku řešení byla v kýblech nějaká voda, a že předepíšeme koncový objem vody v každém kýblu.
Algoritmus vloží na začátku do fronty počáteční stav a poté v cyklu postupně vyprazdňuje frontu. Je-li nalezeno řešení problému, je vypsáno a algoritmus se ukončí. Pokud řešení ještě nemáme, jednotlivé kýble jsou postupně naplňovány, vyprazdňovány a přelívány. (search.c, kyble.h, kyble.c, Makefile)
VložDoFronty(pocatecni_stav); // Vloží počáteční stav do fronty while(state = VyberZFronty()) // Postupně vyprazdňuje frontu { // Nalezeno řešení problému? if(Reseni(state)) { print(state); // Vypíše řešení problémů return; // Ukončí algoritmus } // Naplnění, vylití a přelití kýblů for(VšechnyKýble) if(NeníPlný(i)) VložDoFronty(NaplňKýbl(i)); for(VšechnyKýble) if(NeníPrázdný(i)) VložDoFronty(VyprázdniKýbl(i)); for(VšechnyKýble) for(VšechnyKýble) if(JeMožnéPřelít(i, j)) VložDoFronty(PřelijKýble(i, j)); }
Existují dva základní způsoby, jak navrhnout heuristickou funkci. U prvního z nich je nutné se ponořit do různých závislostí v problému a zjištěné poznatky zanést do heuristiky. Problémem je složitá analýza, netriviální odlaďování nejrůznějších parametrů a nejisté výsledky na odlišných datech, než se právě používají pro testy. Druhou možností je zavést jednoduchý pomocný parametr, který směruje výpočet ke správnému řešení. Výhodou je pak jednoduchost návrhu a obecnost pro libovolná data.
Řešení v této práci jde druhou uvedenou cestou. Implementovaná heuristika pracuje na principu vybírání uzlů z prioritní fronty, priorita je zde definována jako absolutní hodnota rozdílu sum objemů kýblů požadované konfigurace a aktuálně zpracovávané konfigurace. Čím nižší číslo dostaneme, tím je priorita dané konfigurace vyšší.
priorita = abs(sum(final_bucket[i]) - sum(actual_bucket[i]))
Příklad bude názornější. Máme dva kýble a chceme dosáhnout objemů 5 a 7 litrů. Při procházení jednotlivých konfigurací narazíme například na objemy 3 a 2 litrů a poté na objemy 6 a 9 litrů. Priorita těchto konfigurací bude následující.
priorita1 = abs((5 + 7) - (3 + 2)) = 7 priorita2 = abs((5 + 7) - (6 + 9)) = 3
Protože hledáme objem kýblů, který se co nejméně liší od požadovaného, budeme v dalším kroku zpracovávat druhou konfiguraci, která má prioritu rovnou třem.
Algoritmus řešení heuristikou je velice podobný algoritmu, který byl použit při prohledávání stavového prostoru do šířky. Liší se pouze počítáním priorit a vybíráním uzlů z prioritní namísto z obyčejné fronty. (search.c, kyble.h, kyble.c, Makefile)
VložDoFronty(pocatecni_stav); // Vloží počáteční stav do fronty // Postupně vyprazdňuje prioritní frontu while(state = VyberZFrontyKonfiguraciSNejvyššíPrioritou()) { // Nalezeno řešení problému? if(Reseni(state)) { print(state); // Vypíše řešení problémů return; // Ukončí algoritmus } // Naplnění, vylití a přelití kýblů for(VšechnyKýble) if(NeníPlný(i)) SpočtiPriorituAVložDoFronty(NaplňKýbl(i)); for(VšechnyKýble) if(NeníPrázdný(i)) SpočtiPriorituAVložDoFronty(VyprázdniKýbl(i)); for(VšechnyKýble) for(VšechnyKýble) if(JeMožnéPřelít(i, j)) SpočtiPriorituAVložDoFronty(PřelijKýble(i, j)); }
Následující tabulka obsahuje změřené hodnoty pro jednotlivé instance problému. Sloupec Operací obsahuje počty operací nad kýbli (naplnění, vyprázdnění apod.) nutných k dosažení řešení. Sloupec Otevřených uzlů zaznamenává celkový počet všech uzlů, které byly otevřeny během vyhledávání.
BFS | Heuristika | |||
---|---|---|---|---|
Instance | Operací | Otevřeno uzlů | Operací | Otevřeno uzlů |
1.1 | 10 | 8991 | 11 | 1897 |
1.2 | 8 | 8906 | 8 | 2815 |
1.3 | 8 | 8876 | 8 | 529 |
1.4 | 3 | 751 | 3 | 1109 |
2.1 | 16 | 49351 | 25 | 32062 |
2.2 | 12 | 46080 | 20 | 17542 |
2.3 | 11 | 42172 | 13 | 11143 |
2.4 | 5 | 2325 | 11 | 457 |
2.5 | 7 | 11913 | 7 | 3068 |
3.1 | 14 | 59203 | 35 | 9216 |
3.2 | 12 | 59194 | 16 | 18783 |
3.3 | 10 | 51691 | 13 | 10455 |
3.4 | 5 | 4403 | 6 | 1389 |
3.5 | 7 | 18364 | 7 | 3091 |
3.6 | 9 | 40457 | 12 | 10838 |
Je nutné poznamenat, že hodnoty v tabulce se mohou lišit při změně pořadí operací, která se s jednotlivými kýbli provádějí. V obou testovaných programech byla pořadí operací následující
V práci byly implementovány dva různé způsoby řešení zobecněného problému dvou kýblů. Prvním z nich bylo prohledávání stavového prostoru do šířky a druhým řešení heuristikou. Z tabulky v sekci Měření je jasně vidět, že ačkoli je heuristická funkce svým návrhem velice jednoduchá a výpočetně relativně nenáročná, poskytuje na většině testovacích dat velice dobré výsledky.