X36PAA, Úloha #3 - Problém batohu

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


Zadání

Zpráva by měla obsahovat:


Specifikace problému

Zadáno je

Cílem je

Najít takovou podmnožinu množiny věcí, která se ještě vejde do batohu, a zároveň je její cena nejvyšší možná. Řečeno formálněji: Najít množinu X = {x1, x2, ..., xn}, xi = {0, 1}, tak, aby platilo:

(tj. batoh nebyl přetížen) a aby výraz

nabýval maximální hodnoty pro všechny takové množiny.


Řešení metodou větví a hranic

Algoritmus rekurzivně prochází všechny zadané věci, v každém zanoření se rozhoduje, zda určitá věc bude ve výsledném množině, či ne. Ořezávání se provádí podle dvou jednoduchých pravidel. 1) Rekurze se v dané větvi ukončí, když přidání aktuální věci přetíží batoh. 2) Do batohu se fiktivně přidají všechny zbývající/neprověřené věci a pokud je celková cena tohoto fiktivního řešení menší než cena dosud nejlepšího nalezeného, tak se rekurze v dané větvi opět ukončí. (branch_bound.cpp, Makefile)

Knapsack(index, cost, weight, things)
{
	// End of recursion?
	if(index >= n)
		return;

	// Actual thing won't be there
	if(cost + remaining_costs > currently_the_best)
		SolveKnapsack(index + 1, cost, weight, things);

	// Actual thing will be there
	if(weight + actual_thing.weight <= M)
	{
		if(cost + remaining_costs > currently_the_best)
		{
			// Update the best solution
			if(cost + actual_thing.cost > currently_the_best)
			{
				currently_the_best = cost + actual_thing.cost;
				things[index] = true;
				resolution_things = things;
			}

			things[index] = true;

			SolveKnapsack(index + 1,
				cost + actual_thing.cost,
				weight + actual_thing.weight,
				things);
		}
	}
}

Knapsack(0, 0, 0, 0);

Řešení dynamickým programováním

Algoritmus si v pseudopolynomiálním čase vygeneruje dvourozměrnou tabulku s výsledky řešení jednotlivých podproblémů a poté pouze zobrazí výsledek na požadovaném indexu tabulky. (dynprog.cpp, Makefile)

void Knapsack(ID, n, M, data)
{
	ZeroFirstRow(table);
	ZeroFirstColumn(table);

	// Calculate table items
	for(i = 1; i <= M; i++)
	{
		for(j = 1; j <= n; j++)
		{
			if(cur.weight > i)
			{
				table(actual).cost = table(left).cost;
				table(actual).weight = table(left).weight;
				table(actual).things = table(left).things;
			}
			else
			{
				if(table(left).cost > table(down).cost + cur.cost)
				{
					table(actual).cost = table(left).cost;
					table(actual).weight = table(left).weight;
					table(actual).things = table(left).things;
				}
				else
				{
					table(actual).cost = table(down).cost + cur.cost;
					table(actual).weight = table(down).weight + cur.weight;
					table(actual).things = table(down).things;
					table(actual).things[j-1] = true;
				}
			}
		}
	}

	DisplaySolution(table[n, M]);
}

Řešení heuristikou podle poměru cena/váha s testem nejcennější věci

Výsledek původní heuristiky podle poměru cena/váha z první úlohy je srovnán s řešením, kdy obsahem batohu bude pouze nejcennější věc. Jako výsledek se vezme to řešení, které vychází lépe. (cost_weight_maxval.cpp, Makefile)

Knapsack(ID, n, M, data)
{
	T = {t1, t2, ..., tn};			// Seznam věcí
	sort(T);				// Řazení podle cena/hmotnost sestupně
	ret = 0;				// Nulování řešení
	for(i = 0; i < n; i++)			// Všechny věci
	{
		if(hm(cur) + hm(ret) <= M)	// Vejde se ještě?
			knapsack.add(cur);	// Vložit do batohu
	}

	thing = FindMostValuableThing();	// Nejhodnotnější věc

	if(thing.cost > knapsack.sumOfCosts())	// Je dražší než původní řešení?
	{
		knapsack.removeAllThings();	// Odstraní všechny věci
		knapsack.add(thing);		// Vloží pouze nejcennější věc
	}

	print(knapsack);			// Zobrazí výsledek
}

