Algoritam za rešavanje lavirinta

Postoji nekoliko različitih varijanti algoritama za rešavanje lavirinta, koji predstavljaju automatizovane metode za rešavanje bilo kog lavirinta.

Nasumični algoritam miša, algoritam praćenja zida, algoritam Džona Pledža (en. Jon Pledge) i Tremauksov (fr. Charles Pierre Trémaux) algoritam su dizajnirani tako da se koriste kada se osoba nalazi unutar samog lavirinta bez ikakvog poznavanja strukture i izgleda samog lavirinta, dok su algoritam ispunjavanja ćorsokaka i algritam najkraćeg puta dizajnirani tako da se koristi kada osoba ili računarski program unapred poznaje strukturu i izgled čitavog lavirinta.

Lavirinti koji ne sadrže petlje poznati su kao *standardni* ili *savršeni* lavirinti, i kao takvi ekvivalentni su stablima u teoriji grafova preko kojih se mogu i predstavljati. Prema tome većina algoritama za rešavanje lavirinta se oslanjaju na teoriju grafova.

Intuitivno, ako bi se svi putevi u lavirintu ispravno izdvojili, kao rezultat mogli bi dobiti nešto što dobro podseća na strukturu stabla.


Nasumični algoritam miša

uredi

Ovo je trivijalni algoritam koji može biti iskorišćen kod veoma neinteligentnih robota ili na miševe. Algoritam se sastoji od praćenja putanje kojom se krećemo sve dok ne dođemo do raskrsnice sa više mogućnosti skretanja, tada nasumišno odabiramo koju ćemo putanju nadalje slediti. Iako će ovakav algoritam na kraju uvek pronaći pravo rešenje, ovaj algoritam može biti i ekstremno spor. Razlog za to je što se putanje koje smo već jednom prošli ne eliminišu iz ponovnog razmatranja kada se na njih ponovo naiđe.

Algoritam praćenja zida

uredi
 
Prolazak korišženjem pravila desne ruke

Algoritam praćenja zida, predstavlja jedan od najpoznatijih algoritama za pronalaženje izlaza iz lavirinta, a takođe je poznat i pod nazivima „pravilo leve ruke“ i „pravilo desne ruke“.

Ukoliko je lavirint povezan, odnosno ako su svi njegovi zidovi međusobno spojeni ili ako su spojeni sa spoljašnjom granicom lavirinta, tada ako osoba unutar lavirinta drži ruku u kontaktu sa zidom tokom celog prolaska kroz lavirint garantovano dolazi do izlaza iz lavirinta ako izlaz uopšte i postoji, u suprotnom osoba bi se vratila na ulaz u lavirint i pritom bi obišla svaku putanju u lavirintu barem jedanput.

Ako ovaj problem pogledamo s druge, topološke, strane primećuje se zašto je algoritam praćenja zida u mogućnosti da uvek pronađe pravo rešenje. Ako su zidovi povezani, to znači da se mogu raširiti u kružnicu.[1] Tako se algoritam praćenja zida redukuje na prolazak ivicama te kružnice od ulaza do izlaza.

Ukoliko lavirint nije povezan (npr. ako su ulaz u lavirint ili izlazi iz njega u centru samog lavirinta ili putanje prelaze jedna preko druge ili jedna ispod druge), nije garantovano da će ovaj algoritam zaista u takvim slučajevima pronaći izlaz iz lavirinta.

Algoritam praćenja zidova može se koristiti i u trodimenzionalnim i višedimenzionalnim okruženjima ukoliko te višedimenzione putanje mogu biti projektovane u dvodimenzionalnoj ravni na deterministički način.

Na primer, ukoliko u trodimenѕionalnom algoritmu putanje „nagore“ smatramo putanjama ka severo-zapadu, a putanje „nadole“ smatramo putanjama ka jugo-istoku, tada možemo upotrebiti standardna pravila algoritma praćenja zida. Međutim, za razliku od dvodimenzionalnog prostora, ovde je potrebno u svakom momentu znati i trenutnu orijentaciju, da bi se znalo koji smer je prvi nalevo ili nadesno..

Algoritam Džona Pledža

uredi

