Κοινή χρήση τεχνολογίας

Σημειώσεις Μελέτης Αλγορίθμου (8) - Βασικά στοιχεία Δυναμικού Προγραμματισμού

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Πίνακας περιεχομένων

Βασικό περιεχόμενο:

Δυναμικός προγραμματισμός:

Παρουσιάζονται προβλήματα στην κατανόηση του δυναμικού προγραμματισμού:

Ανάλυση: (βίαιη οπισθοδρόμηση)

Παράδειγμα κώδικα:

Βάναυση αναζήτηση:

Παράδειγμα κώδικα Dfs: (αναζήτηση)

Δέντρο αναδρομής που δημιουργείται από αναδρομή ωμής δύναμης:

Απομνημονευμένη αναζήτηση:

Παράδειγμα κώδικα:

Δυναμικός προγραμματισμός:

Παράδειγμα κώδικα: (δυναμικός προγραμματισμός, ξεκινώντας από το μικρότερο υποπρόβλημα)

Διαδικασία εκτέλεσης (δυναμικός προγραμματισμός):

Ανάλυση: (δυναμικός προγραμματισμός)

Βελτιστοποίηση χώρου:

Παράδειγμα κώδικα:

Ανάλυση:


Βασικό περιεχόμενο:

Τι είναι ο δυναμικός προγραμματισμός, ποια προβλήματα μπορεί να λύσει ο δυναμικός προγραμματισμός ως μέσο, ​​η ταξινόμηση του δυναμικού προγραμματισμού και η ταξινόμηση συγκεκριμένων προβλημάτων που μπορούν να λύσουν συγκεκριμένες ταξινομήσεις.

Δυναμικός προγραμματισμός:

Είναι ένα σημαντικό παράδειγμα αλγορίθμου που αναλύει ένα πρόβλημα σε μια σειρά από μικρότερα υποπροβλήματα και αποφεύγει τους επαναλαμβανόμενους υπολογισμούς αποθηκεύοντας λύσεις υποπροβλημάτων, βελτιώνοντας έτσι σημαντικά τη χρονική απόδοση.

Παρουσιάζονται προβλήματα στην κατανόηση του δυναμικού προγραμματισμού:

Αυτό το πρόβλημα εισάγεται μέσω της περίπτωσης αναρρίχησης σκαλοπατιών Δεδομένου ότι μια σκάλα με n συνολικά σκαλοπάτια, μπορείτε να ανεβείτε 1 ή 2 σκαλοπάτια σε κάθε σκαλί.

Ανάλυση: (βίαιη οπισθοδρόμηση)

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

παράδειγμα κώδικα

  1. # python代码示例
  2. def backrack(choices,state,n,res) :
  3. if state == n :
  4. res[0] += 1
  5. for choice in choices :
  6. if state + choice > n :
  7. continue
  8. backrack(choices,state+choice,n,res)
  9. def climbing_stairs_backrack(n) :
  10. choices = [1,2]
  11. state = 0
  12. res = [0]
  13. backrack(choices,state,n,res)
  14. return res[0]
  15. n = int(input())
  16. print(climbing_stairs_backrack(n))
  1. // c++代码示例
  2. void backrack(vector<int> &choices, int state, int n, vector<int> &res)
  3. {
  4. if (state == n )
  5. {
  6. res[0]++ ;
  7. }
  8. for (auto &choice : choices)
  9. {
  10. if (state + choice > n)
  11. {
  12. continue ;
  13. }
  14. backrack(choices, state + choice, n, res)
  15. }
  16. }
  17. int climbingStairsBackrack(int n)
  18. {
  19. vector<int> choices = {1 , 2 } ;
  20. int state = 0 ;
  21. vector<int> res = [0] ;
  22. backrack(choices, state, n, res) ;
  23. return res[0] ;
  24. }

Βάναυση αναζήτηση:

Οι αλγόριθμοι backtracking συνήθως δεν διαλύουν ρητά το πρόβλημα, αλλά αντιμετωπίζουν το πρόβλημα ως μια σειρά βημάτων λήψης αποφάσεων και αναζητούν όλες τις πιθανές λύσεις μέσω ευρετικών και περικοπών.

Μπορούμε να προσπαθήσουμε να αναλύσουμε αυτό το ερώτημα από την προοπτική της αποσύνθεσης του προβλήματος. Ας υποθέσουμε ότι υπάρχουν λύσεις dp[i] για να ανεβείτε στο i-ο επίπεδο, τότε το dp[i] είναι το αρχικό πρόβλημα και τα υποπροβλήματά του περιλαμβάνουν:

dp[i-1], dp[i-2], dp[1], dp[2]