Měření

Následující tabulka obsahuje dobu trvání spuštění programu vždy pro padesát testovacích instancí problému batohu, které jsou uvedené na webu předmětu.

čas [s]
n Hrubá síla B&B Dyn. prog.
4 0.000292 0.005 0.003
10 0.03996 0.008 0.004
15 1.6481 0.013 0.012
20 66.412 0.073 0.021
22 286.350 0.259 0.022
25 2583.32 1.720 0.028
27 - 6.609 0.037
30 - 52.371 0.049
32 - 0.910 * 0.051
35 - 2.339 * 0.064
37 - 2.522 * 0.069
40 - 18.719 * 0.082

Ze souborů instancí označených hvězdičkou byla odstraněna konfigurace, která způsobovala, že se metoda B&B chovala téměř jako metoda hrubou silou, a tudíž nebylo možné získat výsledky během relativně krátkého času. V příkladu níže jsou hodnoty hmotností takové, že se batoh hned ze začátku nepřetíží, cena poslední věci způsobí, že prořezávání nezafunguje ani podle ceny. Z tohoto příkladu je vidět, že B&B může být výrazně závislá na složení vstupních dat problému a v nejhorším případě může dosahovat původní exponenciální složitosti.

9405 32 450 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14
14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14
14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 450 449
Graf pro dynamické programování
Graf pro metodu B&B
Společný graf pro dynamické programování a B&B

Porovnají-li se výstupy z programů a správné výsledky uváděné na webu předmětu v diffovacím nástroji (diff, Meld, Araxis Merge...), je vidět, že některé z uvedených instancí mají více optimálních řešení. Protože je výsledná cena všech věcí v batohu v obou případech stejná, nejedná se o chybu v programu.

Horní řádek: výsledky z webu předmětu
Dolní řádek: výsledky získané metodou dynamického programování

9588 40 4805  1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 0 0 1 1 1 1 0 1 1 1 0 0 1
9588 40 4805  1 0 1 1 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 0 0 1 1 1 1 0 0 1 1 1 0 1

9596 40 4244  1 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 1 0 1 1 1 1 1 0 1 1 1 0 1 1 1 1 0 1 1 1 0 1 0 1
9596 40 4244  1 1 1 1 1 1 1 1 0 1 1 1 1 1 0 0 1 1 1 1 0 1 1 0 1 0 1 0 1 1 1 1 0 1 1 1 0 1 1 1

Všechny programy byly napsány v jazyce C++ s použitím šablon ze standardní knihovny STL. Programy byly spouštěny na počítači s procesorem Intel Celeron 1,7 GHz pod operačním systémem Debian Sarge GNU/Linux. Doba jejich trvání byla pro jednotlivé datové soubory (tedy vždy 50 různých instancí problému) měřena pomocí standardního programu time (položka user).

Závěr

Srovná-li se heuristika z první úlohy s kombinovanou heuristiou, že vidět, že porovnání výsledků a zvolení lepší hodnoty, opravdu pomáhá. U malých instací je to více zřetelné než u velkých. Nevyžadují-li se naprosto přesné výsledky je implementovaná heuristika vhodným řešením. Odchylka od optimálního řešení je poté maximálně 50% (důkaz na stránkách předmětu).

Pokud je nutné přesné řešení, vychází nejlépe dynamické programování. To ale může být na druhou stranu limitováno velikostí dostupné paměti použité pro cachování mezivýsledků.

U většiny instancí problému se bude velice dobře chovat i metoda větví a hranic. Doba hledání řešení je však závislá i na vlastních hodnotách v zadání problému a za určitých okolností může dokonce narůst na původní exponenciální složitost metody hrubé síly.