Nepovezani algoritmi se ipak na određene načine mogu rešiti korišćenjem algoritma praćenja zida, ukoliko se ulaz i izlazi iz lavirinta nalaze na spoljašnjim zidovima lavirinta. Međutim, ukoliko se ulaz nalazi u unautrašnjosti lavirinta, može se naći na zidu koji nije spojan sa zidom na kome se nalazi izlaz, pa bi se korišćenjem algoritma praćenja zidova osoba vrtela u krug i vraćala na početak ne pronalazeći izlaz. Algoritam Džona Pledža može da reši ovaj problem.

Pledžov algoritam, dizajniran da zaobiđe prepreke, zahteva kretanje u unapred određenom smeru.

Kada se naiđe na prepreku, jedna ruka (npr. desna) nastavlja da prati prepreku a takođe prati se i broj okreta na levu i desnu stranu korišćenjem brojača okreta. Okretanje na desnu stranu povećava vrednost brojača, a okret na levu stranu smanjuje njegovu trenutnu vrednost. Kada se osoba ponovo okrene ka originalnom unapred određenom smeru, tada je brojač okreta ponovo vraćen na vrednost 0 i prepreka je uspešno savladana, osoba nastavlja sa kretanjem u početnom originalnom smeru ka svom cilju pa ako se ponovo naiđe na prepreku prethodni postupak se ponavlja. Ruka više ne prati zid samo kada je brojač okreta ponovo postavljen na 0. Ovakav pristup omogućava algoritmu da izbegne zamke koje su na primer oblika velikog latiničnog slova G - ("G").

U suštini algoritam je vrlo jednostavan i sastoji se od provere nekoliko pravila koja se proveravaju tokom prolaska kroz lavirint:

(algoritam zasnovan na pravilu desne ruke)

1. Osoba se kreće unapred određenim smerom.
2. Kada se naiđe na prepreku prvi put osoba se okreće na levu stranu. Brojač okreta se tada umanjuje za 1.

3. Vrši se provera postojanja prepreka. Osoba proverava da li postoji prepreka ispred, desno ili levo.

  • Ukoliko nema prepreke na desnoj strani osoba se okreće na desnu stranu. Brojač okreta se uvećava za 1.
  • Ukoliko postoji prepreka na desnoj strani, a ne postoji prepreka ispred, osoba se pomera unapred za jedan korak. Brojač okreta ne menja svoju trenutnu vrednost.
  • Ukoliko postoji prepreka na desnoj strani i postoji prepreka ispred, osoba se okreće na levu stranu. Brojač okreta se tada umanjuje za 1.

Prethodni podkoraci koraka 3 se navedenim redosledom proveravaju sve dok se brojač okreta ponovo ne vrati na vrednost 0 i tada smatramo da je prepreka uspešno savladana.


4. Osoba nastavlja da se kreće početnim smerom, sve dok se ponovo ne naiđe na prepreku ili se stigne do izlaza.

Algoritam zasnovan na pravilu leve ruke može se analogno izvesti iz prethodnog.

Na ovaj način algoritam omogućava osobi koja ume da proverava postojanje prepreka ispred, levo i desno, uspešno pronalaženje izlaza na spoljašnjem zidu iz bilo kog dvodimenzionalnog lavirinta, nezavisno od početne pozicije osobe u lavirintu. Međutim ovaj algoritam ne pruža mogućnost rešavanja suprotnog problema prethodnom problemu. Algoritam neće raditi ako se ulaz nalazi na spoljašnjem zidu lavirinta, a neki cilj u unutrašnjosti lavirinta.

Java kod algoritma Džona Pledža zasnovanog na pravilu leve ruke za pokretanje robota

