Ежедневная часть (e) C++ # 30, Распространенная проблема на собеседовании: Самый длинный возрастающий путь

Сегодня мы рассмотрим распространенную задачу интервью C++: самый длинный возрастающий путь.

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

В этом примере самый длинный путь — {1, 2, 4, 7, 8, 9}. Прежде чем вы продолжите читать решение, я рекомендую вам попробовать решить его самостоятельно. Вот ссылка на Compiler Explorer с парой тестов: https://compiler-explorer.com/z/hPTMbPa1e.

Решение

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

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

Наконец, нам нужно отслеживать лучший подпуть в процессе исследования.

int longestPath(const std::vector<std::vector<int>>& matrix) {
  // Helper comparator (to avoid re-checking boundaries)
  auto less = [&matrix](Coord left, Coord right) {
    if (left.ridx < 0 || right.ridx < 0)
      return false;
    if (left.cidx < 0 || right.cidx < 0)
      return false;
    if (left.ridx >= std::ssize(matrix) || 
        right.ridx >= std::ssize(matrix))
      return false;
    if (left.cidx >= std::ssize(matrix[left.ridx]) || 
        right.cidx >= std::ssize(matrix[right.ridx]))
      return false;
    
    return matrix[left.ridx][left.cidx] < 
      matrix[right.ridx][right.cidx];
  };
  // Helper that returns the 4 neighbours of a coordinate
  auto neighbours = [](Coord c) {
      return std::array<Coord,4>(
        {{c.ridx-1,c.cidx},{c.ridx,c.cidx-1},
         {c.ridx+1,c.cidx},{c.ridx,c.cidx+1}});
  };

  // We need the number of univisited paths for each element
  std::vector<std::vector<std::pair<int,int>>> counts(matrix.size());
  // And a queue for the topological sort
  std::queue<std::tuple<ptrdiff_t,ptrdiff_t,int>> tails;

  for (ptrdiff_t ridx = 0; ridx < std::ssize(matrix); ridx++) {
  for (ptrdiff_t cidx = 0; cidx < std::ssize(matrix[ridx]); cidx++) {
    counts[ridx].push_back({
        // how many neighbours are higher values
        std::ranges::count_if(
            neighbours({ridx,cidx}), 
            std::bind_front(less, Coord{ridx,cidx})),
        // initial best path
        1});
    // if this is a terminal node (no neighbours with higher values)
    // add it to the queue
    if (counts[ridx][cidx].first == 0)
        tails.push(std::make_tuple(ridx,cidx,1));
  }
  }

  // topological sort
  int max_cnt = 1;
  while (!tails.empty()) {
    // pick one of the terminal elements
    auto [ridx,cidx,len] = tails.front();
    tails.pop();

    // try to extend the path to a lower number
    for (auto &n : neighbours({ridx,cidx})) {
      if (!less(n, {ridx,cidx})) continue;

      // decrease the number of unvisited paths
      counts[n.ridx][n.cidx].first--;
      // update the best path for this element
      counts[n.ridx][n.cidx].second = 
        std::max(counts[n.ridx][n.cidx].second, len+1);
      // and overall
      max_cnt = std::max(max_cnt, counts[n.ridx][n.cidx].second);
      // if there are no unvisited paths remaining
      // add this element to the queue
      if (counts[n.ridx][n.cidx].first == 0)
          tails.push(std::make_tuple(n.ridx, n.cidx, 
                                     counts[n.ridx][n.cidx].second));
    }
  }
  return max_cnt;
}

Откройте решение в Compiler Explorer.

Сложность этого решения равна O(m*n), где m и n — размеры матрицы. Каждый узел добавляется в очередь один раз (как только количество его исходящих путей достигает нуля), а максимальное количество исходящих путей для каждого узла равно четырем (для общего числа уменьшений).