Как написать алгоритм для прохода конем и ладьей всей шахматной доски (от 3на3 до 10на10) без повторов?

Здесь вставляю свой код, который только ищет возможные ходы конем или ладьей из каждой позиции. Нужно написать алгоритм который будет проходить всю шахматною доску от 3на3 до 10на10 не повторяясь (т.е. нельзя чтобы фигура ходила в ту ячейку, в которой была ранее). Возможны случаи когда одну клетку невозможно пройти как при доске 3на3 с фигурой коня. В моем коде я использовал объекты с индексами ячейки и номером ячейки, индексы от 1, для выполнения алгоритма поиска возможный путей.

 function runApp() {     //Запуск приложения
                    let boardSize = document.getElementById("selectSize").value;    //Получения размера шахматной доски для создания
                    createChessBoard(boardSize);    //Создание шахматной доски
                    searchWays(boardSize);      //Генерация правил для заданной фигуры и размера шахматной доски
                }   
                
                function createChessBoard(boardSize) {      //Функция для создания шахматной доски
                    clearOldChessBoard();       //Удаления старой шахматной доски
                    
                    let output = document.getElementById("chessBoard");
                    let table = document.createElement("table");
                    table.setAttribute("id", "chessBorder");
                    table.style.marginLeft = "auto";
                    table.style.marginRight = "auto";

                    let size = boardSize;
                    let cell = 1;

                    for (let i = 0; i < size; i++) { 
                        let tr = document.createElement('tr');

                        for (let j = 0; j < size; j++) {
                            let td = document.createElement('td');                    
                            td.textContent = cell;
                            td.style.outline = "1px solid black";
                            td.style.textAlign = "center";
                            td.style.fontWeight = "bold";                            
                            td.width = "50px";
                            td.height = "50px";

                            if(i%2 == j%2) {
                                td.style.backgroundColor = "black";
                                td.style.color = "white";
                            } 

                            cell++;  
                            tr.appendChild(td);        
                        }
                             
                        table.appendChild(tr);
                    }

                    output.appendChild(table);      //Добавления новой шахматной доски на странице приложения       
                }
                
                function searchWays(boardSize) {        //Поиск возможных ходов для фигуры
                    clearOldText();         //Удаление старых правил(текста)
                    clearOldTotalWays();        //Удаление старого общего количества правил(текста)

                    let chessMan = document.getElementById("selectChessman").value;
                    let cellNumb = Number(boardSize)*Number(boardSize);  
                    let output = document.getElementById("information");          
                    let numPossibleWays = 1;
                    let totalPossibleWays = document.createElement("h3");
                    let currentPosition = {
                        i: 1,
                        j: 1,
                        cell: 1
                    }
                    let nextPosition = {
                        i: 1,
                        j: 1,
                        cell: 1
                    }

                    while (currentPosition.cell != cellNumb+1) { 
                        nextPosition.cell = 1;
                        
                        for (let i = 1; i <= boardSize; i++) {

                            for (let j = 1; j <= boardSize; j++) { 
                                let possibleMove = document.createElement("p");
                                possibleMove.setAttribute("class", "text");
                                possibleMove.style.textAlign = "center";

                                nextPosition.i = i;
                                nextPosition.j = j;     

                                if(searchKnightWay(currentPosition, nextPosition) &&
                                    chessMan == "knight"){
                                    possibleMove.textContent = `Правило №${numPossibleWays} █ 
                                        Если конь находится на позиции [${currentPosition.cell}],
                                        то он может переместиться на позицию [${nextPosition.cell}]. █ 
                                        [${currentPosition.cell}] => [${nextPosition.cell}] .`

                                    numPossibleWays ++;

                                    output.appendChild(possibleMove);
                                } 
                                
                                else if(searchRookWay(currentPosition, nextPosition) &&
                                    chessMan == "rook" ) {
                                        possibleMove.textContent = `Правило №${numPossibleWays} █ 
                                        Если конь находится на позиции [${currentPosition.cell}],
                                        то он может переместиться на позицию [${nextPosition.cell}]. █ 
                                        [${currentPosition.cell}] => [${nextPosition.cell}] .`

                                    numPossibleWays ++;

                                    output.appendChild(possibleMove);    
                                }
                                nextPosition.cell += 1;                     
                            }                                     
                        }        
                        currentPosition.j ++; 

                        if(currentPosition.cell % boardSize === 0){
                            currentPosition.i ++;                                    
                            currentPosition.j = 1;
                        }   
                        currentPosition.cell ++;
                    }
                                        
                    totalPossibleWays.setAttribute("id","totalWays")
                    totalPossibleWays.textContent = `Общее количество правил: ${numPossibleWays-1}`;

                    let parentElement = document.getElementById("information");
                    let firstElement = document.querySelector(".text");

                    parentElement.insertBefore(totalPossibleWays, firstElement);
                }

                function searchKnightWay(currentPosition, nextPosition) {       //Поиск путей для шахматного коня
                    if(((Math.abs(nextPosition.i - currentPosition.i)) === 1 &&
                        (Math.abs(nextPosition.j - currentPosition.j)) === 2
                    )) {
                        return true;
                    }
                    
                    if(((Math.abs(nextPosition.i - currentPosition.i)) === 2 &&
                        (Math.abs(nextPosition.j - currentPosition.j)) === 1
                    )) {
                        return true;
                    }
                    return false;
                }
            
                function searchRookWay(currentPosition, nextPosition) {     //Поиск путей для шахматной ладьи
                    if(nextPosition.i !== currentPosition.i && nextPosition.j === currentPosition.j) {
                        return true;
                    }

                    if(nextPosition.j !== currentPosition.j && nextPosition.i === currentPosition.i) {
                        return true;
                    }
                    return false;
                }

                function clearOldChessBoard() {         //Функция для удаления старой шахматной доски
                    let oldTable = document.getElementById("chessBorder");

                    if(oldTable !== null) {
                        oldTable.remove();
                    }
                }

                function clearOldText() {       //Функция для удаления старых правил
                    let oldText = document.getElementById("information");
                    let oldTextsArr = document.querySelectorAll("p");

                    if(oldTextsArr !== null) {                
                        for (let i = 0; i < oldTextsArr.length; i++) {
                            oldText.removeChild(oldTextsArr[i]);            
                        }
                    }
                }

                function clearOldTotalWays() {      //Функция для удаления старого общего количества правил
                    let oldTotalWays = document.getElementById("totalWays");

                    if(oldTotalWays !== null){
                        oldTotalWays.remove();
                    }                    
                }