uredi

 1 // код који се односи на кретање особе унапред
 2 public boolean isActive() { 
 3    return true;
 4 }  
 5 public Velocities act() {
 6    double rotationalVelocity = 0.0;
 7    return new Velocities(TRANSLATIONAL_VELOCITY, rotationalVelocity);
 8 }
 9
 10 
 11 // код који се односи на скретање у десну страну
 12 public boolean isActive() {
 13    if (turningRightCount > 0) {
 14       return true;
 15    }
 16    RangeSensorBelt bumpers = getSensors().getBumpers();
 17    // провера постојања предње препреке.
 18    if (bumpers.hasHit(0)) {
 19       backingUpCount = 10;
 20       turningRightCount = getRotationCount();
 21
 22       return true;
 23    } else {
 24       return false;
 25    }
 26 }
 27       
 28 public Velocities act() {
 29    if (backingUpCount > 0) {
 30    
 31    backingUpCount--;
 32
 33    return new Velocities(-TRANSLATIONAL_VELOCITY, 0.0);
 34    } else {
 35       turningRightCount--;
 36
 37       return new Velocities(0.0, -Math.PI / 2);
 38    }
 39 }
 40
 41
 42 public boolean isActive() {
 43    if (postGoingForwardCount > 0) {
 44    return true;
 45    }
 46
 47    RangeSensorBelt sonars = getSensors().getSonars();
 48    // провера левог сензора.
 49    if (sonars.getMeasurement(1) > 1.0) {
 50       // могуће је скренути лево.
 51       preGoingForwardCount = 20;
 52       postGoingForwardCount = 40;
 53       turnLeftCount = getRotationCount();
 54
 55       return true;
 56    } else {
 57       return false;
 58    }
 59 }
 60        
 61 public Velocities act() {
 62    if (preGoingForwardCount > 0) {
 63       preGoingForwardCount--;
 64
 65       return new Velocities(TRANSLATIONAL_VELOCITY, 0.0);
 66    } else if (turnLeftCount > 0) {
 67       turnLeftCount--;
 68
 69       return new Velocities(0.0, Math.PI / 2);
 70    } else {
 71       postGoingForwardCount--;
 72
 73       return new Velocities(TRANSLATIONAL_VELOCITY, 0.0);
 74    }
 75 }
 76
 77
 78 public boolean isActive() {
 79    RangeSensorBelt sonars = getSensors().getSonars();
 80
 81    // да ли смо стигли ван лавиринта?
 82    double clearDistance = 1.2;
 83    return sonars.getMeasurement(0) > clearDistance
 84        && sonars.getMeasurement(1) > clearDistance
 85        && sonars.getMeasurement(3) > clearDistance
 86        && sonars.getMeasurement(2) > clearDistance;
 87 }
 88
 89 public Velocities act() {
 90    // заустављање
 91    return new Velocities(0.0, 0.0);
 92 }

Tremauksov algoritam

uredi

Tremauksov algoritam predstavlja efikasan metod pronalaženja izlaska iz lavirinta koji zahteva obeležavanje staze kojom osoba prolazi. Ovim algoritmom je garantovano pronalaženje izlaza za sve lavirinte koji imaju dobro definisane putanje. Svaka od putanja je ili još uvek ne posećena ili je označena jednom ili je označena dva puta.[2]

Na ulazu odabira se nasumično smer kojim se krećemo (ukoliko postoji više smerova od jednog). Krećemo se odabranim smerom sve do ne dođemo do prve raskrsnice ili do ćorsokaka. Putanju koju smo prešli obeležavamo jednom. Ako smo naišli na raskrsnicu na kojoj postoji više od jednog puta koji su još uvek neobeleženi, ponovo nasumično odabiramo put kojim nastavljamo dalje i njega obeležavamo. Ukoliko na raskrsnici samo jedan put nije obeležen nastavljamo tim putem i njega obeležavamo. Ukoliko naiđemo na ćorsokak vraćamo se nazad do prve raskrsnice i put obeležavamo po drugi put. Ukoliko, kada se vratimo na raskrsnicu nema više neobeleženih puteva nijednom, tada se dalje vraćamo nekim od puteva koji smo prošli jednom i taj put obeležavamo po drugi put. Svi putevi obeleženi dva puta se dalje ne razmatraju. Daljim proverama na osnovu prethodno navedenih pravila garantovano je da ćemo doći do izlaza iz lavirinta, a putanja koja nas vodi od izlaza nazad do ulaza biće ona putanja koja je obeležena jednom. Ukoliko ne postoji izlaz iz lavirinta, ovim metodom vratićemo se ponovo na ulaz nakon što obeležimo sve putanje po dva puta, po jednom u svakom smeru.

Primetimo da je ovo jedna od varijanti algoritma pretrage u dubinu.

Java kod Tremauksovog algoritma za pokretanje robota

