Λυσεις προβλημάτων τελικής φάσης Παγκύπριου Μαθητικού Διαγωνισμού Πληροφορικής 2007 Πρόβλημα 1 Το πρώτο πρόβλημα λύνεται με τη μέθοδο του Δυναμικού Προγραμματισμού. Για να το λύσουμε με Δυναμικό Προγραμματισμό πρέπει να βρούμε τα επόμενα δύο βήματα: 1. Βέλτιστη λύση για μικρότερα κομμάτια (μέρη) του προβλήματος (optimal substructure) 2. Αναδρομική φόρμουλα που να τα ενώνει και να παράγει την βέλτιστη λύση για το αρχικό πρόβλημα (recurrence formula). Ξεκινούμε με το πρώτο. Υπάρχει; Ναι. Έστω μια πυραμίδα που ξεκινά από το σημείο (2,1) [γραμμή 2, στήλη 1]. Μπορούμε να υπολογίσουμε το βέλτιστο άθροισμα γι' αυτή την πυραμίδα. Γενικεύοντας, για οποιοδήποτε κόμβο μπορούμε να σχηματίσουμε μια πυραμίδα από κάτω του και να υπολογίσουμε το μέγιστο άθροισμά της. Εξασφαλίσαμε τη βέλτιστη λύση για μικρότερα κομμάτια του προβλήματος. Μπορούμε να βρούμε μια αναδρομική συνάρτηση που να χρησιμοποεί τις λύσεις για μικρότερα κομμάτια του προβλήματος και να παράγει το τελικό αποτέλεσμα; Ναι. f i, j = { min f i 1, j, f i 1, j 1 A[i, j],if i N A[i, j],if i=n } Όπου f(i,j) το μέγιστο άθροισμα ξεκινώντας από τον κόμβο (i,j) [i αντιστοιχεί στη γραμμή και j αντιστοιχεί στη στήλη] μέχρι το τέλος της πυραμίδας και A[i,j] η τιμή του κόμβου στη θέση (i,j). Με απλά λόγια, η πιο πάνω φόρμουλα μας λέει ότι το μέγιστο άθροισμα ισούται με το μεγαλύτερο από τα δύο μέγιστα αθροίσματα των από κάτω κόμβων (αριστερά και δεξιά) + την τιμή στη θέση αυτή. Στην περίπτωση της τελευταίας γραμμής το μέγιστο άθροισμα ισούται με την τιμή του κόμβου. Πως υλοποιείται αυτό; Πολύ απλά, ξεκινόντας από το τελευταίο επίπεδο της πυραμίδας και πηγαίνοντας προς τα πάνω μπορούμε να υπολογίσουμε το f(i,j) για όλους τους κόμβους. Η τελική απάντηση θα είναι το f(1,1). 1/5
Πρόβλημα 2 Το πρόβλημα 2 χωρίζεται σε δύο μέρη. Στο πρώτο το ζητούμενο είναι να υπολογίσετε τον αριθμό των αντικειμένων στο διάγραμμα. Υπάρχουν πολλές λύσεις γι' αυτό το πρόβλημα. Θα δοθεί η λύση με γράφο (θεωρία γράφων) η οποία είναι πολύ γενική και μπορεί να χρησιμοποιηθεί σε πολλά προβλήματα. Έστω το παράδειγμα: Κατασκευάζουμε ένα μη κατευθυνόμενο, μη ζυγισμένο γράφο με κόμβους τα μαύρα κουτιά και ακμές μεταξύ δύο γειτονικών κουτιών, όπως παρακάτω: Για τη λύση του πρώτου μέρους, τρέχουμε τον αλγόριθμο προσπέλασης γράφου DFS (Depth First Search) ή τον BFS (Breath First Search) για όλους τους κόμβους που δεν έχουμε ήδη επισκεφτεί (από προηγούμενες προσπελάσεις). Κάθε νέα φορά που καλούμε την συνάρτηση του αλγορίθμου, έχουμε και ένα καινούριο αντικείμενο (object). Δεύτερο Μέρος του προβλήματος. Στο δεύτερο μέρος, δίνονται σκόπιμα μονάδες για μη βέλτιστες λύσεις, με αποτέλεσμα πολλοί αλγόριθμοι να μπορούν να πάρουν αρκετές μονάδες χωρίς να είναι και οι καλύτεροι. Εμείς όμως θα περιγράψουμε τον καλύτερο (γρηγορότερο) αλγόριθμο για τη λύση αυτού του προβλήματος. Ο αλγόριθμός αυτός είναι βασισμένος στον αλγόριθμο του Kruskal για εύρεση του Minimum Spanning Tree (δέντρου με το μικρότερο βάρος/κόστος, που ενώνει όλους τους κόμβους σε ένα γράφο). Ο αλγόριθμος του Kruskal είναι ο εξής: Kruskal-MST(G) sort όλες τις ακμές του γράφου με βάση το βάρος τους count = 0 while (count < V-1) do (v,w) = η επόμενη πιο μικρή ακμή if ( component(v) <> component(w) ) then (v,w) είναι ανοίκει στο MST που ψάχνουμε merge component(v) και component(w) count = count + 1 2/5
(όπου V είναι ο αριθμός των κόμβων - vertices) Το MST που ψάχνουμε αποτελείται από ακριβώς V-1 ακμές (αν πιάσουμε λιγότερες δεν θα ενώνει τον γράφο, αν πιάσουμε περισσότερες θα περιέχει κύκλους δεν θα είναι MST). Ο πιο πάνω αλγόριθμός τρέχει πιάνοντας τις V-1 πιο μικρές ακμές που ενώνουν ξεχωριστά components (σύνολο κόμβων που συνδέονται μαζί) και κάθε φορά που πιάνει μια ακμή, ενώνει τα components στα άκρα της ακμής. Ο αλγόριθμος αυτός δουλεύει. Έστω ότι τρέχαμε αυτό τον αλγόριθμο και δεν μας παρήγαγε το σωστό αποτέλεσμα. Αυτό σημαίνει ότι σε κάποια φάση ο αλγόριθμος διάλεξε μια λάθος ακμή (μη βέλτιστη) και δύο components δεν ενώθηκαν με τον πιο βέλτιστο τρόπο. Αυτό σημαίνει ότι θα πρέπει να υπάρχει μια ακμή η οποία να ενώνει τα δύο αυτά components με καλύτερο τρόπο (μικρότερη ακμή). Αν υπήρχε τέτοια ακμή όμως θα την επεξεργαζόταν πιο πριν ο αλγόριθος μας (επειδή είναι πιο μικρή) και θα την επέλεγε. Έτσι, δεν μπορεί να υπάρχει αυτή η πιο μικρή ακμή, με αποτέλεσμα ο αλγόριθμος να είναι πάντα σωστός. Στο πρόβλημά μας τώρα, θα χρησιμοποιήσουμε τον πιο πάνω αλγόριθμο (την ιδέα) για να υπολογίσουμε ποια αντικείμενα να ενώσουμε μαζί και σε ποια κουτιά ώστε να ενωθούν όλα τα αντικείμενα μετατρέποντας όσα λιγότερα άσπρα κουτιά γίνεται. Με βάση τον αλγόριθμο του Kruskal θα υπολογίζαμε τις αποστάσεις μεταξύ των αντικειμένων και θα τρέχαμε τον ίδιο αλγόριθμο. Δεν είναι όμως τόσο εύκολα τα πράγματα επειδή μπορεί περισσότερα από 2 αντικείμενα (ας πούμε 3) να ενώνονται σε ένα κοινό κουτί και να δίνουν καλύτερη λύση απ' ότι αν ενώναμε το ένα με το άλλο. Γι' αυτό, παρά να παίρνουμε τις αποστάσεις των αντικειμένων και να επιλέγουμε την μικρότερη (για χρήση στον αλγόριθμο του Kruskal) παίρνουμε τις αποστάσεις (το κόστος) αν ενώναμε 1 ή περισσότερα αντικείμενα σε κάποιο κουτί και επιλέγουμε το κουτί με το μικρότερο κόστος. Πως το υλοποιούμε εύκολα αυτό; Για κάθε αντικείμενο του διαγράμματος που ξεχωρίσαμε με τον αλγόριθμο του πρώτου μέρους παράγουμε τον πίνακα αποστάσεών του. Δηλαδή ένα πίνακα που να μας λέει πόση απόσταση απέχει το κάθε κουτί του διαγράμματος από το αντικείμενο. Για παράδειγμα, για το σκιαμμογραφημένο αντικείμενο δίνεται ο πίνακας αποστάσεών του. 2 1 1 2 3 4 1 1 2 3 1 1 2 3 4 2 1 2 3 4 5 3 2 3 4 5 6 4 3 4 5 6 7 Ο πίνακας αυτός μπορεί να ευρεθεί εύκολα και αποτελεσματικά (γρήγορα) με τον αλγόριθμο προσπέλασης γράφου BFS (Breath First Search), σημαδεύοντας στη αντίστοιχη θέση του πίνακα αποστάσεων το βάθος της αναζήτησης που βρίσκεσαι για κάθε κουτί. Έχοντας αυτό τον πίνακα για κάθε αντικείμενο του διαγράμματος μπορούμε να υπολογίσουμε εύκολα για κάθε κουτί πόσο θα είναι το κόστος αν θα ενώναμε όλα τα αντικείμενα στο συγγεκριμένο κουτι. Όλα καλά ως εδώ, αλλά για κάθε κουτί που ξέρουμε ότι μας συμφέρει να ενώσουμε όλα τα 3/5
αντικείμενα; Δεν θα πρέπει να ελέγξουμε για κάθε πιθανό συνδιασμό αντικειμένων σε κάθε κουτί; Ναι αυτό είναι σωστό. Υπολογίζοντας μόνο την περίπτωση όλων των αντικειμένων να συναντιούνται σε ένα κουτί δεν δίνει τη σωστή λύση. Παίρνοντας όλους τους πιθανούς συνδιασμούς, με κάποιες μαθηματικές πράξεις βρίσκουμε ότι ειναι πολύ μεγάλο ποσό συνδιασμών, άρα πολύ αργό πρόγραμμα. Γι' αυτό, συνεχίζουμε να βρούμε κάτι άλλο που θα μας βοηθήσει. Και αυτό το άλλο είναι μια παρατήρηση που προέρχεται από το ζητούμενό μας (το MST). Έστω ότι έχουμε τα εξής αντικείμενα: 1 2 3 x και ελέγχουμε αν στο κουτί (x) μπορούν να ενωθούν τα πιο πάνω 3 αντικείμενα και αν συμφέρει. Παρατηρούμε ότι δεν υπάρχει λόγος να ελέγξουμε το κουτί (x) για τα 3 αντικείμενα επειδή δεν γίνεται να είναι βέλτιστη λύση, γιατί το αντικείμενο 3 απέχει από το κοντινότερό του αντικείμενο 2 κουτιά ενώ από το (x) 3 κουτιά. Ενώνοντας το αντικείμενο 3 με το κοντινότερό του (αντικείμενο 1) έχουμε πιο μικρή λύση και πάλι τα 3 θα είναι ενωμένα. Με βάση αυτή την παρατήρηση μπορούμε να περιορίσουμε τον πίνακα αποστάσεων και να υπολογίζουμε αποστάσεις μέχρι μέγιστο βάθος όσο είναι η απόσταση του κοντινότερου αντικειμένου. Για παράδειγμα το πιο κάτω αντικείμενο απέχει 2 κουτιά από το κοντινότερό του. 2 1 1 2 1 1 2 1 1 2 2 1 2 2 Έτσι τώρα, οι συγκρίσεις είναι πολύ πιο λίγες και αν σε κάποιο κουτί συναντιώνται 2 ή περισσότερα αντικείμενα, σίγουρα θα είναι συμφέρουσα τιμή. Το μόνο που μένει τώρα είναι να υπολογίσουμε πόσα αντικείμενα συναντιώνται σε κάθε κουτί και πόσο είναι το κόστος τους. Ανάμεσα στα κουτιά που συναντιώνται τα περισσότερα αντικείμενα (πρώτο κριτήριο επιλογής) επιλέγουμε εκείνο με το μικρότερο κόστος (δεύτερο κριτήριο επιλογής). Τώρα που έχουμε έτοιμο τον αλγόριθμο για επιλογή του καλύτερου κουτιού μπορούμε να καθορίσουμε τον τελικό αλγόριθμο: 4/5
Task2Β-Solver(A) do k = αριθμός αντικειμένων if (k > 1) then βρες το κουτί με το χαμηλότερο κόστος ένωσε τα αντικείμενα που συμφέρει να ενωθούν στο κουτί αυτό while (k > 1) 5/5