Зарегистрируйтесь, чтобы продолжить обучение

Алгоритм Литтла: реализация в коде Алгоритмы на графах

В этом уроке мы перейдем от теории к практике. Вы узнаете, как реализовать алгоритм Литтла в коде.

Чтобы реализовать алгоритм Литтла, мы написали такой объемный код:

Нажмите, чтобы увидеть код
class Node {
  matrix;
  bound;
  route;

    constructor(matrix, bound, route) {
    this.matrix = matrix;
    this.bound = bound;
    this.route = route;
  }

  static cloneMatrix(matrix) {
    return matrix.map((row) => row.slice());
  }

  static rowMins(matrix) {
    const mins = [];
    for (let row = 0; row < matrix.length; row += 1) {
      mins[row] = matrix[row][0];
    }

    for (let row = 0; row < matrix.length; row += 1) {
      for (let column = 1; column < matrix.length; column += 1) {
        if (mins[row] > matrix[row][column]) {
          mins[row] = matrix[row][column];
        }
      }
    }

    mins.sumFinites = function sumFinites() {
      return this.reduce((a, b) => (isFinite(b) ? a + b : a), 0);
    };

    return mins;
  }

  static columnMins(matrix) {
    const mins = [];
    for (let column = 0; column < matrix.length; column += 1) {
      mins[column] = matrix[column][0];
    }

    for (let row = 1; row < matrix.length; row += 1) {
      for (let column = 0; column < matrix.length; column += 1) {
        if (mins[column] > matrix[row][column]) {
          mins[column] = matrix[row][column];
        }
      }
    }

    mins.sumFinites = function sumFinites() {
      return this.reduce((a, b) => (isFinite(b) ? a + b : a), 0);
    };

    return mins;
  }

  static reduceRows(matrix, mins) {
    for (let row = 0; row < matrix.length; row += 1) {
      for (let column = 0; column < matrix.length; column += 1) {
        if (isFinite(mins[row])) {
          matrix[row][column] = matrix[row][column] - mins[row];
        }
      }
    }
  }

  static reduceColumns(matrix, mins) {
    for (let row = 0; row < matrix.length; row += 1) {
      for (let column = 0; column < matrix.length; column += 1) {
        if (isFinite(mins[column])) {
          matrix[row][column] = matrix[row][column] - mins[column];
        }
      }
    }
  }

  static reduce(matrix) {
    const rowMins = Node.rowMins(matrix);
    Node.reduceRows(matrix, rowMins);

    const columnMins = Node.columnMins(matrix);
    Node.reduceColumns(matrix, columnMins);

    return rowMins.sumFinites() + columnMins.sumFinites();
  }

  getCellWithMaxPenalty() {
    let maxPenalty = -Infinity;
    let cellWithMaxPenalty = null;
    for (let row = 0; row < this.matrix.length; row += 1) {
      for (let column = 0; column < this.matrix.length; column += 1) {
        if (this.matrix[row][column] === 0) {
          let rowMin = Infinity;
          for (let i = 0; i < this.matrix.length; i += 1) {
            if (!isFinite(this.matrix[row][i]) || i === column) {
              continue;
            }

            if (rowMin > this.matrix[row][i]) {
              rowMin = this.matrix[row][i];
            }
          }

          let columnMin = Infinity;
          for (let i = 0; i < this.matrix.length; i += 1) {
            if (!isFinite(this.matrix[i][column]) || i === row) {
              continue;
            }

            if (columnMin > this.matrix[i][column]) {
              columnMin = this.matrix[i][column];
            }
          }

          const penalty = rowMin + columnMin;
          if (maxPenalty < penalty) {
            maxPenalty = penalty;
            cellWithMaxPenalty = [row, column, maxPenalty];
          }
        }
      }
    }

    return cellWithMaxPenalty;
  }
}

const isFinite = (value) => Number.isFinite(value);

const findNextStartCity = (edges, startCity) => {
  for (let i = 0; i < edges.length; i += 1) {
    if (edges[i][1] === startCity) {
      return i;
    }
  }

  return -1;
};

const findNextEndCity = (edges, endCity) => {
  for (let i = 0; i < edges.length; i += 1) {
    if (edges[i][0] === endCity) {
      return i;
    }
  }

  return -1;
};