<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body id="main" style="text-align: center;">
        <h2>
            Выберите размер шахматной доски
        </h2>
        <select id="selectSize">
            <option value="3">3x3</option>
            <option value="4">4x4</option>
            <option value="5">5x5</option>
            <option value="6">6x6</option>
            <option value="7">7x7</option>
            <option value="8">8x8</option>
            <option value="9">9x9</option>
            <option value="10">10x10</option>
        </select>
        <h2>
            Выберите шахматную фигуру
        </h2>
        <select id="selectChessman">
            <option value="knight">Конь</option>
            <option value="rook">Ладья</option>
        </select>
        <button id="btnSubmit" onClick="runApp()" style="margin-bottom: 10px; ">Подтвердить</button>
        <div id="output">
            <div id="chessBoard">

            </div>
            <div id="information">

            </div>
        </div>
        <footer>
            <script>
               
            </script>
        </footer>
</body>
</html>


Ответы (1 шт):

Автор решения: Laukhin Andrey

Что-то я увлекся конякой. Решение в три этапа:

  1. Определяем массив возможных ходов конём.
  2. Подготовка доски. Формируем условную матрицу смежности, для каждого поля сохраняем список возможных ходов и признак посещения. Данные будем хранить в одномерном массиве, так будет проще взаимодействовать с DOM-элементами.
  3. Алгоритм обхода.

