Γλώσσα Προγραμματισμού ΙΙ

Διάλεξη 7ης Μαΐου 2015 - Αλγόριθμοι. Το πρόβλημα του σακιδίου

Ένας διαρρήκτης έχει ένα σακίδιο το οποίο χωράει 20 κιλά. Σκοπός του είναι κάθε φορά που ληστεύει ένα σπίτι να παίρνει μαζί του τα πολυτιμότερα αντικείμενα χωρίς το συνολικό βάρος τους να υπερβαίνει την χωρητικότητα του σακιδίου. Ας υποθέσουμε, για παράδειγμα, ότι έχει να επιλέξει μεταξύ των παρακάτω αντικειμένων (δίπλα στο όνομα κάθε αντικειμένου αναγράφεται η αξία του, το βάρος του και η αξία του αντικειμένου ανά μονάδα βάρους):

Η ερώτηση είναι, βεβαίως, ποιά από τα παραπάνω αντικείμενα πρέπει να επιλέξει έτσι ώστε να μεγιστοποιήσει την αξία των κλοπιμαίων και χωρίς να υπερβεί την χωρητικότηατα του σακιδίου του. Ο διαρρήκτης μπορεί να καταστρώσει την εξής στρατηγική για την επιλογή των κλοπιμαίων που θα βάλει στο σακίδιό του: να επιλέξει πρώτα το "καλύτερο" αντικείμενο, στη συνέχεια το δεύτερο "καλύτερο" αντικείμενο κ.ο.κ., μέχρι να εξαντλήσει τη χωρητικότητα του σακιδίου. Πρέπει, φυσικά, να επιλέξει την έννοια του όρου "καλύτερο": είναι το αντικείμενο με την μεγαλύτερη αξία, το αντικείμενο με το μικρότερο βάρος ή το αντικείμενο με τον μεγαλύτερο λόγο αξίας προς μονάδα βάρους; Μπορούμε να γράψουμε μερικές γραμμές κώδικα οι οποίες θα βοηθήσουν το διαρρήκτη να αποφασίσει:


class Item:
	def __init__(self, name, value, weight):
		self.name = name
		self.value = float(value)
		self.weight = float(weight)

	def getName(self):
		return self.name

	def getValue(self):
		return self.value

	def getWeight(self):
		return self.weight

	def __str__(self):
		return self.name + ', ' + str(self.value) + ', ' + str(self.weight)

def value(item):
	retrun item.getValue()

def weightInverse(item):
	return 1.0 / item.getWeight()

def ratio(item):
	return item.getValue() / item.getWeight()

def buildItems():
	name = ['clock', 'painting', 'radio', 'vase', 'book', 'computer']
	values = [175, 90, 20, 50, 10, 200]
	weights = [10, 9, 4, 2, 1, 20]

	Items = []
	for i in range(len(values)):
		Items.append( Item(names[i], values[i], weights[i]) )

	return Items

Μπορούμε τώρα να εξετάσουμε ποιά από τις τρεις στρατηγικές που αναφέραμε παραπάνω θα είναι η περισσότερο επικερδής για τον διαρρήκτη:


def greedy(Items, maxWeight, keyFcn):
	ItemsCopy = sorted(Items, key=keyFcn, reverse=True)

	totalValue = 0.0
	totalWeight = 0.0
	result = []
	i = 0

	while totalWeight < maxWeight and i < len(Items):
		if totalWeight + ItemsCopy[i].getWeight() <= maxWeight:
			result.append(ItemsCopy[i])
			totalWeight += ItemsCopy[i].getWeight()
			totalValue += ItemsCopy[i].getValue()
		i += 1

	return (result, totalVal)

def testGreedy(Items, constraint, getKey):
	taken, val = greedy(Items, constraint, getKey)
	print 'Total value of items = ' + str(val)
	for item in taken:
		print item

def testAll(maxWeight = 20):
	Items = buildItems()

	print 'use value strategy'
	testGreedy(Items, maxWeight, value)

	print 'use inverse weight strategy'
	testGreedy(Items, maxWeight, weightInverse) 

	print 'use value per unit weight strategy'
	testGreedy(Items, maxWeight, ratio) 

Αν καλέσουμε την συνάρτηση testAll() τα αποτελέσματα θα είναι τα ακόλουθα:


use value strategy
Total value of items = 200.0
computer, 200.0, 20.0

use inverse weight strategy
Total value of items = 170.0
book, 10.0, 1.0
vase, 50.0, 2.0
radio, 20.0, 4.0
painting, 90.0, 9.0

use value per unit weight strategy
Total value of items = 255.0
vase, 50.0, 2.0
clock, 175.0, 10.0
book, 10.0, 1.0
radio, 20.0, 4.0

