Berbagi teknologi

Catatan Studi Algoritma (8) - Dasar-dasar Pemrograman Dinamis

2024-07-12

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

Daftar isi

Konten dasar:

Pemrograman dinamis:

Masalah dalam memahami pemrograman dinamis diperkenalkan:

Analisis: (mundur dengan kekerasan)

Contoh kode:

Pencarian brutal:

Contoh kode Dfs: (pencarian)

Pohon rekursi yang dihasilkan oleh rekursi brute force:

Pencarian yang dihafal:

Contoh kode:

Pemrograman dinamis:

Contoh kode: (pemrograman dinamis, dimulai dari submasalah terkecil)

Proses eksekusi (pemrograman dinamis):

Analisis: (pemrograman dinamis)

Pengoptimalan ruang:

Contoh kode:

Analisis:


Konten dasar:

Apa itu pemrograman dinamis, masalah apa yang dapat dipecahkan oleh pemrograman dinamis, klasifikasi pemrograman dinamis, dan klasifikasi masalah spesifik yang dapat diselesaikan oleh klasifikasi spesifik.

Pemrograman dinamis:

Ini adalah paradigma algoritma penting yang menguraikan masalah menjadi serangkaian sub-masalah yang lebih kecil dan menghindari penghitungan berulang dengan menyimpan solusi sub-masalah, sehingga sangat meningkatkan efisiensi waktu.

Masalah dalam memahami pemrograman dinamis diperkenalkan:

Soal ini muncul melalui kasus menaiki tangga. Diberikan sebuah tangga yang jumlah anak tangganya sebanyak n, maka anda dapat menaiki 1 atau 2 anak tangga pada setiap anak tangga. Berapa banyak pilihan yang ada untuk naik ke puncak gedung?

Analisis: (mundur dengan kekerasan)

Tujuan dari pertanyaan ini adalah untuk menemukan jumlah solusi,Kita dapat mempertimbangkan untuk menghabiskan semua kemungkinan secara mendalam dengan melakukan kemunduran . Secara khusus, bayangkan menaiki tangga sebagai proses seleksi multi-putaran: mulai dari tanah, memilih satu atau dua langkah setiap putaran, menambahkan 1 ke jumlah pilihan setiap kali Anda mencapai puncak tangga, dan menambah jumlah pilihan saat Anda mencapai puncak tangga.

contoh kode

  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. }

Pencarian brutal:

Algoritma backtracking biasanya tidak membongkar masalah secara eksplisit, namun memperlakukan masalah sebagai serangkaian langkah pengambilan keputusan, dan mencari semua solusi yang mungkin melalui heuristik dan pemangkasan.

Kita dapat mencoba menganalisis pertanyaan ini dari perspektif penguraian masalah. Asumsikan bahwa ada solusi dp[i] untuk naik ke level ke-i, maka dp[i] adalah permasalahan awal, dan sub-masalahnya meliputi:

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

Karena kita hanya bisa menaiki 1 atau 2 anak tangga dalam setiap putaran, maka ketika kita berdiri di anak tangga ke-i, kita hanya bisa berdiri di anak tangga ke-i-1 atau i-2 pada putaran sebelumnya. Dengan kata lain, kita hanya dapat berpindah dari level ke-i-1 atau ke-i-2 ke level ke-i.

Dari sini kita dapat menarik kesimpulan penting: banyaknya plan yang naik ke level i-1 ditambah banyaknya plan yang naik ke level i-2 sama dengan banyaknya plan yang naik ke level i. tingkat -th. Rumusnya adalah sebagai berikut:

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

Artinya terdapat hubungan rekursif dalam masalah pendakian gedung, dan masalah awal dapat diselesaikan dengan mengkonstruksi solusi dari sub-masalah tersebut.

Contoh kode Dfs: (pencarian)

  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. }

Pohon rekursif dihasilkan oleh rekursi kekerasan

Untuk mengatasi masalah duplikasi pohon rekursif yang disebutkan di atas, metode pencarian memori dapat digunakan untuk menghapus sejumlah besar subpohon identik yang dibangun berulang kali, sehingga meningkatkan efisiensi perhitungan. (submasalah yang tumpang tindih

Pencarian yang dihafal:

Untuk menghitung semua submasalah yang tumpang tindih hanya satu kali, Anda perlu mendeklarasikan array nem untuk mencatat solusi setiap submasalah, dan memangkas submasalah yang tumpang tindih selama proses pencarian.

  1. Ketika dp[i] dihitung untuk pertama kalinya, ia dicatat dalam nem[i] untuk penggunaan selanjutnya.
  2. Ketika dp[i] dihitung lagi, hasilnya diperoleh langsung di nem[i] untuk menghindari penghitungan sub-masalah yang berulang.

Contoh kode:

  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. }

Setelah memoisasi, semua submasalah yang tumpang tindih dihitung hanya satu kali, dan kompleksitas waktu dioptimalkan menjadi O(n).

Pemrograman dinamis:

Pencarian memo adalah metode "atas-ke-bawah". Kita mulai dari masalah awal (simpul akar) dan secara rekursif menguraikan sub-masalah yang lebih besar menjadi sub-masalah yang lebih kecil hingga kita menyelesaikan sub-masalah terkecil yang diketahui (simpul daun). . Setelah itu, solusi terhadap sub-masalah dikumpulkan lapis demi lapis melalui penelusuran mundur untuk membangun solusi terhadap masalah awal.

Sebaliknya, pemrograman dinamis adalah pendekatan "dari bawah ke atas": dimulai dengan solusi terhadap submasalah terkecil dan secara iteratif membangun solusi untuk submasalah yang lebih besar hingga solusi terhadap masalah awal diperoleh.

Karena pemrograman dinamis tidak menyertakan proses backtracking, maka hanya perlu diimplementasikan menggunakan iterasi loop tanpa menggunakan rekursi.

Contoh kode: (pemrograman dinamis, dimulai dari submasalah terkecil)

  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. }

Proses eksekusi (pemrograman dinamis):

Analisis: (pemrograman dinamis)

Mirip dengan algoritma backtracking, pemrograman dinamis juga menggunakan konsep "keadaan" untuk mewakili tahap tertentu dari pemecahan masalah. Setiap keadaan berhubungan dengan sub-masalah dan solusi optimal lokal yang sesuai. Contoh: Status soal menaiki tangga didefinisikan sebagai urutan i dari tangga saat ini.

Berdasarkan penjelasan di atas, kita dapat meringkas istilah umum untuk istilah dinamis:

  1. Array dp disebut {dp table}, dp[i] mewakili solusi untuk sub-masalah yang sesuai dengan keadaan i
  2. Keadaan yang sesuai dengan submasalah minimum (tangga pertama dan kedua) disebut keadaan awal
  3. Rumus rekursif dp[i] = dp[i-1] + dp[i-2] disebut persamaan keadaan

Pengoptimalan ruang:

dp[i] hanya terkait dengan dp[i-1] dan dp[i-2]

Daripada menggunakan array untuk menyimpan solusi untuk semua submasalah, hanya diperlukan dua variabel untuk menggulir ke depan.

Contoh kode:

  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. }

Analisis:

Ruang yang ditempati oleh array dp dihilangkan, dan kompleksitas ruang dikurangi dari O(n) menjadi O(1)

Dalam masalah pemrograman dinamis, keadaan saat ini hanya terkait dengan sejumlah keadaan sebelumnya. Saat ini, kita hanya dapat mempertahankan keadaan yang diperlukan dan menghemat ruang memori melalui "pengurangan dimensi". . Teknik pengoptimalan ruang ini disebut "variabel bergulir" atau "array bergulir".