Касаемо третьего пункта, решил сильно не углубляться, а пойти простым и наиболее понятным для себя путем. Шахматная доска представляется неориентированным графом: Графы в задаче о ходе коня

А какой самый первый алгоритм обхода графа в приходит в голову? Алгоритм поиска в глубину (DFS). Понятно, что типичная реализация полного перебора туманит перспективы, и уход в глубокую депрессию очевиден. Но в википедии прочитал про правило Варнсдорфа:

При обходе доски конь следует на то поле, с которого можно пойти на минимальное число ещё не пройденных полей.

Я просто добавил сортировку смежных вершин по количеству возможных ходов из них, перед следующим шагом. На удивление, это работает великолепно, хотя такая грубая компоновка не оптимальна. Но код понятнее и для базы сойдет.

Еще одно лекарство от глубокой депрессии: для доски нечетного размера можно даже не начинать поиск, если мы стартуем с черного поля (нечетного), в виду особенностей ходьбы парнокопытного.

Про правило Варнсдорфа также пишут, что малый процент «брака» возможен, но нам обязательно повезёт ;)

Размер доски и начальную позицию можно выбирать:

const knightMoves = [[-2, -1], [-1, -2], [1, -2], [2, -1], [2, 1], [1, 2], [-1, 2], [-2, 1]];
const checkBorders = (x, y) => x >= 0 && x < size && y >= 0 && y < size;

let size = 8;
let numFields = size * size;
let startIndex = 0;
let route = []; // маршрут
let chess = []; // поля доски

drawBoard();

// Поиск пути
function knightTour() {

  // Досрочный ответ
  if (size % 2 == 1 && startIndex % 2 == 1)
    return alert('Путь не найден!');

  // Подготовка поля
  chess = [];

  for (let i = 0; i < size; i++) {
    for (let j = 0; j < size; j++) {

      let idx = i * size + j;
      let adj = [];

      for (let m of knightMoves) {
        let aj = j + m[0],
          ai = i + m[1];

        if (checkBorders(aj, ai)) adj.push(ai * size + aj);
      };

      chess.push({
        visited: false,
        adj: adj
      });
    }
  }

  // Поиск
  route = [];

  if (search(startIndex)) showRoute(route);
  else alert('Путь не найден!');
}

// DFS с сортировкой Варнсдорфа
function search(v, lvl = 1) {
  route.push(v);

  if (lvl == numFields) return true;
  if (chess[v].visited) return false;

  chess[v].visited = true;
  chess[v].adj.sort((a, b) => movesCount(a) - movesCount(b));

  for (let neighbor of chess[v].adj) {
    if (!chess[neighbor].visited) {
      if (search(neighbor, lvl + 1)) return true;
    }
  }

  chess[v].visited = false;
  route.pop();

  return false;
}

// Количество доступных ходов из поля idx
function movesCount(idx) {
  let count = 0;
  let adj = chess[idx].adj;

  for (let i = 0; i < adj.length; i++) {
    count += +!chess[adj[i]].visited;
  }
  return count;
}




////-----------
///
// DOM-события
wrap.onclick = (e) => {
  hideRoute();

  if (e.target.nodeName == 'DIV')
    setStartIndex(e.target.getAttribute('idx'));
}

boardsize.onchange = (e) => {
  hideRoute();
  size = e.target.value;
  numFields = size * size;
  drawBoard();
}


// Вспомогалки DOM-игралки
function drawBoard() {
  chessboard.textContent = '';
  for (let i = 0; i < numFields; i++) appendField(size, i);
  setStartIndex(0);
}


function setStartIndex(idx) {
  chessboard.children[startIndex].classList.remove('first');
  chessboard.children[idx].classList.add('first');
  chessboard.children[idx].innerHTML = '&#9822;';
  startIndex = idx;
}