const getCloseEdges = (route) => {
  const result = [];
  const edges = [...route];

  while (edges.length > 0) {
    let length = 1;
    let startCity = edges[0][0];
    let endCity = edges[0][1];
    edges.splice(0, 1);

    let index = findNextStartCity(edges, startCity);
    while (index !== -1) {
      length += 1;
      [startCity] = edges[index];
      edges.splice(index, 1);
      index = findNextStartCity(edges, startCity);
    }

    index = findNextEndCity(edges, endCity);
    while (index !== -1) {
      length += 1;
      [, endCity] = edges[index];
      edges.splice(index, 1);
      index = findNextEndCity(edges, endCity);
    }

    if (length >= 2) {
      result.push([endCity, startCity]);
    }
  }

  return result;
};

const makeChildren = (minNode) => {
  const [row, column, leftPenalty] = minNode.getCellWithMaxPenalty();

  const leftMatrix = Node.cloneMatrix(minNode.matrix);
  leftMatrix[row][column] = Infinity;
  Node.reduce(leftMatrix);
  const leftBound = minNode.bound + leftPenalty;
  const leftRoute = [...minNode.route];
  const leftChild = new Node(leftMatrix, leftBound, leftRoute);

  const rightMatrix = Node.cloneMatrix(minNode.matrix);
  rightMatrix[column][row] = Infinity;
  for (let i = 0; i < rightMatrix.length; i += 1) {
    rightMatrix[row][i] = Infinity;
    rightMatrix[i][column] = Infinity;
  }

  const rightRoute = [...minNode.route, [row, column]];
  const closeEdges = getCloseEdges(rightRoute);
  closeEdges.forEach(([currRow, currEdge]) => {
    rightMatrix[currRow][currEdge] = Infinity;
  });

  const rightPenalty = Node.reduce(rightMatrix);
  const rightBound = minNode.bound + rightPenalty;
  const rightChild = new Node(rightMatrix, rightBound, rightRoute);

  return [leftChild, rightChild];
};

const little = (matrix) => {
  const rootMatrix = Node.cloneMatrix(matrix);
  const minBound = Node.reduce(rootMatrix);
  const root = new Node(rootMatrix, minBound, []);
  const priorityQueue = [root];
  let record = null;

  while (priorityQueue.length > 0) {
    let minIndex = 0;
    let minNode = priorityQueue[minIndex];
    for (let i = 1; i < priorityQueue.length; i += 1) {
      if (minNode.bound > priorityQueue[i].bound) {
        minNode = priorityQueue[i];
        minIndex = i;
      }
    }

    priorityQueue.splice(minIndex, 1);

    if (record !== null) {
      if (record.length <= minNode.bound) {
        break;
      }
    }

    if (minNode.route.length === matrix.length - 2) {
      for (let row = 0; row < matrix.length; row += 1) {
        for (let column = 0; column < matrix.length; column += 1) {
          if (isFinite(minNode.matrix[row][column])) {
            minNode.bound += minNode.matrix[row][column];
            minNode.route.push([row, column]);
          }
        }
      }

      if (record == null || record.length > minNode.bound) {
        record = { length: minNode.bound, route: minNode.route };
      }
    } else {
      const [leftChild, rightChild] = makeChildren(minNode);

      priorityQueue.push(leftChild);
      priorityQueue.push(rightChild);
    }
  }

  return record;
};

https://replit.com/@hexlet/algorithms-graphs-littles-algorithm-code

В таком объемном коде сложно разобраться самостоятельно, поэтому мы подробно обсудим, что происходит на каждом шаге.

Здесь мы реализовали вспомогательный класс Node, в котором сосредоточили функции для работы с узлами дерева решений.

JavaScript хранит ссылку на объекты, в том числе на массивы. Если мы хотим создать копию матрицы смежности в каждом узле, мы не можем просто скопировать массив. Нужно создать новый массив и скопировать в него все значения исходного массива.

Матрица — это массив массивов. Поэтому мы выполняем глубокое копирование:

static cloneMatrix(matrix) {
    return matrix.map(row => row.slice());
}

Еще нам нужно создать две функции:

  • Первая функция будет искать минимумы в каждой строке

  • Вторая — редуцировать, то есть вычитать минимумы из каждого элемента каждой строки

