R1NQFAQDH3EG9

I_R_W14_M35_C++ Programowanie dynamiczne - znajdowanie spójnego podciągu

Źródło: Gerd Altman, from Pixabay, domena publiczna.

Znajdowanie w ciągu podciągów o różnorodnych własnościach

W analizie algorytmicznej często spotykamy się z problemami dotyczącymi wyszukiwania fragmentów ciągów (tablic, list) spełniających określone warunki. Takie fragmenty nazywamy podciągami spójnymi (ang. subarrays). W tym rozdziale poznasz najważniejsze typy takich podciągów oraz algorytmy, które pozwalają je znaleźć w czasie liniowym lub bliskim liniowemu.

Podstawowe pojęcia

Ciąg

Ciąg to uporządkowany zbiór elementów, np.:

A=[a1,a2,a3,,an]

1. Podciąg spójny

Podciąg spójny to fragment ciągu zawierający kolejne elementy:

[ai,ai+1,,aj]

Przykład:

Dla ciągu [3,1,4,1,5] podciągami spójnymi są m.in.:

  • [3]

  • [1,4]

  • [4,1,5]

ale nie jest nim [3,4,5], bo elementy nie są sąsiadujące.

2. Najdłuższy spójny podciąg niemalejący

Definicja

Podciąg niemalejący to taki, w którym każdy kolejny element jest nie mniejszy od poprzedniego:

akak+1

Przykład

Dla ciągu:

[2,2,3,1,4,4,5]

najdłuższy podciąg niemalejący to:

[1,4,4,5]

Algorytm (czas O(n))

  1. Ustaw długość bieżącego podciągu na 1.

  2. Przechodź po elementach ciągu.

  3. Jeśli a [ i ] a [ i + 1 ], wydłuż bieżący podciąg.

  4. W przeciwnym razie zacznij nowy.

  5. Zapamiętuj maksymalną długość.

3. Spójny podciąg o największej sumie

Dany jest ciąg liczb (mogą być ujemne). Szukamy podciągu spójnego o maksymalnej sumie elementów.

Dla ciągu:

[2,3,1,4,5,2]

podciąg o największej sumie to:

[3,1,4]

bo jego suma wynosi 6.

4. Przykłady krok po kroku

Najdłuższy podciąg niemalejący

Ciąg: [1,2,2,1,3,4,3]

Przebieg:

i

a[i]

a[i]≤a[i+1]?

długość bieżąca

max

0

1

tak

2

2

1

2

tak

3

3

2

2

nie

1

3

3

1

tak

2

3

4

3

tak

3

3

5

4

nie

1

3

6

3

Wynik: 3

Polecenie 1

Masz daną tablicę liczb reprezentującą dzienne zmiany temperatury:

[2,3,-1,4,4,-2,5,5,5,-3,1]

Twoim zadaniem jest: znaleźć najdłuższy spójny fragment tablicy, w którym wszystkie kolejne wartości są nie mniejsze od poprzednich.

Nie rozwiązuj jeszcze zadania - spróbuj najpierw zastanowić się, jak można to zadanie rozbić na mniejsze kroki, które da się zapamiętać i wykorzystać ponownie. To właśnie pierwszy ruch w stronę programowania dynamicznego.

Szukamy najdłuższego spójnego fragmentu, w którym każda kolejna wartość jest ≥ poprzedniej.

Przejdźmy przez nasz ciąg:

  • 2 → 3 rośnie [2,3] (długość 2)

  • 3 → -1  spadek // reset - szukamy kolejnego fragmentu

  • -1 → 4 → 4  nie maleje [-1,4,4] (długość 3)

  • 4 → -2  spadek // reset

  • -2 → 5 → 5 → 5 nie maleje [-2,5,5,5] (długość 4)

  • 5 → -3 spadek // reset

  • -3 → 1 rośnie [-3,1]

Najdłuższy spójny niemalejący podciąg to:

[-2,5,5,5]

Długość: 4

Szukamy najdłuższego spójnego fragmentu, w którym każda kolejna liczba jest nie mniejsza od poprzedniej (czyli ciąg niemalejący).

Krok 1: Ustal, czego szukamy

  • Warunek: dla każdej sąsiedniej pary liczb (ai,ai+1) ma być

ai+1ai

  • Spójny fragment: wybieramy kolejne elementy z tablicy

Chcemy znaleźć najdłuższy taki fragment.

Krok 2: Przejdź po tablicy od lewej do prawej

Będziemy „budować” aktualny fragment niemalejący i zapamiętywać najdłuższy dotąd znaleziony.

Zaczynamy od pierwszego elementu:

  • Aktualny fragment: [2]

  • Najlepszy dotąd: [2]

Krok 3: Sprawdzaj kolejne pary

Para: 2 → 3

  • Czy 32? Tak 

  • Możemy przedłużyć fragment.

Aktualny fragment: [2,3] Najlepszy dotąd: [2,3] (długość 2)

Para: 3 → -1

  • Czy 13? Nie

  • Warunek nie jest spełniony → kończymy fragment.

Porównujemy długości:

  • dotychczasowy najlepszy: [2,3](długość 2)

  • aktualny (też 2) → remis, nic nie zmieniamy.

Zaczynamy nowy fragment od -1:

Aktualny fragment: [-1] Najlepszy dotąd: [2,3]

