Reklama

Grafy - pomoc dla żółtodzioba

  1. Dark Eagle
    Dark Eagle
    Dzień dobry drodzy koderzy!
    Może na wstępie króciutko o sobie: z c++ miałem styczność w gimnazjum, ale szybko się zniechęciłem, bo nie mogłem wyznaczyć sobie celów (o Boże jak ja teraz nad tym ubolewam). Teraz koduję jakoś od 4 może 5 miesięcy. Kończę przerabiać 1 tom "Symfonii c++ standard", pomału zaznajamiam się z książką "Algorytmy, struktury danych i techniki programowania" Piotra Wróblewskiego.

    Na kółku informatycznym profesor wprowadził nam pojęcie grafów. Mniej więcej to ogarnąłem, ale mój problem tkwi w tym, że rozumiem metody przeszukiwania tj. BFS, DFS, ale nie mam pojęcia jak je zaimplementować.
    Zostałem nauczony, że taki graf w dość łatwy sposób można napisać za pomocą vectorów. Za ich pomocą udało mi się napisać jakąś nieskomplikowaną gierkę ->link (dla zainteresowanych polecam odpalić w konsolce - jedynie tak zadziała ;d)
    Na podstawie tego kodu źródłowego możecie stwierdzić co mniej więcej potrafię i w jaki sposób implementuję grafy.

    Konkretny problem:
    Mam za zadanie stworzyć graf, oraz napisać algorytm, który wyznaczy optymalną ścieżkę do jego końca (przejścia z punktów grafu są kosztowne, mamy przejść drogą najmniej kosztowną).

    Na niebiesko zaznaczyłem koszty przejść.

    Pierwszą przeszkodą okazało się pytanie: jak w ogóle zaimplementować te wartości?
    Tutaj kod źródłowy - do czego udało mi się dojść, dalej nie potrafię ruszyć. Nawet nie jestem pewny czy taki sposób nadania wartości ( w osobnej tablicy ) jest poprawny D:

    Torgowi programiści, jesteście w stanie mi pomóc?
  2. Killavus
    Killavus
    Jasne.

    Są co najmniej dwie metody reprezentacji grafów w pamięci.

    Pierwsza to tzw. lista sąsiedztwa - dla każdego wierzchołka w grafie v przechowujemy listę sąsiadów, do których prowadzi krawędźz v. To implementuje się za pomocą std::vector zazwyczaj, albo std:t.

    Jeżeli graf jest ważony, można to rozwiązać na kilka sposobów - albo zadeklarowaćdodatkową, dwuwymiarową tablicę (jeżeli graf jest prosty, tj. nie ma pętli i krawędzi wielokrotnych) jeżeli jest mały, lub też w wektorach/listach przechowywać pary <koszt, wierzchołek>. Przykład takiej reprezentacji:
    Kod :
    std::vector<int> graf[4]; // Reprezentujemy graf czterowierzchołkowy.
    graf[2].push_back(3); // Wierzchołek 2 jest połączony krawędzią (skierowaną) z 3.
    // ... i tak dalej. Jeżeli graf ma byćnieskierowany zawsze dodajemy dwie krawędzie - z v do w i z w do v.
    Przejście przez wszystkie krawędzie wychodzące z danego wierzchołka realizujemy za pomocą prostej pętli:
    Kod :
    for(int v = 0; v < graf[2].size(); ++v) {
      // g[2][v] - wierzchołek z którego jest krawędźz 2.
    }
    Drugą reprezentacją, przydatną szczególnie gdy potrzebujemy sprawdzić, czy v jest połączone z w w czasie stałym jest macierz sąsiedztwa. Mając np. graf 10x10 tworzymy sobie tablicę 10x10 i wsadzamy jeden (lub koszt krawędzi, jeżeli graf jest skierowany) gdy krawędź istnieje, lub 0 gdy nie istnieje. Zauważmy, że nie jesteśmy w stanie reprezentować w ten sposób grafów złożonych (znaczy, mała modyfikacja byłaby wymagana - musielibyśmy zamiast np. intów trzymać listę intów w każdym polu).

    Ta reprezentacja zajmuje więcej w pamięci (V*V gdzie V to liczba wierzchołków), ale czasem jest przydatna.

    A co do algorytmów DFS i BFS - ich ogólna idea jest identyczna, zmienia się tylko struktura danych, która pod nimi leży. W przypadku DFSa jest to stos (LIFO - last in, first out), a w przypadku BFS - kolejka. Załóżmy,że mamy generyczny typ kolekcja, który ma metody niepusta, dodaj i wyciągnij_pierwszy, gdzie pierwszy to w przypadku kolejki pierwszy wrzucony element, a w przypadku stosu ostatni wrzucony element. Wyciąga go i usuwa. Implementacja tych algorytmów w tej kolejce jest IDENTYCZNA (zmienia się struktura wewnątrz kolejki) i realizuje ją następujący pseudokod:

    Kod :
    <wstaw wierzchołek startowy do kolejki>
    dopóki Kolejka.niepusta()
      Wierzchołek w = Kolejka.wyciągnij_pierwszy()
      odwiedzony[w] = true
      dla wszystkich sąsiadów w, nazwijmy ich v:
        jeżeli v nie jest odwiedzony:
          Kolejka.dodaj(v)
    Z racji tego, że stos to taka struktura danych, która jest naturalna przy rekurencji i nie trzeba jej implementować, implementuje się DFS zwyczajową za pomocą rekurencji, a w BFSie potrzeba kolejki.

    Kod :
    // zakładam, że graf realizujemy jako listę sąsiedztwa
    void dfs(int v) {
      used[v] = true;
      for(int i = 0; i < g[v].size(); ++i) {
        if(!used(g[v][i]))
          dfs(g[v][i]);
      }
    }
    
    // Zakładam, że istnieje kolejka k z wrzuconym elementem startowym.
    // std::queue<int> q;
    // q.push(1);
    void bfs() {
      while(!k.empty()) {
         int w = k.pop();
         used[w] = true;
         for(int i = 0; i < g[w].size(); ++i) {
           if(!used[g[w][i]]) {
              k.push(g[w][i]);
           }
         }
      }
    }
    Jak widzisz, implementacja BFS jest bardzo podobna do pseudokodu. Implementacja DFS korzysta z tego, że stos rekurencji jest stosem - więc nie wygląda już tak samo.
    Implementację na podstwie macierzy sąsiedztwa pozostawiam jako proste ćwiczenie ;). Podpowiem, że wystarczy zmienić wewnętrzną pętlę na inny format.

    Pozdrawiam
    Killavus
  3. Killavus
    Killavus
    Co do samego problemu, który masz - są co najmniej 4 algorytmy rozwiązujące Twój problem. Zacznę od najszybszego.

    Pierwszy z nich to tzw. algorytm Dijkstry. Zakłada on, że wagi są dodatnie. Jest w ogólności, jest to BFS w którym podmieniamy normalną kolejkę na kolejkę priorytetową (zobacz sobie, co to jest kopiec). Działa w czasie O(|V|log|V|), czyli bardzo dobrym.

    Drugi to algorytm Bellmana-Forda. Jest wolniejszy, ale znajduje ścieżki w grafach z ujemnymi wagami. Korzysta z obserwacji, że najtańsza ścieżka w grafie przechodzi przez maksymalnie V-1 krawędzi. Stąd też ścieżkę możemy "wydłużyć" V-1 razy (nazywa się to relaksacją krawędzi). Jeżeli po V-tym podejściu uda nam się coś poprawić, znaczy to, że w grafie jest ujemny cykl, czyli chodząc po nim możesz w nieskończonośćskracać ścieżkę.

    Trzeci to algorytm Floyda-Warshalla, najwolniejszy ale posiadający bardzo ciekawą własność - mianowicie wyszukuje wszystkie najkrótsze ścieżki, dla każdej pary wierzchołków jednocześnie.

    Czwarty to algorytm A*, często stosowany w praktyce. Korzysta on z funkcji heurystycznej aby sprytnie wybierać bardziej obiecujące trasy. W ogólności jednak, gdy dane są kiepskie wykonuje on tyle samo operacji co Dijkstra.

    Wszystkie te algorytmy są opisane na Wikipedii, wraz z pseudokodem. Myślę, że poradzisz sobie z ich implementacją. To klasyczny problem informatyczny, więc w necie jest mnóstwo informacji na ten temat.

    Pozdrawiam
    Killavus
  4. Zeimer
    Zeimer
    Nie powinieneś podchodzić do tego zadania bez znajomości grafów od strony algorytmicznej (dobry opis algorytmów grafowych jest np. w Algorytmach Sedgewicka i Wayne'a). Uważam też, że nie powinieneś używać prowizorycznych rozwiązań, które pokazał Killavus, tylko napisać własne implementacje albo użyć już gotowych (chyba są takie w boost). Sam mam zrobione implementacje grafów, w większości działające, zaraz wrzucę.

    #edit
    Albo i nie, nie mam nawet możliwości sprawdzić czy to się kompiluje ; x

    #edit 2
    Jasne, że jest to tak reprezentowane, ale lepiej opakować to w klasy i cieszyć się bardziej przejrzystym i eleganckim rozwiązaniem.
  5. Killavus
    Killavus
    @up:

    Tak naprawdę to nawet pewnie w booście jest to reprezentowane w ten sposób. Taka reprezentacja spokojnie pozwoliła mi na rozwiązanie wszystkich problemów grafowych, z jakimi się spotkałem.

    Pozdrawiam
    Killavus
Pokazuje wyniki od 1 do 5 z 5
Bookmarks