Так выглядит реализация в коде:

static rowMins(matrix) {
    const mins = [];
    for (let row = 0; row < matrix.length; row += 1) {
      mins[row] = matrix[row][0];
    }

    for (let row = 0; row < matrix.length; row += 1) {
      for (let column = 1; column < matrix.length; column += 1) {
        if (mins[row] > matrix[row][column]) {
          mins[row] = matrix[row][column];
        }
      }
    }

    mins.sumFinites = function sumFinites() {
      return this.reduce((a, b) => (isFinite(b) ? a + b : a), 0);
    };

    return mins;
}

static reduceRows(matrix, mins) {
    for (let row = 0; row < matrix.length; row += 1) {
      for (let column = 0; column < matrix.length; column += 1) {
        if (isFinite(mins[row])) {
          matrix[row][column] = matrix[row][column] - mins[row];
        }
      }
    }
  }

Далее мы напишем похожие функции и для столбцов. Затем реализуем метод редуцирования матрицы и по строкам и по столбцам:

static reduce(matrix) {
    const rowMins = Node.rowMins(matrix);
    Node.reduceRows(matrix, rowMins);

    const columnMins = Node.columnMins(matrix);
    Node.reduceColumns(matrix, columnMins);

    return rowMins.sumFinites() + columnMins.sumFinites();
}

Последний метод в классе Node — это getCellWithMaxPenalty. Он находит ячейку матрицы с наибольшим штрафом.

Как мы помним, метод пробегает по всем нулевым элементам и складывает минимумы в той же строке и в том же столбце. При вычислении минимума сам элемент не учитывается:

getCellWithMaxPenalty() {
    let maxPenalty = -Infinity;
    let cellWithMaxPenalty = null;
    for (let row = 0; row < this.matrix.length; row += 1) {
      for (let column = 0; column < this.matrix.length; column += 1) {
        if (this.matrix[row][column] === 0) {
          let rowMin = Infinity;
          for (let i = 0; i < this.matrix.length; i += 1) {
            if (!isFinite(this.matrix[row][i]) || i === column) {
              continue;
            }

            if (rowMin > this.matrix[row][i]) {
              rowMin = this.matrix[row][i];
            }
          }

          let columnMin = Infinity;
          for (let i = 0; i < this.matrix.length; i += 1) {
            if (!isFinite(this.matrix[i][column]) || i === row) {
              continue;
            }

            if (columnMin > this.matrix[i][column]) {
              columnMin = this.matrix[i][column];
            }
          }

          const penalty = rowMin + columnMin;
          if (maxPenalty < penalty) {
            maxPenalty = penalty;
            cellWithMaxPenalty = [row, column, maxPenalty];
          }
        }
      }
    }

    return cellWithMaxPenalty;
  }
}

Во время работы алгоритма Литтла мы должны исключить из рассмотрения закрывающие ребра — те, которые могут завершить маршрут раньше времени.

Для поиска закрывающих ребер напишем функцию getCloseEdges. Для ее работы реализуем вспомогательные функции findNextStartCity и findNextEndCity:

const findNextStartCity = (edges, startCity) => {
  for (let i = 0; i < edges.length; i += 1) {
    if (edges[i][1] === startCity) {
      return i;
    }
  }

  return -1;
};

const findNextEndCity = (edges, endCity) => {
  for (let i = 0; i < edges.length; i += 1) {
    if (edges[i][0] === endCity) {
      return i;
    }
  }

  return -1;
};

const getCloseEdges = (route) => {
  const result = [];
  const edges = [...route];

  while (edges.length > 0) {
    let length = 1;
    let startCity = edges[0][0];
    let endCity = edges[0][1];
    edges.splice(0, 1);

    let index = findNextStartCity(edges, startCity);
    while (index !== -1) {
      length += 1;
      [startCity] = edges[index];
      edges.splice(index, 1);
      index = findNextStartCity(edges, startCity);
    }

    index = findNextEndCity(edges, endCity);
    while (index !== -1) {
      length += 1;
      [, endCity] = edges[index];
      edges.splice(index, 1);
      index = findNextEndCity(edges, endCity);
    }

    if (length >= 2) {
      result.push([endCity, startCity]);
    }
  }

  return result;
};

