Penyelesaian Maze di Jawa

1. Pengenalan

Dalam artikel ini, kita akan meneroka kemungkinan cara untuk menavigasi labirin, menggunakan Java.

Anggap labirin sebagai gambar hitam putih, dengan piksel hitam mewakili dinding, dan piksel putih mewakili jalur. Dua piksel putih istimewa, satu adalah pintu masuk ke labirin dan jalan keluar yang lain.

Memandangkan labirin seperti itu, kami ingin mencari jalan dari pintu masuk ke pintu keluar.

2. Memodelkan Maze

Kami akan menganggap labirin sebagai array integer 2D. Makna nilai numerik dalam larik akan sesuai dengan konvensi berikut:

  • 0 -> Jalan
  • 1 -> Dinding
  • 2 -> Kemasukan labirin
  • 3 -> Jalan keluar labirin
  • 4 -> Sel bahagian jalan dari pintu masuk ke pintu keluar

Kami akan memperagakan labirin sebagai grafik . Masuk dan keluar adalah dua simpul khas, di mana jalan yang akan ditentukan.

Graf khas mempunyai dua sifat, nod, dan tepi. Tepi menentukan kesambungan grafik dan menghubungkan satu simpul ke nod yang lain.

Oleh itu, kita akan menganggap empat sisi tersirat dari setiap nod, menghubungkan nod yang diberikan ke simpul kiri, kanan, atas dan bawahnya.

Mari tentukan tandatangan kaedah:

public List solve(Maze maze) { }

Input ke metode ini adalah labirin, yang berisi array 2D, dengan konvensi penamaan yang ditentukan di atas.

Respons kaedah adalah senarai nod, yang membentuk jalan dari simpul masuk ke nod keluar.

3. Backtracker Rekursif (DFS)

3.1. Algoritma

Salah satu pendekatan yang cukup jelas adalah untuk meneroka semua jalan yang mungkin, yang akhirnya akan menemui jalan jika ada. Tetapi pendekatan seperti itu akan mempunyai kerumitan eksponensial dan tidak akan bertambah baik.

Walau bagaimanapun, adalah mungkin untuk menyesuaikan penyelesaian brute force yang disebutkan di atas, dengan mengundurkan dan menandakan nod yang dikunjungi, untuk mendapatkan jalan dalam waktu yang wajar. Algoritma ini juga dikenali sebagai carian Depth-first.

Algoritma ini dapat digariskan sebagai:

  1. Sekiranya kita berada di dinding atau simpul yang sudah dikunjungi, kegagalan kembali
  2. Jika tidak, jika kita adalah simpul jalan keluar, maka pulangkanlah kejayaan
  3. Jika tidak, tambahkan simpul dalam senarai laluan dan perjalanan secara berulang-ulang keempat-empat arah. Sekiranya kegagalan dikembalikan, lepaskan simpul dari jalan dan kembali kegagalan. Senarai laluan akan mengandungi jalan yang unik semasa jalan keluar dijumpai

Mari kita gunakan algoritma ini pada labirin yang ditunjukkan dalam Gambar-1 (a), di mana S adalah titik permulaan, dan E adalah jalan keluar.

Untuk setiap nod, kami melintasi setiap arah mengikut urutan: kanan, bawah, kiri, atas.

Dalam 1 (b), kita meneroka jalan dan menembusi dinding. Kemudian kami mundur sehingga simpul dijumpai yang mempunyai tetangga tanpa dinding, dan meneroka jalan lain seperti yang ditunjukkan di 1 (c).

Kami sekali lagi memukul dinding dan mengulangi proses untuk akhirnya menemukan jalan keluar, seperti yang ditunjukkan dalam 1 (d):

3.2. Pelaksanaan

Sekarang mari kita lihat pelaksanaan Java:

Pertama, kita perlu menentukan empat arah. Kita dapat menentukannya dari segi koordinat. Koordinat ini, apabila ditambahkan ke koordinat tertentu, akan mengembalikan salah satu koordinat yang berdekatan:

private static int[][] DIRECTIONS = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } }; 

Kami juga memerlukan kaedah utiliti yang akan menambah dua koordinat:

private Coordinate getNextCoordinate( int row, int col, int i, int j) { return new Coordinate(row + i, col + j); }

Kita sekarang boleh menentukan kaedah penyelesaian tandatangan . Logiknya di sini mudah - jika ada jalan dari entri ke pintu keluar, kemudian kembalikan jalannya, jika tidak, kembalikan senarai kosong:

public List solve(Maze maze) { List path = new ArrayList(); if ( explore( maze, maze.getEntry().getX(), maze.getEntry().getY(), path ) ) { return path; } return Collections.emptyList(); }