function showRoute(route) {
  let d = '';
  let k = chessboard.offsetWidth / size;
  let l = route.length - 1;

  route.forEach((v, i) => {
    chessboard.children[v].innerHTML = (i == l ? '&#9822' : i + 1);

    let y = (~~(v / size) + 0.5) * k;
    let x = ((v % size) + 0.5) * k;

    d += (i ? (i == 1 ? ' L' : ' ') : ' M') + x + ',' + y;
  });

  path.setAttribute('d', d);
  chessboard.classList.remove('mute');
  drawing.style.display = 'block';
}

function hideRoute() {
  drawing.style.display = 'none';
  chessboard.classList.add('mute');
}


function appendField(size, idx) {
  let div = document.createElement('div');

  let q = size % 2 ? 0 : ~~(idx / size) % 2;
  if ((idx + q) % 2) div.className = 'dark';

  div.setAttribute('idx', idx);
  div.style.width = div.style.height = (100 / size).toFixed(3) + '%';
  chessboard.append(div);
}
#wrap {
  width: 400px;
  height: 400px;
  position: relative;
}

#chessboard {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
  height: 100%;
  margin-top: 1em;
}

#chessboard div {
  display: flex;
  box-sizing: border-box;
  position: relative;
  background: #fece9e;
  justify-content: center;
  align-items: center;
  font-size: 1em;
  cursor: pointer;
}

#chessboard div:hover { border: 2px solid red; }
#chessboard div.dark { background: #d08c47; }

#drawing {
  position: absolute;
  width: 100%;
  height: 100%;
  z-index: 0;
  top: 0;
  left: 0;
  display: none;
}

.mute { color: rgba(255, 255, 255, 0); }
#chessboard div.first { background: yellow; color: green; }
#chessboard div.last { background: blue; }
Размер: <input id="boardsize" name="size" type="number" min="3" max="50" value="8" />
<button onclick="knightTour()">Найти</button>
<div id="wrap">
  <div id="chessboard"></div>
  <svg id="drawing"><path id="path" d="" stroke="blue" fill="none" /></svg>
</div>

UPD На досуге я углубился в задачу, в частности, поиска пути для ладьи, и мне удалось значительно улучшить алгоритм в целом. Чтобы добро не пропадало, решил поделится окончательным вариантом.

Мы знаем, что ладья может ходить на любое доступное поле по вертикали/горизонтали. Задача упрощается, если мы положим, что ладья может ходить только на одну клетку. И когда мы найдем возможность обойти всё поле, мы просто отфильтруем лишние ходы в одном направлении. Этот подход позволяет делать обход в рамках существующего алгоритма для коня.

Матрица ходов для ладьи:

const rookMoves = [[1, 0], [0, 1], [-1, 0], [0, -1]];

Это работает, но как и в случае с конем, результат нестабилен даже на небольших досках.

Виной всему неопределенность выбора по правилу Варнсдорфа, когда несколько потенциальных ходов имеют одинаковый ранг. Рассматривая работу алгоритма для ладьи, я решил добавить дополнительную эвристику в виде матрицы весов такого вида:
Матрица весов
Соответственно, при выборе из равноранговых полей, мы ходим в то, у которого вес меньше. Что это даёт на практике? Начиная ход с центральной области доски, где ранг у соседей будет одинаковый, мы шагаем по наименьшим весам в матрице, а значит первые ходы будут всегда направлены в сторону ближайшего края доски. Это важная особенность. Далее, матрица будет «заспираливать» маршрут от края к центру.

На мое удивление, это дает стабильный результат для любого размера доски (проверял до 100x100), а также улучшает поиск для коня (в коде выше проблемы начинаются при 12x12, а с применением нового правила нет проблем до 41x41).

Еще один этап оптимизации – пробовать разные матрицы ходов. Если зашли в тупик, то пробуем другую последовательность. Всего возможных перестановок – n!. Для коня – 40320, для ладьи – 24.

Я же взял 16 для коня (8 смещений по часовой, 8 против) и 8 для ладьи по тому же принципу. Т.е. если исходная матрица ходов для ладьи: →↓←↑, то следующие варианты: ↓←↑→, ←↑→↓, ↑→↓←, затем против часовой.

