Skip to content

longest_common_subsequence.hpp

SECTIONDP INCLUDEnoya/longest_common_subsequence.hpp

Return matched index pairs for one longest common subsequence in O(nm) time and memory.

求两个序列的一组最长公共子序列匹配下标;适合需要恢复方案而不只求长度的题目。

Implementation

View on GitHub

#ifndef NOYA_LONGEST_COMMON_SUBSEQUENCE_HPP
#define NOYA_LONGEST_COMMON_SUBSEQUENCE_HPP 1

/// @complexity Time: O(nm) for a certificate; bit-parallel length uses
/// O(n log n + d ceil(n/64) + m(log d + ceil(n/64))) time and
/// Space: O(nm) for a certificate or O(d ceil(n/64)) for bit-parallel length,
/// where d is the number of distinct first-sequence values.

#include <algorithm>
#include <bit>
#include <cstdint>
#include <utility>
#include <vector>

namespace noya {

/// @brief Return matched index pairs for one longest common subsequence in
/// O(nm) time and memory.
template <class First, class Second>
std::vector<std::pair<int, int>>
longest_common_subsequence(const First &first, const Second &second) {
  int n = int(first.size());
  int m = int(second.size());
  std::vector dp(n + 1, std::vector<int>(m + 1));
  for (int i = n - 1; i >= 0; i--) {
    for (int j = m - 1; j >= 0; j--) {
      if (first[i] == second[j]) {
        dp[i][j] = dp[i + 1][j + 1] + 1;
      } else {
        dp[i][j] = std::max(dp[i + 1][j], dp[i][j + 1]);
      }
    }
  }
  std::vector<std::pair<int, int>> result;
  int i = 0;
  int j = 0;
  while (i < n && j < m) {
    if (first[i] == second[j] && dp[i][j] == dp[i + 1][j + 1] + 1) {
      result.emplace_back(i++, j++);
    } else if (dp[i + 1][j] >= dp[i][j + 1]) {
      i++;
    } else {
      j++;
    }
  }
  return result;
}

/// @brief Return only the LCS length using the bit-parallel DP-row-difference
/// update. Consecutive cells of a standard LCS DP row differ by zero or one,
/// so the complete row is represented by a bitset D of those differences.
/// For the next value, X marks equal positions in the first sequence. The
/// next row is `D = (D | X) & ~((D | X) - ((D << 1) | 1))`; the loop below
/// performs that subtraction word by word, carrying the shift and borrow
/// across 64-bit blocks. The popcount of the final difference row is the LCS
/// length. This is intended for long sequences when the quadratic certificate
/// table is too large.
template <class Sequence>
int longest_common_subsequence_length_bitset(const Sequence &first,
                                             const Sequence &second) {
  using value_type = typename Sequence::value_type;
  const int n = int(first.size());
  const int blocks = (n + 63) / 64;
  if (n == 0 || second.empty()) {
    return 0;
  }

  std::vector<value_type> coordinates(first.begin(), first.end());
  std::sort(coordinates.begin(), coordinates.end());
  coordinates.erase(std::unique(coordinates.begin(), coordinates.end()),
                    coordinates.end());
  std::vector<std::vector<std::uint64_t>> matches(
      coordinates.size(), std::vector<std::uint64_t>(blocks));
  for (int position = 0; position < n; position++) {
    int id = int(std::lower_bound(coordinates.begin(), coordinates.end(),
                                  first[position]) -
                 coordinates.begin());
    matches[id][position / 64] |= std::uint64_t(1) << (position % 64);
  }

  std::vector<std::uint64_t> difference(blocks);
  for (const value_type &value : second) {
    auto iterator = std::lower_bound(coordinates.begin(), coordinates.end(),
                                     value);
    const std::vector<std::uint64_t> *equal =
        iterator != coordinates.end() && *iterator == value
            ? &matches[iterator - coordinates.begin()]
            : nullptr;
    std::uint64_t shift_carry = 1;
    std::uint64_t borrow = 0;
    for (int block = 0; block < blocks; block++) {
      std::uint64_t old = difference[block];
      std::uint64_t combined = old | (equal ? (*equal)[block] : 0);
      std::uint64_t shifted = (old << 1) | shift_carry;
      shift_carry = old >> 63;
      std::uint64_t subtracted = combined - shifted - borrow;
      std::uint64_t next_borrow =
          combined < shifted || (borrow != 0 && combined == shifted);
      difference[block] = combined & ~subtracted;
      borrow = next_borrow;
    }
  }

  int result = 0;
  for (std::uint64_t block : difference) {
    result += std::popcount(block);
  }
  return result;
}

} // namespace noya