Mari tentukan kaedah penerokaan yang dirujuk di atas. Sekiranya ada jalan maka kembali benar, dengan senarai koordinat di jalan argumen . Kaedah ini mempunyai tiga blok utama.

Pertama, kami membuang nod yang tidak sah iaitu nod yang berada di luar labirin atau merupakan bahagian dinding. Selepas itu, kita menandakan node semasa sebagai dikunjungi sehingga kita tidak mengunjungi node yang sama berulang kali.

Akhirnya, kami bergerak secara berulang-ulang ke semua arah jika jalan keluar tidak dijumpai:

private boolean explore( Maze maze, int row, int col, List path) { if ( !maze.isValidLocation(row, col) || maze.isWall(row, col) || maze.isExplored(row, col) ) { return false; } path.add(new Coordinate(row, col)); maze.setVisited(row, col, true); if (maze.isExit(row, col)) { return true; } for (int[] direction : DIRECTIONS) { Coordinate coordinate = getNextCoordinate( row, col, direction[0], direction[1]); if ( explore( maze, coordinate.getX(), coordinate.getY(), path ) ) { return true; } } path.remove(path.size() - 1); return false; }

Penyelesaian ini menggunakan ukuran timbunan hingga ukuran labirin.

4. Varian - Laluan Terpendek (BFS)

4.1. Algoritma

Algoritma rekursif yang dijelaskan di atas mencari jalan, tetapi tidak semestinya jalan terpendek. Untuk mencari jalan terpendek, kita boleh menggunakan pendekatan melintasi grafik lain yang dikenali sebagai carian pertama-lebar.

In DFS, one child and all its grandchildren were explored first, before moving on to another child. Whereas in BFS, we'll explore all the immediate children before moving on to the grandchildren. This will ensure that all nodes at a particular distance from the parent node, are explored at the same time.

The algorithm can be outlined as follows:

  1. Add the starting node in queue
  2. While the queue is not empty, pop a node, do following:
    1. If we reach the wall or the node is already visited, skip to next iteration
    2. If exit node is reached, backtrack from current node till start node to find the shortest path
    3. Else, add all immediate neighbors in the four directions in queue

One important thing here is that the nodes must keep track of their parent, i.e. from where they were added to the queue. This is important to find the path once exit node is encountered.

Following animation shows all the steps when exploring a maze using this algorithm. We can observe that all the nodes at same distance are explored first before moving onto the next level:

4.2. Implementation

Lets now implement this algorithm in Java. We will reuse the DIRECTIONS variable defined in previous section.

Lets first define a utility method to backtrack from a given node to its root. This will be used to trace the path once exit is found:

private List backtrackPath( Coordinate cur) { List path = new ArrayList(); Coordinate iter = cur; while (iter != null) { path.add(iter); iter = iter.parent; } return path; }

Mari sekarang tentukan kaedah teras menyelesaikan. Kami akan menggunakan semula tiga blok yang digunakan dalam pelaksanaan DFS iaitu mengesahkan nod, menandai nod yang dikunjungi dan melintasi nod yang berdekatan.

Kami hanya akan membuat sedikit pengubahsuaian. Daripada melintasi rekursif, kami akan menggunakan struktur data FIFO untuk mengesan jiran dan mengulanginya:

public List solve(Maze maze) { LinkedList nextToVisit = new LinkedList(); Coordinate start = maze.getEntry(); nextToVisit.add(start); while (!nextToVisit.isEmpty()) { Coordinate cur = nextToVisit.remove(); if (!maze.isValidLocation(cur.getX(), cur.getY()) || maze.isExplored(cur.getX(), cur.getY()) ) { continue; } if (maze.isWall(cur.getX(), cur.getY())) { maze.setVisited(cur.getX(), cur.getY(), true); continue; } if (maze.isExit(cur.getX(), cur.getY())) { return backtrackPath(cur); } for (int[] direction : DIRECTIONS) { Coordinate coordinate = new Coordinate( cur.getX() + direction[0], cur.getY() + direction[1], cur ); nextToVisit.add(coordinate); maze.setVisited(cur.getX(), cur.getY(), true); } } return Collections.emptyList(); }

5. Kesimpulan

Dalam tutorial ini, kami menerangkan dua algoritma grafik utama carian Depth-first dan Breadth-first search untuk menyelesaikan labirin. Kami juga menyentuh bagaimana BFS memberikan jalan terpendek dari pintu masuk ke pintu keluar.

Untuk membaca lebih lanjut, cari kaedah lain untuk menyelesaikan labirin, seperti algoritma A * dan Dijkstra.

Seperti biasa, kod lengkap boleh didapati di GitHub.