uredi

 1 package maze.ai;
 2
 3 import java.awt.Dimension;
 4 import java.util.ArrayList;
 5 import java.util.List;
 6
 7 import maze.model.Direction;
 8 import maze.model.MazeCell;
 9
 10 /**
 11 * Maze solving algorithm that provides a right wall follower with a memory so
 12 * it prefers unexplored cells.
 13 */
 14 public class Tremaux extends RobotBase
 15 {
 16   private Direction[][] ballOfString;
 17   private List<RobotStep> moveQueue = new ArrayList<RobotStep>();
 18   private boolean turbo = false;
 19 
 20   /**
 21    * This returns the name of the mathematician who came up with this process
 22    */
 23   @Override
 24   public String toString()
 25   {
 26      return "Tremaux";
 27   }
 28
 29   /**
 30    * This function should be called by the controller any time a new run is to
 31    * commence
 32    */
 33   @Override
 34   public void initialize()
 35   {
 36      super.initialize();
 37
 38      Dimension size = robotLocation.getMazeSize();
 39      ballOfString = new Direction[(int) size.getWidth()][(int) size.getHeight()];
 40
 41      for (int i = 0; i < size.getWidth(); i++)
 42      {
 43         for (int j = 0; j < size.getHeight(); j++)
 44         {
 45            ballOfString[i][j] = null;
 46         }
 47      }
 48      ballOfString[0][size.height - 1] = Direction.North;
 49      this.moveQueue.clear();
 50   }
 51
 52   /**
 53    * This returns the state of the turbo flag. Turbo should be true when
 54    * traversing previously explored territories.
 55    */
 56   public boolean isInTurboMode()
 57   {
 58      return turbo;
 59   }
 60 
 61   /**
 62    * This returns the next step for the robot to take. It should be called by
 63    * the controller.
 64    */
 65   public RobotStep nextStep()
 66   {
 67      RobotStep next;
 68      if (getDirection(robotLocation.getCurrentLocation()) == null)
 69      {
 70         setDirection();
 71      }
 72      if (moveQueue.isEmpty() == true)
 73      {
 74         if ( (robotLocation.isWallRight() == false) && (getRightNeighborDirection() == null))
 75         {
 76            next = RobotStep.RotateRight;
 77            moveQueue.add(RobotStep.MoveForward);
 78            turbo = false;
 79         }
 80         else if ( (robotLocation.isWallFront() == false) && (getFrontNeighborDirection() == null))
 81         {
 82            next = RobotStep.MoveForward;
 83            turbo = false;
 84         }
 85         else if ( (robotLocation.isWallLeft() == false) && (getLeftNeighborDirection() == null))
 86         {
 87            next = RobotStep.RotateLeft;
 88            moveQueue.add(RobotStep.MoveForward);
 89            turbo = false;
 90         }
 91         else
 92         //Retrace the steps
 93         {
 94            turbo = true;
 95            if (robotLocation.getDirection() == getDirection(robotLocation.getCurrentLocation()))
 96            {
 97               next = RobotStep.MoveForward;
 98            }
 99            else if (robotLocation.getDirection().getLeft() == getDirection(robotLocation.getCurrentLocation()))
 100           {
 101              next = RobotStep.RotateLeft;
 102              moveQueue.add(RobotStep.MoveForward);
 103           }
 104           else if (robotLocation.getDirection().getRight() == getDirection(robotLocation.getCurrentLocation()))
 105           {
 106              next = RobotStep.RotateRight;
 107              moveQueue.add(RobotStep.MoveForward);
 108           }
 109           else
 110           {
 111              next = RobotStep.RotateRight;
 112              moveQueue.add(RobotStep.RotateRight);
 113              moveQueue.add(RobotStep.MoveForward);
 114           }
 115        }
 116     }
 117     else
 118     {
 119        next = moveQueue.get(0);
 120        moveQueue.remove(0);
 121     }
 122     return next;
 123  }
 124
 125  /**
 126   * This returns the direction for the understanding for the neighbor to the
 127   * left.
 128   */
 129  private Direction getLeftNeighborDirection()
 130  {
 131     return getNeighborDirection(robotLocation.getDirection().getLeft());
 132  }
 133
 134  /**
 135   * This returns the direction for the understanding for the neighbor to the
 136   * front.
 137   */
 138  private Direction getFrontNeighborDirection()
 139  {
 140     if (robotLocation.getCurrentLocation().getY() == 1)
 141     {
 142        return null;
 143     }
 144     return getNeighborDirection(robotLocation.getDirection());
 145  }
 146
 147  /**
 148   * This returns the direction for the understanding for the neighbor to the
 149   * right.
 150   */
 151  private Direction getRightNeighborDirection()
 152  {
 153     return getNeighborDirection(robotLocation.getDirection().getRight());
 154  }
 155
 156  /**
 157   * This returns the direction for the understanding for the neighbor to the
 158   * direction given from the current cell.
 159   */
 160  private Direction getNeighborDirection(Direction direction)
 161  {
 162     MazeCell here = robotLocation.getCurrentLocation();
 163     MazeCell there;
 164     Dimension size = robotLocation.getMazeSize();
 165     if ( (direction == Direction.North) && (here.getY() != 1))
 166     {
 167        there = new MazeCell(here.getX(), here.getY() - 1);
 168     }
 169     else if ( (direction == Direction.South) && (here.getY() != size.getHeight()))
 170     {
 171        there = new MazeCell(here.getX(), here.getY() + 1);
 172     }
 173     else if ( (direction == Direction.East) && (here.getX() != size.getWidth()))
 174     {
 175        there = new MazeCell(here.getX() + 1, here.getY());
 176     }
 177     else if ( (direction == Direction.West) && (here.getX() != 1))
 178     {
 179        there = new MazeCell(here.getX() - 1, here.getY());
 180     }
 181     else
 182     {
 183        return null;
 184     }
 185     return getDirection(there);
 186  }
 187
 188 
 189  /**
 190   * This sets the direction for the understanding for the current cell.
 191   */
 192  private void setDirection()
 193  {
 194     Direction wayBack = robotLocation.getDirection().getOpposite();
 195     MazeCell here = robotLocation.getCurrentLocation();
 196     ballOfString[here.getX() - 1][here.getY() - 1] = wayBack;
 197  }
 198
 199  /**
 200   * This returns the direction for the understanding for the given cell
 201   */
 202  private Direction getDirection(MazeCell currentLocation)
 203  {
 204     return ballOfString[currentLocation.getX() - 1][currentLocation.getY() - 1];
 205  }
 206
 207  /**
 208   * This returns the understanding of the maze. Tremaux's understanding is the
 209   * directions needed to return to the start.
 210   */
 211  public Direction[][] getUnderstandingDir()
 212  {
 213     return ballOfString;
 214  }
 215}