Δεδομένου ότι μπορούμε να ανεβαίνουμε μόνο 1 ή 2 σκαλοπάτια σε κάθε γύρο, όταν στεκόμαστε στην i-η σκάλα, μπορούσαμε να σταθούμε μόνο στα σκαλιά i-1 ή i-2 στον προηγούμενο γύρο. Με άλλα λόγια, μπορούμε να μετακινηθούμε μόνο από το i-1ο ή i-2ο επίπεδο στο i-ο επίπεδο.

Από αυτό, μπορούμε να βγάλουμε ένα σημαντικό συμπέρασμα: ο αριθμός των σχεδίων που έχουν ανέβει στο επίπεδο i-1ο συν ο αριθμός των σχεδίων που έχουν ανέβει στο επίπεδο i-2ο είναι ίσος με τον αριθμό των σχεδίων που έχουν ανέβει στο i -ο επίπεδο. Ο τύπος έχει ως εξής:

dp[i] = dp[i-1] + dp[i-2]

Αυτό σημαίνει ότι υπάρχει μια αναδρομική σχέση στο πρόβλημα αναρρίχησης κτιρίου και το αρχικό πρόβλημα μπορεί να λυθεί με την κατασκευή των λύσεων των υποπροβλημάτων.

Παράδειγμα κώδικα Dfs: (αναζήτηση)

  1. # python 代码示例
  2. def dfs(i : int) -> int :
  3. if i == 1 or i == 2 :
  4. return i
  5. count = dfs(i - 1) + dfs(i - 2)
  6. return count
  7. def climbing_stairs_dfs(n : int) -> int :
  8. retunr dfs(n)
  1. // c++ 代码示例
  2. int dfs(int i)
  3. {
  4. if (i == 1 || i == 2)
  5. {
  6. return i ;
  7. }
  8. int count = dfs(i - 1) + dfs(i - 2);
  9. return count ;
  10. }
  11. int climbingStairsDFS(int n)
  12. {
  13. retunr dfs(n) ;
  14. }

Αναδρομικό δέντρο που δημιουργείται από βίαιη αναδρομή