Para: -1 → 4

  • Czy 41? Tak

Aktualny fragment: [-1,4] Najlepszy dotąd: [2,3] (długość 2)

Para: 4 → 4

  • Czy 44? Tak 

Aktualny fragment: [-1,4,4] Długość aktualnego: 3 → lepszy niż [2, 3] (długość 2).

Najlepszy dotąd: [-1,4,4] (długość 3)

Para: 4 → -2

  • Czy 24? Nie

Koniec fragmentu [-1,4,4]. Porównujemy długości:

  • najlepszy: [-1,4,4] (długość 3)

  • aktualny: [-1,4,4] (długość 3) → bez zmian.

Zaczynamy nowy fragment od -2:

Aktualny fragment: [-2] Najlepszy dotąd: [-1,4,4]

Para: -2 → 5

  • Czy 52? Tak

Aktualny fragment: [-2,5]

Para: 5 → 5

  • Czy 55? Tak

Aktualny fragment: [-2,5,5]

Para: 5 → 5 (druga)

  • Czy 55? Tak

Aktualny fragment: [-2,5,5,5] Długość: 4 → lepszy niż dotychczasowy najlepszy (długość 3).

Najlepszy dotąd: [-2,5,5,5] (długość 4)

Para: 5 → -3

  • Czy 35? Nie

Koniec fragmentu [-2,5,5,5].

Porównujemy:

  • najlepszy: [-2,5,5,5] (długość4)

  • aktualny: [-2,5,5,5] (długość4) → bez zmian.

Zaczynamy nowy fragment od -3:

Aktualny fragment: [-3] Najlepszy dotąd: [-2,5,5,5]

Para: -3 → 1

  • Czy 13? Tak

Aktualny fragment: [-3,1] (długość 2) To krótsze niż najlepszy (długość 4), więc nie zmieniamy najlepszego.

Krok 4: Podsumowanie

Po przejściu całej tablicy:

  • Najdłuższy spójny niemalejący fragment:

[-2,5,5,5]

  • Długość: 4

To jest odpowiedź.

Pseudokod (DP) - najdłuższy spójny podciąg niemalejący

Założenie: Dany jest ciąg A[0..n‑1]. Chcemy znaleźć najdłuższy spójny fragment, w którym

A[i]A[i+1]

Wejście: tablica liczb A[0..n‑1]

Wyjście: początek i koniec najdłuższego niemalejącego podciągu spójnego

Utwórz tablicę DP[0..n‑1]

Linia 1. DP otwórz nawias kwadratowy 0 zamknij nawias kwadratowy otwórz nawias ostrokątny minus minus 1 prawy ukośnik prawy ukośnik każdy element sam w sobie tworzy fragment długości 1. Linia 2. najlepsza podkreślnik dlugosc otwórz nawias ostrokątny minus minus 1. Linia 3. najlepszy podkreślnik koniec otwórz nawias ostrokątny minus minus 0. Linia 4. Dla i od 1 do n‑1 dwukropek. Linia 5. Jeżeli A otwórz nawias kwadratowy i zamknij nawias kwadratowy ≥ A otwórz nawias kwadratowy i‑1 zamknij nawias kwadratowy dwukropek. Linia 6. DP otwórz nawias kwadratowy i zamknij nawias kwadratowy otwórz nawias ostrokątny minus minus DP otwórz nawias kwadratowy i‑1 zamknij nawias kwadratowy plus 1. Linia 7. Inaczej dwukropek. Linia 8. DP otwórz nawias kwadratowy i zamknij nawias kwadratowy otwórz nawias ostrokątny minus minus 1. Linia 9. Jeśli DP otwórz nawias kwadratowy i zamknij nawias kwadratowy zamknij nawias ostrokątny najlepsza podkreślnik dlugosc dwukropek. Linia 10. najlepsza podkreślnik dlugosc otwórz nawias ostrokątny minus minus DP otwórz nawias kwadratowy i zamknij nawias kwadratowy. Linia 11. najlepszy podkreślnik koniec otwórz nawias ostrokątny minus minus i. Linia 12. najlepszy podkreślnik poczatek otwórz nawias ostrokątny minus minus najlepszy podkreślnik koniec minus najlepsza podkreślnik dlugosc plus 1. Linia 13. Zwróć otwórz nawias okrągły najlepszy podkreślnik poczatek przecinek najlepszy podkreślnik koniec zamknij nawias okrągły.

Co robi DP w tym algorytmie?

  • DP[i] oznacza długość najdłuższego niemalejącego spójnego fragmentu kończącego się na pozycji i.

  • Jeśli A[i] ≥ A[i‑1], to możemy „przedłużyć” poprzedni fragment.

  • Jeśli nie - zaczynamy nowy fragment od i.

  • Najlepszy wynik aktualizujemy na bieżąco, w każdej iteracji pętli.

  • Po zakończeniu pętli obliczamy początek fragmentu na podstawie długości i końca.

To klasyczny wzorzec DP: DP[i] zależy tylko od DP[i−1], więc obliczenia są liniowe.

Wynik dla naszego przykładu

Dla tablicy:

[2,3,-1,4,4,-2,5,5,5,-3,1]

algorytm zwróci:

początek = 5

koniec = 8

czyli fragment:

[-2,5,5,5]