3

Grafy reprezentované maticami incidencií v Jave

 2 years ago
source link: https://novotnyr.github.io/scrolls/grafy-reprezentovane-maticami-incidencii-v-jave/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Grafy reprezentované maticami incidencií v Jave

Grafy reprezentované maticami incidencií v Jave

2008/05/13

Matica incidencií

V minulom príklade pre topologické triedenie sme používali vlastnú dátovú štruktúru, ktorá umožňovala jednoduché a efektívne vykonávanie operácií nad grafom (hľadanie uzla bez predchodcov, hľadanie nasledovníkov). Graf ako matematická štruktúra má ešte niekoľko ďalších spôsobov reprezentácie v programovacích jazykoch.

Jednou z možností je použitie matice incidencie, čo je tabuľka s n riadkami a stĺpcami (kde n je počet uzlov v grafe). Prvok v r-tom riadku a s-tom stĺpci má hodnotu 1, ak v grafe existuje hrana (teda prepojenie) medzi r-tým a s-tým uzlom.

Matica incidencie pre graf na obrázku má 10 riadkov a 10 stĺpcov.

1 2 3 4 5 6 7 8 9 10
1 X
2
3
4
5
6
7
8
9
10

Na obrázku máme napr. hranu z uzla 1 do uzla 2, preto v matici incidencie dáme na 1. riadok a 2. stĺpec značku (napr. X). Ďalšia hrana je medzi uzlom 2. a 4., čiže v 2. riadku a 4. stĺpci je značka. Týmto spôsobom pokračujeme až kým nevyplníme celú maticu:

1 2 3 4 5 6 7 8 9 10
1 X X
2 X X
3 X
4 X
5 X
6 X
7 X X
8
9 X X
10

Na tejto matici vieme potom vykonávať rôzne operácie zodpovedajúce veciam, ktoré chceme z grafu zistiť, či ukázať.

Nasledovníci uzla

Počet nasledovníkov uzla vieme zistiť podľa počtu značiek v riadku zodpovedajúcom danému uzlu.

Konkrétnych nasledovníkov uzla zistíme tak, že sa pozrieme do riadku zodpovedajúcemu danému uzlu a tie stĺpce, v ktorých je značka, zodpovedajú uzlom nasledovníkov. Potom je jasné, že uzol bez nasledovníkov má zodpovedajúci riadok prázdny.

V nasledovnej tabuľke nemajú uzly 8 a 10 žiadnych nasledovníkov, uzol s 1 má dvoch nasledovníkov (2 a 3).

Predchodcovia uzla

Ak chceme zistiť počet predchodcov uzla, pozrieme sa na počet značiek v stĺpci zodpovedajúcom danému uzlu. Konkrétnych predchodcov uzla zistíme pohľadom do stĺpca zodpovedajúcemu danému uzlu a riadky, v ktorých je značka, zodpovedajú predchodcom. Uzol bez predchodcov má zodpovedajúci stĺpec prázdny.

V nasledovnej tabuľke nemajú uzly 1 a 7 žiadnych nasledovníkov, uzol 3 má dvoch predchodcov (1 a 6).

Pridanie uzla do grafu

Ak chceme pridať uzol grafu, znamená to pridanie nového riadka a stĺpca do matice.

1 2 3 4 5 6 7 8 9 10 11
1 X X .
2 X X .
3 X .
4 X .
5 X .
6 X .
7 X X .
8 .
9 X X .
10 X X .
11 . . . . . . . . . . .

Odobratie uzla z grafu

Odobratie uzla z grafu znamená odstránenie príslušného riadka a stĺpca z matice. S odstránením uzla sa samozrejme odstránia aj hrany vychádzajúce a vchádzajúce do uzla.

Ak odstránime uzol 6, matica sa nám zmenší o jeden riadok a jeden stĺpec.

1 2 3 4 5 6 7 8 9 10
1 X X |
2 X |
3 X |
4 |
5 | X
6 +
7 X | X
8 |
9 X |
10 X | X

Pridanie hrany do grafu

Pridanie hrany medzi dvoma uzlami X a Y znamená nastavenie značky v X-tom riadku a Y-tom stĺpci.

Odobratie hrany z grafu

Odobratie hrany medzi dvoma uzlami X a Y znamená zrušenie značky v X-tom riadku a Y-tom stĺpci.

Implementácia v Jave

Implementácia grafu založeného na matici incidencií v Jave je založená na dvoch triedach: uzle a grafe.

Uzol Uzol je jednoduchá trieda - uzol je charakterizovaný svojim popiskom.


public class Uzol {
  private String label;