Για την επίλυση του προαναφερθέντος προβλήματος διπλασιασμού στο αναδρομικό δέντρο, η μέθοδος αναζήτησης μνήμης μπορεί να χρησιμοποιηθεί για την αφαίρεση μεγάλου αριθμού πανομοιότυπων υποδέντρων που κατασκευάζονται επανειλημμένα, βελτιώνοντας έτσι την απόδοση υπολογισμού. (επικαλυπτόμενα υποπροβλήματα

Απομνημονευμένη αναζήτηση:

Για να υπολογίσετε όλα τα επικαλυπτόμενα υποπροβλήματα μόνο μία φορά, θα πρέπει να δηλώσετε ένα nem πίνακα για να καταγράψετε τη λύση κάθε υποπροβλήματος και να κλαδέψετε τα επικαλυπτόμενα υποπροβλήματα κατά τη διαδικασία αναζήτησης.

  1. Όταν το dp[i] υπολογίζεται για πρώτη φορά, καταγράφεται σε nem[i] για μεταγενέστερη χρήση.
  2. Όταν υπολογιστεί ξανά το dp[i], το αποτέλεσμα λαμβάνεται απευθείας σε nem[i] για να αποφευχθούν οι επαναλαμβανόμενοι υπολογισμοί των υποπροβλημάτων.

Παράδειγμα κώδικα:

  1. # python 代码示例
  2. def dfs(i : int, mem : list[int]) -> int :
  3. if i == 1 or i == 2 :
  4. return i
  5. if mem[i] != -1 :
  6. return mem[i]
  7. count = dfs(i - 1, mem) + dfs(i - 2, mem)
  8. # 记录dfs(i)
  9. mem[i] = count
  10. return count
  11. def climbing_stairs_dfs_mem(n : int) -> int :
  12. mem = [-1] * (n + 1)
  13. return dfs(n, mem)
  1. // c++ 代码示例
  2. int dfs(int i, vector<int> &mem)
  3. {
  4. if (i == 1 || i == 2)
  5. {
  6. return i ;
  7. }
  8. if (mem != -1)
  9. {
  10. return mem[i] ;
  11. }
  12. int count = dfs(i - 1, mem) + dfs(i - 2, mem) ;
  13. mem[i] = count ;
  14. return count ;
  15. }
  16. int climbingStairsDFSMem(int n)
  17. {
  18. vector<int> mem(n + 1, -1) ;
  19. return dfs(n, mem) ;
  20. }

Μετά την απομνημόνευση, όλα τα επικαλυπτόμενα υποπροβλήματα υπολογίζονται μόνο μία φορά και η χρονική πολυπλοκότητα βελτιστοποιείται σε O(n).

Δυναμικός προγραμματισμός:

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

Αντίθετα, ο δυναμικός προγραμματισμός είναι μια προσέγγιση «από κάτω προς τα πάνω»: ξεκινώντας με μια λύση στο μικρότερο υποπρόβλημα και επαναλαμβανόμενη δημιουργία λύσεων σε μεγαλύτερα υποπροβλήματα μέχρι να επιτευχθεί μια λύση στο αρχικό πρόβλημα.

Δεδομένου ότι ο δυναμικός προγραμματισμός δεν περιλαμβάνει μια διαδικασία backtracking, χρειάζεται μόνο να υλοποιηθεί χρησιμοποιώντας επανάληψη βρόχου χωρίς τη χρήση αναδρομής.

Παράδειγμα κώδικα: (δυναμικός προγραμματισμός, ξεκινώντας από το μικρότερο υποπρόβλημα)

  1. # python 代码示例
  2. def clibing_stairs_dp(n) :
  3. if n == 1 or n == 2 :
  4. return n
  5. dp = [0] * (n + 1)
  6. dp[1], dp[2] = 1, 2
  7. for i in range(3,n + 1) :
  8. dp[i] = dp[i-1] + dp[i- 2]
  9. return dp[n]
  1. // c++ 代码示例
  2. int climbingStairsDP(int n)
  3. {
  4. if (n == 1 || n == 2)
  5. {
  6. retunr n ;
  7. }
  8. vector<int> dp(n + 1, -1) ;
  9. dp[1] = 1 ;
  10. dp[2] = 2 ;
  11. for (int i = 3 ; i <= n ; i++)
  12. {
  13. dp[i] = dp[i - 1] + dp[i- 2] ;
  14. }
  15. return dp[n] ;
  16. }

Διαδικασία εκτέλεσης (δυναμικός προγραμματισμός):

Ανάλυση: (δυναμικός προγραμματισμός)

Παρόμοια με τον αλγόριθμο backtracking, ο δυναμικός προγραμματισμός χρησιμοποιεί επίσης την έννοια της "κατάστασης" για να αναπαραστήσει ένα συγκεκριμένο στάδιο επίλυσης προβλημάτων Κάθε κατάσταση αντιστοιχεί σε ένα υποπρόβλημα και στην αντίστοιχη τοπική βέλτιστη λύση. Παράδειγμα: Η κατάσταση του προβλήματος αναρρίχησης σκάλας ορίζεται ως η σειρά i της τρέχουσας σκάλας.

Με βάση τα παραπάνω, μπορούμε να συνοψίσουμε τους κοινούς όρους για δυναμικούς όρους:

  1. Ο πίνακας dp ονομάζεται {dp table}, το dp[i] αντιπροσωπεύει τη λύση στο υποπρόβλημα που αντιστοιχεί στην κατάσταση i
  2. Η κατάσταση που αντιστοιχεί στο ελάχιστο υποπρόβλημα (η πρώτη και η δεύτερη σκάλα) ονομάζεται αρχική κατάσταση
  3. Ο αναδρομικός τύπος dp[i] = dp[i-1] + dp[i-2] ονομάζεται εξίσωση κατάστασης

Βελτιστοποίηση χώρου:

Το dp[i] σχετίζεται μόνο με τα dp[i-1] και dp[i-2]

Αντί να χρησιμοποιείτε έναν πίνακα για την αποθήκευση των λύσεων σε όλα τα υποπροβλήματα, χρειάζονται μόνο δύο μεταβλητές για κύλιση προς τα εμπρός.

Παράδειγμα κώδικα:

  1. # python 代码示例
  2. def clibing_stairs_dp_comp(n) :
  3. if n == 1 or n == 2 :
  4. return n
  5. a, b = 1, 2
  6. for _ in range(3, n + 1) :
  7. a, b = b , a + b
  8. return b
  1. // c++ 代码示例
  2. int climbingStairsComp(int n)
  3. {
  4. if (n == 1 || n == 2)
  5. {
  6. return n ;
  7. }
  8. int a = 1 , b = 2 ;
  9. for (int i = 3 ; i <= n ; i++)
  10. {
  11. int temp = b ;
  12. b = a + b ;
  13. a = temp ;
  14. }
  15. return b ;
  16. }

Ανάλυση:

Ο χώρος που καταλαμβάνει ο πίνακας dp παραλείπεται και η πολυπλοκότητα του χώρου μειώνεται από O(n) σε O(1)

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