Поскольку число ходов ладьи не равно количеству клеток, то из 8 вариантов можно выбрать путь с наименьшим числом ходов.

При этом я исключил возвраты при обходе и убрал рекурсию. До 100x100 я не нашел тупиковых ситуаций как для ладьи, так и для коня. Что там дальше, мне неведомо.

И напоследок, добавил обход зиг-загом для ладьи. Для этого не используем матрицу весов, а даем двойной ранг уже пройденным клеткам, в следствие чего, путь «магнитится» к пройденным линиям.

const rookMoves = [[1, 0], [0, 1], [-1, 0], [0, -1]];
const knightMoves = [[-2, -1], [-1, -2], [1, -2], [2, -1], [2, 1], [1, 2], [-1, 2], [-2, 1]];

const checkBorders = (x, y) => x >= 0 && x < size && y >= 0 && y < size;
chessman.onchange = (e) => mode.disabled = chessman.value === 'knightTour';

let size = 8;
let numFields = size * size;
let startIndex = 0;
let route = []; // маршрут
let chess = []; // доска
let wtmat = []; // доп матрица весов
let useWtmat = true;

drawBoard();

// Инициализация доски
function prepareBoard(moves) {
  chess = [];

  for (let i = 0; i < size; i++) {
    for (let j = 0; j < size; j++) {
      let idx = i * size + j;
      let adj = [];

      for (let m of moves) {
        let aj = j + m[0], ai = i + m[1];
        if (checkBorders(aj, ai)) adj.push(ai * size + aj);
      };

      chessboard.children[idx].innerHTML = '';
      chess.push({ visited: false, adj: adj, rank: adj.length });
    }
  }

  wtmat = getWtmat(size);
}

// Поиск пути для ладьи
function rookTour() {
  // Досрочный ответ
  if (size % 2 == 1 && startIndex % 2 == 1)
    return alert('Путь не найден!');

  useWtmat = !!+mode.value;

  let bestRoute = [];
  let n = 0;

  while (n++ < 8) {
    if (n % 5 == 0) rookMoves.reverse();

    prepareBoard(rookMoves);
    route = [];

    if (search(startIndex, 2 - mode.value)) {
      simplifyRoute();
      if (bestRoute.length == 0) bestRoute = route.slice();
      else if (route.length < bestRoute.length) bestRoute = route.slice();
    }

    rookMoves.push(rookMoves.shift());
  }

  if (bestRoute.length)
    showRoute(bestRoute);
  else
    alert('Путь не найден!');
}

// Поиск пути для коня
function knightTour() {
  // Досрочный ответ
  if (size % 2 == 1 && startIndex % 2 == 1)
    return alert('Путь не найден!');

  useWtmat = true;
  let n = 0, success = false;

  while (!success && n++ < 16) {
    if (n % 9 == 0) knightMoves.reverse();

    prepareBoard(knightMoves);
    route = [];

    success = search(startIndex, 1)
    if (!success) knightMoves.push(knightMoves.shift());
  }

  if (success)
    showRoute(route);
  else
    alert('Путь не найден!');
}

// Обход доски без возвратов
function search(v, dr = 1) {
  route.push(v);

  for (let lvl = 1; lvl < numFields; lvl++) {
    chess[v].visited = true;
    changeRank(v, -dr);
    chess[v].adj.sort(sortMoves);

    let next = chess[v].adj.findIndex(i => !chess[i].visited);

    if (next == -1) {
      resetBoard();
      route = [];
      return false;
    }

    v = chess[v].adj[next];
    route.push(v);
  }

  return true;
}

function resetBoard() {
  for (let i = 0; i < numFields; i++) {
    chess[i].visited = false;
    chess[i].rank = chess[i].adj.length;
  }
}

function changeRank(v, r) {
  chess[v].rank += r;
  chess[v].adj.forEach(q => chess[q].rank += r);
}

function sortMoves(a, b) {
  let ma = chess[a].rank;
  let mb = chess[b].rank;

  if (useWtmat && ma == mb) return wtmat[a] - wtmat[b];
  else
    return ma - mb;
}