Algoritam ispunjavanja ćorsokaka

uredi

Algoritam ispunjavanja ćorsokaka je još jedan od algoritama za pronalaženje izlaza iz lavirinta koji ispunjava sve putanje koje ne vode ka izlazu i ostavlja neispunjenu samo putanju koja vodi do cilja. Može se koristiti kako za rešavanje lavirinta na papiru, tako i za rešavanje lavirinta pomoću programa na računaru, ali ne može se koristiti ukoliko se osoba nalazi u samom lavirintu. Tada nije poznata celokupna struktura lavirinta, a da bi ovaj metod radio potrebno je poznavanje čitave strukture lavirinta na samom startu.

Ovaj algoritam funkcioniše na sledeći način:[3]

1 pronaći sve ćorsokake u lavirintu.

2 „ispuniti“ putanju od svakog od ćorsokaka do prve raskrsnice.

Ovaj algoritam ne može prekinuti putanju od ulaza do izlaza iz lavirinta jer svaki korak u algoritmu očuva topologiju lavirinta. Dalje, ovaj metod se neće završiti ni prerano jer završni rezultat ne sadrži ćorsokake.

Stoga, ako je ispunjavanje ćorsokaka urađeno na savršenom lavirintu (lavirintu bez petlji), jedina putanja koja preostane biće rešenje. Ukoliko se primeni na lavirint koji ima petlje, svako moguće rešenje će ostati kao rezultat i ništa više.

Algoritam najkraćeg puta

uredi

Kada lavirint ima više rešenja, možda želimo da pronađemo ono rešenje koje nam daje najkraći put od početka do kraja. Jedan od mogućih algporitama pronalazi najkraću putanju implementirajući algoritam pretrage u dubinu, dok drugi, A* algoritam, koristi heuristike. Algoritam pretrage u dubinu koristi redove kako bi se posetile putanje u redosledu koji podrazumeva uvećanje udaljenosti od početka sve dok se ne dođe do kraja. Svaka posećena putanja čuva podatak o udaljenosti od početka. Kada se pronađe lpkacija kraja, prati se putanja unazad do početka, što predstavlja najkraću putanju.

Vidi takođe

uredi

Reference

uredi