X36PAA, Úloha #2 - Problém kýblů

Vypracoval Michal Turek, cvičení Po 12:45-14:15 (Petr Fišer)


Zadání

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ě.


Specifikace problému

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.


Řešení prohledáváním stavového prostoru do šířky

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));
}

Řešení heuristikou

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));
}

Měření

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
InstanceOperací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í

  1. Naplnění všech kýblů
  2. Vyprázdnění všech kýblů
  3. Přelití všech kýblů

Závěr

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.