// Фильтруем лишние шаги для ладьи
function simplifyRoute() {
  let out = [];
  let prev = 0;

  for (i = 0; i < route.length - 1; i++) {
    let amb = Math.abs(route[i] - route[i + 1]);
    if (amb != prev) out.push(route[i]);
    prev = amb;
  }

  out.push(route[i]);
  route = out;
}

// Дополнителная матрица весов
function getWtmat(size) {
  let mat = Array(size * size).fill(0);
  let o = 0;
  let s = size;

  while (s > 0) {
    for (let i = 0; i < s; i++) {
      let q = o + s - 1;
      let r = (o + i);

      mat[o * size + r] = o;
      mat[q * size + r] = o;
      mat[r * size + o] = o;
      mat[r * size + q] = o;
    }
    s -= 2;
    o++;
  }

  return mat;
}




////-----------
///
// DOM-события
wrap.onclick = (e) => {
  hideRoute();

  if (e.target.nodeName == 'DIV')
    setStartIndex(e.target.getAttribute('idx'));
}

boardsize.onchange = (e) => {
  hideRoute();
  size = e.target.value;
  numFields = size * size;
  drawBoard();
}


// Вспомогалки DOM-игралки
function drawBoard() {
  chessboard.textContent = '';
  for (let i = 0; i < numFields; i++) appendField(size, i);
  setStartIndex(0);
}


function setStartIndex(idx) {
  if (startIndex < numFields)
    chessboard.children[startIndex].classList.remove('first');
  chessboard.children[idx].classList.add('first');
  chessboard.children[idx].innerHTML = '&#9822;';
  startIndex = idx;
}

function showRoute(route) {
  let d = '';
  let k = chessboard.offsetWidth / size;
  let l = route.length - 1;

  route.forEach((v, i) => {
    chessboard.children[v].innerHTML = (i == l ? '&#9822' : i + 1);

    let y = (~~(v / size) + 0.5) * k;
    let x = ((v % size) + 0.5) * k;

    d += (i ? (i == 1 ? ' L' : ' ') : ' M') + x + ',' + y;
  });

  path.setAttribute('d', d);
  chessboard.classList.remove('mute');
  drawing.style.display = 'block';
}

function hideRoute() {
  drawing.style.display = 'none';
  chessboard.classList.add('mute');
}


function appendField(size, idx) {
  let div = document.createElement('div');

  let q = size % 2 ? 0 : ~~(idx / size) % 2;
  if ((idx + q) % 2) div.className = 'dark';

  div.setAttribute('idx', idx);
  div.style.width = div.style.height = (100 / size).toFixed(3) + '%';
  chessboard.append(div);
}
#wrap {
  width: 400px;
  height: 400px;
  position: relative;
}

#chessboard {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
  height: 100%;
  margin-top: 1em;
}

#chessboard div {
  display: flex;
  box-sizing: border-box;
  position: relative;
  background: #fece9e;
  justify-content: center;
  align-items: center;
  font-size: 1em;
  cursor: pointer;
}

#chessboard div:hover { border: 2px solid red; }
#chessboard div.dark { background: #d08c47; }

#drawing {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  display: none;
}

.mute {
  color: rgba(255, 255, 255, 0);
}

#chessboard div.first { background: yellow; color: green; }
#chessboard div.last { background: blue; }
Размер: <input id="boardsize" name="size" type="number" min="3" max="100" value="8" />
<select id="chessman">
  <option value="rookTour" selected>Ладья</option>
  <option value="knightTour">Конь</option>
</select>
<select id="mode">
  <option value="1" selected>Спираль</option>
  <option value="0">Зиг-заг</option>
</select>
<button onclick="window[chessman.value]()">Найти</button>
<div id="wrap">
  <div id="chessboard"></div>
  <svg id="drawing"><path id="path" d="" stroke="blue" fill="none" /></svg>
</div>

→ Ссылка