#endif // NOYA_LONGEST_COMMON_SUBSEQUENCE_HPP
#include <algorithm>
#include <bit>
#include <cstdint>
#include <utility>
#include <vector>

/// @complexity Time: O(nm) for a certificate; bit-parallel length uses
/// O(n log n + d ceil(n/64) + m(log d + ceil(n/64))) time and
/// Space: O(nm) for a certificate or O(d ceil(n/64)) for bit-parallel length,
/// where d is the number of distinct first-sequence values.

namespace noya {

/// @brief Return matched index pairs for one longest common subsequence in
/// O(nm) time and memory.
template <class First, class Second>
std::vector<std::pair<int, int>>
longest_common_subsequence(const First &first, const Second &second) {
  int n = int(first.size());
  int m = int(second.size());
  std::vector dp(n + 1, std::vector<int>(m + 1));
  for (int i = n - 1; i >= 0; i--) {
    for (int j = m - 1; j >= 0; j--) {
      if (first[i] == second[j]) {
        dp[i][j] = dp[i + 1][j + 1] + 1;
      } else {
        dp[i][j] = std::max(dp[i + 1][j], dp[i][j + 1]);
      }
    }
  }
  std::vector<std::pair<int, int>> result;
  int i = 0;
  int j = 0;
  while (i < n && j < m) {
    if (first[i] == second[j] && dp[i][j] == dp[i + 1][j + 1] + 1) {
      result.emplace_back(i++, j++);
    } else if (dp[i + 1][j] >= dp[i][j + 1]) {
      i++;
    } else {
      j++;
    }
  }
  return result;
}

/// @brief Return only the LCS length using the bit-parallel DP-row-difference
/// update. Consecutive cells of a standard LCS DP row differ by zero or one,
/// so the complete row is represented by a bitset D of those differences.
/// For the next value, X marks equal positions in the first sequence. The
/// next row is `D = (D | X) & ~((D | X) - ((D << 1) | 1))`; the loop below
/// performs that subtraction word by word, carrying the shift and borrow
/// across 64-bit blocks. The popcount of the final difference row is the LCS
/// length. This is intended for long sequences when the quadratic certificate
/// table is too large.
template <class Sequence>
int longest_common_subsequence_length_bitset(const Sequence &first,
                                             const Sequence &second) {
  using value_type = typename Sequence::value_type;
  const int n = int(first.size());
  const int blocks = (n + 63) / 64;
  if (n == 0 || second.empty()) {
    return 0;
  }

  std::vector<value_type> coordinates(first.begin(), first.end());
  std::sort(coordinates.begin(), coordinates.end());
  coordinates.erase(std::unique(coordinates.begin(), coordinates.end()),
                    coordinates.end());
  std::vector<std::vector<std::uint64_t>> matches(
      coordinates.size(), std::vector<std::uint64_t>(blocks));
  for (int position = 0; position < n; position++) {
    int id = int(std::lower_bound(coordinates.begin(), coordinates.end(),
                                  first[position]) -
                 coordinates.begin());
    matches[id][position / 64] |= std::uint64_t(1) << (position % 64);
  }

  std::vector<std::uint64_t> difference(blocks);
  for (const value_type &value : second) {
    auto iterator = std::lower_bound(coordinates.begin(), coordinates.end(),
                                     value);
    const std::vector<std::uint64_t> *equal =
        iterator != coordinates.end() && *iterator == value
            ? &matches[iterator - coordinates.begin()]
            : nullptr;
    std::uint64_t shift_carry = 1;
    std::uint64_t borrow = 0;
    for (int block = 0; block < blocks; block++) {
      std::uint64_t old = difference[block];
      std::uint64_t combined = old | (equal ? (*equal)[block] : 0);
      std::uint64_t shifted = (old << 1) | shift_carry;
      shift_carry = old >> 63;
      std::uint64_t subtracted = combined - shifted - borrow;
      std::uint64_t next_borrow =
          combined < shifted || (borrow != 0 && combined == shifted);
      difference[block] = combined & ~subtracted;
      borrow = next_borrow;
    }
  }

  int result = 0;
  for (std::uint64_t block : difference) {
    result += std::popcount(block);
  }
  return result;
}

} // namespace noya