Είναι η τελευταία επιλογή των αντικειμένων η καλύτερη δυνατή; Μπορεί το κέρδος του διαρρήκτη να υπερβει το 255; Στο συγκεκριμένο παράδειγμα δεν είναι δύσκολο να διαπιστώσει κανείς ότι όντως υπάρχει μια επιλογή των αντικειμένων που επιφέρει κέρδος μεγαλύτερο του 255. Για να το κάνουμε αυτό όμως θα πρέπει να εξετάσουμε όλους τους δυνατούς συνδιασμούς αυτών των αντικειμένων. Η εύρεση συνδιασμού των αντικειμένων που επιφέρει το μεγαλύτερο κέρδος απαιτεί τον έλεγχο όλων των υποσυνόλων του συνόλου των αντικειμένων. Αν έχουμε ένα σύνολο με $n$ στοιχεία τότε είναι εύκολο να δεί κανείς ότι ο αριθμός όλων των δυνατών υποσυνόλων του είναι $2^n$. Πράγματι, μπορούμε να αντιστοιχίσουμε σε κάθε υποσύνολο ενός συνόλου $n$ αντικειμένων έναν δυαδικό αριθμό ο οποίος έχει το ψηφίο 1 στην $i$-οστή θέση αν επιλέγουμε το αντικείμενο με αριθμό $i$, μηδέν διαφορετικά.

Στον κώδικα που ακολουθεί κατασκευάζουμε κάθε υποσύνολο του συνόλου των έξι αντικειμένων που χρησιμοποιήσαμε ως παράδειγμα και ελέγχουμε τόσο αν το συνολικό τους βάρος είναι το πολύ 20 κιλά αλλά και το κέρδος που αποφέρουν. Η συνάρτηση genPowerSet() υπολογίζει και επιστρέφει το σύνολο όλων των υποσυνόλων ενός συνόλου που δίδεται ως όρισμα. Επιλέγουμε να αναπαραστήσουμε το σύνολο ως μια λίστα και το σύνολο των υποσυνόλων, το λεγόμενο δυναμοσύνολο ως μια λίστα από λίστες. Ο κώδικας της genPowerSet() δίδεται παρακάτω:


def getBinaryRep(n, numDigits)
        result = ''
        while n > 0:
                result = str(n%2) + result
                n = n / 2
        for i in range(numDigits - len(result)):
                result = '0' + result

        return result

def genPowerSet(L):
        powerSet = []
        for i in range(0, 2**len(L)):
                binStr = getBinaryRep(i, len(L))
                subset = []
                for j in range(len(binStr)):
                        subset.append(L[j])
                powerSet.append(subset)
        return powerSet

Δεδομένης της συνάρτησης genPowerSet() είναι εύκολο να βρούμε εκείνο το υποσύνολο του συνόλου των αντικειμένων που θα επιφέρει το μεγαλύτερο δυνατό κέρδος με βάση τον προφανή αλγόριθμο:


def chooseBest(pset, constraint, getVal, getWeight):
	bestVal = 0.0
	bestSet = None

	for Items in pset:
		ItemsVal = 0.0
		ItemsWeight = 0.0
		for item in Items:
			ItemsVal += getVal(item)
			ItemsWeight += getWeight(item)
		if ItemsWeight <= constraint and ItemsVal > bestVal:
			bestVal = ItemsVal
			bestSet = Items
	
	return (bestSet, bestVal)

def testBest(maxWeight = 20):
	Items = buildItems()
	pset = genPowerSet(Items(
	loot, val = chooseBest(pset, maxWeight, Item.getValue, Item.getWeight)
	print 'Total value of items = ' + str(val)
	for item in loot:
		print item

Αν εκτελέσουμε τον παραπάνω κώδικα θα πάρουμε το αποτέλεσμα


Total value of items = 275.0
clock, 175.0, 10.0
painting, 90.0, 9.0
book, 10.0, 1.0

Παρατηρήστε ότι το υποσύνολο clock, painting και book έχει συνολική αξία 275 η οποία είναι μεγαλύτερη από την αξία των υποσυνόλων των αντικειμένων που βρήκαμε με την προηγούμενη μέθοδο. Η λύση του προβλήματος με την εξέταση όλων των δυνατών υποσυνόλων είναι βέβαια βέλτιστη αλλά αξίζει να αναρωτηθούμε πόσο στοίχισε ο υπολογισμός της σε συνάρτησει πάντα με το μέγεθος του προβλήματος, συγκεκριμένα τον αριθμό των αντικειμένων από τα οποία πρέπει να διαλέξουμε. Αν λοιπόν έχουμε ένα σύνολο $n$ αντικειμένων, πρέπει πρώτα να φτιάξουμε το δυναμοσύνολό του, το οποίο έχει $2^n$ στοιχεία, στη συνέχεια να υπολογίσουμε το αξία καθενός και να βρούμε το υποσύνολο με την μεγαλύτερη αξία. Αυτό απαιτεί $n 2^n$ πράξεις περίπου επομένως το κόστος του αλγορίθμου είναι, όπως λέμε εκθετικό ως προς το μέγεθός του. Αντίθετα, οι στρατηγικές που περιγράψαμε νωρίτερα ναι μεν απέτυχαν να υπολογίσουν την καλύτερη δυνατή λύση αλλά έλυσαν το πρόβλημα προσεγγιστικά με πολύ χαμηλότερο υπολογιστικό κόστος, μόνο γραμμικό ως προς το μέγεθος του προβλήματος.