  public Uzol(String label) {
    super();
    this.label = label;
  }

  public String getLabel() {
    return label;
  }

  public void setLabel(String label) {
    this.label = label;
  }
}

Samotný graf, v tomto prípade s názvom IncidenceMatrix, triedou obaľujúcou maticu incidencií a poskytujúcou metódy na prácu s ňou.

Maticu môžeme reprezentovať ako zoznamom zoznamov, inak povedané ako objektom typu List<List<Boolean>. Tento zoznam si možno predstaviť ako zoznam riadkov v matici, kde každý riadok predstavuje samostatný zoznam booleovských položiek.

Okrem toho budeme v triede udržiavať samostatný zoznam objektov Uzol. Objekt typu Uzol v tomto zozname sa nachádza na nejakom indexe. Napr. desiaty uzol má index 9 a v matici incidencií mu zodpovedá zoznam s indexom 9, resp. prvky v zoznamoch s indexom 9.

public class IncidenceMatrix {
  private List<List<Boolean>> matica = new ArrayList<List<Integer>>();

  private List<Uzol> uzly = new ArrayList<Uzol>();

Pridanie uzla do grafu

Ako sme spomínali vyššie, pridanie uzla do grafu zodpovedá pridaniu nového riadka a nového stĺpca. Ako sa to prejaví v prípade zoznamu zoznamov? Rozdeľme si túto operáciu na dva (plus jeden) kroky:

  1. každý z existujúcich zoznamov predĺžime o jeden prvok - tým pridáme do matice jeden „stĺpec".
  1. do zoznamu zoznamov pridáme nový zoznam (zodpovedajúci novému riadku), ktorý má dĺžku rovnú aktuálnemu počtu uzlov + 1. (Ak pridávame do matice desiaty uzol, nový zoznam musí mať dĺžku 9 + 1, teda 10.) V každom prvku bude false.
  1. do zoznamu uzlov pridajme nový prvok zodpovedajúci aktuálne pridávanému uzlu.

Tomu zodpovedá metóda pridajUzol(), ktorou pridáme uzol do grafu.

public void pridajUzol(Uzol uzol) {
  // na koniec kazdeho zoznamu prida *false*
  for(List<Boolean> matica : matica) {
    matica.add(false);
  }

  // prida novy zoznam dlzky pocetUzlov + 1 naplneny *false*
  List<Boolean> riadok = new ArrayList<Boolean>();
  for(int i = 0; i < uzly.size() + 1; i++) {
    riadok.add(false);
  }   
  matica.add(riadok);
  // prida uzol do zoznamu uzlov
  uzly.add(uzol);
}

Odstránenie uzla z grafu

Odstránenie uzla je opačnou operáciou aj v zmysle vykonaných činností. Pre daný uzol si zistíme jeho index (na to použijeme metódu indexOf() na zozname java.util.List, ktorá vráti index prvého výskytu daného objektu v ňom) k. Z matice potom odstránime k-ty zoznam a z každého zoznamu odstránime k-ty prvok.

protected void odstráňUzol(Uzol node) {
  // zisti index daného uzla
  int index = uzly.indexOf(node);
  if(index == -1) {
    // taký uzol nejestvuje, nerobíme nič
    return;
  }
  // z matice odstránime príslušný zoznam  
  matica.remove(index);
  // z každého riadka v matici odstránime príslušnú bunku
  for (List<Integer> matica : matica) {
    matica.remove(index);
  }
  // zo zoznamu uzlov vyhodíme daný uzol
  uzly.remove(node);
}

Pridanie hrany do grafu

Ďalšou užitočnou operáciou je pridanie hrany medzi dvoma uzlami. V grafickom znázornení to zodpovedá natiahnutiu čiary medzi dvoma „guličkami". Ak máme v grafe dva existujúce uzly C a F, stačí nájsť v zozname uzlov ich zodpovedajúce indexy. Ak má uzol C index 5 a uzol F index 8, hrana medzi uzlami znamená nastavenie značky v riadku s indexom 5 na pozícii osem. To je samozrejme v prípade, že hrany sú orientované - čiže môžeme prejsť z C do F, ale nie naopak. Ak máme neorientované hrany, musíme ju nastaviť aj v opačnom smere, teda aj na riadku s indexom osem na prvku s pozíciou 5.

0 1 2 3 4 5 6 7 8 9
0 X X
1 X X
2 X X
3 X
4 X
5 X X
6 X
7 X X
8
9 X X

Metóda vyzerá nasledovne:

public void pridajHranu(Uzol node1, Uzol node2) {
  // zistíme index prvého uzla
  int index1 = uzly.indexOf(node1);
  if(index1 == -1) {
    // taký uzol neexistuje!
    throw new IllegalArgumentException("Uzol v grafe neexistuje! " + node1);
  }
  // zistíme index druhého uzla
  int index2 = uzly.indexOf(node2);
  if(index2 == -1) {
    // taký uzol neexistuje!
    throw new IllegalArgumentException("Uzol v grafe neexistuje! " + node2);
  }   
  // na index1-tom zozname nastavíme hodnotu na pozícii index2 na true
  matica.get(index1).set(index2, true);
  // pre neorientovane hrany tiez matica.get(index2).set(index1, true);
}

Algoritmus topologického triedenia na matici incidencií

Topologické triedenie

V predošlom článku sme spomínali pekný algoritmus riešiaci problém správneho poradia pri zápise predmetov v študijnom programe nazývaný topologické triedenie.

Algoritmus bol jednoduchý:

  1. nájdime v grafe uzol bez predchodcov, vypíšme ho. Ak uzol nejestvuje, končíme.
  2. prejdime zoznam jeho nasledovníkov. Každému nasledovníkovi znížme počet predchodcov o 1…
  3. … pretože tento uzol bez predchodcov z grafu vyhodíme.
  4. prejdime na krok 1.

Samotná implementácia v Jave zodpovedá priamo algoritmu:

public void topologickeTriedenie() {
  Uzol uzolBezPredchodcov = null;
  while((uzolBezPredchodcov = hľadajUzolBezPredchodcov()) != null) {
    System.out.println(uzolBezPredchodcov);
    odstráňUzol(uzolBezPredchodcov);     
  }
  if(!uzly.isEmpty()) {
    throw new IllegalStateException("V grafe sú cykly.");
  }
}

Metóda hľadajUzolBezPredchodcov() nájde v grafe uzol, ktorý nemá predchodcov, teda v matici incidencie má príslušný stĺpec bez značiek (teda sú v ňom samé hodnoty false). V predošlej ukážkovej tabuľke máme dva uzly bez predchodcov: ich indexy sú 0 a 7.

// ideme "po stlpcoch" v matici. Pre kazdy stlpec pozrieme,
// ci v nom je *true* prvok. Ak ano, znamena
// vchadzajucu hranu a teda predka. Ak nie,
// mame uzol bez predchodcov, ktory vratime.
for (int i = 0; i < uzly.size(); i++) {
  boolean máPredchodcov = false;
  for (List<Boolean> riadok : matica) {
    if(riadok.get(i) == true) {
      // staci napisat if(riadok.get(i)) {
      máPredchodcov = true;
      break;
    }
  }
  if(!máPredchodcov) {
    // v i-tom stlpci je uzol bez predka, 
    // dohladame v zozname uzlov prislusny objekt 
    // Uzol a vratime ho
    return uzly.get(i);
  }
}
return null;

Metóda odstráňUzol(), ktorá odstraňuje uzol z grafu, už bola popísaná vyššie.

Budovanie štruktúry

Vybudovanie grafu pre topologické triedenie je tiež veľmi podobné klasickému prístupu popísanému minule. Ak načítavame dvojice prerekvizita -predmet zo súboru, metóda pridajDvojicu() vyzerá nasledovne:

public void pridajDvojicu(String prerekvizita, String predmet) {
  // nájdi v grafe prvý uzol
  Uzol uzolPrerekvizity = hľadajPodľaNázvu(prerekvizita);
  // ak prvý uzol nejestvuje, vytvor ho a pridaj do grafu
  if(uzolPrerekvizity == null) {
    uzolPrerekvizity = new Uzol(prerekvizita);
    pridajUzol(uzolPrerekvizity);
  }
  // nájdi v grafe druhý uzol
  Uzol uzolPredmetu = hľadajPodľaNázvu(predmet);
  // ak druhý uzol nejestvuje, vytvor a pridaj ho do grafu
  if(uzolPredmetu == null) {
    uzolPredmetu = new Uzol(predmet);
    pridajUzol(uzolPredmetu);
  }

  pridajHranu(uzolPrerekvizity, uzolPredmetu);
}

Metóda na hľadanie uzla podľa názvu je úplne rovnaká ako v prípade vlastnej štruktúry - stačí prejsť zoznam uzlov a nájsť ten požadovaný.

protected Uzol hľadajPodľaNázvu(String nazov) {
  // prechádzame zoznam uzlov
  for (Uzol uzol : uzly) {
    // ak sme našli uzol, končíme
    if(uzol.getNazov().equals(nazov)) {
      return uzol;
    }
  }
  // uzol sme nenašli, vraciame null
  return null;
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK