დინამიკური პროგრამირება ფიბონაჩის
რიცხვების პოვნა უმოკლესი გზების
პოვნა
მარიამ გობრონიძე

დინამიკური პროგრამირება
დინამიკური პროგრამირება წარმოადგენს ალგორითმულ ტექნიკას,
რომელსაც აქვს შემდეგი მახასიათებლები:

იგი ძირითადად წარმოადგენს ოპტიმიზაციას ჩვეულებრივი
რეკურსიის მიმართ. როცა გვხვდება რეკურსიული ამოხსნა,
რომელიც ერთი და იმავე ამოცანის ამოსახსნელად განმეორებით
გამოძახებებს იყენებს, შეგვიძლია მისი ოპტიმიზაცია სწორედ
დინამიკური პროგრამირების გამოყენებით.

ძირითადი იდეა მდგომარეობს იმაში, რომ შევინახოთ ქვეამოცანების
შედეგები, რათა მოგვიანებით აღარ მოგვიწიოს მათი ხელახლა
გამოთვლა. ეს მარტივი ოპტიმიზაცია, როგორც წესი, დროით
სირთულეს ექსპონენციალურიდან პოლინომურამდე ამცირებს.

დინამიკური პროგრამირებით ამოხსნადი პოპულარული ამოცანებია:
ფიბონაჩის რიცხვები, ბელმან–ფორდის Shortest Path ალგორითმი,
ფლოიდ–ვარშალის ალგორითმი, მატრიცების ჯაჭვური გამრავლება
(Matrix Chain Multiplication) და ა.შ.

ფიბონაჩის რიცხვები
მოცემულია დადებითი მთელი რიცხვი n. ამოცანაა ვიპოვოთ
ფიბონაჩის მიმდევრობის მე-n წევრი.
ფიბონაჩის მიმდევრობა არის რიცხვების მიმდევრობა, სადაც ყოველი
მომდევნო წევრი წინა ორი წევრის ჯამია.
ფიბონაჩის მიმდევრობა გამოიყურება ასე:
0, 1, 1, 2, 3, 5, 8, 13, 21, ..

ამ ამოცანაში მარტივად ჩანს რეკურსიული ხასიათი. მე-n რიცხვის
დასათვლელად გვჭირდება (n-1)-ე და (n-2)-ე რიცხვები.

სირთულე
დროითი სირთულე : O(2^n)
დამხმარე მეხსიერება : O(n), რეკურსიული გამოძახებებისთვის

წინა მიდგომაში გვხვდება განმეორებითი გამოთვლები — იგივე მნიშვნელობები თავიდან და
თავიდან ითვლება. ამ პრობლემის თავიდან ასაცილებლად, შეგვიძლია შევინახოთ უკვე
გამოთვლილი ფიბონაჩის რიცხვები სპეციალურ მეხსიერების ცხრილში (memo table).
ამ გზით თითოეული ფიბონაჩის რიცხვი მხოლოდ ერთხელ გამოითვლება, რაც თავიდან
აიცილებს ზედმეტ გამოთვლებს და მნიშვნელოვნად გააუმჯობესებს მუშაობის დროით
სირთულეს — O(2^n) დროის სირთულე შემცირდება ბევრად ეფექტურ O(n)-მდე.

სირთულის შეფასება
დროითი სირთულე : O(n), ფიბონაჩის ყოველი რიცხვი გამოითვცლება მხოლოდ ერთხელ;
დამხმარე მეხსიერება : O(n).

მოსალოდნელი მიდგომა 2
ამ მიდგომაში ფიბონაჩის ამოცანა დინამიკური პროგრამირების
გამოყენებით გვარდება — უკვე გამოთვლილი ფიბონაჩის რიცხვები
ინახება, რაც თავიდან გვარიდებს რეკურსიულ მიდგომაში არსებულ
გამეორებულ გამოთვლებს.
რეკურსიულად პრობლემის დაშლის ნაცვლად, ამ მიდგომაში
ამოხსნა ქვევიდან ზევით ნელ-ნელა ყალიბდება — ფიბონაჩის
რიცხვები თანმიმდევრულად ითვლება მცირე მნიშვნელობებიდან
დაწყებით.

სირთულის შეფასება
დროითი სირთულე : O(n)
დამხმარე მეხსიერება : O(n).

სივრცითი ოპტიმიზაცია
ეს მიდგომა წარმოადგენს ზემოთ აღწერილი იტერაციული მიდგომის
ოპტიმიზაციას. იმის ნაცვლად, რომ ფიბონაჩის რიცხვების შესანახად
დამატებითი მასივი გამოვიყენოთ, შეგვიძლია მათი მნიშვნელობები
უბრალოდ ცვლადებში შევინახოთ. ვინახავთ მხოლოდ წინა ორ
რიცხვს, რადგან შემდეგი ფიბონაჩის რიცხვის გამოსათვლელად
მხოლოდ ეს ორი მნიშვნელობაა საჭირო.