На каждом шаге алгоритма мы строим левое и правое поддеревья из какого-то узла дерева решений.

Вынесем код построения в функцию makeChildren. Используем написанные нами ранее методы и функции — getCellWithMaxPenalty, cloneMatrix и reduce:

const makeChildren = (minNode) => {
  const [row, column, leftPenalty] = minNode.getCellWithMaxPenalty();

  const leftMatrix = Node.cloneMatrix(minNode.matrix);
  leftMatrix[row][column] = Infinity;
  Node.reduce(leftMatrix);
  const leftBound = minNode.bound + leftPenalty;
  const leftRoute = [...minNode.route];
  const leftChild = new Node(leftMatrix, leftBound, leftRoute);

  const rightMatrix = Node.cloneMatrix(minNode.matrix);
  rightMatrix[column][row] = Infinity;
  for (let i = 0; i < rightMatrix.length; i += 1) {
    rightMatrix[row][i] = Infinity;
    rightMatrix[i][column] = Infinity;
  }

  const rightRoute = [...minNode.route, [row, column]];
  const closeEdges = getCloseEdges(rightRoute);
  closeEdges.forEach(([currRow, currEdge]) => {
    rightMatrix[currRow][currEdge] = Infinity;
  });

  const rightPenalty = Node.reduce(rightMatrix);
  const rightBound = minNode.bound + rightPenalty;
  const rightChild = new Node(rightMatrix, rightBound, rightRoute);

  return [leftChild, rightChild];
};

Наконец, реализуем функцию little. В стандартной библиотеке JavaScript нет структуры данных, которая представляла бы очередь с приоритетами. Поэтому мы сделаем собственную простую структуру и будем выбирать узел с наименьшей нижней границей каждом шаге алгоритма:

const little = (matrix) => {
  const rootMatrix = Node.cloneMatrix(matrix);
  const minBound = Node.reduce(rootMatrix);
  const root = new Node(rootMatrix, minBound, []);
  const priorityQueue = [root];
  let record = null;

  while (priorityQueue.length > 0) {
    let minIndex = 0;
    let minNode = priorityQueue[minIndex];
    for (let i = 1; i < priorityQueue.length; i += 1) {
      if (minNode.bound > priorityQueue[i].bound) {
        minNode = priorityQueue[i];
        minIndex = i;
      }
    }

    priorityQueue.splice(minIndex, 1);

    if (record !== null) {
      if (record.length <= minNode.bound) {
        break;
      }
    }

    if (minNode.route.length === matrix.length - 2) {
      for (let row = 0; row < matrix.length; row += 1) {
        for (let column = 0; column < matrix.length; column += 1) {
          if (isFinite(minNode.matrix[row][column])) {
            minNode.bound += minNode.matrix[row][column];
            minNode.route.push([row, column]);
          }
        }
      }

      if (record == null || record.length > minNode.bound) {
        record = { length: minNode.bound, route: minNode.route };
      }
    } else {
      const [leftChild, rightChild] = makeChildren(minNode);

      priorityQueue.push(leftChild);
      priorityQueue.push(rightChild);
    }
  }

  return record;
};

const matrix = [
    [Infinity, 27, 43, 16, 30, 26],
    [7, Infinity, 16, 1, 30, 25],
    [20, 13, Infinity, 35, 5, 0],
    [21, 16, 25, Infinity, 18, 18],
    [12, 46, 27, 48, Infinity, 5],
    [23, 5, 5, 9, 5, Infinity]
];

console.log(little(matrix)); // {
                             //   length: 63,
                             //   route: [ [ 0, 3 ], [ 1, 0 ],
                             //            [ 4, 5 ], [ 2, 4 ],
                             //            [ 3, 2 ], [ 5, 1 ] ]
                             // }

Выводы

В этом уроке мы закончили изучать алгоритм Литтла. Теперь вы знаете, как он работает в теории и как реализовать его в коде. Повторим ключевые выводы, к которым мы пришли в прошлом теоретическом уроке:

  • Алгоритм Литтла находит оптимальное решение в дереве решений

  • Для работы алгоритма важно, чтобы оценка нижней границы была корректной

  • Обычно алгоритм Литтла работает быстрее метода перебора, в худшем случае — с той же скоростью


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

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