Διάλεξη 7ης Μαΐου 2015 - Αλγόριθμοι. Το πρόβλημα του σακιδίου
Ένας διαρρήκτης έχει ένα σακίδιο το οποίο χωράει 20 κιλά. Σκοπός του είναι κάθε φορά που ληστεύει ένα σπίτι να παίρνει μαζί του τα πολυτιμότερα αντικείμενα χωρίς το συνολικό βάρος τους να υπερβαίνει την χωρητικότητα του σακιδίου. Ας υποθέσουμε, για παράδειγμα, ότι έχει να επιλέξει μεταξύ των παρακάτω αντικειμένων (δίπλα στο όνομα κάθε αντικειμένου αναγράφεται η αξία του, το βάρος του και η αξία του αντικειμένου ανά μονάδα βάρους):
- Clock, 175, 10, 17.5
- Painting, 90, 9, 10
- Radio, 20, 4, 5
- Vase, 50, 2, 25
- Book, 10, 1, 10
- Computer, 200, 20, 10
Η ερώτηση είναι, βεβαίως, ποιά από τα παραπάνω αντικείμενα πρέπει να επιλέξει έτσι ώστε να μεγιστοποιήσει την αξία των κλοπιμαίων και χωρίς να υπερβεί την χωρητικότηατα του σακιδίου του. Ο διαρρήκτης μπορεί να καταστρώσει την εξής στρατηγική για την επιλογή των κλοπιμαίων που θα βάλει στο σακίδιό του: να επιλέξει πρώτα το "καλύτερο" αντικείμενο, στη συνέχεια το δεύτερο "καλύτερο" αντικείμενο κ.ο.κ., μέχρι να εξαντλήσει τη χωρητικότητα του σακιδίου. Πρέπει, φυσικά, να επιλέξει την έννοια του όρου "καλύτερο": είναι το αντικείμενο με την μεγαλύτερη αξία, το αντικείμενο με το μικρότερο βάρος ή το αντικείμενο με τον μεγαλύτερο λόγο αξίας προς μονάδα βάρους; Μπορούμε να γράψουμε μερικές γραμμές κώδικα οι οποίες θα βοηθήσουν το διαρρήκτη να αποφασίσει:
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. Για να το κάνουμε αυτό
όμως θα πρέπει να εξετάσουμε όλους τους δυνατούς συνδιασμούς αυτών των αντικειμένων.
Η εύρεση συνδιασμού των αντικειμένων που επιφέρει το μεγαλύτερο κέρδος
απαιτεί τον έλεγχο
Στον κώδικα που ακολουθεί κατασκευάζουμε κάθε υποσύνολο του συνόλου των έξι αντικειμένων που
χρησιμοποιήσαμε ως παράδειγμα και ελέγχουμε τόσο αν το συνολικό τους βάρος είναι το πολύ 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 η οποία είναι μεγαλύτερη
από την αξία των υποσυνόλων των αντικειμένων που βρήκαμε με την προηγούμενη μέθοδο. Η λύση του προβλήματος
με την εξέταση όλων των δυνατών υποσυνόλων είναι βέβαια