სირთულის შეფასება
დროითი სირთულე : O(n)
დამხმარე მეხსიერება : O(1).

Climbing stairs to reach the top
მოცემულია n საფეხური, და ადამიანი დგას კიბის ძირში. მას სურს ავიდეს კიბეზე და მიაღწიოს
ზედა საფეხურს. თითოეულ ნაბიჯზე შეუძლია ავიდეს ან 1 საფეხურით, ან 2 საფეხურით.
ამოცანაა დათვალოს, რამდენი განსხვავებული გზა არსებობს, რომ ადამიანი კიბის ზედა
საფეხურამდე მივიდეს.
შენიშვნა : ეს ამოცანა ჰგავს „Nth საფეხურზე ასვლის გზების დათვლას (როდესაც რიგითობა
არ არის მნიშვნელოვანო)“, თუმცა განსხვავება იმაშია, რომ ამ შემთხვევაში ნაბიჯების
სხვადასხვა მიმდევრობა უნიკალურ გზებად ითვლება , ანუ განსხვავებული თანმიმდევრობით
ასვლები ერთმანეთისგან განსხვავდება.

ამ ამოცანის რეკურსიული ხასიათის გააზრება მარტივია. ადამიანს შეუძლია მიაღწიოს
მე-n-ე საფეხურს ან (n-1)-ე საფეხურიდან, ან (n-2)-ე საფეხურიდან. შესაბამისად, ყოველი
მე-n-ე საფეხურისთვის ვთვლით იმ გზების რაოდენობას, რომლითაც შეიძლება მივიდეთ
(n-1)-ე და (n-2)-ე საფეხურებზე, და ვაჯამებთ მათ, რათა მივიღოთ საერთო რაოდენობა
n-ე საფეხურამდე მისვლის გზებისა.

ფორმულა – ways(n) = ways(n - 1) + ways(n - 2)

სირთულე
დროითი სირთულე : O(2^n)
დამხმარე მეხსიერება : O(n), რეკურსიული გამოძახებებისთვის

Top-Down DP (Memoization) – O(n) Time
and O(n) Space

1. ოპტიმალური ქვესტრუქტურა (Optimal Substructure):
ნაბიჯების რაოდენობა, რომლითაც შესაძლებელია მე-n-ე საფეხურამდე მისვლა (ანუ
countWays(n)), დამოკიდებულია ქვეამოცანების ოპტიმალურ გადაწყვეტებზე:
countWays(n-1) და countWays(n-2). ამ ქვეამოცანების გაერთიანებით შეგვიძლია
ეფექტურად მივიღოთ მე-n-ე საფეხურამდე ასვლის საერთო რაოდენობა.

2. გადაფარვადი ქვეამოცანები (Overlapping Subproblems):
როცა რეკურსიულ მიდგომას ვიყენებთ, ვამჩნევთ, რომ ზოგი ქვეამოცანა არაერთხელ
ითვლება. მაგალითად, countWays(4) ითვლის countWays(3) და countWays(2),
მაგრამ countWays(3) თავის მხრივ ისევ ითვლის countWays(2) – ანუ countWays(2)
ზედმეტად მეორდება. ეს იწვევს ქვეამოცანების განმეორებას.

გადაფარვადი ქვეამოცანების მაგალითი
რეკურსიული გადაწყვეტისას მხოლოდ ერთი პარამეტრი იცვლება (n), რომელიც 0-დან n-მდე
მერყეობს. ამიტომ ვქმნით ერთგანზომილებიან მასივს ზომით n+1 მემოიზაციისთვის.
●

მასივის ყველა ელემენტი თავდაპირველად ინიციალიზებულია მნიშვნელობით -1, რაც
მიუთითებს, რომ მნიშვნელობა ჯერ არ არის გამოთვლილი.

●

რეკურსიულ ფუნქციას ისე ვცვლით, რომ სანამ რეკურსიულად გამოიძახებს ქვეამოცანას,
პირველ რიგში ამოწმებს, უკვე გამოთვლილია თუ არა (ტოლია თუ არა -1).

●

ასე თავიდან ვიცილებთ ერთი და იმავე ქვეამოცანის მრავალჯერ გამოთვლას და
ვზოგავთ დროს.

სირთულის შეფასება
დროითი სირთულე : O(n)
დამხმარე მეხსიერება : O(n).

Using Bottom-Up DP (Tabulation) – O(n)
Time and O(n) Space

ეს მიდგომა მსგავსია წინა მემოიზაციის მეთოდისა, თუმცა ამჯერად ამოცანის რეკურსიულად
დაყოფის ნაცვლად, ამოცანა იხსნება იტერაციულად — ქვემოდან ზემოთ (Bottom-Up)
პრინციპით.
ამ მიდგომით ჩვენ ვინახავთ მიღებულ შედეგებს dp[] მასივში, სადაც dp[i] აღნიშნავს იმ
გზების რაოდენობას, რომლითაც შესაძლებელია i-ე საფეხურზე ასვლა.

საწყისი პირობები (Base Case):
●

როცა i = 0 ან i = 1, dp[i] = 1

რეკურსიული შემთხვევა (Recursive Case):
●

როცა i > 1, dp[i] = dp[i - 1] + dp[i - 2]

სირთულის შეფასება
დროითი სირთულე : O(n)
დამხმარე მეხსიერება : O(n).

Using Space Optimized DP – O(n) Time and
O(1) Space

ზემოაღნიშნული მიდგომის ანალიზისას ვხედავთ, რომ თითოეული
ქვეამოცანა დამოკიდებულია მხოლოდ წინა ორი ქვეამოცანის
შედეგზე, ანუ dp[i] დამოკიდებულია მხოლოდ dp[i - 1] და
dp[i - 2] მნიშვნელობებზე.
ამრიგად, შეგვიძლია სივრცის სირთულე ეფექტურად შევამციროთ
და სრულმასშტაბიანი მასივის გამოყენების ნაცვლად, მხოლოდ ორი
ცვლადი გამოვიყენოთ ამ ორი ბოლო მნიშვნელობის შესანახად.

სირთულის შეფასება
დროითი სირთულე : O(n)
დამხმარე მეხსიერება : O(1).

უმოკლესი გზის პოვნა
Guessing - გამოცნობა

ძირითდი იდეა
● გვინდა ვიპოვოთ ყველაზე მოკლე გზა კვანძიდან s კვანძამდე v.
● შემოვიტანოთ რეკურსიული ფორმულა:

ძირითდი იდეა
●

სადაც δ(s,v)— ყველაზე მოკლე მანძილია s-დან v-მდე.

● ეს რეკურსია ამბობს: თუ მინდა მივიდე v-ში, საუკეთესო
ვარიანტია ვნახო ყველა კვანძი u, საიდანაც v-ში წასვლა
შეიძლება და ავიღო s-დან u-მდე საუკეთესო გზა + (u, v) წიბოს
წონა.

დამახსროვრება და ციკლების პოვნა
თუ გრაფში ციკლებია, რეკურსიამ შეიძლება უსასრულოდ
იტრიალოს.
ამიტომ აუცილებელია ციკლების სწორად დამუშავება — ანუ,
გრაფებთან, რომლებიც შეიცავენ ციკლებს, ან “წარმოვადგენთ
აციკურად” ან ვიყენებით ისეთ ალგორითმს, როგორიცაა
Bellman-Ford.

ძირითადი იდეა შეიძლება შეიძლება გრდავქმნათ შემდეგნაირად
შემდეგნაირად:

ძირითადი რეკურსია:

საბაზისო შემთხვევა:

როგორ განვსაზღვროთ რეკურსია?
არ ვიცით, რომელი იქნება s-დან v წიბომდე არსებული ბილიკის
ბოლო წიბო
"Guessing" ნიშნავს, რომ უბრალოდ ყველა შესაძლო ბოლო წიბო
ვცადოთ და საუკეთესო ავირჩიოთ:
● ვვარაუდობთ, რომ ბოლო წიბოა (u,v)
● მაშინ გზა = (s → u-მდე უმოკლესი გზა) + (u, v)

ეს იძლევა რეკურსიულ განსაზღვრებას:

ვცდით ყველა ვარიანტს. ავარჩევთ საუკეთესოს

DAG
თუ გრაფი არის Directed Acyclic Graph (DAG), მაშინ დინამიკური
პროგრამირება მუშაობს ძალიან ეფექტურად.
დროს განვიხილავთ როგორც „დონეებს“, ანუ ყოველი ნაბიჯი —
ახალი „დონეა“.
ყოველი კვანძის მდგომარეობა (v, k ნაბიჯის შემდეგ) შეიძლება
წარმოვიდგინოთ როგორც განსხვავებული კვანძი ახალ DAG-ში.

ძირითადი იდეები
დინამიკური პროგრამირება ≈ რეკურსია +
memoization(დამახსოვრება) + guessing(გამოცნობა)
თუ გრაფი აციკლურია, მაშინ პასუხის მიღება O(V + E) დროშია
შესაძლებელი.
ციკლების შემთხვევაში — Bellman-Ford პასუხს გვიბრუნებს O(VE)
დროში.

https://www.programiz.com/dsa/dynamic-programming

გმადლობთ ყურადღებისთვის!

