ホーム
/
作ったもの
/
考えたこと
/
きろく

C++競プロ用ライブラリ

使い方・計算量

はじめに

 競プロ用のメモです。随時更新します。 なお、このライブラリはAIを用いて整備しています。 再配布等はしてほしくないですが、ご自由にお使いください。

入力・出力

getline

用途: 空白を含む1行の文字列入力を受け取る

計算量: 行長を L とすると O(L)。 文字列を丸ごと読むときの基本。

string s;
getline(cin, s);

浮動小数点出力まとめ

用途: 有効数字・小数桁・指数表記の制御

#include <iomanip>

double x = 3.141592653589793;

// 有効数字(全体桁数)
cout << setprecision(10) << x;

// 小数点以下固定
cout << fixed << setprecision(5) << x;

// 指数表記
cout << scientific << setprecision(3) << x;

// 元に戻す
cout << defaultfloat << x;

文字列・数値の読み分け

用途: 1トークンずつ読む基本形

計算量: 入力サイズに比例して O(文字数)。 空白区切りの入力で最もよく使う。

int a, b;
long long x;
string s;
char c;
double d;

cin >> a >> b;
cin >> x;
cin >> s;
cin >> c;
cin >> d;

注意: string は空白で切られる。文全体を読みたいときは getline を使う。

文字列

stoi / stoll / stod

用途: 文字列を数値に変換する

int n = stoi("123");
long long m = stoll("999999999999");
double d = stod("3.14159");

to_string

用途: 数値を文字列に変換する

計算量: O(桁数)。 デバッグ出力やラベル生成で非常に便利。

string s1 = to_string(42);
string s2 = to_string(1000000000000LL);

substr / find / replace

用途: 部分文字列の切り出し・探索・置換

計算量: substr は O(長さ)、find は実装依存だが最悪 O(NM) を意識、replace は O(N) 近傍。 文字列処理は定数倍が重いことがある。

string s = "competitive programming";
string t = s.substr(0, 10);        // "competitive"
size_t p = s.find("program");
if (p != string::npos) {
    s.replace(p, 8, "contest");
}

stringstream

用途: 文字列を空白区切りで分解・結合する

計算量: O(文字数)。 パース処理に便利だが、速度は直読みより遅いことが多い。

stringstream ss;
ss << "10 20 30";
int a, b, c;
ss >> a >> b >> c;

Hamming Distance

用途: 同寸法の文字列・配列・ビット列における対応要素の不一致数の計算、データ誤り検出、類似度判定

計算量: 配列・文字列比較: O(N)(N は要素数)、整数(ビット列): O(1)。 同じ長さをもつ 2 つのデータ列において、対応する位置の要素が異なる箇所の個数を求める。ACL不要。

// 2 つの文字列または配列間のハミング距離を計算 (O(N))
template <typename T>
long long hamming_distance(const T& a, const T& b) {
    if (a.size() != b.size()) return -1; // 長さが異なる場合は定義不可
    long long dist = 0;
    for (size_t i = 0; i < a.size(); ++i) {
        if (a[i] != b[i]) dist++;
    }
    return dist;
}

// 2 つの整数 (ビット列) 間のハミング距離を計算 (O(1))
long long hamming_distance_bit(unsigned long long a, unsigned long long b) {
    return __builtin_popcountll(a ^ b);
}

【主要な関数・引数】
・hamming_distance(a, b) : 同じ長さの文字列や配列 a, b の不一致箇所の個数を返す。長さが異なるときは -1 を返す。
・hamming_distance_bit(a, b) : 64ビット整数 a, b をビット列とみなした際、異なっているビットの個数を高速に計算する。

▼ 応用テンプレート(コピペ&即使用用)

基本的な文字列・配列のハミング距離
string S = "karolin";
string T = "kathrin";

long long dist = hamming_distance(S, T);
// dist -> 3 ('r'!= 't', 'o'!= 'h', 'l'!= 'r' の3箇所が不一致)
整数(ビット列)のハミング距離
unsigned long long A = 0b10111; // 23
unsigned long long B = 0b10001; // 17

long long bit_dist = hamming_distance_bit(A, B);
// bit_dist -> 2 (2進数表示時の2文字目・3文字目が不一致)

K-Hamming search (K回以下の不一致を許容する文字列検索)

用途: 曖昧検索、DNA・塩基配列のパターンマッチング、ノイズを含むテキストからのキーワード検出

計算量: 1箇所の照合判定: O(K log N)、全体: O(N K log N)。 (N は文字列 S の長さ)
長さ N の文字列 S から長さ M の文字列 T を探す際、最大 K 文字までの不一致(ハミング距離 K 以下)を許容してマッチする箇所を全て探す。ローリングハッシュと二分探索を組み合わせて「一致している連続区間(LCP)」を O(log N) でスキップし、不一致箇所でのみ K を消費することで、O(NM) の愚直な照合を劇的に高速化する。ACL不要。

// ローリングハッシュ構造体 (衝突対策として 2^61-1 のモジュロを使用)
struct RollingHash {
    using ull = unsigned long long;
    const ull mod = (1ULL << 61) - 1;
    const ull base = 2023; // 任意の基数
    vector hash, power;
    
    ull calc_mod(ull x) const {
        ull res = (x >> 61) + (x & mod);
        if (res >= mod) res -= mod;
        return res;
    }
    
    ull mul(ull a, ull b) const {
        ull au = a >> 31, ad = a & ((1ULL << 31) - 1);
        ull bu = b >> 31, bd = b & ((1ULL << 31) - 1);
        ull mid = ad * bu + au * bd;
        ull midu = mid >> 30, midd = mid & ((1ULL << 30) - 1);
        return calc_mod(au * bu * 2 + midu + (midd << 31) + ad * bd);
    }
    
    RollingHash(const string& s) {
        int n = s.size();
        hash.assign(n + 1, 0);
        power.assign(n + 1, 1);
        for (int i = 0; i < n; i++) {
            hash[i + 1] = calc_mod(mul(hash[i], base) + s[i]);
            power[i + 1] = mul(power[i], base);
        }
    }
    
    // [l, r) のハッシュ値を取得
    ull get(int l, int r) const {
        ull res = hash[r] + mod - mul(hash[l], power[r - l]);
        return calc_mod(res);
    }
    
    // 二分探索による LCP (Longest Common Prefix) の計算: O(log N)
    int get_lcp(const RollingHash& b, int l1, int r1, int l2, int r2) const {
        int len = min(r1 - l1, r2 - l2);
        int low = 0, high = len + 1;
        while (high - low > 1) {
            int mid = (low + high) / 2;
            if (get(l1, l1 + mid) == b.get(l2, l2 + mid)) low = mid;
            else high = mid;
        }
        return low;
    }
};

// 最大 K 回の不一致を許容して S から T を検索し、マッチする開始インデックスのリストを返す
vector search_with_k_mismatches(const string& S, const string& T, int K) {
    vector res;
    int n = S.size(), m = T.size();
    if (n < m) return res;
    
    RollingHash rhS(S), rhT(T);
    
    for (int i = 0; i <= n - m; i++) {
        int mismatches = 0;
        int s_pos = i, t_pos = 0;
        
        while (t_pos < m && mismatches <= K) {
            // 一致している区間を O(log N) で一気にスキップ
            int lcp = rhS.get_lcp(rhT, s_pos, n, t_pos, m);
            s_pos += lcp;
            t_pos += lcp;
            
            // 不一致箇所を見つけた場合
            if (t_pos < m) {
                mismatches++;
                s_pos++;
                t_pos++;
            }
        }
        
        // 不一致が K 回以内なら条件を満たす
        if (mismatches <= K) {
            res.push_back(i);
        }
    }
    return res;
}

【主要な関数・引数】
・search_with_k_mismatches(S, T, K) : 文字列 S から文字列 T との不一致が K 箇所以内である部分文字列を探し、マッチした開始位置(0-indexed)のリストを返す。
・get_lcp(b, l1, r1, l2, r2) : 自身とハッシュ b の指定区間の先頭から一致している文字数(LCP)を二分探索を用いて O(log N) で求める。

▼ 応用テンプレート(コピペ&即使用用)

DNA配列などでの1文字不一致(K=1)を許容する検索
string S = "AGCTTAGCTAGCT";
string T = "AGCC";
int K = 1;

vector match_positions = search_with_k_mismatches(S, T, K);
// match_positions -> {0, 5, 9}
// S[0..3]="AGCT" (T="AGCC"と1文字不一致)
// S[5..8]="AGCT" (1文字不一致)
// S[9..12]="AGCT" (1文字不一致)
完全一致検索としての利用(K=0)
string text = "abracadabra";
string pattern = "abra";

// K=0 を指定すれば通常のローリングハッシュを用いた文字列検索(O(N))として動作する
vector exact_matches = search_with_k_mismatches(text, pattern, 0);
// exact_matches -> {0, 7}

Levenshtein Distance (レーベンシュタイン距離 / 編集距離)

用途: 文字列や配列の類似度計算、スペルミス訂正(曖昧検索)、DNA配列の比較、上限Kを指定した高速な近傍検索

計算量: 時間計算量 O(|S||T|) または O(K min(|S|, |T|))、空間計算量 O(|T|)。 文字列 S に「挿入」「削除」「置換」の操作を施して文字列 T に変換するための最小操作回数を求める。許容最大距離 K を指定することで、バンドDP(Ukkonen's Algorithm)により計算範囲を対角線近傍に絞り、劇的な高速化が可能。ACL不要。

// 2つの配列・文字列の編集距離を空間 O(|T|) で求める。
// K を指定した場合、距離が K を超えれば K + 1 を返し、時間 O(K min(|S|, |T|)) で動作する。
template <typename Seq>
long long levenshtein_distance(const Seq& S, const Seq& T, long long K = -1) {
    long long n = S.size();
    long long m = T.size();
    if (n == 0) return (K == -1 || m <= K) ? m : K + 1;
    if (m == 0) return (K == -1 || n <= K) ? n : K + 1;

    // 文字数差の時点で K を超えるなら早期リターン
    if (K != -1 && abs(n - m) > K) return K + 1;

    const long long INF = 1e16;
    vector<long long> dp(m + 1, INF);

    // 0行目の初期化
    long long j_limit = (K == -1) ? m : min(m, K);
    for (long long j = 0; j <= j_limit; ++j) {
        dp[j] = j;
    }

    for (long long i = 1; i <= n; ++i) {
        // バンドDPのための計算範囲(K=-1なら全範囲)
        long long j_min = (K == -1) ? 1 : max(1LL, i - K);
        long long j_max = (K == -1) ? m : min(m, i + K);

        long long prev = dp[j_min - 1]; // dp[i-1][j_min-1] に相当
        if (j_min > 1) {
            dp[j_min - 1] = INF; // バンド左外側からの遷移を無効化
        } else {
            dp[0] = (K == -1 || i <= K) ? i : INF;
        }

        for (long long j = j_min; j <= j_max; ++j) {
            long long temp = dp[j];
            long long cost = (S[i - 1] == T[j - 1]) ? 0 : 1;
            
            dp[j] = min({
                dp[j] + 1,      // Sの文字を削除 (上から)
                dp[j - 1] + 1,  // Tの文字を挿入 (左から)
                prev + cost     // 置換または一致 (左上から)
            });
            prev = temp;
        }
    }
    
    long long res = dp[m];
    return (K != -1 && res > K) ? K + 1 : res;
}

【主要な関数・引数】
・K (任意) : 許容する最大編集距離。「K手以内で一致できるか」の判定に使用する。指定すると、DPの探索範囲が対角線からの幅 $2K$(バンド状)に限定され、実行時間が $O(K \min(|S|, |T|))$ まで短縮される。距離が $K$ を超える場合は強制的に $K+1$ を返す。
・j_min, j_max : バンドDPにおける配列走査の下限と上限。無駄な状態計算を省くためのクリッピング処理を担う。

▼ 応用テンプレート(コピペ&即使用用)

上限Kを利用した高速なスペルチェック・曖昧検索
string target = "algorithm";
vector<string> dict = {"algorism", "logarithm", "rhythm", "algo", "alghoritm"};
long long K = 2; // 2手以内(タイポ2文字まで)を許容

vector<string> candidates;
for (const auto& word : dict) {
    // Kを指定することで、O(|S||T|) の重い探索を O(K min(|S|, |T|)) で打ち切る
    if (levenshtein_distance(word, target, K) <= K) {
        candidates.push_back(word);
    }
}
// candidates -> {"algorism", "alghoritm"}
基本的な文字列の編集距離(無制限)
string S = "kitten";
string T = "sitting";

// Kを省略すると通常の O(|S||T|) で正確な距離を算出
long long dist = levenshtein_distance(S, T);
// dist -> 3 
// k -> s (置換), e -> i (置換), 末尾に g を追加 (挿入)
配列 (vector) に対する編集距離
vector<int> A = {1, 2, 3, 4, 5};
vector<int> B = {1, 3, 4, 9, 5};

long long dist = levenshtein_distance(A, B);
// dist -> 2 (2を削除、9を挿入)

最長共通部分列 (LCS - 愚直解/基本形)

用途: 2つの文字列(または配列)に共通して含まれる部分列のうち、最も長いものの「長さ」や「部分列そのもの」を求める

計算量: O(|S||T|)
設計: 2次元の動的計画法 (DP) を用います。dp[i][j] を「文字列 S の i 文字目までと、文字列 T の j 文字目までを見たときの LCS の長さ」と定義します。末尾の文字が一致すれば dp[i-1][j-1] + 1、一致しなければ max(dp[i-1][j], dp[i][j-1]) としてテーブルを埋めていきます。

// 2つの文字列 S, T の LCS の長さを O(|S||T|) で求める
ll lcs_length(const string& S, const string& T) {
    ll n = S.size();
    ll m = T.size();
    
    // dp[i][j] := Sをi文字、Tをj文字目まで見たときのLCSの長さ
    vector<vector<ll>> dp(n + 1, vector<ll>(m + 1, 0));

    for (ll i = 0; i < n; i++) {
        for (ll j = 0; j < m; j++) {
            if (S[i] == T[j]) {
                // 文字が一致していれば、両方から1文字ずつ進める
                dp[i + 1][j + 1] = dp[i][j] + 1;
            } else {
                // 一致していなければ、どちらかを1文字進めた状態の最大値を取る
                dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]);
            }
        }
    }
    
    return dp[n][m];
}

注意: 空間計算量も O(|S||T|) となります。文字数(|S|, |T|)がそれぞれ 3000〜5000 程度までなら通常は問題なく動作しますが、105 オーダーになるとメモリ(および実行時間)の制限に引っかかるため、配列の使い回し(空間計算量 O(|T|) への削減)や、先に挙げた高速化手法を検討する必要があります。

▼ 使い方と応用パターン

応用:LCS の長さだけでなく、文字列そのものを復元する
string restore_lcs(const string& S, const string& T) {
    ll n = S.size();
    ll m = T.size();
    vector<vector<ll>> dp(n + 1, vector<ll>(m + 1, 0));

    for (ll i = 0; i < n; i++) {
        for (ll j = 0; j < m; j++) {
            if (S[i] == T[j]) dp[i + 1][j + 1] = dp[i][j] + 1;
            else dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]);
        }
    }

    // DPテーブルを後ろから辿って文字列を復元する
    string res = "";
    ll i = n, j = m;
    while (i > 0 && j > 0) {
        if (dp[i][j] == dp[i - 1][j]) {
            i--; // S側の文字を減らす
        } else if (dp[i][j] == dp[i][j - 1]) {
            j--; // T側の文字を減らす
        } else {
            // 左上から来た場合(文字が一致している)
            res += S[i - 1];
            i--;
            j--;
        }
    }
    // 後ろから辿ったので反転させる
    reverse(res.begin(), res.end());
    return res;
}

// 使い方
// string S = "axyb";
// string T = "aby";
// cout << restore_lcs(S, T) << endl; // "ay" または "ab" (この場合 "ay")

高速化された LCS (片方の要素が重複しない場合)

用途: 文字列 S の各文字(または配列の要素)がすべて異なるとき、S と T の最長共通部分列 (LCS) の長さを高速に求め、さらにその文字列自体を復元する

計算量: O(|S| + |T| log |T|)
設計: S の文字に重複がないことを利用し、S の各文字を「S の中でのインデックス」に置き換えます。その後、T の各文字を S の対応するインデックスに変換した数列を作ると、この数列における最長増加部分列 (LIS) の長さが、元の S と T の LCS の長さと一致します。LIS は二分探索(std::lower_bound)を用いて O(|T| log |T|) で計算できます。

// S の各文字が互いに異なる場合の LCS を O(|S| + |T|log|T|) で求める
ll optimized_lcs(const string& S, const string& T) {
    // 1. S の各文字の出現位置を記録(S の文字は重複しない前提)
    // ※文字の種類が ASCII (256種類) に収まる場合の配列。
    // ※数値配列や種類が多い場合は map<ll, ll> などを利用してください。
    vector<ll> pos(256, -1); 
    for (ll i = 0; i < (ll)S.size(); i++) {
        pos[S[i]] = i;
    }

    // 2. T の各文字を S でのインデックスに変換し、配列 A を作成
    vector<ll> A;
    for (char c : T) {
        if (pos[c] != -1) {
            A.push_back(pos[c]);
        }
    }

    // 3. 配列 A の LIS (最長増加部分列) の長さを求める
    const ll INF = 1e18;
    vector<ll> dp(A.size() + 1, INF);
    
    for (ll x : A) {
        // x 以上の値が最初に現れる位置を二分探索で見つけて更新
        auto it = lower_bound(dp.begin(), dp.end(), x);
        *it = x;
    }
    
    // INF でない要素の数が LIS (=LCS) の長さ
    ll lcs_len = lower_bound(dp.begin(), dp.end(), INF) - dp.begin();
    return lcs_len;
}

注意: このアルゴリズムは S に同じ要素が2つ以上存在しない という強い制約がある場合にのみ有効です。数列(vector<ll>)に対して適用する場合は、pos の管理を map<ll, ll> や座標圧縮を用いた配列に変更してください。

▼ 使い方と応用パターン

基本の使い方
void solve() {
    // S は文字に重複がない ("abcde")
    string S = "abcde";
    // T は文字に重複があってもよい
    string T = "acebd";

    ll ans = optimized_lcs(S, T);
    
    // T の 'a', 'c', 'e' もしくは 'a', 'b', 'd' などを拾う
    // 最長は "ace" や "abd" なので長さは 3
    cout << "LCSの長さ: " << ans << endl; // 3
}
応用:インデックスを追跡して LCS の文字列そのものを復元する
// 長さだけでなく、復元された文字列を返すバージョン
string restore_optimized_lcs(const string& S, const string& T) {
    vector<ll> pos(256, -1);
    for (ll i = 0; i < (ll)S.size(); i++) pos[S[i]] = i;

    vector<ll> A;
    for (char c : T) {
        if (pos[c] != -1) A.push_back(pos[c]);
    }

    if (A.empty()) return "";

    const ll INF = 1e18;
    vector<ll> dp(A.size(), INF);
    // 復元用の配列
    vector<ll> id(A.size(), -1);  // dp[i] に入っている要素の、配列 A におけるインデックス
    vector<ll> pre(A.size(), -1); // A[i] が LIS に追加されたときの、直前の要素の A におけるインデックス
    
    ll lcs_len = 0;
    ll last_idx = -1; // LIS の末尾となる要素の A におけるインデックス

    for (ll i = 0; i < (ll)A.size(); i++) {
        ll p = lower_bound(dp.begin(), dp.end(), A[i]) - dp.begin();
        dp[p] = A[i];
        id[p] = i;
        if (p > 0) pre[i] = id[p - 1]; // LIS における一つ前の要素のインデックスを記録
        
        if (p + 1 > lcs_len) {
            lcs_len = p + 1;
            last_idx = i;
        }
    }

    // 記録したインデックスを後ろから辿って文字列を復元
    string res = "";
    ll curr = last_idx;
    while (curr != -1) {
        res += S[A[curr]]; // A[curr] は S におけるインデックス
        curr = pre[curr];
    }
    reverse(res.begin(), res.end());
    
    return res;
}

// 使い方
// string S = "abcde";
// string T = "acebd";
// cout << restore_optimized_lcs(S, T) << endl; // "ace" または "abd" (この実装では "abd" が出力される)

Z-Algorithm

用途: O(N)での最長共通接頭辞(LCP)の計算、文字列検索、周期性判定

計算量: O(N)。 文字列 S と、S の i 文字目以降の接尾辞 S[i:|S|-1] との最長共通接頭辞の長さを線形時間で構築する。ACL不要。

// S と S[i:|S|-1] の最長共通接頭辞(LCP)を O(N) で求める構造体
struct ZArray {
  ll n;
  vector<ll> z;

  // 通常の文字列用
  ZArray(const string& s) {
    build(s);
  }

  // 複数文字列を扱う場合や数列用(番兵を利用可能)
  template <typename T>
  ZArray(const vector<T>& v) {
    build(v);
  }

private:
  template <typename Container>
  void build(const Container& s) {
    n = s.size();
    z.assign(n, 0);
    if (n == 0) return;
    z[0] = n;
    ll i = 1, j = 0;
    while (i < n) {
      while (i + j < n && s[j] == s[i + j]) j++;
      z[i] = j;
      if (j == 0) {
        i++;
        continue;
      }
      ll k = 1;
      while (i + k < n && k + z[k] < j) {
        z[i + k] = z[k];
        k++;
      }
      i += k;
      j -= k;
    }
  }
};

【構造体を構成する変数】
・n : 対象の文字列(または配列)全体の長さ(整数)
・z : Z-Algorithmによって計算された配列本体。z[i]には S と S[i:|S|-1] の共通する先頭文字数が格納される。

▼ 応用テンプレート(コピペ&即使用用)

基本的なZ配列の取得
string S = "aaabaaaab";
ZArray z_arr(S);

// z_arr.z は {9, 2, 1, 0, 3, 4, 2, 1, 0} になる
// z[0] は常に S の文字数と等しい
// z[5] = 4 ("aaab" が先頭と一致)
文字列検索 (Pattern Matching / O(|T| + |P|))
string T = "abracadabra"; // 検索対象
string P = "abra";        // 検索パターン

// vectorを用いた汎用検索(文字種に依存せず安全に番兵を使える)
vector<ll> V;
for (char c : P) V.push_back(c);
V.push_back(-1); // PにもTにも含まれない番兵
for (char c : T) V.push_back(c);

ZArray z_arr(V);
vector<ll> match_indices;
ll p_len = P.size();

for (ll i = p_len + 1; i < z_arr.n; i++) {
  if (z_arr.z[i] == p_len) {
    // マッチした開始位置(元のTにおけるインデックス)
    match_indices.push_back(i - (p_len + 1)); 
  }
}
// match_indices -> {0, 7}
文字列の最小周期の判定 (Periodicity Check)
string S = "ababab";
ZArray z_arr(S);
ll n = S.size();
ll min_period = n; // デフォルトは文字列全体の長さ

for (ll i = 1; i < n; i++) {
  // i が周期となる条件: 
  // 1. 文字列長 n が i で割り切れる
  // 2. i 文字目以降の接尾辞が先頭と n - i 文字一致する
  if (n % i == 0 && z_arr.z[i] == n - i) {
    min_period = i;
    break;
  }
}
// min_period -> 2 ("ab" の繰り返し)

Manacher's Algorithm

用途: O(N)での最長回文部分文字列の検出、各文字を中心とする回文の半径の計算

計算量: O(N)。 文字列 S の各文字(または要素)を中心とした最長回文の半径(中心から端までの文字数)を線形時間で構築する。ACL不要。

// S の各文字を中心とする最長回文の半径を O(N) で求める構造体
struct Manacher {
  ll n;
  vector<ll> R;

  // 通常の文字列用
  Manacher(const string& s) {
    build(s);
  }

  // 複数文字列を扱う場合や数列用、ダミー文字挿入後の配列用
  template <typename T>
  Manacher(const vector<T>& v) {
    build(v);
  }

private:
  template <typename Container>
  void build(const Container& s) {
    n = s.size();
    R.assign(n, 0);
    ll i = 0, j = 0;
    while (i < n) {
      while (i - j >= 0 && i + j < n && s[i - j] == s[i + j]) ++j;
      R[i] = j;
      ll k = 1;
      while (i - k >= 0 && i + k < n && k + R[i - k] < j) {
        R[i + k] = R[i - k];
        ++k;
      }
      i += k;
      j -= k;
    }
  }
};

【構造体を構成する変数】
・n : 対象の文字列(または配列)全体の長さ(整数)
・R : Manacherによって計算された配列本体。R[i]には S[i] を中心とする最長回文の半径(自身を含め片側に何文字延びるか)が格納される。

▼ 応用テンプレート(コピペ&即使用用)

基本的な最長回文半径の取得(奇数長)
string S = "abaaab";
Manacher man(S);

// man.R は {1, 2, 1, 2, 3, 1} になる
// S[4] ('a') を中心とする回文は "baaab" なので半径は 3 (man.R[4] = 3)
// 各 i について、回文の実際の長さは 2 * man.R[i] - 1 となる
偶数長の回文も検出する (ダミー文字の挿入)
string S = "abba"; 
// 通常の Manacher では各文字を中心とする奇数長しか取れないため、
// 文字の間にダミー文字(Sに含まれない文字)を挟むことで偶数長回文の中心を作る

vector<ll> V;
for (char c : S) {
  V.push_back(c);
  V.push_back(-1); // ダミー文字(文字コードと被らない値)
}
V.pop_back(); // 最後のダミー文字は削除 (a, -1, b, -1, b, -1, a) になる

Manacher man(V);

// V における各中心の半径 man.R[i] から、元の文字列での回文長は (man.R[i] + 1) / 2 となる
ll max_len = 0;
for (ll i = 0; i < man.n; i++) {
  ll palin_len = man.R[i]; // ダミー文字を挟んだ状態での半径と、元の文字列での回文長は一致する(※番兵なし構築の場合)
  
  // 例: V[3] (-1, 中心は "bb" の間) の半径は 4 (b, -1, b, -1) -> 実際の長さは 4
  if (palin_len > max_len) {
      max_len = palin_len;
  }
}
// max_len -> 4 ("abba")
文字列の部分区間が回文かどうかの判定 O(1)
string S = "abacaba";
Manacher man(S);

// 区間 [l, r) (元の文字列における 0-indexed)が回文か判定する関数
// 奇数長・偶数長問わず対応するには、上記のようにダミー文字を挟んだ配列 V を用意し、
// それに対して Manacher を構築しておく必要がある。

// ※ダミー文字を挟んだ長さ 2|S|-1 の配列で構築した場合:
// 元の区間 [l, r) の中心は V における l + r - 1 の位置になる
// したがって、man.R[l + r - 1] >= r - l であれば回文である。

auto is_palindrome = [&](ll l, ll r, const Manacher& m_dummy) {
  return m_dummy.R[l + r - 1] >= r - l;
};

AtCoder Library (ACL) Suffix Array

用途: O(N)でのSuffix Array・LCP Arrayの構築(文字列検索、LCS、部分文字列解析)

計算量: O(N)。 パース処理に便利だが、速度は直読みより遅いことが多い。

#include <atcoder/string>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

// ACLをラップし、任意2点間LCPクエリ(RMQ)をサポートした実戦用構造体
struct AclStringAutomaton {
  int n;
  vector<int> sa;
  vector<int> lcp;
  vector<int> rank;
  vector<vector<int>> st;
  vector<int> log_table;

  // 通常の文字列用
  AclStringAutomaton(const string& s) {
    n = s.size();
    sa = atcoder::suffix_array(s);
    lcp = atcoder::lcp_array(s, sa);
    build();
  }

  // 複数文字列を扱う場合用(int配列版番兵を利用可能)
  // 例: SとTを合わせる際、Sの各文字、200000(番兵1)、Tの各文字、200001(番兵2) のように格納
  AclStringAutomaton(const vector<int>& v) {
    n = v.size();
    sa = atcoder::suffix_array(v);
    lcp = atcoder::lcp_array(v, sa);
    build();
  }

  // 元の文字配列のインデックス i と j から始まる接尾辞の LCP を O(1) で取得
  int get_lcp(int i, int j) {
    if (i < 0 || i >= n || j < 0 || j >= n) return 0; // 範囲外アクセスを防止
    if (i == j) return n - i;
    int r_i = rank[i], r_j = rank[j];
    if (r_i > r_j) swap(r_i, r_j);
    int l = r_i, r = r_j - 1;
    int len = r - l + 1;
    int k = log_table[len];
    return min(st[k][l], st[k][r - (1 << k) + 1]);
  }

private:
  void build() {
    // rank のサイズは n ぴったりでOKです
    rank.resize(n);

    // i <= n ではなく、i < n に修正(範囲外アクセスを防止)
    for (int i = 0; i < n; i++)
      rank[sa[i]] = i;

    int m = lcp.size(); // m は確実に n-1 になります
    if (m == 0) return; // 要素が1つ以下の場合はSparse Tableの構築をスキップ

    log_table.assign(m + 1, 0);
    for (int i = 2; i <= m; i++)
      log_table[i] = log_table[i >> 1] + 1;

    int max_log = log_table[m] + 1;
    st.assign(max_log, vector<int>(m));
    for (int i = 0; i < m; i++)
      st[0][i] = lcp[i];

    for (int k = 1; k < max_log; k++) {
      for (int i = 0; i + (1 << k) <= m; i++) {
        st[k][i] = min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
      }
    }
  }
};

【automatonを構成する6つの変数】
・n : 番兵を含む、合体配列全体の長さ(整数)
・sa : 接尾辞を辞書順に並び替えた開始インデックスの配列(Suffix Array本体)
・lcp : 辞書順で隣り合う文字列同士が先頭から何文字一致しているかの配列(LCP Array)
・rank : 元の文字列のインデックスから、Suffix Array上の順位を一瞬で引くための逆引き配列
・st : 離れた2点間のLCPを O(1) で呼び出すための区間最小値データ構造(Sparse Table)
・log_table : Sparse Tableの高速クエリに必要な対数(log2)の値をあらかじめ計算しておくための補助テーブル

▼ 応用テンプレート(コピペ&即使用用)

任意の2点間LCPの O(1) 取得 (Basic LCP Query)
string S = "abracadabra";
AclStringAutomaton automaton(S);

// インデックス 0 ("abracadabra") と 8 ("abra") の共通接頭辞の長さ
int len = automaton.get_lcp(0, 8); // -> 4 ("abra")
複数文字列の最長共通部分文字列 (LCS: Longest Common Substring)
// DPで解けるのは部分列、今回のは連続部分列
string A = "banana", B = "ananas";
vector<int> V;
for (char c : A) V.push_back(c);
V.push_back(100005); // A用の番兵
for (char c : B) V.push_back(c);
V.push_back(100006); // B用の番兵

AclStringAutomaton automaton(V);
int max_lcs = 0, best_s_idx = -1;

// i+1 の範囲外アクセスを防ぐため n - 1 までのループに修正
for (int i = 0; i < automaton.n - 1; i++) {
  int s1 = automaton.sa[i], s2 = automaton.sa[i + 1];
  if (s1 == (int)A.size() || s2 == (int)(A.size() + 1 + B.size())) continue;
  
  // 隣り合う接尾辞が異なる文字列由来か判定
  if ((s1 < (int)A.size()) != (s2 < (int)A.size())) {
    if (automaton.lcp[i] > max_lcs) {
      max_lcs = automaton.lcp[i];
      best_s_idx = (s1 < (int)A.size() ? s1 : s2);
    }
  }
}
// 最長共通文字列: A.substr(best_s_idx, max_lcs) -> "anana"
相異なる部分文字列の総数 (Number of Distinct Substrings)
string S = "abracadabra";
int N = S.size();
AclStringAutomaton automaton(S);

// 全部分文字列の候補数から、重複分(LCP配列の総和)を引く
long long ans = (long long)N * (N + 1) / 2;
for (int x : automaton.lcp) {
  ans -= x;
} // ans が相異なる部分文字列の総数
文字列パターン検索 (Pattern Matching / Counting)
string S = "abracadabra";
AclStringAutomaton automaton(S);
string P = "abra";

// Suffix Array のインデックス 0〜N-1 に対して二分探索を行うため初期範囲を修正
int low = -1, high = S.size();
while (high - low > 1) {
  int mid = (low + high) / 2;
  if (S.compare(automaton.sa[mid], P.size(), P) < 0) low = mid;
  else high = mid;
}
int st = high;

low = -1; high = S.size();
while (high - low > 1) {
  int mid = (low + high) / 2;
  if (S.compare(automaton.sa[mid], P.size(), P) <= 0) low = mid;
  else high = mid;
}
int ed = high;

int count = ed - st; // 出現回数

Suffix Tree (via ACL Suffix Array)

用途: O(N)での Suffix Tree 構築(部分文字列の完全な階層構造化、ゲームDP、パターン検索)

計算量: 構築 O(N)。
Ukkonen法による煩雑な直接構築を避け、ACLのSuffix ArrayとLCP配列を利用したCartesian Tree的構築を採用。

#include <atcoder/string>
#include <vector>
#include <string>

using namespace std;

// ACLのSAとLCPを利用してO(N)でSuffix Treeを構築する構造体
struct SuffixTree {
  struct Node {
    int l, r;             // 親からこのノードへ向かう辺の文字列区間 S[l, r)
    int depth;            // 根からこのノードまでの総文字列長
    int parent;           // 親ノードのインデックス
    vector<int> children; // 子ノード群のインデックス

    Node(int l, int r, int depth, int parent)
      : l(l), r(r), depth(depth), parent(parent) {}
  };

  string S;
  vector<Node> nodes;

  SuffixTree(const string& s) : S(s) {
    int n = s.size();
    auto sa = atcoder::suffix_array(s);
    auto lcp = atcoder::lcp_array(s, sa);

    // root (id = 0) を追加
    nodes.emplace_back(0, 0, 0, -1);
    int curr = 0;

    for (int i = 0; i < n; i++) {
      int suffix_idx = sa[i];
      int lcp_val = (i == 0 ? 0 : lcp[i - 1]);

      // 現在のノードから根へ遡り、LCPの深さ以下の分岐点を探す
      while (curr != 0 && nodes[curr].depth > lcp_val) {
        int p = nodes[curr].parent;
        if (nodes[p].depth <= lcp_val) {
          if (nodes[p].depth < lcp_val) {
            // 辺の途中で分岐する必要があるため、ノードを分割 (Edge Split)
            int split_id = nodes.size();
            int split_l = nodes[curr].l;
            int split_r = split_l + (lcp_val - nodes[p].depth);
            nodes.emplace_back(split_l, split_r, lcp_val, p);

            // 親の参照先を新しい分割ノードに更新
            for (int& child : nodes[p].children) {
              if (child == curr) {
                child = split_id;
                break;
              }
            }
            nodes[split_id].children.push_back(curr);
            nodes[curr].parent = split_id;
            nodes[curr].l += (lcp_val - nodes[p].depth);
            
            curr = split_id;
          } else {
            // ちょうど分岐点(親ノード)が LCP の深さと一致する場合
            curr = p;
          }
          break;
        }
        curr = p;
      }

      // 新しい葉ノードを追加
      int leaf_id = nodes.size();
      nodes.emplace_back(suffix_idx + lcp_val, n, n - suffix_idx, curr);
      nodes[curr].children.push_back(leaf_id);
      curr = leaf_id;
    }
  }
};

【SuffixTreeを構成する変数】
・S : 元の文字列
・nodes : ツリーを構成する全ノード(頂点)の配列。インデックス 0 が根(Root)になります。※ノード分割(Edge Split)が発生するため、インデックスの大小とトポロジカル順は必ずしも一致しません。

【各Nodeが持つ変数】
・l, r : 親からこのノードへ来るまでの辺に割り当てられた文字列区間(S[l, r))。辺の文字数は r - l で求まります。
・depth : 根からこのノードまでの総文字列長(部分文字列の長さ)。
・parent : 親ノードのインデックス。
・children : 子ノードのインデックスを格納する配列。

▼ 使い方・情報取得コマンド

1. 構築と基本情報の取得
string S = "banana";
S += '$'; // 必須: すべての接尾辞を独立した葉にするため終端文字(番兵)を追加
SuffixTree tree(S); // 構築 (O(N))

// 木全体の頂点数(最大でも 2N 程度に収まります)
int num_nodes = tree.nodes.size();

// 根(Root)のインデックスは常に 0 です
int root_id = 0;
2. 辺の情報(文字列・長さ)へのアクセス
int u = 1; // 任意のノード(u > 0)

// 親からノード u へ向かう辺の文字数(圧縮された長さ)
int edge_len = tree.nodes[u].r - tree.nodes[u].l;

// 辺に割り当てられている実際の文字列を取得
string edge_str = S.substr(tree.nodes[u].l, edge_len);

// 根からノード u までの総文字数(部分文字列の長さ)
int total_len = tree.nodes[u].depth;
3. 木の探索(親・子への移動)
int u = 0; // 根からスタート

// 子ノードへの遷移
for (int v : tree.nodes[u].children) {
  // v が子ノードのインデックス
  cout << "Child ID: " << v << endl;
}

// 親ノードへの遷移 (根の親は -1 となります)
int p = tree.nodes[u].parent;
4. 木DP(葉から根へのボトムアップ走査)
// Edge Splitによりインデックス順≠トポロジカル順となるため、
// 配列の逆順ループではなく、ローカル構造体を用いた通常の再帰(DFS)でボトムアップに処理します。
struct Dfs {
  const SuffixTree& tree;
  int run(int u) {
    if (tree.nodes[u].children.empty()) {
      // 葉ノードの場合の初期化など
      return 1;
    }
    int sum = 0;
    for (int v : tree.nodes[u].children) {
      // 子から親へ情報をまとめる
      sum += run(v);
    }
    return sum;
  }
};

Dfs dfs{tree};
// 根(0)から探索を開始し、dp_rootに木全体(根)の答えが入る
int dp_root = dfs.run(0);

Trie Tree (Prefix Tree)

用途: 文字列集合の高速な管理、共通接頭辞(Prefix)の検索、辞書順走査、Trie木上でのDP

計算量: 挿入・検索・削除・LCP取得など 基本操作はすべて O(|S|) (|S| は文字列の長さ)。
競技プログラミング等の本番で要求されやすい「特定文字列の正確な削除」「辞書順k番目の取得」を追加し、内部構造の拡張性も確保。

// char_size: 文字の種類数 (英小文字のみなら26)
// base: 基準となる文字 ('a', 'A', '0' など)
template <int char_size = 26, int base = 'a'>
struct Trie {
  struct Node {
    vector<int> next;       // 子ノードのインデックス (-1は子が存在しない)
    int accept_count;       // このノードを「終端」とする文字列の数
    int pass_count;         // このノードを「通過」した文字列の数 (部分木の要素数)
    
    // --- 汎用拡張データ ---
    vector<int> accept_ids; // このノードで終わる文字列のラベル(追加時のIDなど)
    long long val;          // 関数値、重み、DPの結果などを持たせる汎用変数

    Node() : next(char_size, -1), accept_count(0), pass_count(0), val(0) {}
  };

  vector<Node> nodes;

  Trie() {
    nodes.push_back(Node()); // root (id = 0) を追加
  }

  // 現在のTrie木に含まれる全文字列の数を取得 O(1)
  int size() const {
    return nodes[0].pass_count;
  }

  // 文字列の挿入
  void insert(const string &s, int id = -1, long long val = 0) {
    int curr = 0;
    nodes[curr].pass_count++;
    for (char c : s) {
      int to = c - base;
      if (nodes[curr].next[to] == -1) {
        nodes[curr].next[to] = nodes.size();
        nodes.push_back(Node());
      }
      curr = nodes[curr].next[to];
      nodes[curr].pass_count++;
    }
    nodes[curr].accept_count++;
    if (id != -1) nodes[curr].accept_ids.push_back(id);
    nodes[curr].val += val; 
  }

  // 単一文字列の正確な削除 (集合からsを1つだけ減らす)
  // 成功したらtrue, そもそも存在しなかったらfalseを返す
  bool erase(const string &s) {
    int curr = 0;
    vector<int> path;
    for (char c : s) {
      path.push_back(curr);
      int to = c - base;
      if (nodes[curr].next[to] == -1) return false; // 存在しない
      curr = nodes[curr].next[to];
    }
    if (nodes[curr].accept_count == 0) return false; // 終端として存在しない
    
    nodes[curr].accept_count--;
    nodes[curr].pass_count--;
    for (int p : path) nodes[p].pass_count--; // 祖先の通過数を減らす
    return true;
  }

  // 完全一致する文字列の個数を取得
  int count(const string &s) {
    int curr = 0;
    for (char c : s) {
      int to = c - base;
      if (nodes[curr].next[to] == -1) return 0;
      curr = nodes[curr].next[to];
    }
    return nodes[curr].accept_count;
  }

  // 終端ノードのID検索 (見つかれば終端ノードのIDを、なければ -1)
  int search(const string &s) {
    int curr = 0;
    for (char c : s) {
      int to = c - base;
      if (nodes[curr].next[to] == -1) return -1;
      curr = nodes[curr].next[to];
    }
    return nodes[curr].accept_count > 0 ? curr : -1;
  }

  // Prefix(接頭辞)検索 (S から始まる文字列の数を返す)
  int count_prefix(const string &s) {
    int curr = 0;
    for (char c : s) {
      int to = c - base;
      if (nodes[curr].next[to] == -1) return 0;
      curr = nodes[curr].next[to];
    }
    return nodes[curr].pass_count;
  }

  // 特定の文字列をprefixに持つ要素を全て削除する
  void erase_by_prefix(const string &s) {
    if (s.empty()) { 
      nodes[0] = Node(); // 空文字列指定は全クリアと同義
      return;
    }
    int curr = 0;
    vector<int> path;
    for (char c : s) {
      path.push_back(curr);
      int to = c - base;
      if (nodes[curr].next[to] == -1) return; // 存在しない
      curr = nodes[curr].next[to];
    }
    int removed = nodes[curr].pass_count;
    if (removed == 0) return;
    
    nodes[path.back()].next[s.back() - base] = -1; // 木から切断
    for (int p : path) nodes[p].pass_count -= removed;
  }

  // sがprefixとなるような、木内の文字列の「総数」ではなく
  // 「sのprefixとなるような木内の文字列」の個数 (s="catch"なら"cat", "catch"等)
  int count_prefixes_of(const string &s) {
    int curr = 0, res = 0;
    for (char c : s) {
      res += nodes[curr].accept_count;
      int to = c - base;
      if (nodes[curr].next[to] == -1) return res;
      curr = nodes[curr].next[to];
    }
    res += nodes[curr].accept_count;
    return res;
  }

  // 最長共通接頭辞 (LCP) のマッチ長取得
  // 木に含まれるいずれかの文字列と、sが先頭から何文字一致するか
  int get_lcp_length(const string &s) {
    int curr = 0, len = 0;
    for (char c : s) {
      int to = c - base;
      if (nodes[curr].next[to] == -1) break;
      curr = nodes[curr].next[to];
      len++;
    }
    return len;
  }

  // 辞書順で k 番目 (1-indexed) の文字列を取得
  // k が要素数を超える、または k <= 0 の場合は空文字列を返す
  string kth_element(int k) {
    if (k <= 0 || k > nodes[0].pass_count) return "";
    int curr = 0;
    string res = "";
    while (true) {
      if (k <= nodes[curr].accept_count) return res;
      k -= nodes[curr].accept_count;
      for (int i = 0; i < char_size; i++) {
        int nxt = nodes[curr].next[i];
        if (nxt != -1) {
          if (k <= nodes[nxt].pass_count) {
            res += (char)(base + i);
            curr = nxt;
            break;
          } else {
            k -= nodes[nxt].pass_count;
          }
        }
      }
    }
  }
};

【各Nodeが持つ変数の詳細な意味】
・next[c] : 文字 c に対応する子ノードのインデックス。無い場合は -1。
・accept_count : 「このノードでピッタリ終わる」文字列がいくつ登録されたか。削除されると減ります。
・pass_count : 「このノードを通る」(または終わる)文字列の総数。つまり、このノードを根とする部分木の要素数です。

▼ 応用テンプレート(コピペ&即使用用)

0. 内部構造についてのメモ

Q. 親ノードに遡りたい場合は?

Trie木はデフォルトでは親へのポインタを持っていません。対処法は2つです。
1. 探索時に vector<int> path; を用意し、進むたびに path.push_back(curr); して履歴を残す(eraseの実装を参考にしてください)。
2. 構造体 Node の定義に int parent; を足し、コンストラクタで -1 に初期化。insert内のノード作成時に nodes[nodes.size()-1].parent = curr; を追加すれば、curr = nodes[curr].parent; で遡れるようになります。

Q. 特定の文字の子ノードが存在するか調べたい

int c = 'x' - 'a'; // 'a'の部分はbaseに合わせて
if (trie.nodes[curr].next[c] != -1) {
    // 子が存在する
    int child = trie.nodes[curr].next[c];
}

Q. 木全体を走査してDP等を回したい

pass_count が 0 のノードは実質「空」なので、探索を枝刈りできます。また、子へ再帰するDFSを書くことで、辞書順走査や木DPが簡単に実装可能です(後述のテンプレート3を参照)。

Q. ノードの「値」を更新したい

search 等で返ってきた node_id を使って、直接 trie.nodes[node_id].val = 100; のようにアクセスして書き換えるのが一番簡単です。

1. 文字列の追加 / 削除 / 個数カウント
Trie<26, 'a'> trie;

trie.insert("apple");
trie.insert("apple");
trie.insert("app");

cout << trie.size() << endl; // 3 (全要素数)
cout << trie.count("apple") << endl; // 2 (appleの数)

trie.erase("apple"); // 1つだけ消す
cout << trie.count("apple") << endl; // 1になる
cout << trie.size() << endl; // 2になる
2. Prefix (接頭辞) 検索とLCPの取得
Trie<26, 'a'> trie;
trie.insert("camera");
trie.insert("camp");
trie.insert("cat");

// "ca" から始まる文字列の数を O(|S|) で取得
int cnt = trie.count_prefix("ca"); // -> 3
int cnt2 = trie.count_prefix("cam"); // -> 2

// "camel" と木内の文字列が最大何文字一致するか (LCP)
int lcp = trie.get_lcp_length("camel"); // -> 3 ("cam"まで一致)
3. 辞書順 k番目の取得 / DFSによる走査
// 計算量は、出力される文字列をSとして、O(S)
// 例えば、出力される文字列の長さの和に関して制約があれば有効
// そうでないときは、suffix arrayに逃げると良い
                                    
Trie<26, 'a'> trie;
trie.insert("cat");
trie.insert("apple");
trie.insert("banana");

// 辞書順で1番目(1-indexed)を取得 -> "apple"
string first = trie.kth_element(1);
// 3番目を取得 -> "cat"
string third = trie.kth_element(3);

// 【自作DFSで辞書順に全列挙する例】
auto dfs = [&](auto self, int u, string current_str) -> void {
    // このノードで終わる文字列があるなら出力等を行う
    for (int k = 0; k < trie.nodes[u].accept_count; k++) {
        cout << current_str << endl;
    }
    // 辞書順(0〜25)に子ノードへ潜る
    for (int i = 0; i < 26; i++) {
        int v = trie.nodes[u].next[i];
        if (v != -1 && trie.nodes[v].pass_count > 0) {
            self(self, v, current_str + char('a' + i));
        }
    }
};
dfs(dfs, 0, ""); // 根(0)から空文字列でスタート
4. 複数削除と内包Prefixのカウント
Trie<26, 'a'> trie;
trie.insert("cat");
trie.insert("catch");
trie.insert("camera");

// "catch" のprefixとなる木の要素数をカウント ("cat", "catch" の2つ)
int pre_cnt = trie.count_prefixes_of("catch"); // -> 2

// "ca" をprefixに持つ要素を全て削除 ("cat", "catch", "camera" 全て消える)
trie.erase_by_prefix("ca");
cout << trie.size() << endl; // -> 0

Trie Tree (Prefix Tree) - 数列バージョン

用途: 数列集合の高速な管理、共通接頭辞(Prefix)の検索、辞書順走査、Trie木上でのDP

計算量: 挿入・検索・削除・LCP取得など 基本操作はすべて O(|S| log U) (|S| は数列の長さ、Uは各ノードから出る分岐の数。内部にmapを使用)。
競技プログラミング等の本番で要求されやすい「特定数列の正確な削除」「辞書順k番目の取得」を追加し、内部構造の拡張性も確保。

// T: 数列の要素の型 (int, long long など)
template <typename T = long long>
struct Trie {
  struct Node {
    map<T, int> next;       // 子ノードのインデックス
    int accept_count;       // このノードを「終端」とする数列の数
    int pass_count;         // このノードを「通過」した数列の数 (部分木の要素数)
    
    // --- 汎用拡張データ ---
    vector<int> accept_ids; // このノードで終わる数列のラベル(追加時のIDなど)
    long long val;          // 関数値、重み、DPの結果などを持たせる汎用変数

    Node() : accept_count(0), pass_count(0), val(0) {}
  };

  vector<Node> nodes;

  Trie() {
    nodes.push_back(Node()); // root (id = 0) を追加
  }

  // 現在のTrie木に含まれる全数列の数を取得 O(1)
  int size() const {
    return nodes[0].pass_count;
  }

  // 数列の挿入
  void insert(const vector<T> &s, int id = -1, long long val = 0) {
    int curr = 0;
    nodes[curr].pass_count++;
    for (const T& c : s) {
      if (!nodes[curr].next.count(c)) {
        nodes[curr].next[c] = nodes.size();
        nodes.push_back(Node());
      }
      curr = nodes[curr].next[c];
      nodes[curr].pass_count++;
    }
    nodes[curr].accept_count++;
    if (id != -1) nodes[curr].accept_ids.push_back(id);
    nodes[curr].val += val; 
  }

  // 単一数列の正確な削除 (集合からsを1つだけ減らす)
  // 成功したらtrue, そもそも存在しなかったらfalseを返す
  bool erase(const vector<T> &s) {
    int curr = 0;
    vector<int> path;
    for (const T& c : s) {
      path.push_back(curr);
      if (!nodes[curr].next.count(c)) return false; // 存在しない
      curr = nodes[curr].next[c];
    }
    if (nodes[curr].accept_count == 0) return false; // 終端として存在しない
    
    nodes[curr].accept_count--;
    nodes[curr].pass_count--;
    for (int p : path) nodes[p].pass_count--; // 祖先の通過数を減らす
    return true;
  }

  // 完全一致する数列の個数を取得
  int count(const vector<T> &s) {
    int curr = 0;
    for (const T& c : s) {
      if (!nodes[curr].next.count(c)) return 0;
      curr = nodes[curr].next[c];
    }
    return nodes[curr].accept_count;
  }

  // 終端ノードのID検索 (見つかれば終端ノードのIDを、なければ -1)
  int search(const vector<T> &s) {
    int curr = 0;
    for (const T& c : s) {
      if (!nodes[curr].next.count(c)) return -1;
      curr = nodes[curr].next[c];
    }
    return nodes[curr].accept_count > 0 ? curr : -1;
  }

  // Prefix(接頭辞)検索 (S から始まる数列の数を返す)
  int count_prefix(const vector<T> &s) {
    int curr = 0;
    for (const T& c : s) {
      if (!nodes[curr].next.count(c)) return 0;
      curr = nodes[curr].next[c];
    }
    return nodes[curr].pass_count;
  }

  // 特定の数列をprefixに持つ要素を全て削除する
  void erase_by_prefix(const vector<T> &s) {
    if (s.empty()) { 
      nodes[0] = Node(); // 空数列指定は全クリアと同義
      return;
    }
    int curr = 0;
    vector<int> path;
    for (const T& c : s) {
      path.push_back(curr);
      if (!nodes[curr].next.count(c)) return; // 存在しない
      curr = nodes[curr].next[c];
    }
    int removed = nodes[curr].pass_count;
    if (removed == 0) return;
    
    nodes[path.back()].next.erase(s.back()); // 木から切断
    for (int p : path) nodes[p].pass_count -= removed;
  }

  // sがprefixとなるような、木内の数列の「総数」ではなく
  // 「sのprefixとなるような木内の数列」の個数 (s={1,2,3}なら{1,2}, {1,2,3}等)
  int count_prefixes_of(const vector<T> &s) {
    int curr = 0, res = 0;
    for (const T& c : s) {
      res += nodes[curr].accept_count;
      if (!nodes[curr].next.count(c)) return res;
      curr = nodes[curr].next[c];
    }
    res += nodes[curr].accept_count;
    return res;
  }

  // 最長共通接頭辞 (LCP) のマッチ長取得
  // 木に含まれるいずれかの数列と、sが先頭から何要素一致するか
  int get_lcp_length(const vector<T> &s) {
    int curr = 0, len = 0;
    for (const T& c : s) {
      if (!nodes[curr].next.count(c)) break;
      curr = nodes[curr].next[c];
      len++;
    }
    return len;
  }

  // 辞書順で k 番目 (1-indexed) の数列を取得
  // k が要素数を超える、または k <= 0 の場合は空の数列を返す
  vector<T> kth_element(int k) {
    if (k <= 0 || k > nodes[0].pass_count) return {};
    int curr = 0;
    vector<T> res;
    while (true) {
      if (k <= nodes[curr].accept_count) return res;
      k -= nodes[curr].accept_count;
      // mapはキー(要素の値)で自動的に昇順ソートされている
      for (auto& [val, nxt] : nodes[curr].next) {
        if (k <= nodes[nxt].pass_count) {
          res.push_back(val);
          curr = nxt;
          break;
        } else {
          k -= nodes[nxt].pass_count;
        }
      }
    }
  }
};

【各Nodeが持つ変数の詳細な意味】
・next[c] : 要素 c に対応する子ノードのインデックス。数列の要素空間が広いため std::map (または要件に応じて std::unordered_map) で管理します。
・accept_count : 「このノードでピッタリ終わる」数列がいくつ登録されたか。削除されると減ります。
・pass_count : 「このノードを通る」(または終わる)数列の総数。つまり、このノードを根とする部分木の要素数です。

▼ 応用テンプレート(コピペ&即使用用)

0. 内部構造についてのメモ

Q. 親ノードに遡りたい場合は?

Trie木はデフォルトでは親へのポインタを持っていません。対処法は2つです。
1. 探索時に vector<int> path; を用意し、進むたびに path.push_back(curr); して履歴を残す(eraseの実装を参考にしてください)。
2. 構造体 Node の定義に int parent; を足し、コンストラクタで -1 に初期化。insert内のノード作成時に nodes[nodes.size()-1].parent = curr; を追加すれば、curr = nodes[curr].parent; で遡れるようになります。

Q. 特定の要素の子ノードが存在するか調べたい

long long c = 100; // 探したい要素
if (trie.nodes[curr].next.count(c)) {
    // 子が存在する
    int child = trie.nodes[curr].next[c];
}

Q. 木全体を走査してDP等を回したい

pass_count が 0 のノードは実質「空」なので、探索を枝刈りできます。また、子へ再帰するDFSを書くことで、辞書順走査や木DPが簡単に実装可能です(後述のテンプレート3を参照)。

1. 数列の追加 / 削除 / 個数カウント
Trie<long long> trie;

vector<long long> A = {1, 2, 3};
vector<long long> B = {1, 2};

trie.insert(A);
trie.insert(A);
trie.insert(B);

cout << trie.size() << endl; // 3 (全要素数)
cout << trie.count(A) << endl; // 2 (Aの数)

trie.erase(A); // 1つだけ消す
cout << trie.count(A) << endl; // 1になる
cout << trie.size() << endl; // 2になる
2. Prefix (接頭辞) 検索とLCPの取得
Trie<int> trie;
trie.insert({3, 1, 4, 1, 5});
trie.insert({3, 1, 4, 9});
trie.insert({3, 2, 1});

// {3, 1} から始まる数列の数を O(|S| log U) で取得
int cnt = trie.count_prefix({3, 1}); // -> 2
int cnt2 = trie.count_prefix({3, 1, 4}); // -> 2

// {3, 1, 4, 2} と木内の数列が最大何要素一致するか (LCP)
int lcp = trie.get_lcp_length({3, 1, 4, 2}); // -> 3 ({3, 1, 4}まで一致)
3. 辞書順 k番目の取得 / DFSによる走査
Trie<int> trie;
trie.insert({3, 2});
trie.insert({1, 2, 3});
trie.insert({1, 5});

// 辞書順で1番目(1-indexed)を取得 -> {1, 2, 3}
vector<int> first = trie.kth_element(1);
// 3番目を取得 -> {3, 2}
vector<int> third = trie.kth_element(3);

// 【自作DFSで辞書順に全列挙する例】
auto dfs = [&](auto self, int u, vector<int> current_seq) -> void {
    // このノードで終わる数列があるなら出力等を行う
    for (int k = 0; k < trie.nodes[u].accept_count; k++) {
        for (int x : current_seq) cout << x << " ";
        cout << endl;
    }
    // mapは自動的にキーで昇順ソートされているため、そのまま回せば辞書順になる
    for (auto& [val, v] : trie.nodes[u].next) {
        if (trie.nodes[v].pass_count > 0) {
            current_seq.push_back(val);
            self(self, v, current_seq);
            current_seq.pop_back(); // バックトラック
        }
    }
};
dfs(dfs, 0, {}); // 根(0)から空数列でスタート
4. 複数削除と内包Prefixのカウント
Trie<int> trie;
trie.insert({1, 2});
trie.insert({1, 2, 3});
trie.insert({1, 5, 9});

// {1, 2, 3} のprefixとなる木の要素数をカウント ({1, 2}, {1, 2, 3} の2つ)
int pre_cnt = trie.count_prefixes_of({1, 2, 3}); // -> 2

// {1, 2} をprefixに持つ要素を全て削除 ({1, 2}, {1, 2, 3} が消える)
trie.erase_by_prefix({1, 2});
cout << trie.size() << endl; // -> 1 ({1, 5, 9}のみ残る)

Binary Trie (XOR & 区間操作 拡張版)

用途: 整数集合の高速な管理、multiset の代替、XOR 空間上の最大/最小値の検索、全体への XOR 作用下での K 番目の値の取得、区間カウント・区間の一括削除

計算量: 挿入・検索・削除・XORクエリ・K番目の取得・区間操作など、基本操作はすべて O(MAX_LOG) (MAX_LOG はビット数、通常 30〜60)。
競技プログラミングで頻出する「集合全体の要素に一括で XOR 演算を適用した上での最大値・最小値の取得」「辞書順 k 番目の取得」に加え、「XOR 後の [L, R] 区間の一括削除 / カウント」をセグメント木の手法(枝刈り)で O(MAX_LOG) で実現した超高機能・過保護モデルです。

// T: 扱う整数の型 (int なら MAX_LOG=30, long long なら MAX_LOG=60 が目安)
template <typename T = long long, int MAX_LOG = 60>
struct BinaryTrie {
  struct Node {
    int next[2];       // 0/1 の子ノードへのインデックス (-1は子が存在しない)
    int accept_count;  // このノードを「終端」とする整数の数 (重複管理用)
    int pass_count;    // このノードを「通過」した整数の数 (部分木の要素数)

    Node() : accept_count(0), pass_count(0) {
      next[0] = next[1] = -1;
    }
  };

  vector<Node> nodes;

  BinaryTrie() {
    nodes.push_back(Node()); // root (id = 0) を追加
  }

  // 現在のTrie木に含まれる全要素数を取得 O(1)
  int size() const {
    return nodes[0].pass_count;
  }

  // 要素の挿入 (count 個まとめて追加可能)
  void insert(T val, int count = 1) {
    if (count <= 0) return;
    int curr = 0;
    nodes[curr].pass_count += count;
    for (int i = MAX_LOG - 1; i >= 0; i--) {
      int b = (val >> i) & 1;
      if (nodes[curr].next[b] == -1) {
        nodes[curr].next[b] = nodes.size();
        nodes.push_back(Node());
      }
      curr = nodes[curr].next[b];
      nodes[curr].pass_count += count;
    }
    nodes[curr].accept_count += count;
  }

  // 要素の削除 (count 個まとめて削除可能)
  bool erase(T val, int count = 1) {
    if (count <= 0) return true;
    int curr = 0;
    int existing = this->count(val);
    int remove_cnt = min(existing, count);
    if (remove_cnt == 0) return false;

    nodes[curr].pass_count -= remove_cnt;
    for (int i = MAX_LOG - 1; i >= 0; i--) {
      int b = (val >> i) & 1;
      curr = nodes[curr].next[b];
      nodes[curr].pass_count -= remove_cnt;
    }
    nodes[curr].accept_count -= remove_cnt;
    return existing >= count;
  }

  // 特定の要素の個数を取得
  int count(T val) const {
    int curr = 0;
    for (int i = MAX_LOG - 1; i >= 0; i--) {
      int b = (val >> i) & 1;
      if (nodes[curr].next[b] == -1) return 0;
      curr = nodes[curr].next[b];
    }
    return nodes[curr].accept_count;
  }

  // xor_val と木内の要素を XOR したとき、最小となる値を返す
  T min_element(T xor_val = 0) const {
    if (size() == 0) return -1; 
    int curr = 0;
    T res = 0;
    for (int i = MAX_LOG - 1; i >= 0; i--) {
      int b = (xor_val >> i) & 1;
      if (nodes[curr].next[b] != -1 && nodes[nodes[curr].next[b]].pass_count > 0) {
        curr = nodes[curr].next[b];
      } else {
        curr = nodes[curr].next[b ^ 1];
        res |= (1LL << i); 
      }
    }
    return res;
  }

  // xor_val と木内の要素を XOR したとき、最大となる値を返す
  T max_element(T xor_val = 0) const {
    if (size() == 0) return -1;
    int curr = 0;
    T res = 0;
    for (int i = MAX_LOG - 1; i >= 0; i--) {
      int b = (xor_val >> i) & 1;
      if (nodes[curr].next[b ^ 1] != -1 && nodes[nodes[curr].next[b ^ 1]].pass_count > 0) {
        curr = nodes[curr].next[b ^ 1];
        res |= (1LL << i); 
      } else {
        curr = nodes[curr].next[b];
      }
    }
    return res;
  }

  // 昇順で k 番目 (1-indexed) の値を取得 (オプションで全体に xor_val を作用)
  T kth_element(int k, T xor_val = 0) const {
    if (k <= 0 || k > size()) return -1;
    int curr = 0;
    T res = 0;
    for (int i = MAX_LOG - 1; i >= 0; i--) {
      int b = (xor_val >> i) & 1;
      int left_child = nodes[curr].next[b];
      int left_cnt = (left_child != -1) ? nodes[left_child].pass_count : 0;

      if (k <= left_cnt) {
        curr = left_child;
      } else {
        k -= left_cnt;
        curr = nodes[curr].next[b ^ 1];
        res |= (1LL << i);
      }
    }
    return res;
  }

  // 指定した値未満(xor_val作用時)の要素がいくつあるか
  int count_less_than(T val, T xor_val = 0) const {
    int curr = 0;
    int res = 0;
    for (int i = MAX_LOG - 1; i >= 0; i--) {
      if (curr == -1) break;
      int b = (val >> i) & 1;
      int x = (xor_val >> i) & 1;

      if (b == 1) {
        int zero_child = nodes[curr].next[x];
        if (zero_child != -1) res += nodes[zero_child].pass_count;
        curr = nodes[curr].next[x ^ 1]; 
      } else {
        curr = nodes[curr].next[x];
      }
    }
    return res;
  }

  // 【追加】指定した区間 [L, R] の要素数を取得 (xor_val 作用時)
  int count_in_range(T L, T R, T xor_val = 0) const {
    if (L > R) return 0;
    return count_less_than(R + 1, xor_val) - count_less_than(L, xor_val);
  }

  // 【追加】指定した区間 [L, R] の要素をまとめて削除 (xor_val 作用時)
  // セグメント木のように、完全に区間に含まれる部分木をまるごと切り落とすため O(MAX_LOG)
  // 削除された要素の総数を返す
  int erase_in_range(T L, T R, T xor_val = 0) {
    if (L > R || size() == 0) return 0;
    return _erase_range_rec(0, MAX_LOG - 1, L, R, xor_val, 0);
  }

private:
  int _erase_range_rec(int curr, int depth, T L, T R, T xor_val, T current_xor_prefix) {
    if (curr == -1 || nodes[curr].pass_count == 0) return 0;

    // 現在の部分木に含まれる要素の (XOR適用後の) 最小値と最大値
    T low = current_xor_prefix;
    T high = current_xor_prefix | (T)((1ULL << (depth + 1)) - 1);

    if (high < L || R < low) return 0; // 完全に削除範囲外ならスキップ

    if (L <= low && high <= R) {
      // 完全に削除範囲内なら、この部分木を根元から切断して一括削除
      int deleted = nodes[curr].pass_count;
      nodes[curr].pass_count = 0;
      nodes[curr].accept_count = 0;
      nodes[curr].next[0] = nodes[curr].next[1] = -1;
      return deleted;
    }

    if (depth < 0) return 0;

    int x = (xor_val >> depth) & 1;
    int deleted = 0;

    // 元の木での 0 の枝 (XOR 適用後は 0 ^ x のビットを持つ)
    if (nodes[curr].next[0] != -1) {
      T next_prefix = current_xor_prefix | ((T)(0 ^ x) << depth);
      deleted += _erase_range_rec(nodes[curr].next[0], depth - 1, L, R, xor_val, next_prefix);
    }
    // 元の木での 1 の枝 (XOR 適用後は 1 ^ x のビットを持つ)
    if (nodes[curr].next[1] != -1) {
      T next_prefix = current_xor_prefix | ((T)(1 ^ x) << depth);
      deleted += _erase_range_rec(nodes[curr].next[1], depth - 1, L, R, xor_val, next_prefix);
    }

    nodes[curr].pass_count -= deleted;
    return deleted;
  }
};

【各Nodeが持つ変数の詳細な意味】
・next[0] / next[1] : 0のビット、1のビットに対応する子ノードのインデックス。無い場合は -1。
・accept_count : そのノードを終端とする同一整数の重複登録数。
・pass_count : 「このノードを通る」要素の総数(部分木のサイズ)。この値を持つことで、K番目の取得や個数カウントが O(1) 分岐で高速に行えます。

▼ 応用テンプレート(コピペ&即使用用)

1. 集合への追加 / 削除 / multiset 的利用
BinaryTrie<int, 30> trie; // int なら 30 bit で十分

trie.insert(5);
trie.insert(5);
trie.insert(9);

cout << trie.size() << endl;    // 3 (全要素数)
cout << trie.count(5) << endl;  // 2 (5の個数)

trie.erase(5); // 1つだけ消す
cout << trie.count(5) << endl;  // 1になる

trie.insert(10, 5); // 10 を 5個まとめて追加できる
cout << trie.count(10) << endl; // 5
2. XOR 空間上の 最大 / 最小値
BinaryTrie<int, 30> trie;
trie.insert(2);  // 0010
trie.insert(10); // 1010
trie.insert(15); // 1111

// 全ての要素に対して 6 (0110) と XOR を取ったとき...
// 最大値は 15 ^ 6 = 9 
// 最小値は 2 ^ 6 = 4 

cout << trie.max_element(6) << endl; // -> 9
cout << trie.min_element(6) << endl; // -> 4

// 木の中の元の要素を知りたい場合は、結果にもう一度 XOR を掛ける
int original = trie.max_element(6) ^ 6;
cout << original << endl; // -> 15
3. K 番目の要素と、全体 XOR 作用下での順序
BinaryTrie<int, 30> trie;
trie.insert(1); // 0001
trie.insert(3); // 0011
trie.insert(5); // 0101

// 普通に昇順で K 番目を取得
cout << trie.kth_element(1) << endl; // -> 1
cout << trie.kth_element(3) << endl; // -> 5

// もし全要素に "2 (0010)" を XOR した場合、
// 集合は { 1^2, 3^2, 5^2 } = { 3, 1, 7 } になる。
// この仮想的な集合において、昇順で1番目(最小値)は 1、2番目は 3。
cout << trie.kth_element(1, 2) << endl; // -> 1
cout << trie.kth_element(2, 2) << endl; // -> 3
4. 指定された値未満の要素数 (Lower Bound 的利用)
BinaryTrie<int, 30> trie;
trie.insert(3);
trie.insert(5);
trie.insert(5);
trie.insert(9);

// 値が 6 未満の要素数はいくつか
cout << trie.count_less_than(6) << endl; // -> 3 (3, 5, 5)

// 全体に 3 を XOR したと仮定したとき、6 未満の要素数はいくつか
// {3^3, 5^3, 5^3, 9^3} = {0, 6, 6, 10}
cout << trie.count_less_than(6, 3) << endl; // -> 1 (0 のみ)
5. 区間 [L, R] のカウント と まとめて削除
BinaryTrie<int, 30> trie;
trie.insert(10);
trie.insert(12);
trie.insert(15);
trie.insert(20);

// [11, 16] の範囲にある要素数をカウント (12, 15 の2つ)
cout << trie.count_in_range(11, 16) << endl; // -> 2

// [11, 16] の範囲にある要素を木から "全て" 削除する (Prefixの削除と同義)
int deleted_count = trie.erase_in_range(11, 16);
cout << deleted_count << " 個削除されました" << endl; // -> 2 個削除

cout << trie.size() << endl; // -> 2 (10, 20 が残る)

// ----------------------------------------------------
// 【XOR 作用下での削除の例】
// 要素に全体 XOR を掛けた「仮想的な状態」での区間削除も可能!
trie.insert(5); 

// 全体に 7 を XOR したと仮定すると、5 ^ 7 = 2。
// 「XOR 適用後に 0 以上 3 以下になるもの」を全て削除
trie.erase_in_range(0, 3, 7); // (元の 5 が削除される)

Binary Trie (LSB-first / 下位ビット優先 拡張版)

用途: 整数集合の管理、下位ビットからのパターンマッチ、mod 2^k での高速な検索・カウント・一括削除、全要素への +1 操作

計算量: 挿入・削除・個数取得は O(MAX_LOG)。mod 2^k での検索・削除は O(k)。全体への +1 操作は O(MAX_LOG)。
注意点: 下位ビット(LSB)から順に木を構築する特殊な Binary Trie です。性質上、大小関係が木構造に反映されないため「K番目の値の取得」「最大/最小値」「区間カウント」はできなくなりますが、代わりに「特定の mod 2^k に一致する要素のカウント/削除」や「全要素へのインクリメント(+1)」が O(MAX_LOG) 以下の計算量で実現できるようになります。

// T: 扱う整数の型 (int なら MAX_LOG=30, long long なら MAX_LOG=60 が目安)
template <typename T = long long, int MAX_LOG = 60>
struct LSBBinaryTrie {
  struct Node {
    int next[2];       // 0/1 の子ノードへのインデックス (-1は子が存在しない)
    int accept_count;  // このノードを「終端」とする整数の数
    int pass_count;    // このノードを「通過」した整数の数 (部分木の要素数)

    Node() : accept_count(0), pass_count(0) {
      next[0] = next[1] = -1;
    }
  };

  vector<Node> nodes;

  LSBBinaryTrie() {
    nodes.push_back(Node()); // root (id = 0) を追加
  }

  // 現在のTrie木に含まれる全要素数を取得 O(1)
  int size() const {
    return nodes[0].pass_count;
  }

  // 要素の挿入 (下位ビットから処理)
  void insert(T val, int count = 1) {
    if (count <= 0) return;
    int curr = 0;
    nodes[curr].pass_count += count;
    for (int i = 0; i < MAX_LOG; i++) { // LSB-first (0 から昇順)
      int b = (val >> i) & 1;
      if (nodes[curr].next[b] == -1) {
        nodes[curr].next[b] = nodes.size();
        nodes.push_back(Node());
      }
      curr = nodes[curr].next[b];
      nodes[curr].pass_count += count;
    }
    nodes[curr].accept_count += count;
  }

  // 要素の削除
  bool erase(T val, int count = 1) {
    if (count <= 0) return true;
    int curr = 0;
    int existing = this->count(val);
    int remove_cnt = min(existing, count);
    if (remove_cnt == 0) return false;

    nodes[curr].pass_count -= remove_cnt;
    for (int i = 0; i < MAX_LOG; i++) {
      int b = (val >> i) & 1;
      curr = nodes[curr].next[b];
      nodes[curr].pass_count -= remove_cnt;
    }
    nodes[curr].accept_count -= remove_cnt;
    return existing >= count;
  }

  // 特定の要素の個数を取得
  int count(T val) const {
    int curr = 0;
    for (int i = 0; i < MAX_LOG; i++) {
      int b = (val >> i) & 1;
      if (nodes[curr].next[b] == -1) return 0;
      curr = nodes[curr].next[b];
    }
    return nodes[curr].accept_count;
  }

  // 【特化機能】 x % (2^k) == val % (2^k) となる要素の数を取得 O(k)
  // xor_val を作用させた状態での評価も可能
  int count_mod(T val, int k, T xor_val = 0) const {
    if (k > MAX_LOG) k = MAX_LOG;
    int curr = 0;
    for (int i = 0; i < k; i++) {
      if (curr == -1) return 0;
      int b = (val >> i) & 1;
      int x = (xor_val >> i) & 1;
      curr = nodes[curr].next[b ^ x];
    }
    return curr == -1 ? 0 : nodes[curr].pass_count;
  }

  // 【特化機能】 x % (2^k) == val % (2^k) となる要素を木から一括削除 O(k)
  int erase_mod(T val, int k, T xor_val = 0) {
    if (k > MAX_LOG) k = MAX_LOG;
    if (size() == 0) return 0;
    return _erase_mod_rec(0, 0, val, k, xor_val);
  }

  // 【特化機能】 全ての要素に +1 する (O(MAX_LOG))
  // ※ xor_val での仮想的なXOR操作とは独立して、実際に木の中身をインクリメントします
  void add_all_1() {
    if (size() > 0) {
      _add_all_1_rec(0, 0);
    }
  }

private:
  int _erase_mod_rec(int curr, int depth, T val, int k, T xor_val) {
    if (curr == -1 || nodes[curr].pass_count == 0) return 0;
    
    if (depth == k) {
      int deleted = nodes[curr].pass_count;
      nodes[curr].pass_count = 0;
      nodes[curr].accept_count = 0;
      nodes[curr].next[0] = nodes[curr].next[1] = -1;
      return deleted;
    }

    int target_b = (val >> depth) & 1;
    int x = (xor_val >> depth) & 1;
    int path = target_b ^ x;

    int deleted = 0;
    if (nodes[curr].next[path] != -1) {
      deleted = _erase_mod_rec(nodes[curr].next[path], depth + 1, val, k, xor_val);
    }

    nodes[curr].pass_count -= deleted;
    return deleted;
  }

  void _add_all_1_rec(int curr, int depth) {
    if (curr == -1 || depth == MAX_LOG) return;
    
    // 0の枝と1の枝を入れ替える (+1の適用)
    swap(nodes[curr].next[0], nodes[curr].next[1]);
    
    // 元々 1 だった枝 (入れ替えられて今は next[0] にある) は
    // 桁上がり(キャリー)が発生するため、さらに上の桁へ +1 を伝播させる
    if (nodes[curr].next[0] != -1) {
      _add_all_1_rec(nodes[curr].next[0], depth + 1);
    }
  }
};

▼ 応用テンプレート(コピペ&即使用用)

1. 基本的な追加 / 削除 / カウント
LSBBinaryTrie<int, 30> trie;

trie.insert(5);  // 00101
trie.insert(13); // 01101
trie.insert(21); // 10101

cout << trie.size() << endl;    // 3
cout << trie.count(13) << endl; // 1

trie.erase(5);
cout << trie.count(5) << endl;  // 0
2. mod 2^k での一致カウント (下位ビットからのパターンマッチ)
LSBBinaryTrie<int, 30> trie;
trie.insert(5);  // 00101 (mod 8 = 5)
trie.insert(13); // 01101 (mod 8 = 5)
trie.insert(15); // 01111 (mod 8 = 7)
trie.insert(21); // 10101 (mod 8 = 5)

// 下位 3 ビット (つまり mod 2^3 = 8) が 5 と等しい要素数
// 5, 13, 21 の 3 つが該当する
cout << trie.count_mod(5, 3) << endl; // -> 3

// 全体に 2 (00010) を XOR したと仮定したとき、mod 8 が 7 となる要素数
// 5^2=7, 13^2=15(mod 8=7), 15^2=13, 21^2=23(mod 8=7)
cout << trie.count_mod(7, 3, 2) << endl; // -> 3
3. mod 2^k での一括削除
LSBBinaryTrie<int, 30> trie;
trie.insert(1); // 0001
trie.insert(5); // 0101
trie.insert(9); // 1001
trie.insert(2); // 0010

// 下位 2 ビット (mod 4) が 1 と等しい要素を全て削除
// 1(mod4=1), 5(mod4=1), 9(mod4=1) が削除され、2 のみ残る
int deleted = trie.erase_mod(1, 2);

cout << deleted << " 個削除されました" << endl; // -> 3
cout << trie.size() << endl; // -> 1 (残りは 2 のみ)
4. 全要素への +1 操作 (インクリメント)
LSBBinaryTrie<int, 30> trie;
trie.insert(3);
trie.insert(7);

// 木に入っている全要素の値を +1 する (O(MAX_LOG))
trie.add_all_1();

// 中身は 3 -> 4, 7 -> 8 に書き換わっている
cout << trie.count(4) << endl; // -> 1
cout << trie.count(8) << endl; // -> 1

// ※ 何度も呼び出せば +X も可能ですが、X回分の呼び出し( X * MAX_LOG ) がかかります。

vector・配列

push_back / emplace_back / pop_back

用途: vector の末尾に追加・末尾を削除

計算量: 末尾追加は償却 O(1)、pop_back は O(1)。 最頻出の基本操作。

vector<int> v;
v.push_back(3);
v.emplace_back(5);
v.pop_back();

resize / assign / clear

用途: サイズ変更・初期化・全消去

計算量: resize は増減要素数に依存、assign は O(N)、clear は O(1) 近傍だが、要素破棄コストは型に依存。

vector<int> v;
v.resize(10);          // 要素数10
v.assign(5, 7);        // [7, 7, 7, 7, 7]
v.clear();             // 空にする

erase / remove idiom

用途: vector / string から要素を消す

計算量: 1回の erase は O(N)。 条件削除は remove_if と組み合わせるのが定番。

vector<ll> v = {1, 2, 3, 2, 4};
v.erase(v.begin() + 1);  // 2番目を削除

v.erase(
    remove(v.begin(), v.end(), 2),
    v.end()
);

// 重複削除と座標圧縮
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(), xs.end()), xs.end());
auto get_id = [&](long long x) {
    return lower_bound(xs.begin(), xs.end(), x) - xs.begin();
};

注意: ループ中の erase は連鎖すると O(N^2) になりやすい。

sort / stable_sort / reverse

用途: 並べ替え・安定ソート・反転

計算量: sort は O(N log N)、stable_sort も O(N log N) だが定数倍は重め、reverse は O(N)。

自作比較関数: 第1引数 a が第2引数 b より「真に小さい(前に来るべき)」ときに true を返します。同等の場合は false を返す必要があるため、<= ではなく < を使用します(Strict Weak Ordering)。

sort(v.begin(), v.end());
sort(v.begin(), v.end(), greater<>());
stable_sort(v.begin(), v.end());
reverse(v.begin(), v.end());

// 2番目の要素(pair.second)で降順ソート
sort(v.begin(), v.end(), [](const auto& a, const auto& b) {
    return a.second > b.second;
});

// 自作関数を別途定義して渡す例
// 1つ目の引数 a が 2つ目の引数 b より「真に小さい(前に来るべき)」場合に true を返す
bool compareData(const Data& a, const Data& b) {
    if (a.score != b.score) {
        return a.score > b.score; // 第1条件: スコアが高い順(a.score > b.score のとき true)
    }
    return a.id < b.id;           // 第2条件: スコアが同じならIDが小さい順(a.id < b.id のとき true)
}

sort(v.begin(), v.end(), compareData);

// ラムダ式で単一・複合条件を指定する例
sort(v.begin(), v.end(), [](const auto& a, const auto& b) {
    // a が b より「真に小さい」ときのみ true(a == b のときは必ず false)
    return a.first < b.first;
});

lower_bound / upper_bound

用途: ソート済み配列で境界を探す

計算量: O(log N)。 「二分探索を標準関数で書く」場面の最重要候補。

auto it1 = lower_bound(v.begin(), v.end(), x); // x以上の最初
auto it2 = upper_bound(v.begin(), v.end(), x); // xより大きい最初

ll idx1 = it1 - v.begin();
ll idx2 = it2 - v.begin();

注意: 未ソート配列に使ってはいけない。

merge

用途: 2つのソート済み範囲をマージする

計算量: O(N1 + N2) (N1, N2はそれぞれの入力範囲の要素数)。

注意点: 入力となる2つの範囲は事前にソートされている必要があります。また、出力先のコンテナには十分なサイズが確保されているか、std::back_inserter などを利用して動的に要素を追加する必要があります。

vector<int> v1 = {1, 3, 5};
vector<int> v2 = {2, 4, 6};

// パターン1: 事前に出力先のサイズを確保してマージ
vector<int> res1(v1.size() + v2.size());
merge(v1.begin(), v1.end(), v2.begin(), v2.end(), res1.begin());

// パターン2: back_inserterを使って末尾に追加しながらマージ
vector<int> res2;
merge(v1.begin(), v1.end(), v2.begin(), v2.end(), back_inserter(res2));

// パターン3: カスタム比較関数を用いる例(降順)
// ※入力範囲 v3, v4 も同じ基準(降順)でソートされている必要があります
vector<int> v3 = {5, 3, 1};
vector<int> v4 = {6, 4, 2};
vector<int> res3;
merge(v3.begin(), v3.end(), v4.begin(), v4.end(), back_inserter(res3), greater<>());

グリッドの回転

用途: 2次元配列を時計回りに90度回転させる

計算量: 配列の全要素にアクセスするため $O(HW)$。 グリッド探索やシミュレーション問題で、状態遷移を簡略化するために使用。

// H x W のグリッドを時計回りに90度回転させる関数
vector<vector<ll>> rotate(const vector<vector<ll>>& a) {
    ll h = a.size(), w = a[0].size();
    vector<vector<ll>> res(w, vector<ll>(h));
    for (ll i = 0; i < h; ++i) {
        for (ll j = 0; j < w; ++j) {
            res[j][h - 1 - i] = a[i][j];
        }
    }
    return res;
}

座標変換による回転(仮想回転)

用途: グリッドを物理的にコピーせず、座標計算のみで回転後の値を取得する

計算量: O(1)
メモリを消費せず、回転回数が多い場合や巨大なグリッドで有効。

// 90度時計回りに回転させた状態の (r, c) の値を取得
ll get_rotated_val(const vector<vector<ll>>& a, ll r, ll c, ll H, ll W) {
    // 元のグリッドのどの位置が (r, c) に来るかを計算
    return a[H - 1 - c][r];
}

set / map / unordered

set / multiset

用途: 昇順に保たれる集合・重複許容集合

概要: set は「重複なし・自動ソートされた集合」、 multiset は「重複あり・自動ソートされた集合」。 内部的には平衡二分木(多くの場合 Red-Black Tree)で管理される。

計算量: 追加・削除・探索はすべて O(log N)。 vector のような線形探索 O(N) を避けたい場合に有効。

重要な性質: 要素は常にソート順で保持されるため、 lower_bound・upper_bound がそのまま使える。 また「存在判定」「最小/最大取得」が高速。

set<ll> st;

// insert: 要素追加
st.insert(10);
st.insert(5);

// find: 探索(なければ end())
auto it = st.find(10);

// erase: 削除(値 or iterator)
st.erase(10);

// size: setの大きさ取得
st.size();

// lower_bound: 以上で最小の要素
auto lb = st.lower_bound(7);

// 最小・最大
ll mn = *st.begin();
ll mx = *st.rbegin();

// イテレータが指す場所を削除し、次のイテレータを返す
it = st.erase(it);
multiset<ll> mst;

mst.insert(5);
mst.insert(5);
mst.insert(2);

// 1個だけ消す(重要)
mst.erase(mst.find(5));

// 全削除はこれ
mst.erase(5);

注意: erase(value) は一致する全要素を消す。 1個だけ消したいなら find して iterator を渡す。 また set はランダムアクセス不可(vectorみたいに st[i] はできない)。

map / multimap

用途: キーと値の対応をソート順で保持する

計算量: 基本操作は O(log N)。 辞書、頻度表、座標圧縮後の管理に便利。

map<string, ll> mp;
mp["apple"]++;
mp["banana"] = 3;

if (mp.count("apple")) {
    // 存在確認
}

for (auto &[key, value] : mp) {
    // 昇順に巡回
}

注意: operator[] は存在しないキーを作る。参照だけしたいなら at や find を使う。

unordered_map / unordered_set

用途: ハッシュベースの高速な辞書・集合

計算量: 平均 O(1)、最悪 O(N)。 速いが、順序は保証されない。

unordered_map<long long, ll> ump;
ump[123456789LL]++;

unordered_set<string> us;
us.insert("alpha");

注意: ハッシュ衝突が多いと遅くなることがある。必要なら reserve を使って再ハッシュを減らす。

count / find / contains

用途: 存在確認を素早く書く

計算量: set/map は O(log N)、unordered 系は平均 O(1)。 contains は C++20 で書き味がよい。

if (st.count(x)) { /* 存在 */ }

auto it = mp.find(key);
if (it != mp.end()) {
    // it->second を使う
}

if (us.contains("hello")) {
    // C++20
}

stack / queue / deque / heap

stack / queue / deque

用途: LIFO / FIFO / 両端キュー

計算量: 基本操作はすべて O(1)。 BFS、単調スタック、スライディングウィンドウで頻出。

// ===== stack =====
stack<ll> st;

st.push(10);      // 要素を追加
st.push(20);

ll x = st.top(); // 一番上を見る(20)
st.pop();         // 一番上を削除

bool empty = st.empty(); // 空か判定
size_t sz = st.size();   // 要素数

// ===== queue =====
queue<ll> q;

q.push(10);        // 後ろに追加
q.push(20);

ll front = q.front(); // 先頭を見る(10)
q.pop();               // 先頭を削除

bool empty2 = q.empty();
size_t sz2 = q.size();

// ===== deque =====
deque<ll> dq;

dq.push_front(10); // 前に追加
dq.push_back(20);  // 後ろに追加

ll first = dq.front(); // 先頭
ll last  = dq.back();  // 末尾

dq.pop_front();    // 前を削除
dq.pop_back();     // 後ろを削除

注意: queue は前からしか取り出せない。両端を触りたいなら deque。

priority_queue

用途: 最大値・最小値を高速に取り出すヒープ

計算量: 追加・取り出しともに O(log N)、先頭参照は O(1)。 ダイクストラ法の相棒。

priority_queue<ll> pq; // 最大ヒープ
pq.push(3);
pq.push(10);
ll mx = pq.top();
pq.pop();

priority_queue<ll, vector<ll>, greater<ll>> minpq; // 最小ヒープ

注意: priority_queue は中身の直接更新が苦手。減少キーは「新しい値を push して古いものを無視する」実装が多い。

bit・bitset

ビット演算の基本

用途: 集合圧縮・状態管理・高速判定

計算量: すべて O(1)。 「何桁まで扱うか」に注意したい。

ll x = 1 << 3;     // 8
bool on = (mask & (1 << i)) != 0;
mask |= (1 << i);    // 立てる
mask ^= (1 << i);    // 反転
mask &= ~(1 << i);   // 落とす

注意: 1 << 31 のような式は型に注意。1LL を使う場面が多い。

ビットリバース(並び順の反転)

用途: 任意長のビット列の前後を鏡のように反転する

計算量: O(N)。 (N は指定された列の長さ)。文字列を経由するよりもメモリ確保のオーバーヘッドがなく高速。

ull reverse_bits(ull val, ll length) {
    ull result = 0;
    for (ll i = 0; i < length; ++i) {
        result = (result << 1) | (val & 1);
        val >>= 1;
    }
    return result;
}

注意: 引数の length が val の型(ここでは 64 ビット)を超えないように注意。

popcount / ctz / bitset

用途: 立っているビット数・末尾の0の数・固定長ビット集合

計算量: popcount や ctz は定数時間に近い命令で動くことが多い。 bitset はサイズ固定のときに強い。

// 1. bitsetの宣言と初期化の例
std::bitset<8> bs1;           // 00000000 (すべて0で初期化)
std::bitset<8> bs2(13);       // 00001101 (整数から初期化)
std::bitset<8> bs3("10110");  // 00010110 (文字列から初期化。右詰めで入る)

// 2. 提示された基本操作の具体例
std::bitset<1000> bs;          // すべて0の1000桁のビット列

bs.set(3);                     // 3番目のビットを1にする   -> bs[3]が1になる
bs.reset(3);                   // 3番目のビットを0に戻す   -> bs[3]が0になる

bool ok = bs.test(3);          // 3番目のビットの状態を取得 -> 0なのでfalse
                               // ※境界チェックのない bs[3] も使用可能

bs.flip();                     // 全てのビットを反転する   -> 全て1になる
bs.flip(3);                    // 3番目のビットだけ反転する -> 1から0になる

// 3. 状態を確認する便利な関数の例
bool any_one = bs.any();       // 1つのビットでも1があればtrue
bool all_zero = bs.none();     // 全てのビットが0ならtrue
bool all_one = bs.all();       // 全てのビットが1ならtrue
size_t num_one = bs.count();   // 1になっているビットの個数を数える (popcountと同じ)

// 4. ビット演算の一括処理例
std::bitset<8> target1("1100");
std::bitset<8> target2("1010");

std::bitset<8> res_and = target1 & target2; // 1000 (AND演算)
std::bitset<8> res_or  = target1 | target2; // 1110 (OR演算)
std::bitset<8> res_xor = target1 ^ target2; // 0110 (XOR演算)
std::bitset<8> res_shl = target1 << 2;      // 00110000 (左シフト)

// 5. 組み込み関数(GCC固有)による高速なビット演算の例
// ※bitsetオブジェクトではなく、通常の整数型(int, unsigned等)に対して使用します
ull x = 13; // 2進数表記で 1101

// __builtin_popcount: 立っている(1の)ビットの数を数える
ll cnt = __builtin_popcount((unsigned)x); 
// 1101 には 1 が3つあるため、cnt = 3 となる

// __builtin_ctz: 末尾(右側)に連続する0の数を数える
// ※ x != 0 が必要(0を渡すと未定義動作)
ll tz = __builtin_ctz((unsigned)x); 
// 1101 は末尾が1(0が連続していない)ため、tz = 0 となる
// 仮に x = 4 (100) であれば、右側に0が2つあるため tz = 2 となる

注意: ctz 系は 0 を渡すと未定義。bitset はコンパイル時サイズ固定である点が重要。

数学系

gcd / lcm

用途: 最大公約数・最小公倍数

計算量: gcd は O(log min(A,B))。 lcm はオーバーフローに注意。

long long g = gcd(a, b);
long long l = a / gcd(a, b) * b;

注意: a * b を先にやると溢れる可能性がある。必ず割ってから掛ける。

整数の平方根 (sqrt)

用途: 浮動小数点数の誤差を回避した正確な整数平方根の計算

計算量: O(1)。 大きな値における sqrt の精度落ちによるバグを防ぐ。

ll isqrt(ll x) {
    if (x <= 0) return 0;
    ll res = sqrt(x);
    while ((res + 1) * (res + 1) <= x) res++;
    while (res * res > x) res--;
    return res;
}

注意: C++ の組み込み sqrt 関数は double 型等で計算されるため、値が大きいと精度が足りず誤差が生じる。そのため前後の値を確認する微調整が必須となる。

Matrix (行列演算)

用途: 行列の積・累乗 (状態遷移DPの高速化など)

計算量: N×N 行列の積は O(N^3)。累乗 pow(K) はダブリングにより O(N^3 log K)。

#include <vector>
using namespace std;
using ll = long long;

struct Matrix {
    vector<vector<ll>> mat;
    
    // コンストラクタ (r行c列の0行列)
    Matrix(int r, int c) : mat(r, vector<ll>(c, 0)) {}
    
    int h() const { return mat.size(); }
    int w() const { return mat[0].size(); }
    
    // 行列の積
    Matrix operator*(const Matrix& a) const {
        int n = h(), m = a.w(), l = w();
        Matrix res(n, m);
        for (int i = 0; i < n; i++) {
            for (int k = 0; k < l; k++) {
                for (int j = 0; j < m; j++) {
                    res.mat[i][j] += mat[i][k] * a.mat[k][j];
                    // res.mat[i][j] %= MOD; // ※MODが必要な場合はここを有効化
                }
            }
        }
        return res;
    }
    
    // 累乗 (A^k)
    Matrix pow(ll k) const {
        int n = h();
        Matrix res(n, n);
        for (int i = 0; i < n; i++) res.mat[i][i] = 1; // 単位行列
        Matrix x = *this;
        while (k > 0) {
            if (k & 1) res = res * x;
            x = x * x;
            k >>= 1;
        }
        return res;
    }
};

▼ 使い方(初期化・行列の積・累乗によるDP高速化)

行列の基本操作と N 乗の計算(フィボナッチ数列の遷移例)
using ll = long long;

// 1. 行列の初期化 (2行2列の0行列)
Matrix A(2, 2);
// 状態遷移行列の設定 (例: フィボナッチ数列)
A.mat[0][0] = 1; A.mat[0][1] = 1;
A.mat[1][0] = 1; A.mat[1][1] = 0;

// 2. 列ベクトルの初期化 (2行1列)
// 初期状態を持たせる
Matrix X(2, 1);
X.mat[0][0] = 1; // F_1
X.mat[1][0] = 0; // F_0

// 3. 行列の積 (A * X)
// 1ステップだけ状態を遷移させる
Matrix next_X = A * X;

// 4. 行列の累乗 (A^k)
// Kステップ先の状態への遷移行列をダブリングで高速に求める (O(N^3 log K))
ll K = 1000000000LL; // 10^9回遷移
Matrix Ak = A.pow(K);

// 5. 累乗した行列と初期状態ベクトルを掛け合わせる
Matrix res = Ak * X;

// 結果の取得
// kステップ遷移した後の値が O(log K) の計算時間で得られる
ll ans = res.mat[0][0];

注意: 定数倍高速化(キャッシュ効率)のため、行列の積のループ順序は i-k-j にしています。値が大きくなる問題で使う場合は、乗算ループ内のMOD計算を適宜有効にしてください。

商の列挙 (Quotient Enumeration / 商のブロック化)

用途: $\lfloor K/i \rfloor$ が同じ値になる $i$ の区間 $[l, r]$ をまとめ、高速に和などを計算する

計算量: O(√K)。 $\lfloor K/i \rfloor$ の値としてあり得るものは高々 $2\sqrt{K}$ 種類しかないため、区間の数も $O(\sqrt{K})$ に削減される。

// 区間 [l, r] と、その区間での商 q をまとめる構造体
struct QuotientRange {
    ll l, r, q;
};

// K / i (切り捨て) が一定となる i の区間 [l, r] を列挙してリストで返す関数
// [1, N] の範囲の i について列挙する
vector<QuotientRange> enumerate_quotients(ll K, ll N) {
    vector<QuotientRange> res;
    for (ll l = 1; l <= N; ) {
        ll q = K / l;
        // q == 0 のときは無限大になってしまうため、上限を N にする
        ll r = (q == 0) ? N : min(N, K / q);
        res.push_back({l, r, q});
        l = r + 1;
    }
    return res;
}

▼ 使い方と応用パターン

基本:Σ (K / i) を求める ( i = 1 から N まで )
ll K = 1000000000000LL;
ll N = 1000000000000LL;
ll ans = 0;

// 区間のリストを取得
vector<QuotientRange> ranges = enumerate_quotients(K, N);

// forループで順番に取り出して処理
for (auto qr : ranges) {
    ll l = qr.l;
    ll r = qr.r;
    ll q = qr.q;
    
    // 区間 [l, r] では K / i = q なので、
    // q が (r - l + 1) 個あることになる
    ans += (r - l + 1) * q;
}

cout << "和: " << ans << endl;
応用:Σ i × (N / i) を求める ( i = 1 から N まで )
ll N = 1000000000LL;
ll ans = 0;

// 今回は K = N として呼び出す
vector<QuotientRange> ranges = enumerate_quotients(N, N);

for (auto qr : ranges) {
    ll l = qr.l;
    ll r = qr.r;
    ll q = qr.q;
    ans += q * ((l + r) * (r - l + 1) / 2);
}

cout << "和: " << ans << endl;
応用:Σ (N % i) を求める ( i = 1 から N まで )
ll N = 1000000000LL;
ll ans = 0;

// N % i = N - i * (N / i) を利用する
vector<QuotientRange> ranges = enumerate_quotients(N, N);

for (auto qr : ranges) {
    ll l = qr.l;
    ll r = qr.r;
    ll q = qr.q;

    // 区間 [l, r] での N の総和: (r - l + 1) * N
    // 区間 [l, r] での i * q の総和: q * (l + r) * (r - l + 1) / 2
    ll count = r - l + 1;
    ll sum_i = (l + r) * count / 2;
    ans += count * N - q * sum_i;
}

cout << "和: " << ans << endl;

atcoder::floor_sum

用途: 一次式の床関数の総和(格子点の数え上げ、modの総和など)の高速計算(ACL実装)

計算量: O(log(n + m + a + b))。
内部は #include <atcoder/math> と using namespace atcoder; が必要です。
数式 Σ_{i=0}^{n-1} floor((a * i + b) / m) の値を計算します。引数の制約は 0 ≤ n, 1 ≤ m, 0 ≤ a, b です。

#include <atcoder/math>
using namespace std;
using namespace atcoder;
using ll = long long;

int main() {
    ll n = 10;
    ll m = 3;
    ll a = 2;
    ll b = 1;
    
    // sum_{i=0}^{n-1} floor((a*i + b) / m) を計算
    // 例: i=0 -> floor(1/3)=0, i=1 -> floor(3/3)=1, ...
    ll ans = floor_sum(n, m, a, b);
    
    return 0;
}

▼ floor_sum 応用テンプレート

負の係数に対応する拡張 floor_sum

ACLの floor_sum は a, b ≥ 0 が条件ですが、実戦では負の数が登場することが多々あります。事前に m の倍数を足し引きして非負に変換する汎用ラッパーです。

using ll = long long;

ll floor_sum_any(ll n, ll m, ll a, ll b) {
    ll ans = 0;
    // a が負の場合の補正 (正の場合も商を外に出して a < m にする)
    if (a < 0) {
        ll a2 = (a % m + m) % m;
        ans -= 1LL * n * (n - 1) / 2 * ((a2 - a) / m);
        a = a2;
    } else if (a >= m) {
        ans += 1LL * n * (n - 1) / 2 * (a / m);
        a %= m;
    }
    // b が負の場合の補正
    if (b < 0) {
        ll b2 = (b % m + m) % m;
        ans -= 1LL * n * ((b2 - b) / m);
        b = b2;
    } else if (b >= m) {
        ans += 1LL * n * (b / m);
        b %= m;
    }
    return ans + atcoder::floor_sum(n, m, a, b);
}
余りの総和 (Sum of Remainders)

Σ (a * i + b) % m の計算。X % m = X - m * floor(X / m) の性質を用いて計算します。

using ll = long long;

// sum_{i=0}^{n-1} ((a*i + b) % m)
ll mod_sum(ll n, ll m, ll a, ll b) {
    // Σ (a*i + b) の計算(オーバーフロー注意)
    ll total_sum = n * b + a * (n * (n - 1) / 2);
    
    // Σ (a*i + b) % m = Σ (a*i + b) - m * Σ floor((a*i+b)/m)
    return total_sum - m * atcoder::floor_sum(n, m, a, b);
}

mod の扱い

用途: 負数処理・剰余の正規化

計算量: O(1)。 見た目以上にバグの出やすい箇所。

long long norm(long long x, long long mod) {
    x %= mod;
    if (x < 0) x += mod;
    return x;
}

注意: C++ の % は負数で負の剰余を返しうる。数学的な mod と同一視しない。

pow / modpow

用途: 累乗計算・mod累乗

計算量: 繰り返し二乗法で O(log N)。 競プロでは mod つきが本命。

long long powll(long long a, long long e) {
    long long r = 1;
    while (e) {
        if (e & 1) r *= a;
        a *= a;
        e >>= 1;
    }
    return r;
}

long long modpow(long long a, long long e, long long mod) {
    long long r = 1;
    while (e) {
        if (e & 1) r = r * a % mod;
        a = a * a % mod;
        e >>= 1;
    }
    return r;
}

注意: 乗算前の型幅に注意。long long でも危険なら __int128 を使う。

汎用繰り返し二乗法 (Generic Fast Exponentiation)

用途: 結合法則を満たす任意の演算(モノイド)において、要素の $E$ 乗(同じ演算を $E$ 回繰り返す操作)を高速に計算する。整数だけでなく、行列の累乗や置換の合成などにも用いる。

計算量: O(log E × [1回の演算の計算量])。 通常の while ループで 1 つずつ掛ける素朴な方法の計算量 $O(E)$ を、二進数展開を利用することで対数時間に劇的に削減します。

template <typename T, typename F>
T power(T a, long long e, T identity, F op) {
    T res = identity;
    while (e > 0) {
        if (e & 1) res = op(res, a);
        a = op(a, a);
        e >>= 1;
    }
    return res;
}

▼ 使い方(目的別の初期化方法)

基本:整数の剰余累乗 ($a^e \bmod M$)
using ll = long long;
ll MOD = 1000000007;

ll a = 2;
ll e = 1000000000000LL; // 2^1000000000000 を計算

// 単位元は 1、演算は乗算の剰余
auto mul_op = [&](ll x, ll y) { return (x * y) % MOD; };

ll ans = power<ll>(a, e, 1LL, mul_op);
cout << ans << endl;
応用:行列の累乗 (例: フィボナッチ数列の第 $N$ 項)
using ll = long long;
using Matrix = vector<vector<ll>>;
ll MOD = 998244353;

// 2x2 行列の積を定義
auto mat_mul = [&](const Matrix& A, const Matrix& B) {
    Matrix C(2, vector<ll>(2, 0));
    for (int i = 0; i < 2; i++) {
        for (int k = 0; k < 2; k++) {
            for (int j = 0; j < 2; j++) {
                C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
            }
        }
    }
    return C;
};

// 2x2 の単位行列
Matrix identity = {{1, 0}, {0, 1}};

// 遷移元の行列
Matrix A = {{1, 1}, {1, 0}};
ll N = 1000000000000LL;

// A の N 乗を計算
Matrix An = power<Matrix>(A, N, identity, mat_mul);

// フィボナッチ数列の第 N 項 (F_0 = 0, F_1 = 1 とした場合)
cout << An[1][0] << endl;
応用:文字列の繰り返し結合
string s = "abc";
long long e = 5;

// 単位元は空文字列、演算は文字列結合
auto str_concat = [](const string& x, const string& y) { return x + y; };

string ans = power<string>(s, e, "", str_concat);
cout << ans << endl; // "abcabcabcabcabc"

注意: 指定する演算 op は結合法則 op(a, op(b, c)) == op(op(a, b), c) を満たしている必要があります。また、引数 identity はその演算における単位元(op(a, identity) == a かつ op(identity, a) == a となる要素)を正しく指定してください。演算のコストが重い場合(大きな行列など)は、ラムダ式の引数を const T& のように参照渡しで受け取ることでコピーのオーバーヘッドを減らすことができます。

nCr / nPr / nHr (非素数mod・modなし / パスカルの三角形)

用途: 素数以外のmodを取る場合や、modを取らない場合の二項係数(組合せ)の計算

計算量: 前計算に O(N2)、各クエリ(二項係数の取得)は O(1)。空間計算量(メモリ)も O(N2)。

vector<vector<ll>> C;

// mx: 必要な最大値(Nなど), mod: 割る数(0の場合はmodをとらない)
void init_nCr(ll mx, ll mod = 0) {
    C.assign(mx + 1, vector<ll>(mx + 1, 0));
    for (ll i = 0; i <= mx; i++) {
        C[i][0] = 1;
        for (ll j = 1; j <= i; j++) {
            C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
            if (mod > 0) C[i][j] %= mod;
        }
    }
}

// 組合せ nCr
ll nCr(ll n, ll r) {
    if (n < 0 || r < 0 || r > n) return 0;
    return C[n][r];
}

// 順列 nPr (※modなしの場合、nが20を超えると階乗がオーバーフローするため注意)
ll nPr(ll n, ll r, ll mod = 0) {
    if (n < 0 || r < 0 || r > n) return 0;
    ll res = 1;
    for (ll i = 0; i < r; i++) {
        res *= (n - i);
        if (mod > 0) res %= mod;
    }
    return res;
}

// 重複組合せ nHr
ll nHr(ll n, ll r) {
    if (n == 0 && r == 0) return 1;
    if (n <= 0 || r < 0) return 0;
    return nCr(n + r - 1, r);
}


// 使用例
int main() {
    // パターン1: modなしの場合 (N <= 60 程度まで)
    ll max_n_no_mod = 60; 
    init_nCr(max_n_no_mod, 0); 
    cout << "60C30 = " << nCr(60, 30) << "\n"; // 118264581564861424

    // パターン2: 任意のmod(非素数など)の場合 (N <= 5000 程度まで)
    ll mod = 100000; // 素数ではないmodの例
    ll max_n_mod = 5000;
    init_nCr(max_n_mod, mod);
    cout << "5000C2500 mod 100000 = " << nCr(5000, 2500) << "\n";
    
    return 0;
}

使える範囲(重要):N ≤ 60 程度まで。62C31 で約 4.6 × 1018 となり ll (long long) の上限に近づく。

注意: init_nCr に渡す mx は、求める可能性のある最大の n (重複組合せ nHr の場合は n + r - 1)をカバーできるよう余裕を持ったサイズで呼び出してください。

factorial / fact_inv / nCr / nPr / nHr (mod)

用途: 階乗・階乗逆元・逆元・二項係数(組合せ)の前計算と高速クエリ

計算量: 前計算に O(N)、各クエリ(階乗・階乗逆元・二項係数・順列・重複組合せの取得)は O(1)。

vector<long long> fact, fact_inv;

// mx: 必要な最大値(Nなど), mod: 割る数(素数前提)
void init_nCr(ll mx, long long mod) {
    fact.assign(mx + 1, 1);
    fact_inv.assign(mx + 1, 1);
    vector<long long> inv(mx + 1, 1);
    for (ll i = 2; i <= mx; i++) {
        fact[i] = fact[i - 1] * i % mod;
        inv[i] = mod - inv[mod % i] * (mod / i) % mod;
        fact_inv[i] = fact_inv[i - 1] * inv[i] % mod;
    }
}

// 階乗 n!
long long get_fact(ll n) {
    if (n < 0) return 0;
    return fact[n];
}

// 階乗の逆元 (n!)^-1
long long get_fact_inv(ll n) {
    if (n < 0) return 0;
    return fact_inv[n];
}

// 組合せ nCr
long long nCr(ll n, ll r, long long mod) {
    if (r < 0 || r > n) return 0;
    return fact[n] * fact_inv[r] % mod * fact_inv[n - r] % mod;
}

// 順列 nPr
long long nPr(ll n, ll r, long long mod) {
    if (r < 0 || r > n) return 0;
    return fact[n] * fact_inv[n - r] % mod;
}

// 重複組合せ nHr
long long nHr(ll n, ll r, long long mod) {
    if (n == 0 && r == 0) return 1;
    if (n <= 0 || r < 0) return 0;
    return nCr(n + r - 1, r, mod);
}


//使用例
int main() {
    long long mod = 1000000007;
    ll max_n = 200000; // 問題内で登場する最大の N

    // 1. クエリを呼ぶ前に必ず一度だけ前計算を走らせる
    init_nCr(max_n, mod);

    // 2. 必要なタイミングで各種関数を呼び出す (各 O(1))
    long long f5 = get_fact(5);         // 5! = 120
    long long fi5 = get_fact_inv(5);    // (5!)^-1 mod 1000000007
    long long ans1 = nCr(10, 3, mod);   // 10C3 = 120

    cout << "5! = " << f5 << endl;
    cout << "(5!)^-1 mod 10^9+7 = " << fi5 << endl;
    cout << "10C3 = " << ans1 << endl;
}

注意: init_nCr に渡す mx は、求める可能性のある最大の n (重複組合せ nHr の場合は n + r - 1)をカバーできるよう余裕を持ったサイズで呼び出すこと。

注意: この逆元計算のアルゴリズムは mod が素数であることを前提としている。素数でない場合は拡張ユークリッドの互除法を個別に呼ぶか、パスカルの三角形による O(N2) の DP でテーブルを作る。

AtCoder Library modint

用途: mod を取りながら加減乗除を安全に行う(逆元計算を含む)

計算量: 加算・減算・乗算は O(1)、除算は逆元計算を行うため O(log MOD)。 pow() は O(log n)。

#include <atcoder/all>
int main() {

    // ===== 初期化 =====
    mint a = 3;
    mint b(5);

    // ===== 四則演算 =====
    mint c = a + b;
    mint d = a - b;
    mint e = a * b;
    mint f = a / b;      // b の逆元を掛ける

    // ===== int や long long と混ぜてもOK =====
    mint x = 10;

    x += 5;
    x -= 2;
    x *= 7;
    x /= 3;

    // ===== 累乗・逆元 =====
    mint p = x.pow(10);  // x^10
    mint inv = x.inv();  // x^{-1}

    // ===== 値の取り出し =====
    cout << x.val() << endl;

    // ===== 配列でも使える =====
    vector<mint> dp(100);

    // ===== 期待値などの分数 =====
    int N = 5;
    mint invN = mint(1) / N; // 1/N (mod)

    mint ans = mint(2) / 3;  // 2/3 (mod)

    // ===== よくある書き方 =====
    ans += mint(100) * invN;
}

注意: 1 / N のように整数同士で割ると整数除算になってしまう。 分数を表したい場合は必ず mint(1) / N や mint(A) / B のように、少なくとも片方を mint にすること。

注意: 出力するときは cout << ans.val(); のように val() を呼ぶ。 mint 型をそのまま cout へ渡すことはできない。

注意: 除算や inv() は法における逆元を利用するため、 割る数が 0 であってはならない。 また、modint998244353 や modint1000000007 は法が素数であることを前提としている。

mod_inv (一般のmod逆元)

用途: 法が素数でない(合成数を含む)任意の mod に対するモジュラ逆元計算

計算量: 拡張ユークリッドの互除法により O(log m)。フェルマーの小定理が使えない状況において最も高速で汎用的な手法。

// a と m が互いに素なとき、a * x ≡ 1 (mod m) となる x を返す
long long mod_inv(long long a, long long m) {
    long long b = m, u = 1, v = 0;
    while (b) {
        long long t = a / b;
        a -= t * b; swap(a, b);
        u -= t * v; swap(u, v);
    }
    u %= m;
    if (u < 0) u += m;
    return u;
}

注意: 引数の a と m は互いに素(gcd(a, m) == 1)である必要があります。互いに素でない場合は逆元が存在せず、この関数は無効な値を返します。必要であれば事前に gcd の確認や素因数の分離を行ってください。

prime factorization

用途: 素因数分解

計算量: 試し割りで O(√N)。 N ≤ 1012 程度なら実用的。

vector<pair<long long, ll>> prime_factorize(long long n) {
    vector<pair<long long, ll>> res;

    for (long long p = 2; p * p <= n; p++) {
        if (n % p != 0) continue;

        ll cnt = 0;
        while (n % p == 0) {
            n /= p;
            cnt++;
        }
        res.push_back({p, cnt});
    }

    if (n > 1) res.push_back({n, 1});

    return res;
}

注意: ループ条件は p * p <= n。 割り進めることで探索範囲も縮むため、 実際の計算量はかなり軽い。

Miller-Rabin Primality Test (64-bit)

用途: 64bit以下の整数 N が素数であるかを決定的かつ超高速に判定する

計算量: O(k log N) (k はテストする底の数。64bit以下の場合は k=7 で確定的)

計算回数とクエリ耐性: 最悪の場合(Nが \(2^{64}\) に近い合成数でギリギリまで判定が長引く場合)、1回の計算で約 7 × 64 = 448 回の __int128_t 乗算と剰余演算が発生します。最新のC++環境であれば、1秒間に 約 100万 〜 500万 クエリ 程度まで余裕で対応可能です。

using u64 = unsigned long long;
using u128 = __int128_t;

// a^b mod m を計算する関数
u64 modpow(u64 a, u64 b, u64 m) {
    a %= m;
    u64 res = 1;
    while (b > 0) {
        if (b & 1) res = (u64)((u128)res * a % m);
        a = (u64)((u128)a * a % m);
        b >>= 1;
    }
    return res;
}

// 64bit整数の素数判定 (Miller-Rabin)
bool is_prime(u64 n) {
    if (n <= 1) return false;
    if (n == 2 || n == 3) return true;
    if (n % 2 == 0) return false;

    u64 d = n - 1;
    int s = 0;
    while (d % 2 == 0) {
        d /= 2;
        s++;
    }

    // 64bit整数を確実に判定できる魔法の7つの底 (Jim Sinclair's bases)
    static const u64 bases[] = {2, 325, 9375, 28178, 450775, 9780504, 1795265022};

    for (u64 a : bases) {
        if (n <= a) break; // nがベース以下の素数の場合はループを抜けてtrue
        
        u64 p = modpow(a, d, n);
        if (p == 1 || p == n - 1) continue;
        
        bool ok = false;
        for (int r = 1; r < s; r++) {
            p = (u64)((u128)p * p % n);
            if (p == n - 1) {
                ok = true;
                break;
            }
        }
        if (!ok) return false;
    }
    return true;
}

注意: 判定時の乗算でオーバーフローを防ぐため __int128_t を使用しています。GCCやClang等の64bit環境で動作します。Jim Sinclair氏によって発見された7つの底(ベース)を使用することで、\(2^{64}\) 未満のすべての数に対して確率的ではなく100%確定的に判定できます。

▼ 使い方と応用パターン

基本の使い方(単一の判定)

巨大な整数が素数かどうかを1発で判定します。

unsigned long long N = 1000000000000000003ull; // 非常に大きい素数

if (is_prime(N)) {
    cout << N << " is prime!" << endl;
} else {
    cout << N << " is composite." << endl;
}
応用:大量のクエリ処理

競技プログラミングなどで、配列内の巨大な要素がそれぞれ素数かどうかのクエリに高速に答える場合に使用します。

int Q;
cin >> Q;
int prime_count = 0;

for (int i = 0; i < Q; ++i) {
    unsigned long long x;
    cin >> x;
    if (is_prime(x)) {
        prime_count++;
    }
}

cout << "Number of primes: " << prime_count << endl;

enumerate divisors

用途: 約数列挙

計算量: O(√N)。 N ≤ 1012 程度なら実用的。

vector<long long> enumerate_divisors(long long n) {
    vector<long long> res;

    for (long long i = 1; i * i <= n; i++) {
        if (n % i != 0) continue;

        res.push_back(i);
        // 重複しない場合のみ、ペアとなる約数も追加
        if (i * i != n) {
            res.push_back(n / i);
        }
    }

    // 小さい順に並び替える
    sort(res.begin(), res.end());
    
    return res;
}

注意: ループ条件は i * i <= n。 約数 i が見つかった際、同時にペアとなる n / i も追加することで、探索範囲を大幅に削減($O(\sqrt{N})$)をカバーできるよう余裕を持ったサイズで呼び出すこと。

sieve of eratosthenes (spf / 拡張版)

用途: 高速素数列挙・高速素因数分解・各種数論関数の前計算(メビウス、φ、約数など)

計算量: 前処理: O(N log log N)、クエリ: O(log N) または O(1)。 N ≤ 107 程度なら高速に動作。
注意: メモリ節約のため、問題に必要な機能だけ ENABLE_XXX フラグを true にして使用してください。

// メモリ節約のため、必要な機能のみ true にする
const bool ENABLE_OMEGA     = false; // 素因数の種類数 (omega)
const bool ENABLE_BIG_OMEGA = false; // 素因数の重複込みの個数 (Big Omega)
const bool ENABLE_MAX_PRIME = false; // 最大素因数
const bool ENABLE_MOBIUS    = false; // メビウス関数 (mu)
const bool ENABLE_PHI       = false; // オイラーのφ関数 (phi)
const bool ENABLE_DIV_CNT   = false; // 約数の個数
const bool ENABLE_DIV_SUM   = false; // 約数の総和

struct Sieve {
    vector<ll> min_factor;
    vector<ll> primes;
    vector<int> omega, big_omega, mu;
    vector<ll> max_prime, phi, div_cnt, div_sum;

    Sieve(ll n) : min_factor(n + 1) {
        if (ENABLE_OMEGA) omega.assign(n + 1, 0);
        if (ENABLE_BIG_OMEGA) big_omega.assign(n + 1, 0);
        if (ENABLE_MAX_PRIME) max_prime.assign(n + 1, 1);
        if (ENABLE_MOBIUS) mu.assign(n + 1, 0);
        if (ENABLE_PHI) phi.assign(n + 1, 0);
        if (ENABLE_DIV_CNT) div_cnt.assign(n + 1, 0);
        if (ENABLE_DIV_SUM) div_sum.assign(n + 1, 0);

        if (ENABLE_MOBIUS) mu[1] = 1;
        if (ENABLE_PHI) phi[1] = 1;
        if (ENABLE_DIV_CNT) div_cnt[1] = 1;
        if (ENABLE_DIV_SUM) div_sum[1] = 1;

        for (ll i = 0; i <= n; i++) min_factor[i] = i;
        for (ll i = 2; i * i <= n; i++) {
            if (min_factor[i] == i) {
                for (ll j = i * i; j <= n; j += i) {
                    if (min_factor[j] == j) {
                        min_factor[j] = i;
                    }
                }
            }
        }
        
        for (ll i = 2; i <= n; i++) {
            if (min_factor[i] == i) primes.push_back(i);
            
            // 各種関数の計算 (SPFを利用したDP)
            ll p = min_factor[i];
            ll prev = i / p;
            
            if (ENABLE_OMEGA) {
                omega[i] = omega[prev] + ((prev % p == 0) ? 0 : 1);
            }
            if (ENABLE_BIG_OMEGA) {
                big_omega[i] = big_omega[prev] + 1;
            }
            if (ENABLE_MAX_PRIME) {
                max_prime[i] = max(p, max_prime[prev]);
            }
            if (ENABLE_MOBIUS) {
                if (prev % p == 0) mu[i] = 0;
                else mu[i] = -mu[prev];
            }
            if (ENABLE_PHI) {
                if (prev % p == 0) phi[i] = phi[prev] * p;
                else phi[i] = phi[prev] * (p - 1);
            }
            if (ENABLE_DIV_CNT || ENABLE_DIV_SUM) {
                ll k = 0, temp = i, pk = 1;
                while (temp % p == 0) {
                    temp /= p;
                    k++;
                    pk *= p;
                }
                if (ENABLE_DIV_CNT) {
                    div_cnt[i] = div_cnt[temp] * (k + 1);
                }
                if (ENABLE_DIV_SUM) {
                    ll p_sum = (pk * p - 1) / (p - 1);
                    div_sum[i] = div_sum[temp] * p_sum;
                }
            }
        }
    }

    bool is_prime(ll x) const {
        if (x < 2) return false;
        return min_factor[x] == x;
    }

    vector<pair<ll, ll>> factor(ll x) const {
        vector<pair<ll, ll>> res;
        while (x > 1) {
            ll p = min_factor[x];
            ll cnt = 0;
            while (x % p == 0) {
                x /= p;
                cnt++;
            }
            res.push_back({p, cnt});
        }
        return res;
    }

    // 値 x の各種情報を取得するヘルパー
    int get_omega(ll x) const { return omega[x]; }
    int get_big_omega(ll x) const { return big_omega[x]; }
    ll get_max_prime(ll x) const { return max_prime[x]; }
    int get_mobius(ll x) const { return mu[x]; }
    ll get_phi(ll x) const { return phi[x]; }
    ll get_div_cnt(ll x) const { return div_cnt[x]; }
    ll get_div_sum(ll x) const { return div_sum[x]; }
};

解説: 各数の「最小の素因数(SPF)」を利用して状態遷移(DP)を行うことで、エラトステネスの篩の処理中に O(1) 〜 O(log X) の軽い計算を追加するだけで、各数論関数のテーブルを前計算しています。

▼ 使い方と応用パターン

基本の使い方

メモリを節約するため、問題で使わない機能は const bool フラグを false に変更してください。

int main() {
    // 1. まず、探索したい最大値(N)を指定して、篩(構造体)を初期化する
    // ※ENABLE_XXX を true にした機能の配列が同時に計算されます。
    Sieve sieve(1000000);

    // 2. 素数判定と素因数分解 (常に有効)
    cout << sieve.is_prime(53) << endl;   // 1 (true)
    
    auto res1 = sieve.factor(24);
    cout << "24の素因数分解: ";
    for (auto p : res1) cout << p.first << "^" << p.second << " ";
    cout << endl; // 2^3 3^1

    // 3. 各種関数の O(1) 取得 (フラグを true にした場合のみ利用可能)
    ll target = 24;

    // ※ENABLE_MOBIUS = true の場合
    cout << "メビウス関数: " << sieve.get_mobius(target) << endl; // 0

    // ※ENABLE_PHI = true の場合
    cout << "オイラーのφ関数: " << sieve.get_phi(target) << endl; // 8

    // ※ENABLE_DIV_CNT = true の場合
    cout << "約数の個数: " << sieve.get_div_cnt(target) << endl; // 8

    // ※ENABLE_DIV_SUM = true の場合
    cout << "約数の総和: " << sieve.get_div_sum(target) << endl; // 60

    return 0;
}

segment sieve (完全拡張版)

用途: 巨大な区間 [L, R] における素数判定、素因数分解、種類数・重複込みの個数、最大素因数、メビウス関数、オイラーのφ関数、約数の個数・総和の高速計算

計算量: 前処理: O(√R log log √R)、構築: O((R - L) log log R)。
注意: R ≤ 1014, R - L ≤ 107 の場合、すべての配列を有効にするとMLEの危険があるため、問題に必要な機能だけ ENABLE_XXX フラグを true にして使用してください。

// 前提:sieve of eratosthenes, isqrt
// メモリ節約のため、必要な機能のみ true にする
const bool ENABLE_FACTOR    = false; // 素因数分解 (※ 10^7要素だとMLE注意!)
const bool ENABLE_IS_PRIME  = true;  // 素数判定
const bool ENABLE_OMEGA     = false;  // 素因数の種類数 (omega)
const bool ENABLE_BIG_OMEGA = false;  // 素因数の重複込みの個数 (Big Omega)
const bool ENABLE_MAX_PRIME = false;  // 最大素因数
const bool ENABLE_MOBIUS    = false;  // メビウス関数 (mu)
const bool ENABLE_PHI       = false;  // オイラーのφ関数 (phi)
const bool ENABLE_DIV_CNT   = false;  // 約数の個数
const bool ENABLE_DIV_SUM   = false;  // 約数の総和

struct SegmentSieve {
    ll L, R;
    vector<ll> val;
    vector<vector<pair<ll, ll>>> factors;
    vector<bool> is_p;
    vector<int> omega, big_omega, mu;
    vector<ll> max_prime, phi, div_cnt, div_sum;

    SegmentSieve(ll L, ll R, const vector<ll>& primes)
        : L(L), R(R), val(R - L + 1) {
        
        ll len = R - L + 1;
        for (ll i = 0; i < len; i++) val[i] = L + i;
        
        // 必要な配列のみメモリ確保
        if (ENABLE_FACTOR) factors.resize(len);
        if (ENABLE_IS_PRIME) is_p.assign(len, true);
        if (ENABLE_OMEGA) omega.assign(len, 0);
        if (ENABLE_BIG_OMEGA) big_omega.assign(len, 0);
        if (ENABLE_MAX_PRIME) max_prime.assign(len, 1);
        if (ENABLE_MOBIUS) mu.assign(len, 1);
        if (ENABLE_PHI) {
            phi.resize(len);
            for (ll i = 0; i < len; i++) phi[i] = L + i;
        }
        if (ENABLE_DIV_CNT) div_cnt.assign(len, 1);
        if (ENABLE_DIV_SUM) div_sum.assign(len, 1);

        // 0と1の例外処理
        if (ENABLE_IS_PRIME) {
            if (L == 0) { if (len > 0) is_p[0] = false; if (len > 1) is_p[1] = false; }
            if (L == 1) { if (len > 0) is_p[0] = false; }
        }

        for (ll p : primes) {
            if (p * p > R) break;
            ll start = (L + p - 1) / p * p;
            if (start == 0) start += p; // L=0 の場合の0除算等回避

            for (ll j = start; j <= R; j += p) {
                ll idx = j - L;
                ll cnt = 0;
                while (val[idx] % p == 0) {
                    val[idx] /= p;
                    cnt++;
                }
                
                if (cnt > 0) {
                    if (ENABLE_FACTOR) factors[idx].push_back({p, cnt});
                    if (ENABLE_IS_PRIME && j != p) is_p[idx] = false;
                    if (ENABLE_OMEGA) omega[idx]++;
                    if (ENABLE_BIG_OMEGA) big_omega[idx] += cnt;
                    if (ENABLE_MAX_PRIME) max_prime[idx] = p; // p は昇順なので常に最大で上書きされる
                    if (ENABLE_MOBIUS) {
                        if (cnt == 1) mu[idx] = -mu[idx];
                        else mu[idx] = 0;
                    }
                    if (ENABLE_PHI) phi[idx] = phi[idx] / p * (p - 1);
                    if (ENABLE_DIV_CNT) div_cnt[idx] *= (cnt + 1);
                    if (ENABLE_DIV_SUM) {
                        // オーバーフロー回避のためループで加算 (p^0 + p^1 + ... + p^cnt)
                        ll sum = 1, cur = 1;
                        for (int k = 0; k < cnt; k++) {
                            cur *= p;
                            sum += cur;
                        }
                        div_sum[idx] *= sum;
                    }
                }
            }
        }
        
        // sqrt(R) より大きい最後の素因数が残っている場合の処理
        for (ll i = 0; i < len; i++) {
            if (val[i] > 1) {
                ll p = val[i];
                if (ENABLE_FACTOR) factors[i].push_back({p, 1});
                if (ENABLE_IS_PRIME && (L + i) != p) is_p[i] = false;
                if (ENABLE_OMEGA) omega[i]++;
                if (ENABLE_BIG_OMEGA) big_omega[i]++;
                if (ENABLE_MAX_PRIME) max_prime[i] = p; // sqrt(R)より大きいので確実に最大
                if (ENABLE_MOBIUS) mu[i] = -mu[i];
                if (ENABLE_PHI) phi[i] = phi[i] / p * (p - 1);
                if (ENABLE_DIV_CNT) div_cnt[i] *= 2;
                if (ENABLE_DIV_SUM) div_sum[i] *= (p + 1);
            }
        }
    }
    
    // 値 x (L <= x <= R) の各種情報を取得するヘルパー
    bool is_prime(ll x) const { return is_p[x - L]; }
    int get_omega(ll x) const { return omega[x - L]; }
    int get_big_omega(ll x) const { return big_omega[x - L]; }
    ll get_max_prime(ll x) const { return max_prime[x - L]; }
    int get_mobius(ll x) const { return mu[x - L]; }
    ll get_phi(ll x) const { return phi[x - L]; }
    ll get_div_cnt(ll x) const { return div_cnt[x - L]; }
    ll get_div_sum(ll x) const { return div_sum[x - L]; }
    vector<pair<ll, ll>> get_factors(ll x) const { return factors[x - L]; }
};

▼ 使い方と応用パターン

基本の使い方

メモリを節約するため、問題で使わない機能は const bool フラグを false に変更してください。

int main() {
    ll L = 999999900000LL;
    ll R = 1000000000000LL;

    // 1. sqrt(R) 以下の素数を用意
    ll sqrt_R = isqrt(R) + 1;
    Sieve sieve(sqrt_R);

    // 2. [L, R] の区間篩を構築(必要な配列のみ構築される)
    SegmentSieve seg_sieve(L, R, sieve.primes);

    // 3. 区間内の特定の値の情報を取得
    ll target = L + 12; // 例として L+12 を調べる
    
    // ※ENABLE_MAX_PRIME = true の場合
    cout << target << "の最大素因数: " << seg_sieve.get_max_prime(target) << endl;

    // ※ENABLE_BIG_OMEGA = true の場合
    cout << target << "の素因数の総数(重複込): " << seg_sieve.get_big_omega(target) << endl;
    
    // ※ENABLE_DIV_CNT = true の場合
    cout << target << "の約数の個数: " << seg_sieve.get_div_cnt(target) << endl;

    return 0;
}

Chinese Remainder Theorem (CRT)

用途: 複数のあまりの条件(x ≡ a_i mod b_i)から元の値 x mod lcm(b_1, ..., b_n) を一括で復元する

計算量: O(N log(lcm(b_i))) (Nは条件の数)

// 拡張ユークリッド互除法: g = gcd(a, b), ax + by = g となる (g, x) を返す
pair<long long, long long> inv_gcd(long long a, long long b) {
    a = (a % b + b) % b;
    if (a == 0) return {b, 0};
    long long s = b, t = a;
    long long m0 = 0, m1 = 1;
    while (t) {
        long long u = s / t;
        s -= t * u;
        swap(s, t);
        m0 -= m1 * u;
        swap(m0, m1);
    }
    if (m0 < 0) m0 += b / s;
    return {s, m0};
}

// 複数条件 x ≡ r_i (mod m_i) を満たす x ≡ rem (mod mod) を求める
// 解が存在しない場合は {0, 0} を返す
pair<long long, long long> CRT(const vector<long long>& r, const vector<long long>& m) {
    assert(r.size() == m.size());
    int n = r.size();
    long long r0 = 0, m0 = 1;

    for (int i = 0; i < n; ++i) {
        assert(m[i] >= 1);
        long long r1 = (r[i] % m[i] + m[i]) % m[i];
        long long m1 = m[i];

        if (m0 < m1) {
            swap(r0, r1);
            swap(m0, m1);
        }
        if (m0 % m1 == 0) {
            if (r0 % m1 != r1) return {0, 0};
            continue;
        }

        auto [g, im] = inv_gcd(m0, m1);
        long long u1 = m1 / g;
        if ((r1 - r0) % g != 0) return {0, 0};

        long long x = (r1 - r0) / g % u1;
        if (x < 0) x += u1;
        x = (long long)((__int128_t)x * im % u1);

        r0 += x * m0;
        m0 *= u1;
        if (r0 < 0) r0 += m0;
    }
    return {r0, m0};
}

注意: 法 b_i 同士が互いに素でない場合でも正しく計算できますが、矛盾する条件が含まれている場合は解なしとして {0, 0} を返す。 また、乗算時のオーバーフローを防ぐため `__int128_t` を使用しています(GCC環境等で動作)。各 b_i は 1 以上である必要がある。

▼ 使い方と応用パターン

基本の使い方(複数条件から一気に復元)

x ≡ 2 (mod 3), x ≡ 3 (mod 5), x ≡ 2 (mod 7) の3つの条件を満たす最小の非負整数 x を求める。

vector<long long> r = {2, 3, 2}; // 余り a_i
vector<long long> m = {3, 5, 7}; // 法 b_i

auto [rem, mod] = CRT(r, m);

if (mod == 0) {
    cout << "No solution" << endl;
} else {
    // x ≡ rem (mod mod)
    // 解は rem + k * mod の形で表され、最小の非負整数解は rem です
    cout << "x ≡ " << rem << " (mod " << mod << ")" << endl; // x ≡ 23 (mod 105)
}
応用:解が存在しない(不能)場合の判定

法同士が互いに素でない場合に矛盾がある場合(例: x ≡ 1 (mod 4) かつ x ≡ 2 (mod 6))、戻り値の `mod`(ペアの2番目)が 0 になります。

vector<long long> r = {1, 2};
vector<long long> m = {4, 6};

auto [rem, mod] = CRT(r, m);

if (mod == 0) {
    cout << "解なし(条件が矛盾しています)" << endl;
}

RSK対応 (Robinson-Schensted-Knuth 対応)

用途: 順列から2つの標準ヤング盤への分解およびその復元、LIS (最長増加部分列) および LDS (最長減少部分列) の構造的解析、順列・ヤング盤に関する数え上げ、ヤング図形の形と共役の取得

計算量: 長さ N の順列に対して時間計算量 $O(N^2 \log N)$、空間計算量 $O(N)$。

// RSK対応によって順列からヤング盤を構築し、また復元する構造体
struct RSK {
    vector<vector<int>> P; // 挿入盤 (Insertion Tableau)
    vector<vector<int>> Q; // 記録盤 (Recording Tableau)

    // 長さ N の順列 p から RSK対応を構築
    // p は要素の重複がない前提 (重複がある場合は半標準ヤング盤になる)
    RSK(const vector<int>& p) {
        for (int i = 0; i < (int)p.size(); i++) {
            int x = p[i];
            int r = 0;
            // Pの各行に対して行挿入 (Row Insertion) を行う
            while (r < (int)P.size()) {
                // 現在の行の中で x より大きい最初の要素を探す
                auto it = upper_bound(P[r].begin(), P[r].end(), x);
                if (it == P[r].end()) {
                    // 見つからなければ行の末尾に追加して終了
                    P[r].push_back(x);
                    Q[r].push_back(i + 1); // 記録盤にはステップ数(1-based)を記録
                    break;
                } else {
                    // 見つかった場合はその要素を追い出し、追い出された要素を次の行へ挿入
                    swap(x, *it);
                    r++;
                }
            }
            // 新しい行が必要になった場合
            if (r == (int)P.size()) {
                P.push_back({x});
                Q.push_back({i + 1});
            }
        }
    }

    // ヤング図形の「形」(各行の長さの配列) を返す (分割 λ を表す)
    vector<int> shape() const {
        vector<int> res;
        for (const auto& row : P) res.push_back(row.size());
        return res;
    }

    // ヤング図形の「共役な形」(各列の長さの配列) を返す (分割 λ' を表す)
    vector<int> conjugate_shape() const {
        if (P.empty()) return {};
        vector<int> res(P[0].size(), 0);
        for (int r = 0; r < (int)P.size(); r++) {
            for (int c = 0; c < (int)P[r].size(); c++) {
                res[c]++;
            }
        }
        return res;
    }

    // 最長増加部分列 (LIS) の長さを返す (ヤング盤の1行目の長さ = shape()[0])
    int lis_length() const {
        if (P.empty()) return 0;
        return P[0].size();
    }

    // 最長減少部分列 (LDS) の長さを返す (ヤング盤の1列目の長さ = 行数)
    int lds_length() const {
        return P.size();
    }

    // PとQの組から元の順列を復元する(逆RSK対応)
    static vector<int> restore_permutation(vector<vector<int>> P_tab, vector<vector<int>> Q_tab) {
        if (P_tab.empty()) return {};
        int N = 0;
        for (const auto& row : P_tab) N += row.size();
        
        vector<int> p(N);
        // Qの値(1〜N)から、追加された位置(r, c)を逆引きする配列
        vector<pair<int, int>> pos(N + 1);
        for (int r = 0; r < (int)Q_tab.size(); r++) {
            for (int c = 0; c < (int)Q_tab[r].size(); c++) {
                pos[Q_tab[r][c]] = {r, c};
            }
        }
        
        // 最後のステップ N から 1 に向かって逆算
        for (int k = N; k >= 1; k--) {
            int r = pos[k].first;
            int c = pos[k].second;
            
            int x = P_tab[r][c];
            P_tab[r].pop_back();
            if (P_tab[r].empty()) P_tab.pop_back(); // 最後の行が空になったら削除
            
            // 逆行挿入 (Reverse Row Insertion)
            for (int i = r - 1; i >= 0; i--) {
                // 上の行の中で x より小さい最大の要素を探す
                auto it = lower_bound(P_tab[i].begin(), P_tab[i].end(), x);
                --it; 
                swap(x, *it);
            }
            p[k - 1] = x;
        }
        return p;
    }
};

▼ 使い方と応用パターン

メモ
  • 順列とヤング盤の全単射: 長さ N の順列と、同じ形を持つ2つの標準ヤング盤 (P, Q) のペアは一対一対応します。「順列を数え上げる問題」を「ヤング盤を数え上げる問題」に変換できます。
  • 逆RSKアルゴリズム: 記録盤 Q の最大値から位置を特定し、挿入盤 P において逆行挿入を繰り返すことで、元の数列を完全に復元できます。
  • ヤング図形の形と共役: shape() は各行の長さを表す分割 $\lambda$ を、conjugate_shape() は各列の長さを表す共役分割 $\lambda'$ を返します。
  • LIS と LDS の性質: 構築されたヤング盤の 1行目の長さが元の順列の LIS の長さに一致し、列の長さ(行数)が LDS の長さに一致します。
基本の使い方(全機能の網羅)
vector<int> p = {3, 1, 4, 5, 2, 6};
RSK rsk(p);

// 1. P盤(挿入盤) と Q盤(記録盤) に書き込まれている数の出力
cout << "[Insertion Tableau P]" << endl;
for (const auto& row : rsk.P) {
    for (int val : row) cout << val << " ";
    cout << endl;
}
/*
1 2 5 6 
3 4 
*/

cout << "[Recording Tableau Q]" << endl;
for (const auto& row : rsk.Q) {
    for (int val : row) cout << val << " ";
    cout << endl;
}
/*
1 3 4 6 
2 5 
*/


// 2. LIS / LDS の取得
cout << "LIS Length: " << rsk.lis_length() << endl; // 4
cout << "LDS Length: " << rsk.lds_length() << endl; // 3


// 3. 形 (各行の長さ) と 共役な形 (各列の長さ) の取得
vector<int> sh = rsk.shape();
cout << "Shape: ";
for (int len : sh) cout << len << " "; 
cout << endl; // 4 2 

vector<int> conj_sh = rsk.conjugate_shape();
cout << "Conjugate Shape: ";
for (int len : conj_sh) cout << len << " "; 
cout << endl; // 2 2 1 1 


// 4. 元の順列の復元 (逆RSK対応)
vector<int> restored = RSK::restore_permutation(rsk.P, rsk.Q);
cout << "Restored p: ";
for (int val : restored) cout << val << " "; 
cout << endl; // 3 1 4 5 2 6 

フック長公式 (Hook Length Formula)

用途: 与えられたヤング図形の形に対する標準ヤング盤 (SYT) の個数の数え上げ、各マスのフック長 (Hook Length) の計算、順列・ヤング図形に関する構造的・組み合わせ論的解析

計算量: マスの総数を N としたとき、時間計算量 O(N + log MOD)、空間計算量 O(N)。
各マスのフック長(右にあるマス数 + 下にあるマス数 + 1)を O(N) で求め、N! / (フック長の積) を mod 下で計算します。

// フック長公式を用いて標準ヤング盤(SYT)の個数を計算する構造体・関数群
struct HookLength {
    // 各マスのフック長(右のマス数 + 下のマス数 + 1)のテーブルを返す
    static vector<vector<int>> get_hook_lengths(const vector<int>& lambda) {
        if (lambda.empty()) return {};
        int r = lambda.size();
        
        // 各列のマス数(共役分割 λ')を計算
        int max_c = lambda[0];
        vector<int> conj(max_c, 0);
        for (int i = 0; i < r; i++) {
            for (int j = 0; j < lambda[i]; j++) {
                conj[j]++;
            }
        }
        
        // フック長の計算: h(i, j) = (lambda[i] - j) + (conj[j] - i) - 1
        vector<vector<int>> h(r);
        for (int i = 0; i < r; i++) {
            h[i].resize(lambda[i]);
            for (int j = 0; j < lambda[i]; j++) {
                h[i][j] = (lambda[i] - j) + (conj[j] - i) - 1;
            }
        }
        return h;
    }

    // 繰り返し二乗法による mod 逆元計算用のヘルパー
    static long long mod_pow(long long a, long long n, long long mod) {
        long long res = 1;
        a %= mod;
        while (n > 0) {
            if (n & 1) res = (res * a) % mod;
            a = (a * a) % mod;
            n >>= 1;
        }
        return res;
    }

    // 形 lambda を持つ標準ヤング盤 (SYT) の個数を mod で返す
    // f^λ = N! / Π h(i, j)
    static long long count_syt(const vector<int>& lambda, long long mod = 998244353) {
        if (lambda.empty()) return 0;
        int N = 0;
        for (int x : lambda) N += x;

        // 分子: N! % mod
        long long num = 1;
        for (int i = 1; i <= N; i++) {
            num = (num * i) % mod;
        }

        // 分母: フック長の積 % mod
        auto h = get_hook_lengths(lambda);
        long long den = 1;
        for (const auto& row : h) {
            for (int val : row) {
                den = (den * val) % mod;
            }
        }

        // num * den^(mod - 2) % mod
        return (num * mod_pow(den, mod - 2, mod)) % mod;
    }
};

▼ 使い方と応用パターン

メモ
  • フック長公式 (Hook Length Formula): 分割 $\lambda$(マスの総数 $N$)のヤング図形に $1 \sim N$ を書き込んで標準ヤング盤を作る方法の個数 $f^\lambda$ は、$N! / \prod h(i,j)$ で求まります。
  • RSK対応との連携: RSK対応により、同じ形 $\lambda$ を持つ 2 つの標準ヤング盤のペア $(P, Q)$ の個数 $(f^\lambda)^2$ の総和 $\sum_{\lambda \vdash N} (f^\lambda)^2$ は、$N!$ (長さ $N$ の順列の総数)に一致します。
  • カタラン数との関連: 2行のヤング図形 $(n, n)$ に対するフック長公式の値は、第 $n$ 項のカタラン数 $C_n = \frac{1}{n+1}\binom{2n}{n}$ に一致します。
基本の使い方(全機能の網羅)
vector<int> lambda = {4, 2}; // ヤング図形の形 (各行の長さ)

// 1. 各マスのフック長の取得・出力
vector<vector<int>> hooks = HookLength::get_hook_lengths(lambda);
cout << "[Hook Lengths]" << endl;
for (const auto& row : hooks) {
    for (int h : row) cout << h << " ";
    cout << endl;
}
/*
5 4 2 1 
2 1 
*/

// 2. 標準ヤング盤 (SYT) の個数の計算 (デフォルト: mod 998244353)
long long syt_count = HookLength::count_syt(lambda);
cout << "SYT Count: " << syt_count << endl; 
// 9 個 (6! / (5*4*2*1 * 2*1) = 720 / 80 = 9)

木上のランダムウォーク(期待値DP & 高速クエリ)

用途: 木上のランダムウォークに関する期待歩数、到達確率、訪問期待回数の計算。
遷移確率が均等の場合や、任意の辺重みに基づく遷移確率の調整にも対応。
DPによる一括計算($O(N)$)と、LCAを用いた任意の2点間クエリ($O(\log N)$)を両立。

計算量: 状態指定の汎用DP: $O(N)$。
2点間の到達期待歩数クエリ: 前計算 $O(N \log N)$、クエリ $O(\log N)$。

using ll = long long;
using ld = long double;

// 汎用性のため、ld (long double) や modint (例: modint998244353) を T に指定できる
template<typename T>
struct TreeRandomWalk {
    struct Edge {
        ll to;
        T w;
    };
    ll n;
    vector<vector<Edge>> g;
    vector<T> weight_sum;
    T total_weight_sum;

    // 高速クエリ(LCA)用
    bool is_built_lca = false;
    ll logV;
    vector<vector<ll>> parent;
    vector<ll> depth;
    vector<T> dist_up;
    vector<T> dist_down;

    TreeRandomWalk(ll n) : n(n), g(n), weight_sum(n, T(0)), total_weight_sum(T(0)) {}
    
    // w: 遷移の重み(等分布なら省略で1になる。遷移確率は周囲の辺の重みの割合で決まる)
    void add_edge(ll u, ll v, T w = T(1)) {
        g[u].push_back({v, w});
        g[v].push_back({u, w});
        weight_sum[u] += w;
        weight_sum[v] += w;
        total_weight_sum += w * T(2);
    }
    
    // --- 1. 高速クエリ (任意の u から v への期待歩数) ---
    void build_fast_query(ll root = 0) {
        logV = 1;
        while ((1LL << logV) <= n) logV++;
        parent.assign(logV, vector<ll>(n, -1));
        depth.assign(n, 0);
        dist_up.assign(n, T(0));
        dist_down.assign(n, T(0));

        vector<T> sub_W(n, T(0));

        // 1回目のDFS: 各部分木の重み和を計算
        auto dfs1 = [&](auto& self, ll u, ll p, ll d) -> void {
            parent[0][u] = p;
            depth[u] = d;
            sub_W[u] = weight_sum[u];
            for (auto& edge : g[u]) {
                ll v = edge.to;
                if (v == p) continue;
                self(self, v, u, d + 1);
                sub_W[u] += sub_W[v];
            }
        };
        dfs1(dfs1, root, -1, 0);

        // 2回目のDFS: 木を切断した際の連結成分の重み和を用いてパスの期待値を累積
        auto dfs2 = [&](auto& self, ll u, ll p) -> void {
            for (auto& edge : g[u]) {
                ll v = edge.to;
                if (v == p) continue;
                T w = edge.w;
                // v から u への期待歩数
                T E_v_to_u = sub_W[v] / w;
                // u から v への期待歩数
                T E_u_to_v = (total_weight_sum - sub_W[v]) / w;

                dist_up[v] = dist_up[u] + E_v_to_u;
                dist_down[v] = dist_down[u] + E_u_to_v;
                
                self(self, v, u);
            }
        };
        dfs2(dfs2, root, -1);

        // ダブリングテーブル構築
        for (ll k = 0; k + 1 < logV; k++) {
            for (ll u = 0; u < n; u++) {
                if (parent[k][u] < 0) parent[k + 1][u] = -1;
                else parent[k + 1][u] = parent[k][parent[k][u]];
            }
        }
        is_built_lca = true;
    }

    ll get_lca(ll u, ll v) {
        if (!is_built_lca) build_fast_query();
        if (depth[u] > depth[v]) swap(u, v);
        for (ll k = 0; k < logV; k++) {
            if (((depth[v] - depth[u]) >> k) & 1) {
                v = parent[k][v];
            }
        }
        if (u == v) return u;
        for (ll k = logV - 1; k >= 0; k--) {
            if (parent[k][u] != parent[k][v]) {
                u = parent[k][u];
                v = parent[k][v];
            }
        }
        return parent[0][u];
    }

    // u から v に到達するまでの期待歩数
    T expected_steps_fast(ll u, ll v) {
        if (!is_built_lca) build_fast_query();
        ll lca = get_lca(u, v);
        T E_u_lca = dist_up[u] - dist_up[lca];
        T E_lca_v = dist_down[v] - dist_down[lca];
        return E_u_lca + E_lca_v;
    }

    // --- 2. 汎用DPソルバー (複数ゴールや確率、訪問回数など) ---
    // is_fixed[u]: 境界条件として固定されているか
    // fixed_val[u]: 固定値
    // C[u]: 1ステップごとの加算コスト
    vector<T> solve(const vector<bool>& is_fixed, const vector<T>& fixed_val, const vector<T>& C) {
        vector<T> A(n, T(0)), B(n, T(0));
        vector<T> ans(n, T(0));
        
        ll root = -1;
        for (ll i = 0; i < n; i++) {
            if (is_fixed[i]) { root = i; break; }
        }
        if (root == -1) return ans;
        
        auto dfs_bottom_up = [&](auto& self, ll u, ll p) -> void {
            T sum_A = T(0), sum_B = T(0);
            T p_weight = T(0);
            
            // 先にすべての子頂点を探索する
            for (auto& edge : g[u]) {
                ll v = edge.to;
                if (v == p) {
                    p_weight = edge.w;
                    continue;
                }
                self(self, v, u);
                sum_A += edge.w * A[v];
                sum_B += edge.w * B[v];
            }
            
            // 子の探索後に自分自身の係数を決定する
            if (is_fixed[u]) {
                A[u] = T(0);
                B[u] = fixed_val[u];
            } else {
                T den = weight_sum[u] - sum_A;
                if (p != -1) A[u] = p_weight / den;
                else A[u] = T(0);
                B[u] = (sum_B + weight_sum[u] * C[u]) / den;
            }
        };
        dfs_bottom_up(dfs_bottom_up, root, -1);
        
        auto dfs_top_down = [&](auto& self, ll u, ll p) -> void {
            if (p == -1) ans[u] = B[u];
            else ans[u] = A[u] * ans[p] + B[u];
            
            for (auto& edge : g[u]) {
                ll v = edge.to;
                if (v == p) continue;
                self(self, v, u);
            }
        };
        dfs_top_down(dfs_top_down, root, -1);
        return ans;
    }
    
    // DP: ゴールリストに到達するまでの期待歩数
    vector<T> expected_steps(const vector<ll>& goals) {
        vector<bool> is_fixed(n, false);
        vector<T> fixed_val(n, T(0));
        vector<T> C(n, T(1));
        for (ll g_node : goals) {
            is_fixed[g_node] = true;
        }
        return solve(is_fixed, fixed_val, C);
    }
    
    // DP: ターゲットのいずれかに到達する確率 (avoid_nodesは吸収されるが到達扱いしない)
    vector<T> hit_probability(const vector<ll>& targets, const vector<ll>& avoid_nodes) {
        vector<bool> is_fixed(n, false);
        vector<T> fixed_val(n, T(0));
        vector<T> C(n, T(0));
        for (ll t : targets) { is_fixed[t] = true; fixed_val[t] = T(1); }
        for (ll a : avoid_nodes) { is_fixed[a] = true; fixed_val[a] = T(0); }
        return solve(is_fixed, fixed_val, C);
    }
    
    // DP: ゴール到達までに特定の頂点を訪問する期待回数
    vector<T> expected_visits(const vector<ll>& goals, ll target_node) {
        vector<bool> is_fixed(n, false);
        vector<T> fixed_val(n, T(0));
        vector<T> C(n, T(0));
        C[target_node] = T(1);
        for (ll g_node : goals) { is_fixed[g_node] = true; }
        return solve(is_fixed, fixed_val, C);
    }
};

▼ 使い方と応用パターン

メモ:原理と遷移確率の調整
  • 遷移確率の調整 (重み付き辺): add_edge(u, v, w) とすることで、頂点から隣接点への遷移確率を $w$ に比例させることができます。第3引数を省略した場合は $w=1$ (等分布) になります。
  • 高速クエリの原理: 辺 $e=(u, v)$ において $u \to v$ に向かうパスの期待値は、その辺で木を切断した際の $u$ 側の成分における全頂点の重み和 (次数和) に比例するという性質を用いています。これをダブリング (LCA) と組み合わせることで、クエリごとに $O(\log N)$ での計算を可能にしています。
【最速】任意の2点間クエリ (等分布/重み付き対応)
ll N = 5;
TreeRandomWalk<ld> trw(N);
trw.add_edge(0, 1);
trw.add_edge(1, 2);
trw.add_edge(1, 3);
trw.add_edge(3, 4);

// 事前構築: 時間計算量 O(N log N)
trw.build_fast_query();

ll Q = 3;
// 各クエリ: 時間計算量 O(log N)
cout << trw.expected_steps_fast(0, 4) << endl;
cout << trw.expected_steps_fast(2, 3) << endl;
cout << trw.expected_steps_fast(4, 1) << endl;
到達確率(modint を使用した重み付き遷移)
using mint = atcoder::modint998244353;
ll N = 5;
TreeRandomWalk<mint> trw(N);
// 遷移確率を調整する例: 辺の重みを指定
trw.add_edge(0, 1, 2); // この辺は2倍選ばれやすい
trw.add_edge(1, 2, 1);
trw.add_edge(1, 3, 3);
trw.add_edge(3, 4, 1);

// 頂点 4 に到達する確率を求める (頂点 0 に行ったら失敗とみなす)
// 計算量: 時間 O(N)
vector<ll> targets = {4};
vector<ll> avoid = {0};
vector<mint> P = trw.hit_probability(targets, avoid);

cout << "頂点2から出発して4に到達する確率: " << P[2].val() << endl;
【DP】複数ゴールへの到達期待歩数 / 訪問回数
ll N = 5;
TreeRandomWalk<ld> trw(N);
// ... 辺の追加 (重みあり/なし両対応) ...

// ゴールが複数ある場合は期待値DPを利用します
// 計算量: 時間 O(N)
vector<ll> goals = {0, 4}; // ゴール
vector<ld> E = trw.expected_steps(goals);
cout << "頂点2からの期待歩数: " << E[2] << endl;

// 特定の頂点(例: 頂点3)を訪問する期待回数
// 計算量: 時間 O(N)
ll target = 3;
vector<ld> V = trw.expected_visits(goals, target);
cout << "頂点1から出発時の頂点3への訪問回数: " << V[1] << endl;
汎用ソルバーの応用 (カスタムスコアの計算)
ll N = 5;
TreeRandomWalk<ld> trw(N);
// ... 辺の追加 ...

// ゴール到達までに、頂点ごとのスコア W[u] の総和の期待値を求める場合
vector<ld> W = {10, 20, 30, 40, 50};

vector<bool> is_fixed(N, false);
vector<ld> fixed_val(N, (ld)0);
vector<ld> C(N, (ld)0);

for(ll i = 0; i < N; i++) {
    C[i] = W[i]; // その頂点に滞在するたびにスコアを加算
}
is_fixed[4] = true; // 頂点4をゴールとする
fixed_val[4] = 0;

// 計算量: 時間 O(N)
vector<ld> ans = trw.solve(is_fixed, fixed_val, C);

幾何

sin / cos / tan / atan2

用途: 三角関数・角度計算・座標幾何

計算量: O(1)。 幾何問題、回転、ベクトル、偏角計算。定数倍が重め。

const double PI = acos(-1.0);

// degree → radian
double rad = deg * PI / 180.0;

// radian → degree
double deg = rad * 180.0 / PI;

double x = cos(rad);
double y = sin(rad);

double t = tan(rad);

// 点 (x, y) の偏角 (-pi ~ pi)
double angle = atan2(y, x);

ラジアンと度数法: C++ の sin・cos・tan・atan・atan2 はすべてラジアンを使う。 180° = π rad、90° = π/2 rad。

注意: atan(y / x) ではなく atan2(y, x) を使う。 象限を正しく判定でき、x = 0 も扱える。

acos / asin / atan

用途: 逆三角関数

計算量: O(1)。 角度復元や幾何計算で使用。

double theta1 = acos(x);
double theta2 = asin(x);
double theta3 = atan(x);

// degree 表示
double deg = acos(x) * 180.0 / PI;

戻り値: acos は [0, π]、 asin は [-π/2, π/2]、 atan は (-π/2, π/2) の範囲を返す。

注意: acos・asin の引数は [-1, 1] の範囲でなければならない。 誤差対策として clamp することが多い。

distance / hypot

用途: ユークリッド距離計算

計算量: O(1)。 幾何問題の基本。

double dx = x2 - x1;
double dy = y2 - y1;

double dist = hypot(dx, dy);
// sqrt(dx * dx + dy * dy) と同等

利点: hypot は内部でオーバーフロー・アンダーフローに強く実装されている。

注意: 距離比較だけなら dx * dx + dy * dy のまま扱い、平方根を取らない方が速い。

floating point comparison

用途: 浮動小数点誤差対策

計算量: O(1)。 実数計算では必須。

const double EPS = 1e-9;

bool equal(double a, double b) {
    return fabs(a - b) < EPS;
}

注意: double 同士を a == b で比較しない。 誤差により期待通りにならないことがある。

外積による偏角ソート (Argument Sort using Cross Product)

用途: 幾何問題・偏角ソート・誤差のない角度比較

計算量: O(N log N)。 atan2 などの浮動小数点数演算を使わず、整数型の外積のみでソート。

struct Point {
    long long x, y;
};

// 座標の上下左右の半平面(領域)を判定する関数
// [ -pi, pi ) の範囲を扱い、下半平面を先行させる例
int get_zone(const Point& p) {
    if (p.x == 0 && p.y == 0) return 0;
    if (p.y < 0) return -1; // 下半平面 (y < 0)
    if (p.y > 0) return 1;  // 上半平面 (y > 0)
    if (p.x > 0) return -1; // 正のx軸 (y == 0, x > 0)
    return 1;               // 負のx軸 (y == 0, x < 0)
}

// 偏角ソート用の比較関数
bool compare_angle(const Point& a, const Point& b) {
    int zone_a = get_zone(a);
    int zone_b = get_zone(b);
    if (zone_a != zone_b) {
        return zone_a < zone_b;
    }
    // 同じ半平面内にある場合は外積で判定
    // 外積 (a.x * b.y - a.y * b.x) > 0 ならば、aから見てbが左側(反時計回り側)にある
    long long cross_product = a.x * b.y - a.y * b.x;
    if (cross_product != 0) {
        return cross_product > 0;
    }
    // 偏角が同じ場合は、原点からの距離(等、任意の基準)でタイブレーク
    return a.x * a.x + a.y * a.y < b.x * b.x + b.y * b.y;
}

// 使用例
    sort(points.begin(), points.end(), compare_angle);
    auto it = lower_bound(points.begin(), points.end(), target, compare_angle);

半平面の分割: 外積を用いた比較は「2つのベクトルのなす角が 180° 未満」であるときに正しく機能する。 そのため、あらかじめ get_zone を用いて座標平面を2つの半平面に分割し、異なる半平面間では領域の番号で比較を行う必要がある。

注意: 外積の計算 a.x * b.y - a.y * b.x では、座標値の最大値の2乗のオーダーまで値が大きくなるのでオーバーフローに注意。

CCW (Counter Clockwise) / 3点の位置関係

用途: 3点の位置関係判定・線分交差判定の基礎・凸包

計算量: O(1)。 ルートや三角関数を一切使わず、内積と外積のみで3点間の厳密な位置関係を整数で判定します。幾何問題において最も利用頻度が高く、バグを防ぐ要となる関数です。

// 2D座標の構造体
struct Point {
    long long x, y;
};

// ベクトルの引き算
Point operator-(const Point& a, const Point& b) {
    return {a.x - b.x, a.y - b.y};
}

// 外積 (Cross Product)
long long cross(const Point& a, const Point& b) {
    return a.x * b.y - a.y * b.x;
}

// 内積 (Dot Product)
long long dot(const Point& a, const Point& b) {
    return a.x * b.x + a.y * b.y;
}

// CCW: 線分 ab に対する 点 c の位置関係を返す
// 戻り値:
//  1: a → b に対して c が反時計回り (左折)
// -1: a → b に対して c が時計回り (右折)
//  2: c - a - b の順で同一直線上
// -2: a - b - c の順で同一直線上
//  0: a - c - b の順で同一直線上 (線分 ab 上に c がある)
int ccw(Point a, Point b, Point c) {
    Point ab = b - a;
    Point ac = c - a;
    
    long long cr = cross(ab, ac);
    if (cr > 0) return 1;       // 反時計回り
    if (cr < 0) return -1;      // 時計回り
    
    // 以下は同一直線上 (cr == 0) の場合
    if (dot(ab, ac) < 0) return 2;  // cがaの反対側にある
    if (dot(ab, ab) < dot(ac, ac)) return -2; // cがbの先にある
    return 0; // cが線分ab上にある (a == c, b == c の場合も含む)
}

▼ 使い方と応用パターン

基本の使い方(位置関係の出力)
Point a = {0, 0};
Point b = {2, 0};

Point c1 = {1, 1};  // 上側
Point c2 = {1, -1}; // 下側
Point c3 = {-1, 0}; // 後ろ
Point c4 = {3, 0};  // 前
Point c5 = {1, 0};  // 線分上

cout << ccw(a, b, c1) << endl; //  1 (反時計回り)
cout << ccw(a, b, c2) << endl; // -1 (時計回り)
cout << ccw(a, b, c3) << endl; //  2 (後ろ)
cout << ccw(a, b, c4) << endl; // -2 (前)
cout << ccw(a, b, c5) << endl; //  0 (線分上)
応用:線分と線分の交差判定(整数だけで完全判定)
// 線分 ab と 線分 cd が交差しているか判定する関数
// 端点が重なっている場合や、同一直線上での重なりも正しく判定可能
bool intersect(Point a, Point b, Point c, Point d) {
    // a-b に対して c と d が反対側にあるか
    bool ab_cross = ccw(a, b, c) * ccw(a, b, d) <= 0;
    // c-d に対して a と b が反対側にあるか
    bool cd_cross = ccw(c, d, a) * ccw(c, d, b) <= 0;
    
    return ab_cross && cd_cross;
}

// 使い方
Point p1 = {0, 0}, p2 = {2, 2};
Point p3 = {0, 2}, p4 = {2, 0};
cout << (intersect(p1, p2, p3, p4) ? "Yes" : "No") << endl; // Yes (バッテン)

Fraction (有理数クラス)

用途: 誤差のない分数計算・交点座標の厳密保持・直線の傾き表現

計算量: 生成時・演算時に O(log(min(分子, 分母))) (gcdの計算量)。 分母を常に正に保ち、自動で約分を行う構造体です。大小比較時に __int128_t を使うことで、桁あふれを防止します。

// GCD (最大公約数)
long long gcd(long long a, long long b) {
    if (a < 0) a = -a;
    if (b < 0) b = -b;
    return b == 0 ? a : gcd(b, a % b);
}

// 有理数 (分数) 構造体
struct Fraction {
    long long num, den; // 分子(numerator), 分母(denominator)

    Fraction(long long n = 0, long long d = 1) {
        if (d < 0) { n = -n; d = -d; } // 分母は常に正にする
        long long g = gcd(n, d);
        num = n / g;
        den = d / g;
    }

    // 比較演算子 (オーバーフロー防止のため __int128_t を使用)
    bool operator==(const Fraction& o) const { return num == o.num && den == o.den; }
    bool operator!=(const Fraction& o) const { return !(*this == o); }
    bool operator<(const Fraction& o) const {
        return (__int128_t)num * o.den < (__int128_t)o.num * den;
    }
    bool operator<=(const Fraction& o) const { return *this < o || *this == o; }
    bool operator>(const Fraction& o) const { return o < *this; }
    bool operator>=(const Fraction& o) const { return o <= *this; }
};

Line (直線に関する完全な操作群)

用途: 直線の生成・平行/垂直な直線の作図・交点や距離の計算

計算量: ほぼ全て O(1) + 正規化 O(log(min(a,b,c)))。
GeoGebraのような感覚で、ある直線に対して「平行な直線」「垂直な直線」を作図したり、「垂直二等分線」や「垂線の足」を計算できる統合セットです。計算は交点と距離以外すべて整数で行われるため、誤差が蓄積しません。

#include <cmath>

// CCW,Fractionを事前に導入しておく必要がある

struct Line {
    long long a, b, c; // ax + by + c = 0

    // a, b, c から直線を生成 (正規化)
    Line(long long a_, long long b_, long long c_) {
        long long g = gcd(gcd(a_, b_), c_);
        a = a_ / g; b = b_ / g; c = c_ / g;
        // 一意性を保つため、aが負、または「a=0かつbが負」の場合は符号を反転
        if (a < 0 || (a == 0 && b < 0)) {
            a = -a; b = -b; c = -c;
        }
    }

    // 2点 p1, p2 を通る直線
    Line(Point p1, Point p2) {
        long long a_ = p2.y - p1.y;
        long long b_ = p1.x - p2.x;
        long long c_ = p2.x * p1.y - p1.x * p2.y;
        *this = Line(a_, b_, c_);
    }

    bool operator<(const Line& o) const {
        if (a != o.a) return a < o.a;
        if (b != o.b) return b < o.b;
        return c < o.c;
    }
    bool operator==(const Line& o) const {
        return a == o.a && b == o.b && c == o.c;
    }
};

// ---------------- 作図・生成系 ----------------

// 2点の垂直二等分線を生成
Line perpendicular_bisector(Point p1, Point p2) {
    long long a = 2 * (p2.x - p1.x);
    long long b = 2 * (p2.y - p1.y);
    long long c = (p1.x * p1.x - p2.x * p2.x) + (p1.y * p1.y - p2.y * p2.y);
    return Line(a, b, c);
}

// 直線lと平行で、点pを通る直線を生成
Line parallel_line(const Line& l, Point p) {
    return Line(l.a, l.b, -(l.a * p.x + l.b * p.y));
}

// 直線lと垂直で、点pを通る直線を生成
Line perpendicular_line(const Line& l, Point p) {
    return Line(-l.b, l.a, l.b * p.x - l.a * p.y);
}

// ---------------- 判定系 ----------------

// 平行判定 (外積が0)
bool is_parallel(const Line& l1, const Line& l2) {
    return l1.a * l2.b - l1.b * l2.a == 0;
}

// 直交判定 (内積が0)
bool is_orthogonal(const Line& l1, const Line& l2) {
    return l1.a * l2.a + l1.b * l2.b == 0;
}

// 交差判定 (平行でない、または同一の直線である)
bool is_intersect(const Line& l1, const Line& l2) {
    return !is_parallel(l1, l2) || (l1 == l2);
}

// ---------------- 座標・距離計算系 ----------------

// 2直線の交点を Fraction (有理数) のペアで返す
// ※事前に is_parallel(l1, l2) が false であることを確認すること
pair<Fraction, Fraction> intersection(const Line& l1, const Line& l2) {
    long long det = l1.a * l2.b - l1.b * l2.a;
    long long x_num = l1.b * l2.c - l2.b * l1.c;
    long long y_num = l2.a * l1.c - l1.a * l2.c;
    return {Fraction(-x_num, det), Fraction(-y_num, det)};
}

// 点pから直線lに下ろした垂線の足 (Fraction のペア)
pair<Fraction, Fraction> foot_of_perpendicular(const Line& l, Point p) {
    Line perp = perpendicular_line(l, p);
    return intersection(l, perp);
}

// 点pと直線lの距離 (ここだけ double を返す)
double distance(const Line& l, Point p) {
    return abs(l.a * p.x + l.b * p.y + l.c) / sqrt(l.a * l.a + l.b * l.b);
}

▼ 使い方と応用パターン

直線の生成と交点の計算
Point p1 = {0, 2}, p2 = {4, 0};
Line l1(p1, p2); // x + 2y - 4 = 0

Point p3 = {0, 0}, p4 = {2, 2};
Line l2(p3, p4); // x - y = 0

if (is_intersect(l1, l2)) {
    if (l1 == l2) {
        cout << "完全に一致" << endl;
    } else {
        auto cross_point = intersection(l1, l2);
        // x = 4/3, y = 4/3
        cout << "交点: (" << cross_point.first.num << "/" << cross_point.first.den << ", "
             << cross_point.second.num << "/" << cross_point.second.den << ")\n";
    }
}
作図機能(平行・垂直・垂線の足)の連鎖
Point A = {0, 0};
Point B = {4, 2};
Line lAB(A, B); // AとBを通る直線

// 点Cを通る、lABの「平行線」と「垂直線」を構成
Point C = {1, 5};
Line para = parallel_line(lAB, C);
Line perp = perpendicular_line(lAB, C);

// 点Cから直線lABに下ろした垂線の足を求める
// (内部的に perp を構成し、lAB との交点を計算している)
auto foot = foot_of_perpendicular(lAB, C);

// 点Cと直線lABの距離を求める
double dist = distance(lAB, C);
応用:三角形の外心を求める(垂直二等分線の交点)
Point A = {0, 0}, B = {4, 0}, C = {2, 4};

// 辺ABと辺BCの垂直二等分線をそれぞれ引く
Line b1 = perpendicular_bisector(A, B);
Line b2 = perpendicular_bisector(B, C);

// その交点が外心 (Circumcenter) となる
auto circumcenter = intersection(b1, b2);
cout << "外心 X: " << circumcenter.first.num << "/" << circumcenter.first.den << "\n";
cout << "外心 Y: " << circumcenter.second.num << "/" << circumcenter.second.den << "\n";

Line Sort (直線の傾き順ソート・偏角ソート・グループ化)

用途: 厳密な整数比較による直線の傾きソート・平行線のグループ分け

計算量: ソート O(N log N)、グループ化 O(N log N)。
直線の傾き $m = -a/b$ を分数形式のまま整数(__int128_t)で比較するため、割り算の誤差が一切発生しません。用途に合わせて「数値としての傾き順($-\infty < m \le \infty$)」と「偏角順($0 \le \theta < \pi$)」の2種類の比較関数を用意しています。

#include <vector>
#include <algorithm>
#include <map>

// 直線の傾きを一意に表すキー (a, b を gcd(a,b) で割って最小約分したペア)
// ※平行な直線はすべて同じキーを返す
pair<long long, long long> get_slope_key(const Line& l) {
    long long g = gcd(l.a, l.b);
    return {l.a / g, l.b / g};
}

// 1. 数値としての傾き m = -a/b (-∞ < m <= +∞) でソートする比較関数
// 順序: 負の傾き (例: -2) < 水平(0) < 正の傾き (例: +3) < 垂直(+∞)
bool compare_by_slope(const Line& l1, const Line& l2) {
    // 垂直線 (b == 0) は傾き +∞ とみなす
    if (l1.b == 0 && l2.b == 0) return l1 < l2; // 傾きが同じなら Line の < 演算子で安定化
    if (l1.b == 0) return false; // l1 が +∞
    if (l2.b == 0) return true;  // l2 が +∞

    // -a1 / b1 < -a2 / b2 の比較 (分母を正にして分子×分母で比較)
    long long n1 = -l1.a, d1 = l1.b;
    if (d1 < 0) { n1 = -n1; d1 = -d1; }
    
    long long n2 = -l2.a, d2 = l2.b;
    if (d2 < 0) { n2 = -n2; d2 = -d2; }

    __int128_t val1 = (__int128_t)n1 * d2;
    __int128_t val2 = (__int128_t)n2 * d1;

    if (val1 != val2) return val1 < val2;
    return l1 < l2; // 傾きが同じ場合は定数項(切片)順でソート
}

// 2. 偏角 [0, π) 順でソートする比較関数 (x軸正の向きからの回転角)
// 順序: 水平(0°) -> 鋭角(例: 45°) -> 垂直(90°) -> 鈍角(例: 135°)
bool compare_by_angle(const Line& l1, const Line& l2) {
    auto get_zone = [](const Line& l) {
        if (l.b == 0) return 1; // 垂直 [90°]
        if (l.b < 0 || (l.a == 0 && l.b > 0)) return 0; // [0°, 90°) : 水平または正の傾き
        return 2; // (90°, 180°) : 負の傾き
    };

    int z1 = get_zone(l1);
    int z2 = get_zone(l2);
    if (z1 != z2) return z1 < z2;
    return compare_by_slope(l1, l2);
}

// 3. 平行な直線(同じ傾きを持つ直線)ごとにグループ化する
map<pair<long long, long long>, vector<Line>> group_by_slope(const vector<Line>& lines) {
    map<pair<long long, long long>, vector<Line>> res;
    for (const auto& l : lines) {
        res[get_slope_key(l)].push_back(l);
    }
    return res;
}

▼ 使い方と応用パターン

傾きソートと偏角ソートの挙動比較
vector<Line> lines = {
    Line({0, 0}, {0, 1}),  // x = 0  (垂直, 90度, 傾き+∞)
    Line({0, 0}, {1, 0}),  // y = 0  (水平,  0度, 傾き 0)
    Line({0, 0}, {1, 1}),  // y = x  (斜め, 45度, 傾き +1)
    Line({0, 0}, {1, -1})  // y = -x (斜め,135度, 傾き -1)
};

// 1. 数値傾き順 (-∞ < m <= +∞)
auto lines_by_slope = lines;
sort(lines_by_slope.begin(), lines_by_slope.end(), compare_by_slope);
// 結果順: y = -x (-1) -> y = 0 (0) -> y = x (+1) -> x = 0 (+∞)

// 2. 偏角順 (0° <= θ < 180°)
auto lines_by_angle = lines;
sort(lines_by_angle.begin(), lines_by_angle.end(), compare_by_angle);
// 結果順: y = 0 (0°) -> y = x (45°) -> x = 0 (90°) -> y = -x (135°)
応用:平行な直線群のカウントと抽出
vector<Line> lines = {
    Line({0, 0}, {1, 2}),
    Line({0, 1}, {1, 3}), // 傾き 2
    Line({0, 5}, {1, 7}), // 傾き 2 (平行)
    Line({0, 0}, {1, -1}) // 傾き -1
};

// 平行なグループに分類
auto groups = group_by_slope(lines);

cout << "異なる傾きの数: " << groups.size() << "\n";
for (const auto& [key, line_list] : groups) {
    cout << "傾きキー (" << key.first << ", " << key.second << ") に属する直線数: " 
         << line_list.size() << "\n";
}

Circle (円の操作・交点・位置関係)

用途: 2円の位置関係判定・直線や円同士の交点座標の計算

計算量: O(1)。
円の中心・半径は誤差のない long long で管理しつつ、ルートが絡む交点座標の計算結果は long double(PointF 構造体)で出力します。位置関係の判定まではすべて整数で処理するため、誤差による判定ミスが起きません。

#include <cmath>
#include <vector>

// CCW,FractionとLineがあると動作する

// 浮動小数点で座標を扱うための構造体
struct PointF {
    long double x, y;
};

// 円の構造体
struct Circle {
    Point c;     // 中心座標 (整数)
    long long r; // 半径 (整数)
};

// 2円の位置関係 (共通接線の数を返す)
// 5: 同一の円, 4: 離れている, 3: 外接する, 2: 交わる, 1: 内接する, 0: 内包する
int circle_relation(Circle c1, Circle c2) {
    long long dx = c1.c.x - c2.c.x;
    long long dy = c1.c.y - c2.c.y;
    long long d2 = dx * dx + dy * dy; // 中心間の距離の2乗
    
    if (d2 == 0 && c1.r == c2.r) return 5;

    long long r_add = (c1.r + c2.r) * (c1.r + c2.r);
    long long r_sub = (c1.r - c2.r) * (c1.r - c2.r);
    
    if (d2 > r_add) return 4;
    if (d2 == r_add) return 3;
    if (d2 > r_sub) return 2;
    if (d2 == r_sub) return 1;
    return 0;
}

// 円と直線の交点 (0個, 1個(接点), または2個の PointF を返す)
vector<PointF> intersection(const Circle& c, const Line& l) {
    long double a = l.a, b = l.b, cc = l.c;
    long double cx = c.c.x, cy = c.c.y;
    long double d_den = a * a + b * b;
    
    // 中心から直線に下ろした垂線の足 (x0, y0)
    long double x0 = (b * (b * cx - a * cy) - a * cc) / d_den;
    long double y0 = (a * (a * cy - b * cx) - b * cc) / d_den;

    long double d2 = (cx - x0) * (cx - x0) + (cy - y0) * (cy - y0);
    long double r2 = (long double)c.r * c.r;

    // 誤差を考慮 (EPS = 1e-9)
    if (d2 > r2 + 1e-9) return {}; // 交点なし
    if (abs(d2 - r2) < 1e-9) return {{x0, y0}}; // 接する (1点)

    // 三平方の定理で交点までのオフセット距離を計算
    long double w = sqrt(max((long double)0.0, r2 - d2));
    long double len = sqrt(d_den);
    long double dx = (b / len) * w;
    long double dy = (-a / len) * w;
    
    return {{x0 + dx, y0 + dy}, {x0 - dx, y0 - dy}};
}

// 円と円の交点 (0個, 1個(接点), または2個の PointF を返す)
vector<PointF> intersection(Circle c1, Circle c2) {
    int rel = circle_relation(c1, c2);
    
    if (rel == 5 || rel == 4 || rel == 0) return {}; // 交点なし

    long double cx1 = c1.c.x, cy1 = c1.c.y;
    long double cx2 = c2.c.x, cy2 = c2.c.y;
    long double d = sqrt((cx1 - cx2) * (cx1 - cx2) + (cy1 - cy2) * (cy1 - cy2));
    
    // c1の中心から交線を結ぶ弦の中点までの距離 a
    long double a = (c1.r * c1.r - c2.r * c2.r + d * d) / (2.0 * d);
    long double h = sqrt(max((long double)0.0, (long double)c1.r * c1.r - a * a));
    
    // 弦の中点
    long double px = cx1 + a / d * (cx2 - cx1);
    long double py = cy1 + a / d * (cy2 - cy1);
    
    if (rel == 1 || rel == 3) return {{px, py}}; // 接する (1点)
    
    // 中点からのオフセットベクトル
    long double dx = -h / d * (cy2 - cy1);
    long double dy =  h / d * (cx2 - cx1);
    
    return {{px + dx, py + dy}, {px - dx, py - dy}};
}

▼ 使い方と応用パターン

基本の使い方(円と円の交点・位置関係)
Circle c1 = {{0, 0}, 5}; // 原点を中心とする半径5の円
Circle c2 = {{8, 0}, 5}; // (8, 0)を中心とする半径5の円

// 位置関係を判定
int rel = circle_relation(c1, c2);
cout << "共通接線の数: " << rel << "\n"; // 2 (交わっている)

// 交点を取得
auto points = intersection(c1, c2);
for (int i = 0; i < points.size(); ++i) {
    // 交点1: (4, 3), 交点2: (4, -3)
    cout << "交点" << i + 1 << ": (" << points[i].x << ", " << points[i].y << ")\n";
}
応用:円と直線の交差判定と切り取られる線分の長さ
Circle c = {{0, 0}, 5};
Line l({0, 4}, {1, 4}); // y = 4 の直線

auto points = intersection(c, l);

if (points.size() == 2) {
    // 弦の長さを三平方の定理で計算
    long double dx = points[0].x - points[1].x;
    long double dy = points[0].y - points[1].y;
    long double len = sqrt(dx * dx + dy * dy);
    
    cout << "円によって切り取られる線分の長さ: " << len << "\n"; // 6
}

Triangle (三角形の五心・面積・Fermat点)

用途: 重心・外接円・内接円・垂心・最適化座標(Fermat点)の計算

計算量: Fermat点のみ O(反復回数)、他はすべて O(1)。
三角形に関する主要な中心座標と半径を PointF (long double) で計算します。垂心はオイラー線($H = 3G - 2O$)の性質を利用して高速に求め、Fermat点(3点からの距離の和が最小になる点)は条件分岐が面倒な幾何的解法を避け、反復法により数行で高精度に近似する実戦的なアプローチを採用しています。

#include <cmath>
#include <algorithm>

// CCW,Line,Fractionが必要

// 2点間の距離 (整数座標版)
long double dist(Point a, Point b) {
    return sqrt((a.x - b.x)*(a.x - b.x) + (a.y - b.y)*(a.y - b.y));
}

// 2点間の距離 (浮動小数点座標版)
long double distF(PointF a, PointF b) {
    return sqrt((a.x - b.x)*(a.x - b.x) + (a.y - b.y)*(a.y - b.y));
}

// 面積 (外積の半分なので、絶対値をとり2で割る)
long double triangle_area(Point a, Point b, Point c) {
    return abs(cross(b - a, c - a)) / 2.0;
}

// 重心 (Centroid) : 3頂点の平均
PointF centroid(Point a, Point b, Point c) {
    return {(a.x + b.x + c.x) / 3.0, (a.y + b.y + c.y) / 3.0};
}

// 内心 (Incenter) と 内接円の半径
// 戻り値: {内心の座標, 内接円の半径}
pair<PointF, long double> incircle(Point a, Point b, Point c) {
    long double la = dist(b, c), lb = dist(c, a), lc = dist(a, b);
    long double peri = la + lb + lc;
    PointF incenter = {
        (la * a.x + lb * b.x + lc * c.x) / peri,
        (la * a.y + lb * b.y + lc * c.y) / peri
    };
    long double r = 2.0 * triangle_area(a, b, c) / peri;
    return {incenter, r};
}

// 傍心 (Excenter) - 頂点Aの向かい側にある傍心のみ例示
// 用途は少ないが、内心の公式の la を -la に変えるだけで求まる
PointF excenterA(Point a, Point b, Point c) {
    long double la = dist(b, c), lb = dist(c, a), lc = dist(a, b);
    long double peri = -la + lb + lc;
    return {
        (-la * a.x + lb * b.x + lc * c.x) / peri,
        (-la * a.y + lb * b.y + lc * c.y) / peri
    };
}

// 外心 (Circumcenter) と 外接円の半径
// 戻り値: {外心の座標, 外接円の半径}
pair<PointF, long double> circumcircle(Point a, Point b, Point c) {
    long double d = 2.0 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
    long double a2 = a.x * a.x + a.y * a.y;
    long double b2 = b.x * b.x + b.y * b.y;
    long double c2 = c.x * c.x + c.y * c.y;
    
    PointF circumcenter = {
        (a2 * (b.y - c.y) + b2 * (c.y - a.y) + c2 * (a.y - b.y)) / d,
        (a2 * (c.x - b.x) + b2 * (a.x - c.x) + c2 * (b.x - a.x)) / d
    };
    long double r = distF({(long double)a.x, (long double)a.y}, circumcenter);
    return {circumcenter, r};
}

// 垂心 (Orthocenter)
// オイラー線 (H = 3G - 2O) を利用して高速計算
PointF orthocenter(Point a, Point b, Point c) {
    PointF g = centroid(a, b, c);
    PointF o = circumcircle(a, b, c).first;
    return {3.0 * g.x - 2.0 * o.x, 3.0 * g.y - 2.0 * o.y};
}

// Fermat点 (3頂点からの距離の和が最小になる点) と その距離の和
// 幾何的解法ではなく Weiszfeld のアルゴリズム(反復法)で解く実戦的実装
pair<PointF, long double> fermat_point(Point a, Point b, Point c) {
    PointF p = centroid(a, b, c); // 重心を初期値とする
    PointF pa = {(long double)a.x, (long double)a.y};
    PointF pb = {(long double)b.x, (long double)b.y};
    PointF pc = {(long double)c.x, (long double)c.y};
    
    // 100回程度反復すれば long double の精度上限付近まで収束する
    for (int i = 0; i < 100; ++i) {
        long double da = max((long double)1e-12, distF(p, pa));
        long double db = max((long double)1e-12, distF(p, pb));
        long double dc = max((long double)1e-12, distF(p, pc));
        p.x = (pa.x/da + pb.x/db + pc.x/dc) / (1.0/da + 1.0/db + 1.0/dc);
        p.y = (pa.y/da + pb.y/db + pc.y/dc) / (1.0/da + 1.0/db + 1.0/dc);
    }
    
    long double sum_dist = distF(p, pa) + distF(p, pb) + distF(p, pc);
    return {p, sum_dist};
}

▼ 使い方と応用パターン

基本の使い方(外心と内心の取得)
Point A = {0, 0};
Point B = {6, 0};
Point C = {0, 8}; // 辺の長さが 6, 8, 10 の直角三角形

// 内心と内接円の半径
auto in = incircle(A, B, C);
cout << "内心: (" << in.first.x << ", " << in.first.y << ")\n"; // (2, 2)
cout << "内接円の半径: " << in.second << "\n"; // 2

// 外心と外接円の半径 (直角三角形なので斜辺の中点になるはず)
auto circum = circumcircle(A, B, C);
cout << "外心: (" << circum.first.x << ", " << circum.first.y << ")\n"; // (3, 4)
cout << "外接円の半径: " << circum.second << "\n"; // 5
応用:Fermat点による距離の最小化
Point A = {0, 0};
Point B = {10, 0};
Point C = {5, 8};

auto fermat = fermat_point(A, B, C);

cout << "Fermat点: (" << fermat.first.x << ", " << fermat.first.y << ")\n";
cout << "3点からの距離の最小値: " << fermat.second << "\n";

// 参考: 重心からの距離の和と比較してみる
PointF g = centroid(A, B, C);
long double g_dist = distF(g, {0,0}) + distF(g, {10,0}) + distF(g, {5,8});
cout << "重心の場合の距離の和: " << g_dist << "\n";
// Fermat点の距離の和の方が小さくなることが確認できる

Convex Hull (凸包)

用途: 点群をすべて包み込む最小の凸多角形の構築

計算量: O(N log N)。
Andrew's Monotone Chain (アンドリューのアルゴリズム) を使用します。最初にX座標・Y座標でソートし、上側と下側の凸包をそれぞれスタックベースで構築する非常に高速でバグの少ない手法です。判定には一番最初に作成した ccw をフル活用します。

#include <vector>
#include <algorithm>

// CCW,Fractionが必要

// Point構造体にソート用の比較演算子を追加 (x優先、次にy)
bool operator<(const Point& a, const Point& b) {
    if (a.x != b.x) return a.x < b.x;
    return a.y < b.y;
}

// 凸包を求める (反時計回りに頂点のリストを返す)
vector<Point> convex_hull(vector<Point> p) {
    int n = p.size();
    if (n <= 2) return p;
    
    sort(p.begin(), p.end()); // 辞書順ソート
    vector<Point> qs;
    
    // 下側凸包の構築
    for (int i = 0; i < n; i++) {
        // 直進 (ccw == 2, -2, 0) や右折 (ccw == -1) の場合はスタックから取り除く
        // ※辺上の点を含めたい場合は != 1 ではなく == -1 に変更する
        while (qs.size() > 1 && ccw(qs[qs.size()-2], qs.back(), p[i]) != 1) {
            qs.pop_back();
        }
        qs.push_back(p[i]);
    }
    
    int t = qs.size();
    // 上側凸包の構築 (右端から左端へ戻る)
    for (int i = n - 2; i >= 0; i--) {
        while (qs.size() > t && ccw(qs[qs.size()-2], qs.back(), p[i]) != 1) {
            qs.pop_back();
        }
        qs.push_back(p[i]);
    }
    
    qs.pop_back(); // 始点と終点が重複するため最後の1つを削除
    return qs;
}

▼ 使い方と応用パターン

点群から凸包の面積を求める
vector<Point> points = {{0, 0}, {2, 0}, {2, 2}, {0, 2}, {1, 1}, {1, -1}};

// 凸包を取得
vector<Point> hull = convex_hull(points);

cout << "凸包の頂点数: " << hull.size() << "\n";
for (auto p : hull) {
    cout << "(" << p.x << ", " << p.y << ")\n";
}

// 凸多角形の面積を外積の和で求める (Shoelace formula)
long double area = 0;
for (int i = 0; i < hull.size(); ++i) {
    Point p1 = hull[i];
    Point p2 = hull[(i + 1) % hull.size()];
    area += cross(p1, p2);
}
area = abs(area) / 2.0;
cout << "面積: " << area << "\n";

Minimum Enclosing Circle (最小包含円)

用途: 点群をすべて内部に含む最小の円の中心と半径の計算

計算量: 期待値 O(N)。
乱択アルゴリズムの金字塔「Welzlのアルゴリズム (Welzl's algorithm)」です。点群をシャッフルし、順次円を更新していくことで、平均計算量 O(N) という圧倒的な速度で最小包含円を特定します。内部で Triangle ライブラリの circumcircle(外接円)を利用しています。

#include <vector>
#include <random>
#include <algorithm>

// Triangle , CCW, Fraction, Line などが必要

// 2点を直径の対向とする円を生成
pair<PointF, long double> circle2(Point a, Point b) {
    PointF c = {(a.x + b.x) / 2.0, (a.y + b.y) / 2.0};
    return {c, distF(c, {(long double)a.x, (long double)a.y})};
}

// 最小包含円 (Welzl's Algorithm)
// 戻り値: {中心座標, 半径}
pair<PointF, long double> min_enclosing_circle(vector<Point> p) {
    if (p.empty()) return {{0.0, 0.0}, 0.0};
    if (p.size() == 1) return {{(long double)p[0].x, (long double)p[0].y}, 0.0};

    // 乱数で頂点順序をシャッフル (期待値O(N)にするための肝)
    mt19937 mt(1333); // 再現性のために固定シードを使用
    shuffle(p.begin(), p.end(), mt);

    // 初期円を最初の1点とする (半径0)
    pair<PointF, long double> c = {{(long double)p[0].x, (long double)p[0].y}, 0.0};

    for (int i = 1; i < p.size(); i++) {
        // 点 p[i] が現在の円の外側にある場合、円を更新
        if (distF(c.first, {(long double)p[i].x, (long double)p[i].y}) > c.second + 1e-9) {
            c = {{(long double)p[i].x, (long double)p[i].y}, 0.0}; // p[i] を中心とする半径0の円
            for (int j = 0; j < i; j++) {
                // 点 p[j] も外側にある場合、p[i] と p[j] を直径とする円に更新
                if (distF(c.first, {(long double)p[j].x, (long double)p[j].y}) > c.second + 1e-9) {
                    c = circle2(p[i], p[j]);
                    for (int k = 0; k < j; k++) {
                        // 点 p[k] も外側にある場合、3点を通る外接円に更新
                        if (distF(c.first, {(long double)p[k].x, (long double)p[k].y}) > c.second + 1e-9) {
                            c = circumcircle(p[i], p[j], p[k]);
                        }
                    }
                }
            }
        }
    }
    return c;
}

▼ 使い方と応用パターン

ランダムな点群をすべて覆う円の計算
vector<Point> points = {
    {0, 0}, {10, 0}, {0, 10}, {2, 2}, {8, 8}, {5, 9}
};

auto mec = min_enclosing_circle(points);

cout << "最小包含円の中心: (" << mec.first.x << ", " << mec.first.y << ")\n"; // (5, 5)
cout << "最小包含円の半径: " << mec.second << "\n"; // 7.07106... (5√2)

// 確認: すべての点が円内 (境界含む) に収まっているか
for (auto p : points) {
    long double d = distF(mec.first, {(long double)p.x, (long double)p.y});
    if (d > mec.second + 1e-9) {
        cout << "エラー: 点 (" << p.x << ", " << p.y << ") が円の外です!\n";
    }
}

探索・走査

K-bit 全探索 (Gosper's Hack)

用途: $N$ ビットのうち立っているビット(要素数)がちょうど $K$ 個である部分集合($_N \mathrm{C}_K$ 個)を全列挙する

計算量: $O(_N \mathrm{C}_K)$

struct FixedBitEnumeration {
    ll N, K;

    FixedBitEnumeration(ll n, ll k) : N(n), K(k) {}

    // Nビットのうち1がK個立っているビットパターンを昇順列挙(Gosper's Hack)
    vector<ll> enumerate_masks() const {
        vector<ll> res;
        if (K < 0 || K > N) return res;
        if (K == 0) {
            res.push_back(0);
            return res;
        }

        ll cur = (1LL << K) - 1;
        while (cur < (1LL << N)) {
            res.push_back(cur);
            ll x = cur & -cur;
            ll y = cur + x;
            cur = (((cur ^ y) / x) >> 2) | y;
        }
        return res;
    }

    // 各パターンにおいて立っているビットのインデックス(0-indexed)のリストとして取得する
    vvl enumerate_indices() const {
        vl masks = enumerate_masks();
        vvl res;
        res.reserve(masks.size());
        for (ll mask : masks) {
            vl indices;
            for (ll i = 0; i < N; i++) {
                if ((mask >> i) & 1) indices.push_back(i);
            }
            res.push_back(indices);
        }
        return res;
    }
};

用途例: 固定サイズ $K$ の組み合わせ探索($_N \mathrm{C}_K$ の全探索)、固定要素数の状態を持つbit DP

注意: Gosper's Hackアルゴリズムを使用。メモリ割当なし・純粋なビット演算のため非常に高速。 $K = 0$ や $K > N$ などのコーナーケースを安全にガードしている。 1LL << N がオーバーフローしないよう $N < 62$ で使用すること。

▼ 使い方と応用パターン

基本の使い方

できることは以下の通り:

int main() {
    ll N = 5, K = 3;
    FixedBitEnumeration fbe(N, K);

    // 1. ビットマスクの数値を直接取得 (5C3 = 10通り)
    vl masks = fbe.enumerate_masks();
    cout << "--- ビットマスク列挙 ---" << endl;
    for (ll mask : masks) {
        // 例: 7 -> 00111 (2進数表示)
        cout << bitset<5>(mask) << " (" << mask << ")" << endl;
    }

    // 2. 選ばれたインデックスのリストとして取得
    vvl indices_list = fbe.enumerate_indices();
    cout << "--- インデックス列挙 ---" << endl;
    for (const vl& indices : indices_list) {
        for (ll idx : indices) cout << idx << " ";
        cout << endl;
    }

    return 0;
}

グレイコード全列挙 (gray code / differential bit search)

用途: 隣り合う状態の差分が常に1ビットのみとなる部分集合の全列挙 (差分更新による高速全探索)

計算量: 全列挙: O(2N)。 各ステップの遷移が O(1) になるため、全体の計算量を O(N · 2N) から O(2N) に削減可能。

struct GrayCode {
    ll N;

    GrayCode(ll n) : N(n) {}

    // i 番目の Gray Code (i ^ (i >> 1)) を返す
    static ll get(ll i) {
        return i ^ (i >> 1);
    }

    // 2^N 通りの Gray Code の数値列挙を返す
    vector<ll> enumerate_masks() const {
        vector<ll> res(1LL << N);
        for (ll i = 0; i < (1LL << N); i++) {
            res[i] = get(i);
        }
        return res;
    }

    // 差分更新(1要素の追加/削除)を行いながら全探索を走査するコールバック駆動関数
    // add_fn(idx): idx番目の要素を追加
    // remove_fn(idx): idx番目の要素を削除
    // process_fn(): 現在の集合状態で判定・更新処理を行う
    template <typename AddFn, typename RemoveFn, typename ProcessFn>
    void run(AddFn add_fn, RemoveFn remove_fn, ProcessFn process_fn) const {
        ll cur_code = 0;
        process_fn(); // 初期状態 (空集合) の処理

        for (ll i = 1; i < (1LL << N); i++) {
            ll next_code = get(i);
            ll diff = cur_code ^ next_code;
            // 変化したビットの位置(下位から何ビット目か)を特定
            ll bit_idx = __builtin_ctzll(diff);

            if ((next_code >> bit_idx) & 1) {
                add_fn(bit_idx);
            } else {
                remove_fn(bit_idx);
            }
            process_fn();
            cur_code = next_code;
        }
    }
};

注意: 通常 Bit 全探索では各ステートで全要素の判定に O(N) かかるところ、Gray Code を使うことで要素の出し入れを O(1) の差分更新(Mo's algorithm のような挙動)で行える。__builtin_ctzll (末尾の0の個数) を用いて変化ビットを判定する。

▼ 使い方と応用パターン

基本の使い方 (差分更新による全探索)

できることは以下の通り:

int main() {
    vector<ll> A = {10, 20, 30};
    ll N = A.size();

    GrayCode gc(N);

    ll current_sum = 0;

    // 差分更新関数を渡して高速全探索
    gc.run(
        [&](ll idx) { current_sum += A[idx]; }, // 追加時の処理
        [&](ll idx) { current_sum -= A[idx]; }, // 削除時の処理
        [&]() {                                 // 各状態での評価
            cout << "Current Sum: " << current_sum << endl;
        }
    );

    return 0;
}

ベクトル全列挙 (dfs / recursion)

用途: 長さK・最大値Nのベクトルの全列挙(重複順列・昇順列挙)

計算量: 全列挙: $O(N^K)$、広義単調増加: $O(_{N + K - 1}\mathrm{C}_{K})$、狭義単調増加: $O(_{N}\mathrm{C}_{K})$。

struct VectorEnumeration {
    ll N, K;

    VectorEnumeration(ll n, ll k) : N(n), K(k) {}

    // ベクトルの全列挙・昇順列挙を行う関数 (1 <= A_i <= N)
    // is_ascending: true の場合、広義単調増加 (A[i] <= A[i+1])
    // strict      : true の場合、狭義単調増加 (A[i] < A[i+1]) ※is_ascending=true 前提
    vector<vector<ll>> enumerate(bool is_ascending = false, bool strict = false) const {
        vector<vector<ll>> res;
        vector<ll> cur;

        auto dfs = [&](auto self, ll min_val) -> void {
            if ((ll)cur.size() == K) {
                res.push_back(cur);
                return;
            }
            ll start = is_ascending ? min_val : 1;
            for (ll i = start; i <= N; i++) {
                cur.push_back(i);
                ll next_min = strict ? i + 1 : i;
                self(self, next_min);
                cur.pop_back();
            }
        };

        dfs(dfs, 1);
        return res;
    }
};

注意: 探索空間の大きさに注意。全列挙は NK 通り、単調増加列挙は組み合わせの数(重複組み合わせまたは通常組み合わせ)になるため、総数が 106 〜 107 程度におさまる範囲で使用すること。 $K,N-K$ が小さい方について全探索するなど工夫が必要な場合もある。

▼ 使い方と応用パターン

基本の使い方

できることは以下の通り:

int main() {
    ll N = 3, K = 3;
    VectorEnumeration ve(N, K);

    // 1. 通常の全列挙 (1~3 からなる長さ3のベクトル: 3^3 = 27通り)
    auto all_vecs = ve.enumerate();
    cout << "--- 全列挙 ---" << endl;
    for (const auto& vec : all_vecs) {
        for (int i = 0; i < K; i++) {
            cout << vec[i] << (i == K - 1 ? "" : " ");
        }
        cout << endl;
    }

    // 2. 広義単調増加の列挙 (1 <= A[0] <= A[1] <= A[2] <= 3)
    auto asc_vecs = ve.enumerate(true, false);
    cout << "--- 広義単調増加 ---" << endl;
    for (const auto& vec : asc_vecs) {
        for (int i = 0; i < K; i++) {
            cout << vec[i] << (i == K - 1 ? "" : " ");
        }
        cout << endl;
    }

    // 3. 狭義単調増加の列挙 (1 <= A[0] < A[1] < A[2] <= 3)
    auto strict_asc_vecs = ve.enumerate(true, true);
    cout << "--- 狭義単調増加 ---" << endl;
    for (const auto& vec : strict_asc_vecs) {
        for (int i = 0; i < K; i++) {
            cout << vec[i] << (i == K - 1 ? "" : " ");
        }
        cout << endl;
    }

    return 0;
}

直積全列挙 (dfs)

用途: 複数のベクトル(要素数が異なっていても可)の直積全列挙

計算量: $O(\prod |A_i|)$。

template <typename T>
struct CartesianProduct {
    vector<vector<T>> groups;

    CartesianProduct(const vector<vector<T>>& g) : groups(g) {}

    // 複数ベクトルの直積(すべての組み合わせ)を全列挙する関数
    vector<vector<T>> enumerate() const {
        vector<vector<T>> res;
        vector<T> cur;

        auto dfs = [&](auto self, size_t depth) -> void {
            if (depth == groups.size()) {
                res.push_back(cur);
                return;
            }
            for (const auto& item : groups[depth]) {
                cur.push_back(item);
                self(self, depth + 1);
                cur.pop_back();
            }
        };

        dfs(dfs, 0);
        return res;
    }
};

注意: 動的な多重 for ループを実現する。groups の中に要素数が 0 の空ベクトルが含まれている場合、直積の結果も 0 件になるため注意。

▼ 使い方と応用パターン

基本の使い方

できることは以下の通り:

int main() {
    // 3つのベクトル(要素数がそれぞれ異なっていてもOK)
    vector<vector<ll>> vecs = {
        {1, 2},
        {10, 20, 30},
        {100}
    };

    CartesianProduct<ll> cp(vecs);
    auto res = cp.enumerate(); // 2 × 3 × 1 = 6 通り

    cout << "--- 直積全列挙 ---" << endl;
    for (const auto& vec : res) {
        for (size_t i = 0; i < vec.size(); i++) {
            cout << vec[i] << (i + 1 == vec.size() ? "" : " ");
        }
        cout << endl;
    }

    return 0;
}

next_permutation

用途: 順列全探索、辞書順で次の並びを生成する

計算量: 1回あたり O(N)。 全列挙は N! なので、サイズには強く注意。

sort(v.begin(), v.end());
do {
    // v を1通り処理
} while (next_permutation(v.begin(), v.end()));

注意: 出力はboolで、一回実行するたびにvの中身が次の順列になる。

分割全列挙 (dfs / recursion)

用途: 正整数Nをいくつかの正整数の和に分解する表現(整数分割)の全列挙

計算量: 分割数 P(N) に比例。 N = 50 で P(50) = 204,226 通り、N ≤ 60 程度まで高速動作。

struct IntegerPartition {
    ll N;

    IntegerPartition(ll n) : N(n) {}

    // Nの整数分割を降順/昇順形式で全列挙する関数
    // max_parts: 最大分割数(0の場合は制限なし)
    vector<vector<ll>> enumerate(ll max_parts = 0) const {
        vector<vector<ll>> res;
        vector<ll> cur;

        auto dfs = [&](auto self, ll rem, ll last_val) -> void {
            if (rem == 0) {
                res.push_back(cur);
                return;
            }
            if (max_parts > 0 && (ll)cur.size() == max_parts) return;

            // 重複を除外するため、前の値以下の整数のみを次に採用する(降順構成)
            ll upper = min(rem, last_val);
            for (ll i = upper; i >= 1; i--) {
                cur.push_back(i);
                self(self, rem - i, i);
                cur.pop_back();
            }
        };

        dfs(dfs, N, N);
        return res;
    }
};

注意: 順序を区別しない和の分解(例: $4 = 3+1$ と $1+3$ は同一視)を効率よく重複なく列挙する。max_parts オプションで「高々 $K$ 個の項への分割」に制限することも可能。

▼ 使い方と応用パターン

基本の使い方

できることは以下の通り:

int main() {
    ll N = 5;
    IntegerPartition ip(N);

    // 1. N = 5 の全分割列挙
    // 出力例: (5), (4,1), (3,2), (3,1,1), (2,2,1), (2,1,1,1), (1,1,1,1,1)
    auto partitions = ip.enumerate();
    cout << "--- N=5 の整数分割全列挙 ---" << endl;
    for (const auto& part : partitions) {
        for (size_t i = 0; i < part.size(); i++) {
            cout << part[i] << (i + 1 == part.size() ? "" : "+");
        }
        cout << endl;
    }

    // 2. 分割数を高々2個に制限する場合 (max_parts = 2)
    // 出力例: (5), (4,1), (3,2)
    auto limited_partitions = ip.enumerate(2);
    cout << "--- 高々2個への分割 ---" << endl;
    for (const auto& part : limited_partitions) {
        for (size_t i = 0; i < part.size(); i++) {
            cout << part[i] << (i + 1 == part.size() ? "" : "+");
        }
        cout << endl;
    }

    return 0;
}

集合の分割全列挙 (dfs / recursion / 複合制約付き)

用途: N個の要素をいくつかの空でないグループに分ける方法(集合の分割)の全列挙。要素数やグループ数に柔軟な制約を指定可能。

計算量: 基本的にはベル数 $B_N$ 以下。条件が厳しい(グループ数や要素数の制約が狭い)ほど強力な枝刈りが効いて爆速で動作。 制約なしの場合、N ≤ 12 程度まで実用的。

struct SetPartition {
    ll N;

    SetPartition(ll n) : N(n) {}

    // 集合 {0, 1, ..., N-1} の分割全列挙
    // min_size   : 各グループの最小要素数 (デフォルト 1)
    // max_size   : 各グループの最大要素数 (-1 の場合は制限なし)
    // min_groups : 分割後の最小グループ数 (デフォルト 1)
    // max_groups : 分割後の最大グループ数 (-1 の場合は制限なし)
    vector<vector<vector<ll>>> enumerate(ll min_size = 1, ll max_size = -1, ll min_groups = 1, ll max_groups = -1) const {
        if (max_size == -1) max_size = N;
        if (max_groups == -1) max_groups = N;

        vector<vector<vector<ll>>> res;
        vector<vector<ll>> current_partition;

        auto dfs = [&](auto self, ll i) -> void {
            ll c = current_partition.size();
            
            // 枝刈り: 既存グループが最小サイズ(min_size)を満たすために必要な追加要素数
            ll needed_for_existing = 0;
            for (const auto& g : current_partition) {
                if ((ll)g.size() < min_size) {
                    needed_for_existing += min_size - (ll)g.size();
                }
            }
            
            // 枝刈り: グループ数の下限(min_groups)を満たすために新規作成すべきグループに必要な要素数
            ll needed_for_new = max(0LL, min_groups - c) * min_size;
            
            // 残りの全要素を使っても条件を満たせない場合は探索打ち切り
            if (needed_for_existing + needed_for_new > N - i) return;

            if (i == N) {
                res.push_back(current_partition);
                return;
            }

            // 1. 既存のグループのいずれかに追加する
            for (size_t g = 0; g < c; g++) {
                if ((ll)current_partition[g].size() < max_size) {
                    current_partition[g].push_back(i);
                    self(self, i + 1);
                    current_partition[g].pop_back();
                }
            }

            // 2. 新しいグループを作成して追加する
            // 現在のグループ数が上限(max_groups)に達していない場合のみ作成可能
            if (c < max_groups) {
                current_partition.push_back({i});
                self(self, i + 1);
                current_partition.pop_back();
            }
        };

        dfs(dfs, 0);
        return res;
    }
};

注意: min_size, max_size は「1つのグループに入れる要素数」の制約。min_groups, max_groups は「作るグループの数」の制約です。不要な制約は -1(または省略)にすることで「指定なし」として扱えます。

▼ 使い方と応用パターン

基本の使い方

できることは以下の通り:

int main() {
    ll N = 5;
    SetPartition sp(N);

    // 【パターン1】 指定なし(すべての分割を列挙)
    vector<vector<vector<ll>>> res1 = sp.enumerate();

    // 【パターン2】 各グループの要素数を 2個以上、3個以下 にする
    // 引数: (min_size, max_size)
    vector<vector<vector<ll>>> res2 = sp.enumerate(2, 3);

    // 【パターン3】 グループの個数を 2個以上、3個以下 にする(要素数は自由)
    // 引数: (min_size, max_size, min_groups, max_groups)
    vector<vector<vector<ll>>> res3 = sp.enumerate(1, -1, 2, 3);

    // 【パターン4】 「ちょうど2つのグループ」に分け、かつ「各グループ2人以上」にする
    vector<vector<vector<ll>>> res4 = sp.enumerate(2, -1, 2, 2);


    // データの取り出し方(例)
    for (ll i = 0; i < res4.size(); i++) {
        // 1つの分割パターン
        for (ll j = 0; j < res4[i].size(); j++) {
            // 1つのグループ
            for (ll idx = 0; idx < res4[i][j].size(); idx++) {
                // 要素にアクセス
                ll element = res4[i][j][idx];
            }
        }
    }

    return 0;
}

Mo's Algorithm (Hilbert Order)

用途: オフラインの区間クエリをHilbert Orderで処理し、通常の平方分割よりも移動距離を短縮・定数倍を高速化する

計算量: O(N√Q)程度(N,Qは10^6程度まで)

struct Mo {
    ll n;
    vector<pair<ll, ll>> lr;

    Mo(ll n) : n(n) {}

    // クエリを半開区間 [l, r) で追加
    void add_query(ll l, ll r) {
        lr.push_back({l, r});
    }

    // ヒルベルト曲線の順序を計算 (最大N, Qが10^6程度まで対応)
    static inline int64_t hilbert_order(ll x, ll y) {
        int64_t d = 0;
        for (ll s = 1 << 20; s > 0; s >>= 1) {
            ll rx = (x & s) > 0, ry = (y & s) > 0;
            d += (int64_t)(3 * rx ^ ry) * s * s;
            if (ry == 0) {
                if (rx == 1) {
                    x = (1 << 21) - 1 - x;
                    y = (1 << 21) - 1 - y;
                }
                swap(x, y);
            }
        }
        return d;
    }

    // 左端追加、右端追加、左端削除、右端削除、クエリ回答の関数を受け取る
    template<typename AL, typename AR, typename DL, typename DR, typename O>
    void build(AL add_left, AR add_right, DL del_left, DR del_right, O out) {
        ll q = lr.size();
        vector<int64_t> h_ord(q);
        for (ll i = 0; i < q; ++i) {
            h_ord[i] = hilbert_order(lr[i].first, lr[i].second);
        }

        vector<ll> ord(q);
        iota(ord.begin(), ord.end(), 0);
        sort(ord.begin(), ord.end(), [&](ll a, ll b) {
            return h_ord[a] < h_ord[b];
        });

        ll l = 0, r = 0;
        for (auto idx : ord) {
            // ※区間が反転しないよう「追加(区間を広げる)」を先に実行する
            while (l > lr[idx].first) add_left(--l);
            while (r < lr[idx].second) add_right(r++);
            while (l < lr[idx].first) del_left(l++);
            while (r > lr[idx].second) del_right(--r);
            out(idx);
        }
    }
};

注意: 配列の要素が更新されるオンライン・クエリには使えない。 また、区間の伸縮順序(l, r の動かし方)を間違えると、区間長がマイナスになるなどバグの原因になるため、必ず「区間を広げる処理(追加)」を「区間を狭める処理(削除)」より先に行う。

▼ 使い方と応用パターン

基本の使い方(左右で伸縮ロジックが同じ場合)

多くの問題では、左端・右端どちらから要素を追加(削除)しても状態の更新方法が変わらないため、共通の関数を渡します。

ll N = /* 配列のサイズ */;
ll Q = /* クエリの数 */;
Mo mo(N);

// 1. クエリの追加 [l, r)
for (ll i = 0; i < Q; ++i) {
    ll l, r;
    // 入力などから l, r を取得
    mo.add_query(l, r);
}

// 2. 状態を管理する変数やデータ構造を用意
vector<long long> ans(Q);
// long long current_val = 0; など

// 3. 要素が追加されたときの処理
auto add = [&](ll idx) {
    // idx 番目の要素が区間に含まれた際の状態更新処理を記述
};

// 4. 要素が削除されたときの処理
auto del = [&](ll idx) {
    // idx 番目の要素が区間から外れた際の状態更新処理を記述
};

// 5. クエリの答えを記録する処理
auto out = [&](ll q_idx) {
    // ans[q_idx] に現在の状態(答え)を保存
};

// 6. 実行 (左右で追加・削除のロジックが同じ場合は同じ関数を渡す)
mo.build(add, add, del, del, out);
応用:左右で伸縮のロジックが異なる場合

左端を操作する時と、右端を操作する時で計算ロジックが変わる場合(例:区間内の転倒数など)は、それぞれ別の関数を定義して渡します。

auto add_left = [&](ll idx) {
    // 左端が伸びる(小さくなる)ときの処理
};

auto add_right = [&](ll idx) {
    // 右端が伸びる(大きくなる)ときの処理
};

auto del_left = [&](ll idx) {
    // 左端が縮む(大きくなる)ときの処理
};

auto del_right = [&](ll idx) {
    // 右端が縮む(小さくなる)ときの処理
};

auto out = [&](ll q_idx) {
    // ans[q_idx] に現在の状態(答え)を保存
};

// 4つの関数をそれぞれ独立して渡す
mo.build(add_left, add_right, del_left, del_right, out);
応用(非推奨):クエリ分解による平面走査としての利用

クエリを [0, A) や [0, B) のように左端が 0 に固定された区間に分解して追加することで、ポインタが一方向にしか進まなくなり、実質的に平面走査(計算量 O((N+Q)log N) など)として機能させることができます。削除処理(del)は一度も呼ばれないため空の関数を渡せますが、設計上本来のMoの使い方ではないため非推奨です。

// クエリ [A, B) を [0, A) と [0, B) に分解して追加する例
ll N = /* 配列のサイズ */;
ll Q = /* クエリの数 */;
Mo mo(N);

vl ans(Q, 0);

// 1. クエリの分解と追加
for (ll i = 0; i < Q; ++i) {
    ll a, b;
    // ... [A, B) の範囲を取得
    mo.add_query(0, a); // 偶数 index (2 * i)     : 引く用
    mo.add_query(0, b); // 奇数 index (2 * i + 1) : 足す用
}

// 2. 追加処理(右方向へ進む処理のみ実行される)
auto add = [&](ll idx) {
    // idx 番目の要素をデータ構造(BIT等)に追加
};

// 3. 削除処理は一度も実行されないため、ダミー関数でよい
auto dummy = [&](ll idx) {};

// 4. クエリ回答処理
auto out = [&](ll q_idx) {
    long long current_val = /* 現在のデータ構造から算出した値 */;
    ll orig_idx = q_idx / 2; // 元のクエリID

    if (q_idx % 2 == 0) {
        ans[orig_idx] -= current_val; // [0, A) の値は引く
    } else {
        ans[orig_idx] += current_val; // [0, B) の値は足す
    }
};

// 5. 実行 (del にはダミー関数を渡す)
mo.build(add, add, dummy, dummy, out);

Mo's Algorithm on Tree

用途: 木上のパスクエリを1次元の区間クエリに変換し、オフラインでMo's Algorithmを用いて処理する

計算量: O(N√Q)程度(Mo's Algorithmに依存)。
前提: Mo、EulerTour、汎用スパーステーブル (SparseTable 等) を利用してLCAを取得できること。

struct TreeMo {
    Mo mo;
    vector<ll> node_at; // 括弧列の各時刻に対応する頂点番号
    struct Query {
        ll u, v, lca;
    };
    vector<Query> queries;

    // EulerTourのサイズ 2N を用いてMoを初期化
    TreeMo(const EulerTour& et) : mo(2 * et.n) {
        node_at.resize(2 * et.n);
        for (ll i = 0; i < et.n; ++i) {
            node_at[et.path_in[i]] = i;
            node_at[et.path_out[i]] = i;
        }
    }

    // パスクエリの追加 (uからvへのパス。LCAを外部で計算して渡す)
    void add_query(ll u, ll v, ll lca, const EulerTour& et) {
        if (et.path_in[u] > et.path_in[v]) swap(u, v);
        queries.push_back({u, v, lca});
        
        // LCAがuの場合(uがvの祖先)、パス上の頂点は [path_in[u], path_in[v]] に1回だけ出現する
        if (lca == u) {
            mo.add_query(et.path_in[u], et.path_in[v] + 1);
        } 
        // 異なる枝にある場合、パス上の頂点は [path_out[u], path_in[v]] に1回だけ出現する
        // ただしLCAはこの区間に含まれないため、実行時に別途足す必要がある
        else {
            mo.add_query(et.path_out[u], et.path_in[v] + 1);
        }
    }

    // flip: 頂点の有効/無効を切り替える関数, out: クエリ回答の関数
    template<typename F, typename O>
    void build(F flip, O out) {
        auto toggle = [&](ll idx) {
            flip(node_at[idx]); // 区間の端が動くたびに頂点の状態を反転
        };
        auto wrap_out = [&](ll q_idx) {
            ll lca = queries[q_idx].lca;
            // 異なる枝の場合、LCAが含まれていないので一時的に反転してパスに含める
            bool need_lca = (lca != queries[q_idx].u);
            if (need_lca) flip(lca);
            
            out(q_idx);
            
            // クエリ回答後、LCAの状態を元に戻す
            if (need_lca) flip(lca);
        };
        
        // 木上のMoは偶数回出現した頂点を無効化するため、追加・削除が全く同じ「反転(flip)操作」になる
        mo.build(toggle, toggle, toggle, toggle, wrap_out);
    }
};

注意: 木上のMo's Algorithmでは、頂点が区間内に奇数回(1回)現れるならパスに含まれ、偶数回(2回:入る・出る)現れるならパスに含まれない、という性質を利用します。そのため状態更新は「追加・削除」ではなく「反転(flip)」で行います。

▼ 使い方と応用パターン

基本の使い方(パスクエリ処理のテンプレート)

EulerTour と汎用 SparseTable (LCA用) を構築した上でクエリを読み込み、反転処理と解答処理を渡します。

// 1. グラフの構築と Euler Tour の準備
ll N, Q;
// (グラフの入力を受け取る...)
EulerTour et(graph, 0);

// 2. LCA 用の Sparse Table 構築 (既存ライブラリを利用)
using pii = pair<ll, ll>;
vector<pii> A(et.tour.size());
for (size_t i = 0; i < et.tour.size(); i++) {
    A[i] = {et.depth[i], et.tour[i]};
}
auto get_min = [](pii a, pii b) { return min(a, b); };
SparseTable<pii, decltype(get_min)> st(A, get_min);

// LCAを求めるラムダ式
auto get_lca = [&](ll u, ll v) {
    ll l = et.first_visit[u];
    ll r = et.first_visit[v];
    if (l > r) swap(l, r);
    return st.query(l, r + 1).second;
};

// 3. TreeMo の準備とクエリ追加
TreeMo tree_mo(et);
for (ll i = 0; i < Q; ++i) {
    ll u, v;
    // 入力等から u, v を取得
    ll lca = get_lca(u, v);
    tree_mo.add_query(u, v, lca, et);
}

// 4. 状態管理変数と flip / out 関数の定義
vector<bool> in_path(N, false);
vector<long long> ans(Q);
// long long current_val = 0; など

auto flip = [&](ll v) {
    in_path[v] = !in_path[v];
    if (in_path[v]) {
        // v がパスに追加された時の処理
        // current_val の更新など
    } else {
        // v がパスから削除された時の処理
        // current_val の更新など
    }
};

auto out = [&](ll q_idx) {
    // ans[q_idx] = current_val; など
};

// 5. 実行
tree_mo.build(flip, out);

Hilbert Order

用途: 2次元座標 (x, y) をヒルベルト曲線に沿った1次元の順序へ変換する。 2次元上の点を空間的に近い順へ並べたい場合や、 Mo's Algorithm、オフラインクエリ、空間的なキャッシュ局所性を利用したい処理に使える。

計算量: 1点あたり O(log N) (N は座標の最大値に対する2のべき乗サイズ)

struct HilbertOrder {
    ll lg;

    // 座標の取りうる範囲を [0, 2^lg) とする
    explicit HilbertOrder(ll n) {
        lg = 0;
        while ((1LL << lg) < n) ++lg;
    }

    // (x, y) をヒルベルト順序へ変換
    ll operator()(ll x, ll y) const {
        ll d = 0;

        for (ll s = 1LL << (lg - 1); s > 0; s >>= 1) {
            ll rx = (x & s) ? 1 : 0;
            ll ry = (y & s) ? 1 : 0;

            d += s * s * ((3 * rx) ^ ry);

            if (ry == 0) {
                if (rx == 1) {
                    x = (1LL << lg) - 1 - x;
                    y = (1LL << lg) - 1 - y;
                }
                swap(x, y);
            }
        }

        return d;
    }

    // 静的関数として直接使いたい場合
    static ll order(ll x, ll y, ll lg) {
        ll d = 0;

        for (ll s = 1LL << (lg - 1); s > 0; s >>= 1) {
            ll rx = (x & s) ? 1 : 0;
            ll ry = (y & s) ? 1 : 0;

            d += s * s * ((3 * rx) ^ ry);

            if (ry == 0) {
                if (rx == 1) {
                    x = (1LL << lg) - 1 - x;
                    y = (1LL << lg) - 1 - y;
                }
                swap(x, y);
            }
        }

        return d;
    }
};

注意: 同じ集合の点を比較する場合は、すべて同じ lg を使う必要がある。 そのため、通常は座標の上限を渡して HilbertOrder hilbert(N); のように1つだけ生成して使う。 座標は 0 <= x, y < 2^lg を満たす必要がある。

▼ 使い方と応用パターン

基本の使い方

例えば、座標の範囲が 0 <= x, y < N なら、HilbertOrder hilbert(N); を作って hilbert(x, y) を呼び出す。

ll N = /* 座標の上限 */;
HilbertOrder hilbert(N);

ll x = /* x座標 */;
ll y = /* y座標 */;

ll ord = hilbert(x, y);
点をヒルベルト順にソート

座標そのものを並べ替える場合は、ヒルベルト順序を比較キーとして使う。

ll N = /* 座標の上限 */;
HilbertOrder hilbert(N);

vector<pair<ll, ll>> points = {
    {x1, y1},
    {x2, y2},
    {x3, y3},
    // ...
};

sort(points.begin(), points.end(), [&](const auto& a, const auto& b) {
    return hilbert(a.first, a.second)
         < hilbert(b.first, b.second);
});
インデックスだけをヒルベルト順に並べる
ll N = /* 座標の上限 */;
ll Q = /* 点の数 */;

HilbertOrder hilbert(N);

vector<pair<ll, ll>> p(Q);

// p[i] = {x, y} を格納

vector<ll> ord(Q);
iota(ord.begin(), ord.end(), 0);

sort(ord.begin(), ord.end(), [&](ll a, ll b) {
    return hilbert(p[a].first, p[a].second)
         < hilbert(p[b].first, p[b].second);
});
ヒルベルト順序だけを直接取得

オブジェクトを作らず、ビット数 lg が既に分かっている場合は HilbertOrder::order(x, y, lg) を使える。

ll lg = 20;

ll x = /* x座標 */;
ll y = /* y座標 */;

ll h = HilbertOrder::order(x, y, lg);

データ構造

Union-Find / DSU

用途: 連結成分の管理・同値類の統合

計算量: ほぼ O(α(N))。 実用上はほぼ定数だが、最悪ケースを避けるには経路圧縮と union by size/rank が重要。

// Union-Find の構造体
struct UnionFind {
    vector<ll> parent; // parent[i] は i の親要素。i が根(リーダー)の場合は -1
    vector<ll> siz;    // siz[i] は i を根とするグループの要素数

    // 構造体の初期化(要素数 N で作成)
    UnionFind(ll n) {
        parent.assign(n, -1); // 最初はみんな自分が根(-1)
        siz.assign(n, 1);     // 最初はみんなサイズ 1
    }

    // 1. Find: 要素 x の根(リーダー)を返す
    ll find(ll x) {
        if (parent[x] == -1) return x; // 自分が根なら自分を返す
        // 経路圧縮:親を「根」に直接繋ぎ変えながら再帰的に探す
        return parent[x] = find(parent[x]);
    }

    // 2. Union: 要素 x と要素 y のグループを合体させる
    bool unite(ll x, ll y) {
        // それぞれのリーダーを探す
        ll root_x = find(x);
        ll root_y = find(y);

        // すでに同じグループなら何もしない
        if (root_x == root_y) return false;

        // Union by Size: 小さい方を大きい方の下に合体させる
        if (siz[root_x] < siz[root_y]) {
            swap(root_x, root_y); // 常に root_x の方がサイズが大きくなるようにする
        }

        // root_y の親を root_x にする(root_x が新しいボス)
        parent[root_y] = root_x;
        siz[root_x] += siz[root_y]; // サイズを合算する
        return true;
    }

    // 3. Same: 要素 x と要素 y が同じグループに属するか判定
    bool same(ll x, ll y) {
        return find(x) == find(y);
    }

    // 4. Size: 要素 x が属するグループのサイズ(要素数)を返す
    ll size(ll x) {
        return siz[find(x)];
    }
};

▼ 使い方と応用パターン

基本の使い方(グループの統合・同一グループ判定・サイズ取得)
ll N = 5;
UnionFind uf(N);

// グループを繋げてみる
uf.unite(0, 1); // 0 と 1 を同じグループに
uf.unite(2, 3); // 2 と 3 を同じグループに

// 同じグループか判定
cout << "0 と 1 は同じ?: " << (uf.same(0, 1) ? "Yes" : "No") << endl; // Yes
cout << "0 と 2 は同じ?: " << (uf.same(0, 2) ? "Yes" : "No") << endl; // No

// さらに繋げる
uf.unite(1, 3); // 0-1 グループ と 2-3 グループが合体!

cout << "合体後、0 と 2 は同じ?: " << (uf.same(0, 2) ? "Yes" : "No") << endl; // Yes
cout << "0 がいるグループのサイズ: " << uf.size(0) << endl; // 4 (0, 1, 2, 3 がいるので)

重み付きUnion-Find / Weighted DSU

用途: 連結成分の管理および、同じグループ内の要素間の相対的な差分(重み)の管理

計算量: ほぼ O(α(N))。 通常のUnion-Findに差分を保持する配列を追加し、経路圧縮時に重みを累積計算する。

// 重み付きUnion-Find の構造体
struct WeightedUnionFind {
    vector<ll> parent;      // parent[i] は i の親要素。i が根の場合は -1
    vector<ll> siz;         // siz[i] は i を根とするグループの要素数
    vector<ll> diff_weight; // 根からの距離(重み)

    // 構造体の初期化(要素数 N で作成)
    WeightedUnionFind(ll n) {
        parent.assign(n, -1);
        siz.assign(n, 1);
        diff_weight.assign(n, 0);
    }

    // 1. Find: 要素 x の根を返しつつ、経路圧縮で重みを更新
    ll find(ll x) {
        if (parent[x] == -1) return x;
        ll r = find(parent[x]);
        // 親の重みを足し合わせて根からの重みに更新
        diff_weight[x] += diff_weight[parent[x]];
        return parent[x] = r;
    }

    // 要素 x の根からの重みを返す
    ll weight(ll x) {
        find(x); // 経路圧縮を実行
        return diff_weight[x];
    }

    // 2. Relate: 要素 y が 要素 x より w 大きい (a_y - a_x = w) という情報で結合
    bool relate(ll x, ll y, ll w) {
        w += weight(x);
        w -= weight(y);
        ll root_x = find(x);
        ll root_y = find(y);

        if (root_x == root_y) return false;

        // Union by Size: 小さい方を大きい方に繋ぐ
        if (siz[root_x] < siz[root_y]) {
            swap(root_x, root_y);
            w = -w; // 繋ぐ方向が逆になるので重みも反転
        }

        parent[root_y] = root_x;
        siz[root_x] += siz[root_y];
        diff_weight[root_y] = w;
        return true;
    }

    // 3. Diff: 要素 x と 要素 y の差分 (a_y - a_x) を返す
    // ※事前に same(x, y) が true であるか確認すること
    ll diff(ll x, ll y) {
        return weight(y) - weight(x);
    }

    // 4. Same: 要素 x と要素 y が同じグループに属するか判定
    bool same(ll x, ll y) {
        return find(x) == find(y);
    }

    // 5. Size: 要素 x が属するグループのサイズ(要素数)を返す
    ll size(ll x) {
        return siz[find(x)];
    }
};

▼ 使い方と応用パターン

基本の使い方(差分の登録・同一判定・差分の取得)
ll N = 5;
WeightedUnionFind uf(N);

// a_y - a_x = z の関係を登録 (relate(x, y, z))
uf.relate(0, 1, 10); // a_1 は a_0 より 10 大きい
uf.relate(1, 2, 5);  // a_2 は a_1 より 5 大きい

// 同じグループか判定し、差分を求める
if (uf.same(0, 2)) {
    // a_2 - a_0 は (a_2 - a_1) + (a_1 - a_0) = 5 + 10 = 15 になるはず
    cout << "a_2 - a_0 = " << uf.diff(0, 2) << endl; // 15
} else {
    cout << "?" << endl;
}

// 別のグループを作成して結合
uf.relate(3, 4, 100);
uf.relate(2, 3, -20); // a_3 は a_2 より 20 小さい (-20 大きい)

// 遠く離れたノード間の差分も O(α(N)) で高速に取得可能
if (uf.same(0, 4)) {
    cout << "a_4 - a_0 = " << uf.diff(0, 4) << endl; // 95
}

Rollback Union-Find

用途: 連結成分の管理・同値類の統合と、操作の取り消し(バックトラック)

計算量: 各操作 O(log N)。
状態の保存と復元(Undo)を可能にするため、経路圧縮を行いません。Union by Size によって木の高さが O(log N) に抑えられるため、実用上も十分高速に動作します。

// Undo 付き Union-Find の構造体
struct UndoUnionFind {
    vector<ll> parent; // parent[i] は i の親要素。i が根(リーダー)の場合は -1
    vector<ll> siz;    // siz[i] は i を根とするグループの要素数
    stack<pair<ll, ll>> history; // 履歴を保存するスタック {子になったノード, 親の変更前のサイズ}

    // 構造体の初期化(要素数 N で作成)
    UndoUnionFind(ll n) {
        parent.assign(n, -1); // 最初はみんな自分が根(-1)
        siz.assign(n, 1);     // 最初はみんなサイズ 1
    }

    // 1. Find: 要素 x の根(リーダー)を返す
    ll find(ll x) {
        if (parent[x] == -1) return x; // 自分が根なら自分を返す
        // ※Undoができるように、経路圧縮は行わずに再帰(またはループ)で根を探す
        return find(parent[x]);
    }

    // 2. Union: 要素 x と要素 y のグループを合体させる
    bool unite(ll x, ll y) {
        // それぞれのリーダーを探す
        ll root_x = find(x);
        ll root_y = find(y);

        // すでに同じグループの場合
        if (root_x == root_y) {
            // 変化しなかった場合も、あとで undo() された際に回数を合わせるためダミーの履歴を積む
            history.push({-1, -1});
            return false;
        }

        // Union by Size: 小さい方を大きい方の下に合体させる
        if (siz[root_x] < siz[root_y]) {
            swap(root_x, root_y); // 常に root_x の方がサイズが大きくなるようにする
        }

        // 変更前の状態を履歴に保存: {子になる root_y, 親になる root_x の元のサイズ}
        history.push({root_y, siz[root_x]});

        // root_y の親を root_x にする(root_x が新しいボス)
        parent[root_y] = root_x;
        siz[root_x] += siz[root_y]; // サイズを合算する
        return true;
    }

    // 3. Undo: 直近の unite 操作を1つ取り消す
    void undo() {
        if (history.empty()) return; // 履歴がなければ何もしない
        auto [child, old_siz] = history.top();
        history.pop();

        // ダミー履歴(合体が発生しなかった unite の取り消し)の場合は何もしない
        if (child == -1) return;

        ll root = parent[child]; // 子の親(統合時の root_x)を取得
        parent[child] = -1;      // 子を根に戻す(木を切り離す)
        siz[root] = old_siz;     // 親のサイズを統合前の状態に戻す
    }

    // 4. Same: 要素 x と要素 y が同じグループに属するか判定
    bool same(ll x, ll y) {
        return find(x) == find(y);
    }

    // 5. Size: 要素 x が属するグループのサイズ(要素数)を返す
    ll size(ll x) {
        return siz[find(x)];
    }
};

▼ 使い方と応用パターン

基本の使い方(グループの統合・Undo機能・同一グループ判定)
ll N = 5;
UndoUnionFind uf(N);

// グループを繋げてみる
uf.unite(0, 1); // 0 と 1 を同じグループに
uf.unite(2, 3); // 2 と 3 を同じグループに

cout << "0 と 1 は同じ?: " << (uf.same(0, 1) ? "Yes" : "No") << endl; // Yes

// さらに繋げる
uf.unite(1, 3); // 0-1 グループ と 2-3 グループが合体!

cout << "合体後、0 と 2 は同じ?: " << (uf.same(0, 2) ? "Yes" : "No") << endl; // Yes
cout << "0 がいるグループのサイズ: " << uf.size(0) << endl; // 4 (0, 1, 2, 3)

// ----- ここから Undo(状態の巻き戻し) -----
uf.undo(); // 直前に行われた uf.unite(1, 3) を取り消す!

cout << "Undo後、0 と 2 は同じ?: " << (uf.same(0, 2) ? "Yes" : "No") << endl; // No (元に戻った)
cout << "Undo後、0 がいるグループのサイズ: " << uf.size(0) << endl; // 2 (0, 1)

Partially Persistent Union-Find (部分永続 Union-Find)

用途: 時間を進める形の状態更新に伴う、過去の任意の時点における連結成分の管理・同値類判定

計算量: 構築 O(N)、クエリ O(log N)。
過去の構造を保持するため経路圧縮は行わず、Union by Size によって木の深さを O(log N) に保ちます。そのため各操作は O(log N) で動作します。※過去のバージョンから新しいバージョンを派生(枝分かれ)させることはできず、直線的な時間軸でのみ使用可能です。

// 部分永続 Union-Find の構造体
struct PartiallyPersistentUnionFind {
    vector<ll> parent; // 親要素
    vector<ll> time;   // 親が変更された時刻
    vector<vector<pair<ll, ll>>> siz; // siz[i] = { {時刻, サイズ}, ... } の履歴
    ll now; // 現在の時刻

    // 構造体の初期化(要素数 N で作成)
    PartiallyPersistentUnionFind(ll n) {
        parent.assign(n, -1);
        time.assign(n, 1e18); // 初期状態では親は変更されないため無限大
        siz.assign(n, {{0, 1}}); // 時刻 0 におけるサイズは 1
        now = 0;
    }

    // 1. Find: 時刻 t における要素 x の根を返す
    ll find(ll t, ll x) {
        if (time[x] > t) return x; // 時刻 t 時点では自分が根(あるいは親変更前)
        return find(t, parent[x]); // 経路圧縮は行わずに親を辿る
    }

    // 2. Union: 現在の時刻を進めつつ、要素 x と y を合体させる
    bool unite(ll x, ll y) {
        ++now; // 時刻を進める
        ll root_x = find(now, x);
        ll root_y = find(now, y);

        if (root_x == root_y) return false;

        // Union by Size
        if (siz[root_x].back().second < siz[root_y].back().second) {
            swap(root_x, root_y);
        }

        // root_y の親を root_x に変更し、その時刻を記録
        parent[root_y] = root_x;
        time[root_y] = now;

        // サイズの更新履歴を追加
        ll new_size = siz[root_x].back().second + siz[root_y].back().second;
        siz[root_x].push_back({now, new_size});
        
        return true;
    }

    // 3. Same: 時刻 t において要素 x と y が同じグループか判定
    bool same(ll t, ll x, ll y) {
        return find(t, x) == find(t, y);
    }

    // 4. Size: 時刻 t において要素 x が属するグループのサイズを返す
    ll size(ll t, ll x) {
        ll root_x = find(t, x);
        // 時刻 t 以下の最大の履歴を二分探索で探す
        auto it = prev(upper_bound(siz[root_x].begin(), siz[root_x].end(), make_pair(t, (ll)1e18)));
        return it->second;
    }

    // 5. TimeToConnect: 二分探索を用いて、要素 x と y が連結になった時刻を返す
    // 最終的に連結にならない場合は -1 を返す。計算量 O(log T log N)
    ll time_to_connect(ll x, ll y) {
        if (!same(now, x, y)) return -1;
        ll ok = now, ng = -1;
        while (abs(ok - ng) > 1) {
            ll mid = (ok + ng) / 2;
            if (same(mid, x, y)) {
                ok = mid;
            } else {
                ng = mid;
            }
        }
        return ok;
    }

};

▼ 使い方と応用パターン

基本の使い方(時刻の管理・過去の状態の参照)
ll N = 5;
PartiallyPersistentUnionFind uf(N);

// 時刻 1 で 0 と 1 を統合
uf.unite(0, 1); // now が 1 になる
// 時刻 2 で 2 と 3 を統合
uf.unite(2, 3); // now が 2 になる
// 時刻 3 で 1 と 2 を統合
uf.unite(1, 2); // now が 3 になる

// 現在(時刻3)では 0 と 3 は同じグループか?
cout << uf.same(uf.now, 0, 3) << endl; // 1 (True)

// 過去の時刻へのクエリ
cout << "時刻1で 0 と 1 は同じ?: " << uf.same(1, 0, 1) << endl; // 1 (True)
cout << "時刻1で 0 と 2 は同じ?: " << uf.same(1, 0, 2) << endl; // 0 (False)

cout << "時刻2での 0 のサイズ: " << uf.size(2, 0) << endl; // 2
cout << "時刻3での 0 のサイズ: " << uf.size(3, 0) << endl; // 4
応用パターン(二分探索で連結になった時刻を取得)

指定した2つの頂点が「いつ繋がったか」を、時間 `[0, now]` に対する二分探索で求めます。

ll N = 5;
PartiallyPersistentUnionFind uf(N);

uf.unite(0, 1); // 時刻 1: 0-1 連結
uf.unite(2, 3); // 時刻 2: 2-3 連結
uf.unite(1, 2); // 時刻 3: 0-1-2-3 連結

// いつ連結になったかを取得
cout << "0 と 1 が繋がった時刻: " << uf.time_to_connect(0, 1) << endl; // 1
cout << "0 と 3 が繋がった時刻: " << uf.time_to_connect(0, 3) << endl; // 3
cout << "0 と 4 が繋がった時刻: " << uf.time_to_connect(0, 4) << endl; // -1 (繋がっていない)

Fully Persistent Union-Find (完全永続 Union-Find)

用途: 任意の過去のバージョンから枝分かれして新しい状態を作ることができる連結成分の管理

計算量: 構築 O(N)、クエリ O(log² N)。
永続セグメント木を「永続配列」として利用し、親要素とサイズを管理します。木の深さ O(log N) と永続セグ木へのアクセス O(log N) が重なるため、O(log² N) となります。

// 前提 : 永続セグメント木

// 永続セグ木に渡すためのダミー関数 (演算は使用せず、配列アクセスとしてのみ利用)
ll op_uf(ll a, ll b) { return 0; }
ll e_uf() { return 0; }

// 完全永続 Union-Find の構造体
struct FullyPersistentUnionFind {
    PersistentSegmentTree<ll, op_uf, e_uf> seg;

    // 構造体の初期化(要素数 N で作成)
    // 値が負の場合は根であり、その絶対値がサイズを表す(初期値は全て -1)
    FullyPersistentUnionFind(ll n) : seg(n) {}

    // 初期状態のバージョン (root) を生成して返す
    ll build() {
        vector<ll> init_vec(seg.n, -1);
        return seg.build(init_vec);
    }

    // 内部用:バージョン root における配列の p 番目の値を取得
    // ※セグ木に get(root, p) が実装されていればそちらを使うと高速です
    ll get_val(ll root, ll p) {
        return seg.prod(root, p, p + 1);
    }

    // 1. Find: バージョン root における要素 x の根を返す
    ll find(ll root, ll x) {
        ll p = get_val(root, x);
        if (p < 0) return x; // 負の値なら自身が根
        return find(root, p); // 経路圧縮は行えない
    }

    // 2. Union: バージョン root の状態から要素 x と y を合体させた新しいバージョンを返す
    ll unite(ll root, ll x, ll y) {
        ll root_x = find(root, x);
        ll root_y = find(root, y);

        if (root_x == root_y) return root; // すでに同じグループならバージョンはそのまま

        ll size_x = -get_val(root, root_x);
        ll size_y = -get_val(root, root_y);

        // Union by Size
        if (size_x < size_y) {
            swap(root_x, root_y);
            swap(size_x, size_y);
        }

        // root_y の親を root_x に変更した新しい木を作成
        ll nroot = seg.update(root, root_y, root_x);
        // root_x のサイズを合算して更新
        nroot = seg.update(nroot, root_x, -(size_x + size_y));

        return nroot;
    }

    // 3. Same: バージョン root において要素 x と y が同じグループか判定
    bool same(ll root, ll x, ll y) {
        return find(root, x) == find(root, y);
    }

    // 4. Size: バージョン root において要素 x が属するグループのサイズを返す
    ll size(ll root, ll x) {
        return -get_val(root, find(root, x));
    }
};

▼ 使い方と応用パターン

基本の使い方(バージョンの枝分かれ)

部分永続と異なり、過去の任意のバージョンをベースにして新しいバージョンを派生させることができます。

ll N = 5;
FullyPersistentUnionFind uf(N);

// バージョン管理用の配列
vector<ll> roots;

// バージョン0 (初期状態) を作成
roots.push_back(uf.build()); 

// バージョン1: バージョン0をベースに 0 と 1 を統合
roots.push_back(uf.unite(roots[0], 0, 1));

// バージョン2: バージョン1をベースに 2 と 3 を統合
roots.push_back(uf.unite(roots[1], 2, 3));

// ★枝分かれ★
// バージョン3: 【バージョン1】をベースに、今度は 1 と 2 を統合
roots.push_back(uf.unite(roots[1], 1, 2));

// 確認
cout << "Ver 2 で 0 と 2 は同じ?: " << uf.same(roots[2], 0, 2) << endl; // 0 (False)
cout << "Ver 3 で 0 と 2 は同じ?: " << uf.same(roots[3], 0, 2) << endl; // 1 (True)
cout << "Ver 3 で 0 のサイズ: " << uf.size(roots[3], 0) << endl; // 3

順列木 (Sequence Tree / Implicit Treap)

用途: 任意位置への要素の挿入・削除、インデックスによるランダムアクセス

計算量: 挿入・削除・アクセス すべて $O(\log N)$。 乱択平衡二分探索木(Implicit Treap)を用いて配列を管理する。std::vector では $O(N)$ かかる途中への挿入・削除を高速に行うことができ、同時にランダムアクセスもサポートする。

template <typename T>
struct SequenceTree {
    struct Node {
        T val;
        unsigned int pri;
        int cnt;
        Node *l, *r;
        Node(T v, unsigned int p) : val(v), pri(p), cnt(1), l(nullptr), r(nullptr) {}
    };

    Node* root;
    mt19937 mt;

    SequenceTree() : root(nullptr), mt(random_device{}()) {}

    int count(Node* t) { return t ? t->cnt : 0; }

    Node* update(Node* t) {
        if (t) t->cnt = count(t->l) + count(t->r) + 1;
        return t;
    }

    void split(Node* t, int k, Node*& l, Node*& r) {
        if (!t) { l = r = nullptr; return; }
        int c = count(t->l);
        if (k <= c) {
            split(t->l, k, l, t->l);
            r = update(t);
        } else {
            split(t->r, k - c - 1, t->r, r);
            l = update(t);
        }
    }

    Node* merge(Node* l, Node* r) {
        if (!l || !r) return l ? l : r;
        if (l->pri > r->pri) {
            l->r = merge(l->r, r);
            return update(l);
        } else {
            r->l = merge(l, r->l);
            return update(r);
        }
    }

    // pos番目に val を挿入 (0-indexed)
    void insert(int pos, T val) {
        Node *l, *r;
        split(root, pos, l, r);
        root = merge(merge(l, new Node(val, mt())), r);
    }

    // pos番目の要素を削除 (0-indexed)
    void erase(int pos) {
        Node *l, *m, *r;
        split(root, pos, l, r);
        split(r, 1, m, r);
        delete m;
        root = merge(l, r);
    }

    // pos番目の要素にアクセス (0-indexed)
    T get(int pos) {
        Node* t = root;
        while (t) {
            int c = count(t->l);
            if (pos == c) return t->val;
            if (pos < c) t = t->l;
            else { pos -= c + 1; t = t->r; }
        }
        throw out_of_range("Index out of bounds");
    }
    
    // 現在の要素数を返す
    int size() { return count(root); }
};

▼ 使い方

基本的な使い方 (任意位置での挿入・削除とランダムアクセス)
SequenceTree<int> st;

// 順次追加 (末尾追加)
st.insert(0, 10); // [10]
st.insert(1, 20); // [10, 20]
st.insert(2, 30); // [10, 20, 30]

// 任意位置への挿入 (インデックス1に 15 を挿入)
st.insert(1, 15); // [10, 15, 20, 30]

// ランダムアクセス
cout << st.get(2) << "\n"; // 20

// 任意位置の削除 (インデックス0を削除)
st.erase(0); // [15, 20, 30]

cout << st.size() << "\n"; // 3
cout << st.get(0) << "\n"; // 15

注意: 定数倍がやや重いため、要素の追加が末尾のみの場合は素直に std::vector を用いること。また、本実装ではメモリリーク対策(デストラクタでの一括 delete)を省いているため、必要に応じて適宜メモリ解放の処理を追加するか、競技プログラミング等の使い捨ての用途に限定すること。

Sqrt Decomposition (平方分割)

用途: 点更新・点加算・区間クエリのデータ構造(更新 O(1) は可換群に特化)

計算量: 構築 O(N)、更新/加算 O(1)、区間クエリ O(√N)。
※ set (点更新) を O(1) で行う場合、古い値を打ち消すための逆元 inv が必要です。add (点加算) のみを使用する場合は逆元は不要なため、ダミーの関数を渡すことでモノイドとしても利用できます。

template <class S, S (*op)(S, S), S (*e)(), S (*inv)(S)>
struct SqrtDecomp {
    ll N, B;
    vector<S> A, bucket;

    SqrtDecomp(ll n) : N(n) {
        B = max(1LL, (ll)sqrt(N));
        A.assign(N, e());
        bucket.assign((N + B - 1) / B, e());
    }
    
    SqrtDecomp(const vector<S>& v) : N(v.size()) {
        B = max(1LL, (ll)sqrt(N));
        A = v;
        bucket.assign((N + B - 1) / B, e());
        for (ll i = 0; i < N; i++) {
            bucket[i / B] = op(bucket[i / B], A[i]);
        }
    }

    // 点更新 O(1): p番目の要素を x に変更 (要 逆元)
    void set(ll p, S x) {
        bucket[p / B] = op(bucket[p / B], inv(A[p]));
        A[p] = x;
        bucket[p / B] = op(bucket[p / B], A[p]);
    }

    // 点加算 O(1): p番目の要素に x を作用させる (逆元不要)
    void add(ll p, S x) {
        A[p] = op(A[p], x);
        bucket[p / B] = op(bucket[p / B], x);
    }

    // 取得 O(1)
    S get(ll p) {
        return A[p];
    }

    // 区間クエリ O(√N): [l, r) の演算結果を取得
    S prod(ll l, ll r) {
        S res = e();
        while (l < r && l % B != 0) res = op(res, A[l++]);
        while (l < r && r % B != 0) res = op(res, A[--r]);
        for (ll i = l / B; i < r / B; i++) res = op(res, bucket[i]);
        return res;
    }
};

▼ 平方分割テンプレート(使い方・応用)

基本的な使い方 (区間和 Range Sum)

群構造の定義(二項演算、単位元、逆元)を用意し、クラスに渡して初期化します。

// S: 要素の型
// op: 二項演算 (可換であること)
// e: 単位元
// inv: 逆元 (setでのみ使用。addしか使わない場合は適当な値を返すダミーで可)

// 1. 区間和のための群定義
using S = ll;
S op(S a, S b) { return a + b; }
S e() { return 0; }
S inv(S a) { return -a; } // 足し算の逆元はマイナス

int main() {
    ll N = 5;
    vector<ll> vec = {1, 2, 3, 4, 5};
    
    // 2. 宣言と構築
    SqrtDecomp<S, op, e, inv> sq(vec);
    
    // 3. 点更新 (set): 2番目の要素を 10 に置き換える
    sq.set(2, 10);
    
    // 4. 点加算 (add): 3番目の要素に 5 を足す
    sq.add(3, 5);
    
    // 5. クエリ: [0, 4) の区間和を取得
    ll ans = sq.prod(0, 4);
    
    return 0;
}
応用例: 区間XOR (Range XOR) の群定義

XOR は自身が逆元となる(自身をXORすると打ち消せる)ため、inv は引数をそのまま返します。

using S = ll;
S op(S a, S b) { return a ^ b; }
S e() { return 0; }
S inv(S a) { return a; } // XORの逆元は自身

// 宣言例
// SqrtDecomp<S, op, e, inv> sq(vec);
値域の平方分割 (要素の追加・削除と区間数え上げ)

配列のインデックスではなく、値域 に対して平方分割を行います。
要素の追加・削除が O(1)、値が [l, r) の範囲にある要素数の取得が O(√MAX_V) で可能です。Mo's Algorithm と組み合わせる際に非常に強力です。

struct ValueSqrtDecomp {
    ll MAX_V, B;
    vector<ll> count, bucket;

    ValueSqrtDecomp(ll max_v) : MAX_V(max_v) {
        B = max(1LL, (ll)sqrt(MAX_V));
        count.assign(MAX_V, 0);
        bucket.assign((MAX_V + B - 1) / B, 0);
    }

    // 値 v を1つ追加 O(1)
    void add(ll v) {
        count[v]++;
        bucket[v / B]++;
    }

    // 値 v を1つ削除 O(1)
    void remove(ll v) {
        count[v]--;
        bucket[v / B]--;
    }

    // 値が [l, r) の範囲に含まれる要素数を取得 O(√MAX_V)
    ll prod(ll l, ll r) {
        ll res = 0;
        while (l < r && l % B != 0) res += count[l++];
        while (l < r && r % B != 0) res += count[--r];
        for (ll i = l / B; i < r / B; i++) res += bucket[i];
        return res;
    }
};

Fenwick Tree / BIT

用途: 点更新・区間和を高速に扱う(累積和から K 番目の位置の復元も可能)

計算量: 更新 O(log N)、prefix sum O(log N)、K 番目の位置の復元 O(log N)。 「配列の和を頻繁に取りたい」ならまず候補になる。

struct BIT {
    ll n;
    vector<long long> bit;
    BIT(ll n): n(n), bit(n + 1, 0) {}

    void add(ll idx, long long val) {
        for (++idx; idx <= n; idx += idx & -idx) bit[idx] += val;
    }

    long long sum(ll idx) { // [0, idx)
        long long s = 0;
        for (; idx > 0; idx -= idx & -idx) s += bit[idx];
        return s;
    }

    // 最小の idx (0-index) で sum(idx + 1) >= k を満たすものを返す
    // ※ bit に格納される値は非負であることが前提
    ll lower_bound(long long k) {
        if (k <= 0) return 0;

        ll idx = 0;
        ll pw = 1;
        while ((pw << 1) <= n) pw <<= 1;

        for (; pw > 0; pw >>= 1) {
            if (idx + pw <= n && bit[idx + pw] < k) {
                k -= bit[idx + pw];
                idx += pw;
            }
        }
        return idx;
    }
};

注意: BIT は区間更新より点更新向き。区間更新は差分や別テクニックを考える。 また、lower_bound() は内部の木構造を利用して探索するため、 追加のデータを保持する必要はなく、更新・sum の計算量にも影響しない。 復元も O(log N) で行える。ただし、各要素の値(頻度)は非負であることが前提。

▼ 使い方と応用パターン

基本の使い方(配列の点更新・区間和)
ll N = 5;
BIT b(N);

// 初期値をセット (配列 [10, 20, 30, 40, 50] を作るイメージ)
b.add(0, 10);
b.add(1, 20);
b.add(2, 30);
b.add(3, 40);
b.add(4, 50);

// 先頭からの区間和 [0, 3) -> 10 + 20 + 30
ll sum_prefix = b.sum(3); // 60

// 任意の区間和 [1, 4) -> 20 + 30 + 40
ll sum_range = b.sum(4) - b.sum(1); // 90

// 点更新: idx=2 に 70 を足す (30 -> 100 になる)
b.add(2, 70);

// 累積和の lower_bound (合計が100以上になる最初のインデックス)
ll idx = b.lower_bound(100); // 2
応用1:値域BITを用いた集合管理 (l以上r以下の個数)

値そのものをインデックスとし、「その値が集合にいくつあるか」を管理する。座標圧縮とセットで使うことが多い。

ll MAX_VAL = 200000; // とりうる最大の値 (必要に応じて座標圧縮)
BIT set_bit(MAX_VAL + 5);

// 集合に値を追加 (x を 1 つ追加)
ll x = 150;
set_bit.add(x, 1);

// 集合から値を削除 (x を 1 つ削除)
// ※ 集合に x が存在することが前提
set_bit.add(x, -1);

// 集合内にある l 以上 r 以下の要素の個数を取得
ll l = 100, r = 200;
ll count_range = set_bit.sum(r + 1) - set_bit.sum(l);
応用2:集合の K 番目の最大値・最小値を O(log N) で取得

O(log N) で求められる

ll MAX_VAL = 200000;
BIT set_bit(MAX_VAL + 5);

// 適当に値を追加
set_bit.add(10, 1);
set_bit.add(50, 1);
set_bit.add(20, 1);
set_bit.add(50, 1); // 重複も許容可能

// 現在の集合の要素数
ll total_elements = set_bit.sum(MAX_VAL + 1);

// 【K番目の最小値】 (1-indexed で K 番目に小さい値)
ll K_min = 2;
// 累積度数が K 以上になる最初の値 (インデックス)
ll val_k_min = set_bit.lower_bound(K_min); // 20

// 【K番目の最大値】 (1-indexed で K 番目に大きい値)
ll K_max = 1;
// 降順で K 番目 = 昇順で (全体の要素数 - K + 1) 番目
ll target_k = total_elements - K_max + 1;
ll val_k_max = set_bit.lower_bound(target_k); // 50

atcoder::segtree

用途: 点更新・区間クエリの汎用データ構造(ACL実装・レパートリー特化)

計算量: 構築 O(N)、更新・クエリ・二分探索すべて O(log N)。
内部は #include <atcoder/segtree> と using namespace atcoder; が必要。0-indexed・半開区間 [l, r) で動作します。

// 1. 型 S と、二項演算 op、単位元 e を定義する
ll op(ll a, ll b) { return max(a, b); }
ll e() { return -1e18; }

int main() {
    int N = 100000;
    
    // 2. 宣言 (初期値は全て e() になる)
    segtree<ll, op, e> seg(N);
    // vectorから構築する場合 O(N): segtree<ll, op, e> seg(vec);

    // 3. 点更新: p番目(0-indexed)の要素を x に更新
    seg.set(2, 15);

    // 4. 1点取得: p番目の要素を取得
    ll val = seg.get(2);

    // 5. 区間クエリ: [l, r) の演算結果を取得
    ll ans = seg.prod(0, 5);
    
    // 6. 全区間クエリ: [0, N) の演算結果を取得
    ll all_ans = seg.all_prod();
}

▼ セグ木上の二分探索テンプレート

max_right / min_left (条件を満たす区間の端を求める)

単調性がある条件 f に対して、O(log N) で限界のインデックスを求めます。左端を固定して右に伸ばすなら max_right、右端を固定して左に伸ばすなら min_left を使います。

// 例:区間最大値のセグ木で、[l, r) の最大値が target 未満となる最大の r を探す
ll target = 10;
int l = 0;

// ラムダ式で条件を定義(条件を満たしている間は進む)
auto f = [&](ll v) { return v < target; };

// max_right: l を固定し、f(seg.prod(l, r)) = true となる最大の r を返す
int r = seg.max_right(l, f);

// min_left: r を固定し、f(seg.prod(l, r)) = true となる最小の l を返す
// int l_min = seg.min_left(r, f);

▼ 演算テンプレート(実戦向け・応用)

基本演算まとめ (和 / 最小値 / XOR / GCD)

単純な演算は用途に合わせて op と e を書き換えて使います。

using ll = long long;

// 【区間和】
ll op(ll a, ll b) { return a + b; }
ll e() { return 0LL; }

// 【区間最小値】 (最大値なら max と -1e18)
ll op(ll a, ll b) { return min(a, b); }
ll e() { return 1e18; }

// 【区間XOR】
ll op(ll a, ll b) { return a ^ b; }
ll e() { return 0LL; }

// 【区間GCD】 (#include <numeric>)
ll op(ll a, ll b) { return std::gcd(a, b); }
ll e() { return 0LL; }

// segtree<ll, op, e> seg(N);
最大値・最小値とそのインデックス (Min/Max with Index)

区間内の最大値・最小値だけでなく、それがどこにあるか(インデックス)も同時に取得したい場合に使います。

struct NodeMinMax {
    ll min_val;
    ll min_idx;
    ll max_val;
    ll max_idx;
};

NodeMinMax op_minmax(NodeMinMax a, NodeMinMax b) {
    NodeMinMax res;
    
    // 最小値(同値の場合は左のインデックスを優先)
    if (a.min_val <= b.min_val) {
        res.min_val = a.min_val;
        res.min_idx = a.min_idx;
    } else {
        res.min_val = b.min_val;
        res.min_idx = b.min_idx;
    }
    
    // 最大値(同値の場合は左のインデックスを優先)
    if (a.max_val >= b.max_val) {
        res.max_val = a.max_val;
        res.max_idx = a.max_idx;
    } else {
        res.max_val = b.max_val;
        res.max_idx = b.max_idx;
    }
    
    return res;
}

NodeMinMax e_minmax() {
    // 単位元のインデックスは適当な値(-1など)でOK
    return {INF, -1, -INF, -1};
}

// segtree<NodeMinMax, op_minmax, e_minmax> seg(N);
// 初期化・更新時、位置 i に値 x を入れる場合は {x, i, x, i} をセットする
区間和 & 区間2乗和 (Sum & Sum of Squares)

分散や標準偏差の計算に。点更新のセグ木では単純にそれぞれ足し合わせるだけでマージできます。

using ll = long long;
struct NodeSq { ll sum, sq_sum; };

NodeSq op_sq(NodeSq a, NodeSq b) {
    return {a.sum + b.sum, a.sq_sum + b.sq_sum};
}
NodeSq e_sq() { return {0LL, 0LL}; }

// segtree<NodeSq, op_sq, e_sq> seg(N);
// 点更新で値 x を入れる場合は {x, x * x} をセットする
文字列のローリングハッシュ (Rolling Hash)

「1文字更新」と「区間ハッシュ取得」が両方 O(log N) で可能になります。 動的に変化する文字列の一致判定や回文判定、LCPの計算で大活躍します。

using ll = long long;
const ll MOD = 998244353;
const ll BASE = 1007; // 衝突回避のための適当な基数

struct HashNode {
    ll hash;
    ll pw; // BASE の (区間長) 乗を保持
};

HashNode op_hash(HashNode a, HashNode b) {
    // a の後ろに b を結合する: a.hash * (BASE^b.len) + b.hash
    return {(a.hash * b.pw + b.hash) % MOD, (a.pw * b.pw) % MOD};
}
HashNode e_hash() { 
    return {0LL, 1LL}; // 長さ0のハッシュは0、BASE^0 = 1
}

// segtree<HashNode, op_hash, e_hash> seg(N);
// 1文字 c (ASCIIコード等) を入れる場合は {c, BASE} をセットする
括弧列の整合性判定 (Parentheses Match)

区間内の未マッチの「(」と「)」の数を管理します。区間が完全に正しい括弧列かどうかの判定や、括弧の反転クエリ(遅延セグ木)への拡張など超頻出のテクニックです。

#include <algorithm>
using namespace std;

struct Paren {
    int unmatch_r; // 未マッチの ')' の数
    int unmatch_l; // 未マッチの '(' の数
};

Paren op_paren(Paren a, Paren b) {
    // a の未マッチ '(' と、b の未マッチ ')' が新しくペアになって打ち消し合う
    int m = min(a.unmatch_l, b.unmatch_r);
    return {
        a.unmatch_r + b.unmatch_r - m,
        a.unmatch_l + b.unmatch_l - m
    };
}
Paren e_paren() { return {0, 0}; }

// segtree<Paren, op_paren, e_paren> seg(N);
// '(' なら {0, 1}、')' なら {1, 0} をセットする
// prod(l, r) の結果が {0, 0} なら完全にマッチした正しい括弧列
区間最大連続部分列の和 (Max Subarray Sum)

区間内の連続する部分列の和のうち、最大のものを取得する定番テクニック。

using ll = long long;
const ll INF = 1e18;
struct NodeM { ll sum, max_l, max_r, max_all; };

NodeM op_max_sub(NodeM a, NodeM b) {
    if (a.max_all == -INF) return b;
    if (b.max_all == -INF) return a;
    return {
        a.sum + b.sum,
        max(a.max_l, a.sum + b.max_l),
        max(b.max_r, a.max_r + b.sum),
        max({a.max_all, b.max_all, a.max_r + b.max_l})
    };
}
NodeM e_sub() { return {0, -INF, -INF, -INF}; }

// segtree<NodeM, op_max_sub, e_sub> seg(N);
// 構築時は各要素 x に対して {x, x, x, x} を入れる
一次関数の区間合成 (Range Composite)

f(x) = ax + b の合成。左から右へ関数を次々に適用していく場合のクエリに。

using ll = long long;
const ll MOD = 998244353;
struct Func { ll a, b; }; // ax + b

Func op_composite(Func f, Func g) {
    // f(g(x)) = f.a * (g.a * x + g.b) + f.b
    return {(f.a * g.a) % MOD, (f.a * g.b + f.b) % MOD};
}
// 恒等関数 f(x) = 1*x + 0 が単位元
Func e_func() { return {1, 0}; } 

// segtree<Func, op_composite, e_func> seg(N);
min-plus代数 / 2×2行列の区間積 (Range min-plus Matrix)

グラフの最短経路更新や、状態遷移が (min, +) で表されるDPの高速化に。

using ll = long long;
const ll INF = 1e18;
struct Mat { ll v[2][2]; };

Mat op_min_plus(Mat a, Mat b) {
    Mat res = {{{INF, INF}, {INF, INF}}};
    for (ll i = 0; i < 2; i++)
        for (ll k = 0; k < 2; k++)
            for (ll j = 0; j < 2; j++)
                res.v[i][j] = min(res.v[i][j], a.v[i][k] + b.v[k][j]);
    return res;
}
// 単位元は対角成分が0、非対角成分がINFの行列
Mat e_min_plus() { return {{{0, INF}, {INF, 0}}}; }

// segtree<Mat, op_min_plus, e_min_plus> seg(N);

atcoder::lazy_segtree

用途: 区間更新・区間加算などに対応した汎用遅延セグメント木(ACL実装)

計算量: 構築 O(N)、区間更新・区間クエリ・二分探索すべて O(log N)。
内部は #include <atcoder/lazysegtree> と using namespace atcoder; が必要。0-indexed・半開区間 [l, r) で動作します。
ACLでは S(データの型)と F(作用素の型)の2つを定義し、5つの関数 op, e, mapping, composition, id を注入して動作を決定します。

// 例:区間加算・区間最小値
using S = ll;
using F = ll;
const ll INF = 1e18;

S op(S a, S b) { return min(a, b); }
S e() { return INF; }
S mapping(F f, S x) { return f + x; }
F composition(F f, F g) { return f + g; } // 既存のgの上に新しいfを適用
F id() { return 0; }

int main() {
    int N = 100000;
    
    // 宣言
    lazy_segtree<S, op, e, F, mapping, composition, id> seg(N);
    // vectorから構築する場合 O(N): lazy_segtree<...> seg(vec);

    // 区間更新: [l, r) に作用素 x を適用
    seg.apply(0, 5, 10); // [0, 5) に 10 を加算
    
    // 1点更新: p番目に作用素 x を適用
    seg.apply(2, 5);

    // 区間クエリ: [l, r) の演算結果を取得
    ll ans = seg.prod(0, 5); // [0, 5) の最小値
}

▼ 遅延セグ木上の二分探索テンプレート

max_right / min_left (条件を満たす区間の端を求める)

遅延評価(遅延タグの伝播)を自動で行いながら、単調性がある条件に対して O(log N) で限界のインデックスを求めます。

// 例:[l, r) の最小値が target 未満となる最大の r を探す
ll target = 10;
int l = 0;

// ラムダ式で条件を定義(条件を満たしている間は進む)
// ※ 引数の型 S はセグ木の要素の型に合わせる
auto f = [&](S v) { return v < target; };

// max_right: l を固定し、f(seg.prod(l, r)) = true となる最大の r を返す
int r = seg.max_right(l, f);

// min_left: r を固定し、f(seg.prod(l, r)) = true となる最小の l を返す
// int l_min = seg.min_left(r, f);

▼ 演算テンプレート(区間長 len が不要なタイプ)

区間加算 & 区間最小値 (Range Add / Range Min)

最も基本的な遅延セグ木です。最大値を求める場合は min を max に、単位元を -INF に変更します。

using S = long long;
using F = long long;
const S INF = 1e18;

S op(S a, S b) { return min(a, b); }
S e() { return INF; }
S mapping(F f, S x) { return x + f; }
F composition(F f, F g) { return f + g; }
F id() { return 0; }

// lazy_segtree<S, op, e, F, mapping, composition, id> seg(N);
区間変更(塗りつぶし) & 区間最小値 (Range Update / Range Min)

指定した区間を特定の値で上書きします。更新操作が来ないことを表すダミー値 (ID) を定義して使います。

using S = long long;
using F = long long;
const S INF = 1e18;
const F ID = 2e18; // 更新されないことを表すあり得ない値

S op(S a, S b) { return min(a, b); }
S e() { return INF; }
S mapping(F f, S x) { return (f == ID ? x : f); }
F composition(F f, F g) { return (f == ID ? g : f); }
F id() { return ID; }

// lazy_segtree<S, op, e, F, mapping, composition, id> seg(N);
区間 chmin & 区間最小値 (Range chmin / Range Min)

a[i] = min(a[i], x) のように、現在の値より小さければ更新します。最短経路の緩和やDPの区間遷移で活躍します。

using S = long long;
using F = long long;
const S INF = 1e18;

S op(S a, S b) { return min(a, b); }
S e() { return INF; }
S mapping(F f, S x) { return min(f, x); }
F composition(F f, F g) { return min(f, g); }
F id() { return INF; }

// lazy_segtree<S, op, e, F, mapping, composition, id> seg(N);

▼ 演算テンプレート(データに区間長 len が必要なタイプ)

区間加算 & 区間和 (Range Add / Range Sum)

区間全体に値 f を足すと、区間和は f * len だけ増加します。構築時の初期化忘れに注意。

using ll = long long;
struct S { ll sum, len; };
using F = ll;

S op(S a, S b) { return {a.sum + b.sum, a.len + b.len}; }
S e() { return {0, 0}; }
S mapping(F f, S x) { return {x.sum + f * x.len, x.len}; }
F composition(F f, F g) { return f + g; }
F id() { return 0; }

// lazy_segtree<S, op, e, F, mapping, composition, id> seg;
// ※構築時は要素を {値, 1} にして初期化する
// std::vector<S> init_v(N, {0, 1});
// seg = lazy_segtree<...>(init_v);
区間変更 & 区間和 (Range Update / Range Sum)
using ll = long long;
struct S { ll sum, len; };
using F = ll;
const F ID = 2e18;

S op(S a, S b) { return {a.sum + b.sum, a.len + b.len}; }
S e() { return {0, 0}; }
S mapping(F f, S x) { return (f == ID ? x : S{f * x.len, x.len}); }
F composition(F f, F g) { return (f == ID ? g : f); }
F id() { return ID; }

// lazy_segtree<S, op, e, F, mapping, composition, id> seg;
// std::vector<S> init_v(N, {0, 1});
// seg = lazy_segtree<...>(init_v);
区間アフィン変換 & 区間和 (Range Affine / Range Sum)

区間内の各要素 $x$ を $ax + b$ に変換します。乗算と加算が混ざるクエリ(例:区間を2倍して3足す)を一度に処理できます。

using ll = long long;
const ll MOD = 998244353;
struct S { ll sum, len; };
struct F { ll a, b; }; // a * x + b

S op(S a, S b) { return {(a.sum + b.sum) % MOD, a.len + b.len}; }
S e() { return {0, 0}; }
S mapping(F f, S x) { return {(f.a * x.sum + f.b * x.len) % MOD, x.len}; }
F composition(F f, F g) { 
    // f_new(g_old(x)) = f.a * (g.a * x + g.b) + f.b
    return {(f.a * g.a) % MOD, (f.a * g.b + f.b) % MOD}; 
}
F id() { return {1, 0}; } // 1 * x + 0

// lazy_segtree<S, op, e, F, mapping, composition, id> seg;
// std::vector<S> init_v(N, {0, 1});
// seg = lazy_segtree<...>(init_v);
区間加算 & 区間二乗和 (Range Add / Range Sum of Squares)

$(x_i + f)^2 = x_i^2 + 2fx_i + f^2$ の展開式を利用し、区間の二乗和を高速に管理します。分散などの統計クエリに必須です。

using ll = long long;
struct S { ll sq_sum, sum, len; };
using F = ll;

S op(S a, S b) { return {a.sq_sum + b.sq_sum, a.sum + b.sum, a.len + b.len}; }
S e() { return {0, 0, 0}; }
S mapping(F f, S x) {
    if (f == 0) return x;
    return {x.sq_sum + 2 * f * x.sum + f * f * x.len, x.sum + f * x.len, x.len};
}
F composition(F f, F g) { return f + g; }
F id() { return 0; }

// lazy_segtree<S, op, e, F, mapping, composition, id> seg;
// std::vector<S> init_v(N, {0, 0, 1});
// seg = lazy_segtree<...>(init_v);
0/1 区間反転 & 区間和 (Range Flip / Range Sum)

0/1 からなる配列で、区間の 0/1 を反転(XOR)させ、区間内の 1 の数を数えます。スイッチのオンオフ管理によく出題されます。

using ll = long long;
struct S { ll sum, len; };
using F = bool;

S op(S a, S b) { return {a.sum + b.sum, a.len + b.len}; }
S e() { return {0, 0}; }
S mapping(F f, S x) {
    return (f ? S{x.len - x.sum, x.len} : x);
}
F composition(F f, F g) { return f ^ g; } // フラグのXOR (反転の反転は元通り)
F id() { return false; }

// lazy_segtree<S, op, e, F, mapping, composition, id> seg;
// std::vector<S> init_v(N, {0, 1}); // 初期値が1なら {1, 1}
// seg = lazy_segtree<...>(init_v);
区間加算 & 区間最大値とその個数 (Range Add / Range Max & Count)

区間内で最大となる要素が「いくつあるか」を同時に管理します。平面走査(Sweep Line)アルゴリズムで頻繁に使われます。

using ll = long long;
const ll INF = 1e18;
struct S { ll mx, cnt; };
using F = ll;

S op(S a, S b) {
    if (a.mx > b.mx) return a;
    if (a.mx < b.mx) return b;
    if (a.mx == -INF) return a; // 単位元同士
    return {a.mx, a.cnt + b.cnt};
}
S e() { return {-INF, 0}; }
S mapping(F f, S x) {
    if (x.mx == -INF) return x;
    return {x.mx + f, x.cnt}; // 加算しても個数は変わらない
}
F composition(F f, F g) { return f + g; }
F id() { return 0; }

// lazy_segtree<S, op, e, F, mapping, composition, id> seg;
// std::vector<S> init_v(N, {初期値, 1});
// seg = lazy_segtree<...>(init_v);

Persistent Segment Tree (永続セグメント木)

用途: 過去の状態(バージョン)を保持しつつ、点更新・区間クエリ・区間コピー・二分探索を行えるデータ構造

計算量: 配列による構築 O(N)、更新・区間コピー・クエリ・二分探索 O(log N)。 空間計算量は更新や区間コピーのたびに O(log N) 増加する。

template <class S, S (*op)(S, S), S (*e)()>
struct PersistentSegmentTree {
    struct Node {
        S val;
        ll l_ch, r_ch;
    };
    vector<Node> nodes;
    ll n;

    PersistentSegmentTree(ll n_) : n(n_) {}

    ll make_node(S val, ll l_ch = -1, ll r_ch = -1) {
        nodes.push_back({val, l_ch, r_ch});
        return (ll)nodes.size() - 1;
    }

    // [初期化用] 単位元 e() でサイズ n の完全二分木を構築 O(N)
    ll build(ll l, ll r) {
        if (r - l == 1) return make_node(e());
        ll mid = (l + r) / 2;
        ll nl = build(l, mid);
        ll nr = build(mid, r);
        return make_node(op(nodes[nl].val, nodes[nr].val), nl, nr);
    }
    ll build() { return build(0, n); }

    // [初期化用] 配列から初期木を構築 O(N)
    ll build(const vector<S>& v, ll l, ll r) {
        if (r - l == 1) return make_node(v[l]);
        ll mid = (l + r) / 2;
        ll nl = build(v, l, mid);
        ll nr = build(v, mid, r);
        return make_node(op(nodes[nl].val, nodes[nr].val), nl, nr);
    }
    ll build(const vector<S>& v) { return build(v, 0, n); }

    // [更新] バージョン root をベースに、p 番目を x に更新した新バージョンを返す O(log N)
    ll update(ll root, ll p, S x, ll l, ll r) {
        if (r - l == 1) return make_node(x);
        ll mid = (l + r) / 2;
        ll nl = nodes[root].l_ch, nr = nodes[root].r_ch;
        if (p < mid) nl = update(nl, p, x, l, mid);
        else         nr = update(nr, p, x, mid, r);
        return make_node(op(nodes[nl].val, nodes[nr].val), nl, nr);
    }
    ll update(ll root, ll p, S x) { return update(root, p, x, 0, n); }

    // [区間コピー] バージョン root_dst の区間 [ql, qr) を、バージョン root_src の区間 [ql, qr) の状態で上書きした新バージョンを返す O(log N)
    ll copy_range(ll root_src, ll root_dst, ll ql, ll qr, ll l, ll r) {
        if (qr <= l || r <= ql) return root_dst;
        if (ql <= l && r <= qr) return root_src;
        ll mid = (l + r) / 2;
        ll nl = copy_range(nodes[root_src].l_ch, nodes[root_dst].l_ch, ql, qr, l, mid);
        ll nr = copy_range(nodes[root_src].r_ch, nodes[root_dst].r_ch, ql, qr, mid, r);
        return make_node(op(nodes[nl].val, nodes[nr].val), nl, nr);
    }
    ll copy_range(ll root_src, ll root_dst, ll ql, ll qr) { 
        return copy_range(root_src, root_dst, ql, qr, 0, n); 
    }

    // [取得] バージョン root における区間 [ql, qr) の演算結果を返す O(log N)
    S prod(ll root, ll ql, ll qr, ll l, ll r) {
        if (qr <= l || r <= ql) return e();
        if (ql <= l && r <= qr) return nodes[root].val;
        ll mid = (l + r) / 2;
        S vl = prod(nodes[root].l_ch, ql, qr, l, mid);
        S vr = prod(nodes[root].r_ch, ql, qr, mid, r);
        return op(vl, vr);
    }
    S prod(ll root, ll ql, ll qr) { return prod(root, ql, qr, 0, n); }

    // [二分探索] バージョン root において、f(op(a[ql], ..., a[r-1])) が true となる最大の r を返す O(log N)
    template <class F>
    ll max_right(ll root, ll ql, F f, ll l, ll r, S& sm) {
        if (r <= ql) return r;
        if (ql <= l && f(op(sm, nodes[root].val))) {
            sm = op(sm, nodes[root].val);
            return r;
        }
        if (r - l == 1) return l;
        ll mid = (l + r) / 2;
        ll res_l = max_right(nodes[root].l_ch, ql, f, l, mid, sm);
        if (res_l < mid) return res_l;
        return max_right(nodes[root].r_ch, ql, f, mid, r, sm);
    }
    template <class F>
    ll max_right(ll root, ll ql, F f) {
        S sm = e();
        return max_right(root, ql, f, 0, n, sm);
    }

    // [二分探索] バージョン root において、f(op(a[l], ..., a[qr-1])) が true となる最小の l を返す O(log N)
    template <class F>
    ll min_left(ll root, ll qr, F f, ll l, ll r, S& sm) {
        if (qr <= l) return l;
        if (r <= qr && f(op(nodes[root].val, sm))) {
            sm = op(nodes[root].val, sm);
            return l;
        }
        if (r - l == 1) return r;
        ll mid = (l + r) / 2;
        ll res_r = min_left(nodes[root].r_ch, qr, f, mid, r, sm);
        if (res_r > mid) return res_r;
        return min_left(nodes[root].l_ch, qr, f, l, mid, sm);
    }
    template <class F>
    ll min_left(ll root, ll qr, F f) {
        S sm = e();
        return min_left(root, qr, f, 0, n, sm);
    }

    // [応用・二分探索] 2つのバージョン (root_l, root_r) の差分木上で、左から k 番目(0-indexed)の要素の位置を返す O(log N)
    // ※ S が整数型で、op が加算(+)として定義されている(頻度配列として使っている)場合のみ有効
    ll kth_smallest(ll root_l, ll root_r, ll k, ll l, ll r) {
        if (r - l == 1) return l;
        ll mid = (l + r) / 2;
        ll count_l = nodes[nodes[root_r].l_ch].val - nodes[nodes[root_l].l_ch].val;
        if (k < count_l) {
            return kth_smallest(nodes[root_l].l_ch, nodes[root_r].l_ch, k, l, mid);
        } else {
            return kth_smallest(nodes[root_l].r_ch, nodes[root_r].r_ch, k - count_l, mid, r);
        }
    }
    ll kth_smallest(ll root_l, ll root_r, ll k) {
        return kth_smallest(root_l, root_r, k, 0, n);
    }
};

▼ 使い方と応用例

基本的な使い方(初期構築・更新・区間コピー・取得)

永続セグメント木では「root(根のインデックス)」=「バージョン(状態)」として扱います。
初期配列から build() で木を作ると、その木の根のインデックスが返ってきます(これをバージョン0とします)。
値を更新する際は、ベースにする過去のバージョン(root)を指定します。すると、既存の木は書き換えず、変更箇所だけを新しく生成して繋ぎ合わせた新しいバージョンの根のインデックスが返ってきます。
また、部分木を直接参照することで、特定の区間を別のバージョンの状態に丸ごとコピーすることも O(log N) で可能です。これらの root を配列に保存していくことで、いつでも過去の特定の状態を指定してクエリを投げることができます。

ll op(ll a, ll b) { return a + b; }
ll e() { return 0LL; }

int main() {
    ll N = 5;
    PersistentSegmentTree<ll, op, e> seg(N);
    
    // 各バージョンの「根のインデックス(root)」を保存する配列
    // roots[t] が「時刻 t のバージョンの状態」を表します
    vector<ll> roots;

    // 1. 配列から初期木を構築 (O(N)で高速)
    vector<ll> A = {1, 2, 3, 4, 5};
    ll root0 = seg.build(A);
    roots.push_back(root0); // roots[0] が「バージョン0」の根

    // 2. 更新 (新しいバージョンの生成)
    // roots[0] の状態をベースに、インデックス 2 を 10 に更新します。
    // ※ roots[0] の木自体は上書きされず、新しく root1 が作られます。
    ll root1 = seg.update(roots[0], 2, 10);
    roots.push_back(root1); // roots[1] が「バージョン1」の根

    // 3. 区間のコピー
    // root1 の状態をベースにしつつ、区間 [1, 3) の状態だけを root0 のものに戻す(コピーする)
    // root_src = roots[0], root_dst = roots[1], 区間 [1, 3)
    ll root2 = seg.copy_range(roots[0], roots[1], 1, 3);
    roots.push_back(root2); // roots[2] が「バージョン2」の根

    // 4. 取得 (特定のバージョンへのアクセス)
    // prod(調べるバージョン, l, r) で、その時点の区間 [l, r) の演算結果を取得できます。

    // roots[0] (更新前) の状態での区間 [1, 4) の和: (2 + 3 + 4) = 9
    ll ans0 = seg.prod(roots[0], 1, 4);
    
    // roots[1] (更新後) の状態での区間 [1, 4) の和: (2 + 10 + 4) = 16
    ll ans1 = seg.prod(roots[1], 1, 4);

    // roots[2] (区間コピー後) の状態での区間 [1, 4) の和: (2 + 3 + 4) = 9
    // ※ インデックス 2 の値が 10 から元の 3 に戻っているため
    ll ans2 = seg.prod(roots[2], 1, 4);
}
永続配列としての利用 (Persistent Array)

区間の演算を持たせず、単に「過去の任意のバージョンの配列の値」を読み書きしたい場合にも、そのまま使えます。構造体に以下の直接アクセス用のメソッドを追記しておくと定数倍が少し軽くなります。

// 構造体内に追記: 1点取得の専用関数(区間クエリを行わずにダイレクトに葉まで降りる)
S get(ll root, ll p, ll l, ll r) {
    if (r - l == 1) return nodes[root].val;
    ll mid = (l + r) / 2;
    if (p < mid) return get(nodes[root].l_ch, p, l, mid);
    else         return get(nodes[root].r_ch, p, mid, r);
}
S get(ll root, ll p) { return get(root, p, 0, n); }

// 使い方: バージョン t の配列の 3番目の値を取得
// ll val = seg.get(roots[t], 3);
2次元矩形領域の点の数え上げ (2D Range Point Count)

「X座標をバージョン番号」「Y座標をセグ木のインデックス」として点を追加(累積)していき、平面走査を用いて計算する。

// X座標で昇順ソート済みの点群 points(x, y) があるとする
// MAX_Y は Y座標の最大値
PersistentSegmentTree<ll, op, e> seg(MAX_Y + 1);

vector<ll> roots(N + 1);
roots[0] = seg.build(); // 初期状態(すべて0)

for(ll i = 0; i < N; i++) {
    // 1つ前のバージョンをベースに、Y座標の位置に +1 していく(※和のopとセット)
    ll cur_val = seg.prod(roots[i], points[i].y, points[i].y + 1);
    roots[i+1] = seg.update(roots[i], points[i].y, cur_val + 1);
}

// クエリ: 処理済みの点区間 [l, r) に対応するバージョン間で、Y座標が [y1, y2) にある点の数を取得
// ll count = seg.prod(roots[r], y1, y2) - seg.prod(roots[l], y1, y2);
区間 K 番目の値の取得 (Range K-th Smallest / Wavelet Matrixの代用)

クエリ毎に異なる区間[L,R)のK-thを問われる場合に有用

// 座標圧縮済みの配列 A (要素の値は 0 ~ M-1 の範囲に収まっている)
ll M = 100000;
PersistentSegmentTree<ll, op, e> seg(M);
vector<ll> roots(N + 1);
roots[0] = seg.build();

for(ll i = 0; i < N; i++) {
    // 1つ前のバージョンをベースに、値 A[i] の出現回数を +1 する
    ll cur_val = seg.prod(roots[i], A[i], A[i] + 1);
    roots[i+1] = seg.update(roots[i], A[i], cur_val + 1);
}

// クエリ例: 元の配列 A の区間 [l, r) の中で、小さい方から k 番目 (0-indexed) の値を求める
ll k = 2; // (例) 3番目に小さい値
ll ans_val = seg.kth_smallest(roots[l], roots[r], k);
過去のバージョン上での二分探索 (max_right / min_left)

通常のセグメント木と同様に、「指定したインデックスから右(または左)へ進んでいき、条件を満たす限界の位置」を $O(\log N)$ で探索できます。
永続セグ木を使えば、過去の任意の時点(バージョン)における条件境界をそのまま探索可能です。

// 条件関数 (例: 和が 15 以下の範囲)
auto f = [](ll x) { return x <= 15; };

// クエリ例: バージョン t の状態において、インデックス 2 から右に見ていき、
// 和が 15 以下を保てる最大の右端 r (半開区間) を求める
ll r = seg.max_right(roots[t], 2, f);

// その時の条件を満たす区間和は prod() で取得可能
ll sum = seg.prod(roots[t], 2, r);

Persistent Lazy Segment Tree (永続遅延セグメント木)

用途: 過去のバージョンを保持したまま、区間更新・区間クエリを両立する最強のデータ構造

計算量: 配列による構築 O(N)、区間更新・区間クエリ O(log N)。 空間計算量は更新のたびに O(log N) 増加するが、取得(クエリ)時のメモリ増加は 0 である(MLE対策済)。

template <class S, S (*op)(S, S), S (*e)(), class F, S (*mapping)(F, S), F (*composition)(F, F), F (*id)()>
struct PersistentLazySegmentTree {
    struct Node {
        S val;
        F lazy;
        ll l_ch, r_ch;
    };
    vector<Node> nodes;
    ll n;

    PersistentLazySegmentTree(ll n_) : n(n_) {}

    ll make_node(S val, F lazy, ll l_ch = -1, ll r_ch = -1) {
        nodes.push_back({val, lazy, l_ch, r_ch});
        return (ll)nodes.size() - 1;
    }

    // [初期化用] 完全二分木の構築
    ll build(ll l, ll r) {
        if (r - l == 1) return make_node(e(), id());
        ll mid = (l + r) / 2;
        ll nl = build(l, mid);
        ll nr = build(mid, r);
        return make_node(op(nodes[nl].val, nodes[nr].val), id(), nl, nr);
    }
    ll build() { return build(0, n); }

    ll build(const vector<S>& v, ll l, ll r) {
        if (r - l == 1) return make_node(v[l], id());
        ll mid = (l + r) / 2;
        ll nl = build(v, l, mid);
        ll nr = build(v, mid, r);
        return make_node(op(nodes[nl].val, nodes[nr].val), id(), nl, nr);
    }
    ll build(const vector<S>& v) { return build(v, 0, n); }

    // [内部処理] 遅延タグを子に押し出し、新しい子ノードを生成する(update時のみ使用)
    ll push(ll root, ll l, ll r) {
        if (nodes[root].lazy == id() || r - l == 1) return root;
        ll nl = nodes[root].l_ch;
        ll nr = nodes[root].r_ch;

        S l_val = mapping(nodes[root].lazy, nodes[nl].val);
        F l_lazy = composition(nodes[root].lazy, nodes[nl].lazy);
        nl = make_node(l_val, l_lazy, nodes[nl].l_ch, nodes[nl].r_ch);

        S r_val = mapping(nodes[root].lazy, nodes[nr].val);
        F r_lazy = composition(nodes[root].lazy, nodes[nr].lazy);
        nr = make_node(r_val, r_lazy, nodes[nr].l_ch, nodes[nr].r_ch);

        // 親ノードの遅延タグは消化されたので id() にして再構成
        return make_node(nodes[root].val, id(), nl, nr);
    }

    // [更新] バージョン root をベースに、区間 [ql, qr) に操作 f を適用
    ll update(ll root, ll ql, ll qr, F f, ll l, ll r) {
        if (qr <= l || r <= ql) return root;
        if (ql <= l && r <= qr) {
            S new_val = mapping(f, nodes[root].val);
            F new_lazy = composition(f, nodes[root].lazy);
            return make_node(new_val, new_lazy, nodes[root].l_ch, nodes[root].r_ch);
        }
        // 既存のタグを子に押し出してから分岐する
        ll pushed_root = push(root, l, r);
        ll mid = (l + r) / 2;
        ll nl = update(nodes[pushed_root].l_ch, ql, qr, f, l, mid);
        ll nr = update(nodes[pushed_root].r_ch, ql, qr, f, mid, r);
        return make_node(op(nodes[nl].val, nodes[nr].val), id(), nl, nr);
    }
    ll update(ll root, ll ql, ll qr, F f) { return update(root, ql, qr, f, 0, n); }

    // [取得] バージョン root における区間 [ql, qr) の演算結果
    // ※ メモリ爆発を防ぐため、pushは行わずタグを lazy_acc として「持ち下げる」
    S prod(ll root, ll ql, ll qr, ll l, ll r, F lazy_acc) {
        if (qr <= l || r <= ql) return e();
        // 親から持ち下ろしてきたタグと、現在のノードのタグを合成
        F cur_lazy = composition(lazy_acc, nodes[root].lazy);
        
        if (ql <= l && r <= qr) {
            return mapping(cur_lazy, nodes[root].val);
        }
        ll mid = (l + r) / 2;
        S vl = prod(nodes[root].l_ch, ql, qr, l, mid, cur_lazy);
        S vr = prod(nodes[root].r_ch, ql, qr, mid, r, cur_lazy);
        return op(vl, vr);
    }
    S prod(ll root, ll ql, ll qr) { return prod(root, ql, qr, 0, n, id()); }
};

▼ 使い方と応用例

基本的な使い方(区間加算・区間和の場合)

AtCoder Library (ACL) の lazy_segtree と全く同じように、5つの関数/単位元を定義してテンプレートに渡します。
更新時に update(root, l, r, f) で区間更新を行い、新しいバージョンの root を保存していきます。

// 区間和・区間加算のための構造体と関数定義
struct S { ll val, size; };
using F = ll;

S op(S a, S b) { return {a.val + b.val, a.size + b.size}; }
S e() { return {0, 0}; }
S mapping(F f, S x) { return {x.val + f * x.size, x.size}; }
F composition(F f, F g) { return f + g; }
F id() { return 0LL; }

int main() {
    ll N = 5;
    PersistentLazySegmentTree<S, op, e, F, mapping, composition, id> seg(N);
    
    vector<ll> roots;

    // 1. 初期構築 (各要素の初期値を設定)
    vector<S> init_data(N);
    for(int i = 0; i < N; i++) init_data[i] = {0, 1}; // 値0, サイズ1
    roots.push_back(seg.build(init_data)); // roots[0]
    
    // 2. 区間更新 (roots[0] をベースに、区間 [1, 4) に +10 を加算)
    ll root1 = seg.update(roots[0], 1, 4, 10);
    roots.push_back(root1); // roots[1]

    // 3. 区間更新 (roots[1] をベースに、区間 [2, 5) に +5 を加算)
    ll root2 = seg.update(roots[1], 2, 5, 5);
    roots.push_back(root2); // roots[2]

    // 4. 過去のバージョンに対するクエリ取得
    // roots[1] の状態での区間 [0, 3) の和: (0) + (10) + (10) = 20
    ll ans1 = seg.prod(roots[1], 0, 3).val;
    
    // roots[2] の状態での区間 [2, 4) の和: (10+5) + (10+5) = 30
    ll ans2 = seg.prod(roots[2], 2, 4).val;
}

SegmentTreeBeats

用途: 区間 chmin / chmax、区間加算、区間和などの取得を同時に処理する拡張遅延セグメント木

計算量: 構築 O(N)、クエリ処理は償却計算量 O(log N) ~ O(log^2 N)。
ACL(AtCoder Library)には含まれていないため、独自にクラスを実装する必要があります。通常の遅延セグメント木では処理できない「区間に対する chmin / chmax 操作」を、Break条件(これ以上更新不要なら終了)とTag条件(この区間全体を一括更新できるなら遅延評価)を用いて効率的に処理(枝刈り)します。

class SegmentTreeBeats {
    static const long long INF = 1e18;
    int n, sz;
    vector max_v, smax_v, max_c;
    vector min_v, smin_v, min_c;
    vector sum, len, lazy_add;

    void update_node_max(int k, long long x) {
        sum[k] += (x - max_v[k]) * max_c[k];
        if (max_v[k] == min_v[k]) { max_v[k] = min_v[k] = x; }
        else if (max_v[k] == smin_v[k]) { max_v[k] = smin_v[k] = x; }
        else { max_v[k] = x; }
    }
    void update_node_min(int k, long long x) {
        sum[k] += (x - min_v[k]) * min_c[k];
        if (max_v[k] == min_v[k]) { max_v[k] = min_v[k] = x; }
        else if (smax_v[k] == min_v[k]) { min_v[k] = smax_v[k] = x; }
        else { min_v[k] = x; }
    }
    void update_node_add(int k, long long x) {
        max_v[k] += x; if (smax_v[k] != -INF) smax_v[k] += x;
        min_v[k] += x; if (smin_v[k] != INF) smin_v[k] += x;
        sum[k] += x * len[k]; lazy_add[k] += x;
    }
    void push(int k) {
        if (lazy_add[k] != 0) {
            update_node_add(2 * k + 1, lazy_add[k]);
            update_node_add(2 * k + 2, lazy_add[k]);
            lazy_add[k] = 0;
        }
        if (max_v[k] < max_v[2 * k + 1]) update_node_max(2 * k + 1, max_v[k]);
        if (max_v[k] < max_v[2 * k + 2]) update_node_max(2 * k + 2, max_v[k]);
        if (min_v[k] > min_v[2 * k + 1]) update_node_min(2 * k + 1, min_v[k]);
        if (min_v[k] > min_v[2 * k + 2]) update_node_min(2 * k + 2, min_v[k]);
    }
    void update(int k) {
        sum[k] = sum[2 * k + 1] + sum[2 * k + 2];
        if (max_v[2 * k + 1] < max_v[2 * k + 2]) {
            max_v[k] = max_v[2 * k + 2];
            max_c[k] = max_c[2 * k + 2];
            smax_v[k] = max(max_v[2 * k + 1], smax_v[2 * k + 2]);
        } else if (max_v[2 * k + 1] > max_v[2 * k + 2]) {
            max_v[k] = max_v[2 * k + 1];
            max_c[k] = max_c[2 * k + 1];
            smax_v[k] = max(smax_v[2 * k + 1], max_v[2 * k + 2]);
        } else {
            max_v[k] = max_v[2 * k + 1];
            max_c[k] = max_c[2 * k + 1] + max_c[2 * k + 2];
            smax_v[k] = max(smax_v[2 * k + 1], smax_v[2 * k + 2]);
        }
        if (min_v[2 * k + 1] < min_v[2 * k + 2]) {
            min_v[k] = min_v[2 * k + 1];
            min_c[k] = min_c[2 * k + 1];
            smin_v[k] = min(smin_v[2 * k + 1], min_v[2 * k + 2]);
        } else if (min_v[2 * k + 1] > min_v[2 * k + 2]) {
            min_v[k] = min_v[2 * k + 2];
            min_c[k] = min_c[2 * k + 2];
            smin_v[k] = min(min_v[2 * k + 1], smin_v[2 * k + 2]);
        } else {
            min_v[k] = min_v[2 * k + 1];
            min_c[k] = min_c[2 * k + 1] + min_c[2 * k + 2];
            smin_v[k] = min(smin_v[2 * k + 1], smin_v[2 * k + 2]);
        }
    }

    void _update_chmin(int a, int b, long long x, int k, int l, int r) {
        if (b <= l || r <= a || max_v[k] <= x) return;
        if (a <= l && r <= b && smax_v[k] < x) { update_node_max(k, x); return; }
        push(k);
        _update_chmin(a, b, x, 2 * k + 1, l, (l + r) / 2);
        _update_chmin(a, b, x, 2 * k + 2, (l + r) / 2, r);
        update(k);
    }
    void _update_chmax(int a, int b, long long x, int k, int l, int r) {
        if (b <= l || r <= a || min_v[k] >= x) return;
        if (a <= l && r <= b && smin_v[k] > x) { update_node_min(k, x); return; }
        push(k);
        _update_chmax(a, b, x, 2 * k + 1, l, (l + r) / 2);
        _update_chmax(a, b, x, 2 * k + 2, (l + r) / 2, r);
        update(k);
    }
    void _update_add(int a, int b, long long x, int k, int l, int r) {
        if (b <= l || r <= a) return;
        if (a <= l && r <= b) { update_node_add(k, x); return; }
        push(k);
        _update_add(a, b, x, 2 * k + 1, l, (l + r) / 2);
        _update_add(a, b, x, 2 * k + 2, (l + r) / 2, r);
        update(k);
    }
    void _update_modulo(int a, int b, long long x, int k, int l, int r) {
        if (b <= l || r <= a || max_v[k] < x) return;
        if (l + 1 == r) {
            long long val = max_v[k] % x;
            max_v[k] = min_v[k] = sum[k] = val;
            return;
        }
        push(k);
        _update_modulo(a, b, x, 2 * k + 1, l, (l + r) / 2);
        _update_modulo(a, b, x, 2 * k + 2, (l + r) / 2, r);
        update(k);
    }
    void _update_divide(int a, int b, long long x, int k, int l, int r) {
        if (b <= l || r <= a) return;
        long long diff_max = max_v[k] - (long long)floor((double)max_v[k] / x);
        long long diff_min = min_v[k] - (long long)floor((double)min_v[k] / x);
        if (a <= l && r <= b && diff_max == diff_min) { update_node_add(k, -diff_max); return; }
        push(k);
        _update_divide(a, b, x, 2 * k + 1, l, (l + r) / 2);
        _update_divide(a, b, x, 2 * k + 2, (l + r) / 2, r);
        update(k);
    }
    long long _query_sum(int a, int b, int k, int l, int r) {
        if (b <= l || r <= a) return 0;
        if (a <= l && r <= b) return sum[k];
        push(k);
        long long vl = _query_sum(a, b, 2 * k + 1, l, (l + r) / 2);
        long long vr = _query_sum(a, b, 2 * k + 2, (l + r) / 2, r);
        return vl + vr;
    }

public:
    SegmentTreeBeats(int n_) {
        n = n_; sz = 1; while (sz < n) sz *= 2;
        max_v.assign(2 * sz - 1, -INF); smax_v.assign(2 * sz - 1, -INF); max_c.assign(2 * sz - 1, 0);
        min_v.assign(2 * sz - 1, INF); smin_v.assign(2 * sz - 1, INF); min_c.assign(2 * sz - 1, 0);
        sum.assign(2 * sz - 1, 0); len.assign(2 * sz - 1, 0); lazy_add.assign(2 * sz - 1, 0);
    }
    void build(const vector& v) {
        for (int i = 0; i < n; i++) {
            max_v[sz - 1 + i] = min_v[sz - 1 + i] = sum[sz - 1 + i] = v[i];
            max_c[sz - 1 + i] = min_c[sz - 1 + i] = len[sz - 1 + i] = 1;
        }
        for (int i = sz - 2; i >= 0; i--) {
            len[i] = len[2 * i + 1] + len[2 * i + 2];
            update(i);
        }
    }
    void update_chmin(int a, int b, long long x) { _update_chmin(a, b, x, 0, 0, sz); }
    void update_chmax(int a, int b, long long x) { _update_chmax(a, b, x, 0, 0, sz); }
    void update_add(int a, int b, long long x) { _update_add(a, b, x, 0, 0, sz); }
    void update_modulo(int a, int b, long long x) { _update_modulo(a, b, x, 0, 0, sz); }
    void update_divide(int a, int b, long long x) { _update_divide(a, b, x, 0, 0, sz); }
    long long query_sum(int a, int b) { return _query_sum(a, b, 0, 0, sz); }
};

▼ Beatsの実装パターンとコアな考え方

区間 chmin & 区間和 (軽量版・chmax不要な場合)

chmax や add を使わず、区間 chmin と区間和のみが求められる場合は、管理する変数を大幅に減らすことができます。バグらせにくく、記述量も減るため実戦的です。

// chmin, sum のみの場合の構造体の状態
// 最大値(max_v), 2番目の最大値(smax_v), 最大値の個数(max_c), 区間和(sum) のみでOK

void _update_chmin(int a, int b, long long x, int k, int l, int r) {
    if(b <= l || r <= a || max_v[k] <= x) return; // 完全に範囲外 or 更新不要
    if(a <= l && r <= b && smax_v[k] < x) {
        // 対象区間が完全に含まれ、かつ「最大値のみが更新される」状態
        // 差分だけ sum を引いて、最大値を x に書き換える
        sum[k] -= (max_v[k] - x) * max_c[k];
        max_v[k] = x;
        return;
    }
    push(k);
    _update_chmin(a, b, x, 2*k+1, l, (l+r)/2);
    _update_chmin(a, b, x, 2*k+2, (l+r)/2, r);
    update(k);
}
具体的な使い方
int main() {
    int N = 5;
    vector A = {10, 20, 30, 40, 50};

    // 1. 構築
    SegmentTreeBeats seg(N);
    seg.build(A); // 必ず build() を呼んで初期化する

    // 2. クエリ処理 (区間は常に [L, R) の半開区間)
    
    // 区間 [1, 4) を chmin(x=25) 
    // -> A は {10, 20, 25, 25, 50} になる
    seg.update_chmin(1, 4, 25); 

    // 区間 [0, 3) を加算(x=5)
    // -> A は {15, 25, 30, 25, 50} になる
    seg.update_add(0, 3, 5);

    // 区間 [0, 5) の総和を取得
    // -> 15 + 25 + 30 + 25 + 50 = 145
    cout << seg.query_sum(0, 5) << endl; 

    // 特殊クエリ: 区間 [1, 5) を mod 12 する
    // -> A は {15, 1, 6, 1, 2} になる
    seg.update_modulo(1, 5, 12);

    // 特殊クエリ: 区間 [0, 3) を 2 で割る(切り捨て)
    // -> A は {7, 0, 3, 1, 2} になる
    seg.update_divide(0, 3, 2);

    return 0;
}

▼ Beatsの考え方を応用したバリエーション

区間 modulo & 区間和

「区間内の要素 $A_i$ を $A_i \bmod X$ にする」というクエリも Beats の考え方で処理できます。

// 区間 Modulo の Break条件とTag条件
// 要素の最大値 max_v を管理しておく

void _update_modulo(int a, int b, long long x, int k, int l, int r) {
    // Break条件: 区間内の最大値が x 未満なら、moduloを取っても値は変わらない
    if(b <= l || r <= a || max_v[k] < x) return;
    
    // Moduloでは chmin のように一括更新(Tag条件)が難しいため、
    // 葉(l + 1 == r)に到達するまで潜り、1点更新を行って再構築する
    if(l + 1 == r) {
        max_v[k] %= x;
        sum[k] = max_v[k];
        return;
    }
    
    push(k);
    _update_modulo(a, b, x, 2*k+1, l, (l+r)/2);
    _update_modulo(a, b, x, 2*k+2, (l+r)/2, r);
    update(k);
}
// ※更新の度に値が半分以下になる性質から、1要素あたりの更新回数はO(log A)回に抑えられ、
// 全体で償却計算量 O((N + Q) log N log A) が保証されます。
区間除算 (Range Divide) & 区間和

区間内の各要素を X で割る(切り捨て)操作も頻出です。最大値と最小値を管理することで Tag 条件を作れます。

// 区間除算のTag条件の考え方
// max_v と min_v を管理する

void _update_divide(int a, int b, long long x, int k, int l, int r) {
    if(b <= l || r <= a) return;
    
    // Tag条件: 割り算による影響(減算値)が、最大値と最小値で等しい場合、
    // それは実質的に「区間全体から同じ値を引く(区間減算)」操作と同義になる
    long long diff_max = max_v[k] - (long long)floor((double)max_v[k] / x);
    long long diff_min = min_v[k] - (long long)floor((double)min_v[k] / x);
    
    if(a <= l && r <= b && diff_max == diff_min) {
        // 「区間全体から diff_max を引く」区間加算の遅延評価として処理できる
        update_node_add(k, -diff_max);
        return;
    }
    
    push(k);
    _update_divide(a, b, x, 2*k+1, l, (l+r)/2);
    _update_divide(a, b, x, 2*k+2, (l+r)/2, r);
    update(k);
}
区間 Bitwise AND / OR & 区間和

「区間内の要素 $A_i$ を $A_i \text{ AND } X$ にする」といったビット演算も可能です。各ノードで区間内の要素の bitwise OR と bitwise AND を管理することで、変更されるビットが区間内で一様かどうかを判定して Tag 条件を作ります。

// 区間 Bitwise AND の Break条件とTag条件
// 区間の bitwise OR (val_or) と bitwise AND (val_and) を管理する

void _update_and(int a, int b, long long x, int k, int l, int r) {
    if(b <= l || r <= a) return;
    
    // x で AND を取ることで「0に落ちるビット」のマスクを作成
    long long mask = ~x;
    
    // Break条件: 0に落ちるビットが、この区間の要素には1つも含まれていない
    if((val_or[k] & mask) == 0) return;
    
    // Tag条件: 0に落ちるビットについて、区間内の全要素で状態が一致している
    // (つまり、あるビット群を全要素一斉に 1 -> 0 に反転させる「区間XOR」として処理できる)
    if((val_or[k] & mask) == (val_and[k] & mask)) {
        // 区間 XOR の遅延評価に変換 (val_or[k] & mask は落とす対象のビット)
        update_node_xor(k, val_or[k] & mask);
        return;
    }
    
    push(k);
    _update_and(a, b, x, 2*k+1, l, (l+r)/2);
    _update_and(a, b, x, 2*k+2, (l+r)/2, r);
    update(k);
}

Li Chao Tree (動的版 / 座標圧縮・静的版)

用途: 追加された直線・線分群に対する、特定 x 座標での最小値(最大値)クエリの高速処理・DP高速化

計算量: 直線の追加・取得ともに O(log(座標幅) または log N)。線分追加は O(log^2)。
値域を気にせず直感的に使える「動的版」と、事前座圧により定数倍が軽くメモリ効率の良い「静的(座圧)版」を併記しています。

// 共通の直線構造体
const long long LCT_INF = 4e18; // 最小値クエリの初期値(十分大きい値)

struct Line {
    long long a, b;
    Line(long long a = 0, long long b = LCT_INF) : a(a), b(b) {}
    long long get(long long x) const { return a * x + b; }
};

// ====================================================================
// 1. 動的 Li Chao Tree (値域が広くてもそのまま使える。線分追加も可能)
// ====================================================================
struct DynamicLiChaoTree {
    struct Node {
        Line line;
        int left, right; // 子ノードの配列インデックス
        Node() : line(Line()), left(-1), right(-1) {}
    };
    
    vector<Node> nodes;
    long long min_x, max_x;

    // xの定義域 [min_x, max_x) を指定して初期化
    DynamicLiChaoTree(long long min_x = -1e9, long long max_x = 1e9 + 1) : min_x(min_x), max_x(max_x) {
        nodes.emplace_back();
    }

    // 直線の追加: y = ax + b
    void add_line(long long a, long long b) {
        update_line(0, min_x, max_x, Line(a, b));
    }

    // 線分の追加: x が [l, r) の範囲でのみ有効な y = ax + b
    void add_segment(long long a, long long b, long long l, long long r) {
        update_segment(0, min_x, max_x, l, r, Line(a, b));
    }

    // x における最小値の取得
    long long query(long long x) {
        long long res = LCT_INF;
        int idx = 0;
        long long l = min_x, r = max_x;
        while (idx != -1) {
            long long m = l + (r - l) / 2;
            res = min(res, nodes[idx].line.get(x));
            if (x < m) {
                idx = nodes[idx].left;
                r = m;
            } else {
                idx = nodes[idx].right;
                l = m;
            }
        }
        return res;
    }

private:
    void update_line(int idx, long long l, long long r, Line new_line) {
        long long m = l + (r - l) / 2;
        bool left_lower = new_line.get(l) < nodes[idx].line.get(l);
        bool mid_lower = new_line.get(m) < nodes[idx].line.get(m);
        bool right_lower = new_line.get(r - 1) < nodes[idx].line.get(r - 1);

        if (mid_lower) swap(nodes[idx].line, new_line);
        if (r - l <= 1) return;

        if (left_lower != mid_lower) {
            if (nodes[idx].left == -1) {
                nodes[idx].left = nodes.size();
                nodes.emplace_back();
            }
            update_line(nodes[idx].left, l, m, new_line);
        } else if (right_lower != mid_lower) {
            if (nodes[idx].right == -1) {
                nodes[idx].right = nodes.size();
                nodes.emplace_back();
            }
            update_line(nodes[idx].right, m, r, new_line);
        }
    }

    void update_segment(int idx, long long l, long long r, long long a, long long b, Line new_line) {
        if (b <= l || r <= a) return;
        if (a <= l && r <= b) {
            update_line(idx, l, r, new_line);
            return;
        }
        long long m = l + (r - l) / 2;
        if (a < m) {
            if (nodes[idx].left == -1) {
                nodes[idx].left = nodes.size();
                nodes.emplace_back();
            }
            update_segment(nodes[idx].left, l, m, a, b, new_line);
        }
        if (m < b) {
            if (nodes[idx].right == -1) {
                nodes[idx].right = nodes.size();
                nodes.emplace_back();
            }
            update_segment(nodes[idx].right, m, r, a, b, new_line);
        }
    }
};

// ====================================================================
// 2. 静的 Li Chao Tree (事前座標圧縮版・メモリ節約・定数倍高速)
// ====================================================================
struct StaticLiChaoTree {
    int n;
    vector<long long> xs;
    vector<Line> nodes;

    // クエリで飛んでくる可能性のある全ての x 座標リスト(未ソートでOK)を渡す
    StaticLiChaoTree(const vector<long long>& xs_in) {
        xs = xs_in;
        sort(xs.begin(), xs.end());
        xs.erase(unique(xs.begin(), xs.end()), xs.end());
        n = 1;
        while (n < (int)xs.size()) n *= 2;
        while ((int)xs.size() < n) xs.push_back(xs.back() + 1); // 2の冪乗になるようダミー追加
        nodes.assign(2 * n, Line());
    }

    // 直線の追加: y = ax + b
    void add_line(long long a, long long b) {
        update_line(1, 0, n, Line(a, b));
    }

    // 線分の追加: 実際の x 座標 [l_x, r_x) で有効な y = ax + b
    void add_segment(long long a, long long b, long long l_x, long long r_x) {
        int l = lower_bound(xs.begin(), xs.end(), l_x) - xs.begin();
        int r = lower_bound(xs.begin(), xs.end(), r_x) - xs.begin();
        update_segment(1, 0, n, l, r, Line(a, b));
    }

    // x における最小値の取得
    long long query(long long x) {
        int idx = lower_bound(xs.begin(), xs.end(), x) - xs.begin();
        idx += n;
        long long res = LCT_INF;
        while (idx > 0) {
            res = min(res, nodes[idx].get(x)); // 計算時のみリアルの x 座標を使用
            idx /= 2;
        }
        return res;
    }

private:
    void update_line(int k, int l, int r, Line new_line) {
        int m = (l + r) / 2;
        long long x_m = xs[m], x_l = xs[l], x_r = xs[r - 1];
        bool left_lower = new_line.get(x_l) < nodes[k].get(x_l);
        bool mid_lower = new_line.get(x_m) < nodes[k].get(x_m);
        bool right_lower = new_line.get(x_r) < nodes[k].get(x_r);

        if (mid_lower) swap(nodes[k], new_line);
        if (r - l <= 1) return;

        if (left_lower != mid_lower) {
            update_line(2 * k, l, m, new_line);
        } else if (right_lower != mid_lower) {
            update_line(2 * k + 1, m, r, new_line);
        }
    }

    void update_segment(int k, int l, int r, int a, int b, Line new_line) {
        if (b <= l || r <= a) return;
        if (a <= l && r <= b) {
            update_line(k, l, r, new_line);
            return;
        }
        int m = (l + r) / 2;
        update_segment(2 * k, l, m, a, b, new_line);
        update_segment(2 * k + 1, m, r, a, b, new_line);
    }
};

▼ 使い方と応用パターン

動的 Li Chao Tree の使い方(値域が広い場合・基本)
// 1. インスタンス作成(デフォルトで x は -10^9 ~ 10^9 に対応)
DynamicLiChaoTree lct;

// 2. 直線 y = 2x + 5 を追加
lct.add_line(2, 5);

// 3. 直線 y = -x + 10 を追加
lct.add_line(-1, 10);

// 4. クエリ取得(最小値)
cout << "x = 0 の最小値: " << lct.query(0) << endl;  // min(5, 10) = 5
cout << "x = 5 の最小値: " << lct.query(5) << endl;  // min(15, 5) = 5

// 5. 線分の追加(x が 0 ~ 10 の間だけ y = -3x + 100)
lct.add_segment(-3, 100, 0, 10);
静的 Li Chao Tree の使い方(クエリ先読み・高速化)
// 1. クエリで聞かれる(または直線の端点となる) x 座標をすべて集める
vector<long long> xs = {10, 50, 1000}; 

// 2. インスタンス作成(内部で自動的にソート&重複削除される)
StaticLiChaoTree lct_static(xs);

// 3. 直線を追加
lct_static.add_line(-1, 100);    // y = -x + 100
lct_static.add_line(-2, 150);    // y = -2x + 150

// 4. クエリ取得(事前に登録した x 座標のみ高速に処理可能)
cout << "x = 50 の最小値: " << lct_static.query(50) << endl; // 50

// ※注意: 取得クエリの x は初期化時の `xs` に含まれている必要があります。
DP高速化への応用(概要メモ)

以下のような一次関数型に展開できる DP の遷移高速化に利用できます。

  • 遷移式: dp[i] = min (dp[j] + A[j] * B[i] + C[j])
  • x = B[i] と置き、各 j の状態を直線 y = A[j] * x + (dp[j] + C[j]) と見なす。
  • 各ステップ i で x = B[i] に対する最小値をクエリで取得して dp[i] を計算し、自身を新しい直線として追加する。
  • 通常の O(N^2) の DP 遷移を O(N log N) または O(N log X) に高速化可能。

グラフ

BFS / DFS

用途: 到達可能性・距離・木探索

計算量: 隣接リストなら O(V + E)。 BFS は最短路の「辺数」、DFS は構造把握に向く。

// ===== BFS =====
// start から各頂点への最短距離(辺数)を求める
vector<ll> bfs(const vector<vector<ll>>& graph, ll start) {
    ll n = graph.size();

    vector<ll> dist(n, -1);
    queue<ll> q;

    dist[start] = 0;
    q.push(start);

    while (!q.empty()) {
        ll v = q.front();
        q.pop();

        for (ll next : graph[v]) {
            if (dist[next] != -1) continue;

            dist[next] = dist[v] + 1;
            q.push(next);
        }
    }

    return dist;
}

// ===== DFS (再帰) =====
void dfs(const vector<vector<ll>>& graph,
         vector<bool>& seen,
         ll v) {

    seen[v] = true;

    for (ll next : graph[v]) {
        if (seen[next]) continue;
        dfs(graph, seen, next);
    }
}

// 呼び出し例
int main() {
    vector<vector<ll>> graph = {
        {1, 2},     // 0
        {0, 3},     // 1
        {0, 3},     // 2
        {1, 2, 4},  // 3
        {3}         // 4
    };

    // BFS
    vector<ll> dist = bfs(graph, 0);

    // DFS
    vector<bool> seen(graph.size(), false);
    dfs(graph, seen, 0);
}

注意: 再帰DFSは深い木でスタックオーバーフローの危険がある。必要なら iterative DFS を使う。

閉路検出および復元

用途: 有向グラフの閉路判定および閉路の復元

計算量: O(V + E)。 DFSを用いて訪問状態(未訪問・訪問中・訪問済)を管理し、バックエッジを検知する。内部で状態配列を自動生成するため事前準備は不要。restore = false に指定することで復元処理をスキップし、判定のみを行える。

// 閉路が存在すればその頂点列 (例: {u, v, w, u}) を返す。存在しなければ空配列を返す。
// restore = false にすると復元を行わず高速に判定のみを行う (閉路ありなら要素数1の配列を返す)。
vector<ll> find_cycle(const vector<vector<ll>> &graph, bool restore = true) {
    ll n = graph.size();
    vector<int> state(n, 0); // 0: 未訪問, 1: 訪問中, 2: 訪問済
    vector<ll> parent(n, -1);
    vector<ll> cycle;

    auto dfs = [&](auto self, ll v) -> bool {
        state[v] = 1;
        for (ll next : graph[v]) {
            if (state[next] == 1) { // バックエッジ検出(閉路存在)
                if (restore) {
                    ll curr = v;
                    while (curr != next) {
                        cycle.push_back(curr);
                        curr = parent[curr];
                    }
                    cycle.push_back(next);
                    reverse(cycle.begin(), cycle.end());
                    cycle.push_back(next); // 始点と終点を合わせる
                } else {
                    cycle.push_back(-1); // 検出判定用のダミー値
                }
                return true;
            } else if (state[next] == 0) {
                parent[next] = v;
                if (self(self, next)) return true;
            }
        }
        state[v] = 2;
        return false;
    };

    for (ll i = 0; i < n; i++) {
        if (state[i] == 0) {
            if (dfs(dfs, i)) return cycle;
        }
    }
    return {};
}

▼ 使い方(目的別の呼び出し方法)

判定のみ(計算量を抑えて有無だけ知りたい場合)
using ll = long long;
vector<vector<ll>> graph(3);
graph[0].push_back(1);
graph[1].push_back(2);
graph[2].push_back(0);

// 第2引数に false を渡すと復元を行わない
bool has_cycle = !find_cycle(graph, false).empty();
閉路の復元(構成する頂点列を取得する場合)
using ll = long long;
vector<vector<ll>> graph(3);
graph[0].push_back(1);
graph[1].push_back(2);
graph[2].push_back(0);

// デフォルト (restore = true) で復元された頂点列を取得
vector<ll> cycle = find_cycle(graph);

if (!cycle.empty()) {
    // 閉路が存在する場合 (例: cycle = {0, 1, 2, 0})
    for (ll v : cycle) {
        cout << v << " ";
    }
}

注意: この実装は有向グラフ専用。 restore = false に指定すると復元処理(配列の反転や親の参照など)をスキップし、検出時点で即座に判定結果を返すため計算量を削減できます。

0-1 BFS

用途: 辺の重みが0または1のみのグラフにおける単一始点最短路

計算量: 両端キュー(deque)を用いて O(V + E)。 ダイクストラ法より高速に動作する。

vector<ll> zero_one_BFS(ll N, ll start, vector<vector<pair<ll, ll>>> G) {
  // 隣接リストGは{頂点番号,重み(0または1)}で与えている
  vector<ll> dist(N, INF);
  deque<ll> todo;
  todo.push_back(start);
  dist[start] = 0;

  while (todo.empty() == false) {
    ll m = todo.front();
    todo.pop_front();

    rep(i, G[m].size()) {
      ll to = G[m][i].first;
      ll cost = G[m][i].second;

      if (dist[m] + cost < dist[to]) {
        dist[to] = dist[m] + cost;
        if (cost == 0) {
          todo.push_front(to);
        } else {
          todo.push_back(to);
        }
      }
    }
  }
  return dist;
}

注意: コスト0の遷移は push_front、コスト1の遷移は push_back に入れることでキュー内の順序性を保つ。

Dijkstra

用途: 非負辺の単一始点最短路・単調性を持つ演算の最適化

計算量: 優先度付きキューで O((V + E) log V) が典型。辺のコストが負になるなど、演算の単調性が崩れる場合は使えない。

vector<ll> Dijkstra(ll N, ll start, vector<vector<pair<ll, ll>>>& G) {
  // 隣接リストGは{頂点番号,重み}で与えている
  vector<ll> dist(N, INF);
  priority_queue<pair<ll, ll>, vector<pair<ll, ll>>, greater<pair<ll, ll>>> todo;
  todo.push({0, start});
  dist[start] = 0;

  while (todo.empty() == false) {
    ll d = todo.top().first;
    ll m = todo.top().second;
    todo.pop();

    // 古い状態を無視(必須)
    if (d > dist[m]) {
      continue;
    }

    rep(i, G[m].size()) {
      ll next_v = G[m][i].first;
      ll cost = G[m][i].second;
      if (d + cost < dist[next_v]) {
        dist[next_v] = d + cost;
        todo.push({dist[next_v], next_v});
      }
    }
  }
  return dist;
}

▼ 使い方と応用パターン(様々な演算)

【基本】通常の最短経路(和の最小化)
ll N = 4;
vector<vector<pair<ll, ll>>> G(N);

// G[u].push_back({v, cost});
G[0].push_back({1, 5});
G[0].push_back({2, 2});
G[2].push_back({1, 1});

vector<ll> dist = Dijkstra(N, 0, G);
cout << "頂点0から頂点1への最短距離: " << dist[1] << endl; // 3
【応用】ボトルネック最小化(Min-Max演算)

経路上の「最大の辺の重み」を最小化したい場合。更新式と初期値を変更します。

// distの初期値はINF、dist[start] = 0 とする
// whileループ内の更新処理を以下のように変更
ll next_v = G[m][i].first;
ll cost = G[m][i].second;

// 経路の最大の重みを更新(ここが変化)
ll next_d = max(d, cost); 

if (next_d < dist[next_v]) {
    dist[next_v] = next_d;
    todo.push({next_d, next_v});
}
【応用】信頼性の最大化(確率・積の最大化)

各辺の成功確率(0〜1)の積を最大化したい場合。優先度付きキューを降順にし、積で更新します。

// 確率なので double を使用
vector<double> prob(N, 0.0); // 初期値は 0.0(到達不能)
// 確率が大きい順に取り出すため、greater ではなく less(デフォルト)を使用
priority_queue<pair<double, ll>> todo; 

prob[start] = 1.0; // 始点の確率は 100%
todo.push({1.0, start});

while (!todo.empty()) {
    double p = todo.top().first;
    ll m = todo.top().second;
    todo.pop();

    if (p < prob[m]) continue; // 確率が小さい場合は無視

    rep(i, G[m].size()) {
        ll next_v = G[m][i].first;
        double edge_prob = G[m][i].second; // 辺の確率 (例: 0.95)
        
        // 確率の積を計算(ここが変化)
        double next_p = p * edge_prob;
        
        // より高い確率で到達できるなら更新
        if (next_p > prob[next_v]) {
            prob[next_v] = next_p;
            todo.push({next_p, next_v});
        }
    }
}

Bellman-Ford

用途: 負の辺を含む単一始点最短路・負の閉路検出

計算量: O(V * E) が典型。 負の辺が含まれていても動作し、負の閉路(無限にコストを減らせるループ)が存在するかどうかも検出できる。

struct Edge {
  ll from;
  ll to;
  ll cost;
};

// 戻り値: 最短距離の配列。ただし負の閉路から到達可能な頂点の距離は -INF とする
// グラフ全体に負の閉路があるかどうかは、返った後に dist の変化やフラグ等で判定可能
vector<ll> BellmanFord(ll N, ll start, vector<Edge> edges, bool &has_negative_cycle) {
  vector<ll> dist(N, INF);
  dist[start] = 0;
  has_negative_cycle = false;

  // 1. 最大 N-1 回の緩和処理で最短路を確定させる
  rep(i, N - 1) {
    for (const auto& e : edges) {
      if (dist[e.from] != INF && dist[e.from] + e.cost < dist[e.to]) {
        dist[e.to] = dist[e.from] + e.cost;
      }
    }
  }

  // 2. 負の閉路の検出と、閉路から到達可能な頂点のマーク
  // さらに N 回ループを回すことで、負の閉路の影響を受ける頂点を確実に -INF に落とす
  rep(i, N) {
    for (const auto& e : edges) {
      if (dist[e.from] != INF && dist[e.from] + e.cost < dist[e.to]) {
        dist[e.to] = -INF;
        has_negative_cycle = true;
      }
    }
  }

  return dist;
}

注意: グラフの表現は、隣接リストではなく「辺の集合(配列)」をそのまま渡す実装がシンプルで一般的。 単に「グラフ内に負の閉路があるか」だけを見る場合は、N回目のループで1度でも更新が発生した時点で判定を打ち切ってよいが、特定の頂点から到達可能かを見る場合は注意が必要。

Warshall-Floyd

用途: 全点対間最短経路(全頂点間の最短距離)

計算量: O(V^3) が典型。 負の辺が含まれていても動作するが、負の閉路があると正しく計算できない。

void WarshallFloyd(ll N, vector<vector<ll>> &dist) {
  // dist[i][j] は頂点iからjへの辺の重み。初期値は dist[i][i] = 0、経路がない場合は INF
  rep(k, N) {
    rep(i, N) {
      rep(j, N) {
        if (dist[i][k] != INF && dist[k][j] != INF) {
          dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
        }
      }
    }
  }
}

注意: 入力するのは、重みのグラフ。そのため、隣接行列を取る必要がある。 グラフの辺数が多いときは全頂点でDijkstraをするより高速。

牛ゲー (System of Difference Constraints)

用途: 変数間の差分に関する制約(x_v - x_u ≤ w または x_v - x_u ≥ w)群から、最短・最長経路問題に帰着させて条件を満たす変数の最大値(max化)や最小値(min化)を高速に求める

計算量: 通常: O(VE) 程度 / Dijkstraモード: O(E log V)

template<typename T = long long>
struct UshiGame {
    int n;
    struct Edge { int to; T cost; };
    // 最短経路用と最長経路用でグラフを分ける
    vector<vector<Edge>> g_max, g_min;
    const T INF = numeric_limits<T>::max() / 2;

    UshiGame(int n) : n(n), g_max(n), g_min(n) {}

    // x_v - x_u <= w (上限制約) を追加
    void add_leq(int u, int v, T w) {
        // max化(最短経路): x_v <= x_u + w
        g_max[u].push_back({v, w});
        // min化(最長経路): x_u >= x_v - w
        g_min[v].push_back({u, -w});
    }

    // x_v - x_u >= w (下限制約) を追加
    void add_geq(int u, int v, T w) {
        // max化(最短経路): x_u <= x_v - w
        g_max[v].push_back({u, -w});
        // min化(最長経路): x_v >= x_u + w
        g_min[u].push_back({v, w});
    }

    // x_v - x_u == w (等号制約) を追加
    void add_eq(int u, int v, T w) {
        add_leq(u, v, w);
        add_geq(u, v, w);
    }

    // ==========================================
    // SPFAモード (負の辺・負閉路検出に対応)
    // ==========================================
    
    // max化(最短経路): x_s を 0 としたときの各変数の最大値(上限)を求める
    // 戻り値: {矛盾がないか(負閉路なし), 距離配列}
    pair<bool, vector<T>> build_max(int s) {
        vector<T> dist(n, INF);
        dist[s] = 0;
        vector<int> count(n, 0);
        vector<bool> in_queue(n, false);
        queue<int> q;
        q.push(s);
        in_queue[s] = true;
        
        while (!q.empty()) {
            int u = q.front(); q.pop();
            in_queue[u] = false;
            for (auto& edge : g_max[u]) {
                if (dist[u] != INF && dist[edge.to] > dist[u] + edge.cost) {
                    dist[edge.to] = dist[u] + edge.cost;
                    if (!in_queue[edge.to]) {
                        q.push(edge.to);
                        in_queue[edge.to] = true;
                        if (++count[edge.to] >= n) return {false, {}}; // 負閉路
                    }
                }
            }
        }
        return {true, dist};
    }

    // min化(最長経路): x_s を 0 としたときの各変数の最小値(下限)を求める
    // 戻り値: {矛盾がないか(正閉路なし), 距離配列}
    pair<bool, vector<T>> build_min(int s) {
        vector<T> dist(n, -INF);
        dist[s] = 0;
        vector<int> count(n, 0);
        vector<bool> in_queue(n, false);
        queue<int> q;
        q.push(s);
        in_queue[s] = true;
        
        while (!q.empty()) {
            int u = q.front(); q.pop();
            in_queue[u] = false;
            for (auto& edge : g_min[u]) {
                if (dist[u] != -INF && dist[edge.to] < dist[u] + edge.cost) {
                    dist[edge.to] = dist[u] + edge.cost;
                    if (!in_queue[edge.to]) {
                        q.push(edge.to);
                        in_queue[edge.to] = true;
                        if (++count[edge.to] >= n) return {false, {}}; // 正閉路
                    }
                }
            }
        }
        return {true, dist};
    }

    // ==========================================
    // Dijkstraモード (高速だが、コストの条件あり)
    // ==========================================
    
    // max化(最短経路): O(E log V) 
    // ※条件: g_maxの全ての辺の重みが 0 以上であること
    vector<T> build_max_dijkstra(int s) {
        vector<T> dist(n, INF);
        dist[s] = 0;
        priority_queue<pair<T, int>, vector<pair<T, int>>, greater<pair<T, int>>> pq;
        pq.push({0, s});
        
        while (!pq.empty()) {
            auto [d, u] = pq.top(); pq.pop();
            if (dist[u] < d) continue;
            for (auto& edge : g_max[u]) {
                if (dist[edge.to] > dist[u] + edge.cost) {
                    dist[edge.to] = dist[u] + edge.cost;
                    pq.push({dist[edge.to], edge.to});
                }
            }
        }
        return dist;
    }

    // min化(最長経路): O(E log V)
    // ※条件: g_minの全ての辺の重みが 0 以下であること (内部で符号反転して計算)
    vector<T> build_min_dijkstra(int s) {
        vector<T> dist(n, INF); // 内部では最短経路として扱う
        dist[s] = 0;
        priority_queue<pair<T, int>, vector<pair<T, int>>, greater<pair<T, int>>> pq;
        pq.push({0, s});
        
        while (!pq.empty()) {
            auto [d, u] = pq.top(); pq.pop();
            if (dist[u] < d) continue;
            for (auto& edge : g_min[u]) {
                T w = -edge.cost; // コストが0以下前提なので反転して0以上にする
                if (dist[edge.to] > dist[u] + w) {
                    dist[edge.to] = dist[u] + w;
                    pq.push({dist[edge.to], edge.to});
                }
            }
        }
        
        vector<T> res(n, -INF);
        for (int i = 0; i < n; ++i) {
            if (dist[i] != INF) res[i] = -dist[i];
        }
        return res;
    }
};

注意: build_max と build_min はそれぞれ独立したアルゴリズムとして動作します。

▼ 使い方と応用パターン

基本の使い方(max化・最大値を求める場合)

「x_v は x_u + w 以下である」という制約群が与えられたとき、基準点からの各変数の最大値を求めます。

ll N = /* 変数の数 */;
UshiGame<long long> ug(N);

// 制約: x_v - x_u <= w を追加していく
// 例: x_2 - x_0 <= 5
ug.add_leq(0, 2, 5);
ug.add_leq(2, 3, 2);

// 変数 0 (x_0 = 0) を基準として最大値を計算
auto [ok, dist] = ug.build_max(0);

if (!ok) {
    cout << "解なし(制約に矛盾あり)" << endl;
} else {
    // 変数 3 の最大値を出力
    if (dist[3] == ug.INF) {
        cout << "上限なし (INF)" << endl;
    } else {
        cout << "x_3 の最大値: " << dist[3] << endl;
    }
}
基本の使い方(min化・最小値を求める場合)

「x_v は x_u + w 以上である」という制約群が与えられたとき、基準点からの各変数の最小値を求めます。

ll N = /* 変数の数 */;
UshiGame<long long> ug(N);

// 制約: x_v - x_u >= w を追加していく
// 例: x_2 - x_0 >= 3
ug.add_geq(0, 2, 3);
ug.add_geq(2, 3, 10);

// 変数 0 (x_0 = 0) を基準として最小値を計算
auto [ok, dist] = ug.build_min(0);

if (!ok) {
    cout << "解なし(制約に矛盾あり)" << endl;
} else {
    // 変数 3 の最小値を出力
    if (dist[3] == -ug.INF) {
        cout << "下限なし (-INF)" << endl;
    } else {
        cout << "x_3 の最小値: " << dist[3] << endl;
    }
}
応用:絶対値制約や等号制約の処理

「|x_v - x_u| ≤ w」のような制約は、「x_v - x_u ≤ w」かつ「x_u - x_v ≤ w」の2つに分解して追加することで対応できます。

// |x_v - x_u| <= w の場合
ug.add_leq(u, v, w);
ug.add_leq(v, u, w);

// x_v - x_u == w の場合
// 既に用意されている add_eq を使えば OK です
ug.add_eq(u, v, w);
高速化:Dijkstraモードの使用条件と使い方

負の辺が存在しない(=制約の形が特定の条件を満たす)場合、SPFAの代わりに計算量が O(E log V) のDijkstra法を使用できます。矛盾の検出(閉路判定)はできないため、解が存在することが保証されている場合に使用します。

  • build_max_dijkstra / build_min_dijkstra の条件: 追加されるグラフ g_max の重みが全て 0 以上であること。(例: add_leq の w ≥ 0、add_geq の w ≤ 0 のみで構築されている場合)
// 全ての制約が w >= 0 の add_leq だけで構成されている場合、Dijkstraが使える
ug.add_leq(0, 1, 10);
ug.add_leq(1, 2, 20);

// SPFAを使わずに高速に最大値を計算 (※戻り値は距離配列のみ)
vector<long long> dist = ug.build_max_dijkstra(0);

if (dist[2] == ug.INF) {
    cout << "上限なし (INF)" << endl;
} else {
    cout << "x_2 の最大値: " << dist[2] << endl;
}

Tree Diameter (木の直径)

用途: 木構造における最遠頂点間の距離(直径)の取得

計算量: O(V)。 任意の頂点から最も遠い頂点 u を探索し、次に u から最も遠い頂点 v を探索する(ダブルスイープ法)。u と v の距離が木の直径となる。

// 2回BFSを行うことで木の直径を求める
pair<ll, ll> bfs_tree_diameter(ll start, ll n, const vector<vector<ll>> &graph) {
  vector<ll> dist(n, -1);
  queue<ll> que;
  dist[start] = 0;
  que.push(start);
  ll far_node = start;
  while (!que.empty()) {
    ll v = que.front();
    que.pop();
    if (dist[v] > dist[far_node]) {
      far_node = v;
    }
    for (ll next : graph[v]) {
      if (dist[next] == -1) {
        dist[next] = dist[v] + 1;
        que.push(next);
      }
    }
  }
  return {dist[far_node], far_node};
}

ll get_tree_diameter(ll n, const vector<vector<ll>> &graph) {
  if (n <= 1) return 0;
  auto p1 = bfs_tree_diameter(0, n, graph);
  auto p2 = bfs_tree_diameter(p1.second, n, graph);
  return p2.first;
}

注意: 出力する長さはパスの長さ = 辺の個数であることに注意!グラフが連結な木であることが前提。

Forest Diameter (森の直径)

用途: 森(複数の木で構成されるグラフ)における最大直径の取得

計算量: O(V)。 未訪問の連結成分(各木)ごとにダブルスイープ法を適用し、それぞれの木の直径を算出してその中の最大値を求める。

// 各連結成分に対して木の直径を求め、その最大値を返す
ll get_forest_diameter(ll n, const vector<vector<ll>> &graph) {
  vector<bool> visited(n, false);
  vector<ll> dist(n, -1);
  ll max_diameter = 0;

  auto bfs = [&](ll start, vector<ll> &component) {
    queue<ll> que;
    dist[start] = 0;
    que.push(start);
    ll far_node = start;
    while (!que.empty()) {
      ll v = que.front();
      que.pop();
      component.push_back(v);
      if (dist[v] > dist[far_node]) {
        far_node = v;
      }
      for (ll next : graph[v]) {
        if (dist[next] == -1) {
          dist[next] = dist[v] + 1;
          que.push(next);
        }
      }
    }
    return far_node;
  };

  auto bfs_fixed = [&](ll start) {
    queue<ll> que;
    dist[start] = 0;
    que.push(start);
    ll max_d = 0;
    while (!que.empty()) {
      ll v = que.front();
      que.pop();
      if (dist[v] > max_d) {
        max_d = dist[v];
      }
      for (ll next : graph[v]) {
        if (dist[next] == -1) {
          dist[next] = dist[v] + 1;
          que.push(next);
        }
      }
    }
    return max_d;
  };

  for (ll i = 0; i < n; ++i) {
    if (visited[i]) continue;
    if (graph[i].empty()) {
      visited[i] = true;
      continue;
    }
    vector<ll> component;
    ll u = bfs(i, component);
    for (ll node : component) {
      visited[node] = true;
      dist[node] = -1;
    }
    ll d = bfs_fixed(u);
    for (ll node : component) {
      dist[node] = -1;
    }
    if (d > max_diameter) {
      max_diameter = d;
    }
  }
  return max_diameter;
}

注意: 出力する長さはパスの長さ = 辺の個数であることに注意!グラフが複数の独立した木(非連結グラフ)で構成されている場合でも正しく動作する。

Tree Center (木の中心)

用途: 木構造において、最も遠い頂点までの距離が最小となる頂点(中心)の取得

計算量: O(V)。 木の直径となるパスを求め、その中点となる頂点を中心として返します。木の中心は必ず1つまたは2つ存在します。

// 木の中心を求める(戻り値は1つまたは2つの頂点)
vector<ll> get_tree_center(ll n, const vector<vector<ll>> &graph) {
  if (n <= 1) return {0};

  // 1回目のBFS(適当な頂点0から最も遠い頂点uを探す)
  vector<ll> dist1(n, -1);
  queue<ll> que;
  dist1[0] = 0;
  que.push(0);
  ll u = 0;
  while (!que.empty()) {
    ll v = que.front();
    que.pop();
    if (dist1[v] > dist1[u]) u = v;
    for (ll next_v : graph[v]) {
      if (dist1[next_v] == -1) {
        dist1[next_v] = dist1[v] + 1;
        que.push(next_v);
      }
    }
  }

  // 2回目のBFS(uから最も遠い頂点wを探しつつ、親を記録してパスを復元可能にする)
  vector<ll> dist2(n, -1);
  vector<ll> parent(n, -1);
  dist2[u] = 0;
  que.push(u);
  ll w = u;
  while (!que.empty()) {
    ll v = que.front();
    que.pop();
    if (dist2[v] > dist2[w]) w = v;
    for (ll next_v : graph[v]) {
      if (dist2[next_v] == -1) {
        dist2[next_v] = dist2[v] + 1;
        parent[next_v] = v;
        que.push(next_v);
      }
    }
  }

  // u から w へのパス(直径)を復元
  vector<ll> path;
  ll curr = w;
  while (curr != -1) {
    path.push_back(curr);
    curr = parent[curr];
  }

  // パスの中点が中心となる
  vector<ll> centers;
  ll len = path.size();
  if (len % 2 == 1) {
    centers.push_back(path[len / 2]);
  } else {
    centers.push_back(path[len / 2 - 1]);
    centers.push_back(path[len / 2]);
  }
  return centers;
}

▼ 使い方と応用パターン

基本の使い方(木の中心の取得)
ll n = 5;
vector<vector<ll>> graph(n);
// 辺の追加 (0-1, 1-2, 2-3, 2-4 のような構成)
graph[0].push_back(1); graph[1].push_back(0);
graph[1].push_back(2); graph[2].push_back(1);
graph[2].push_back(3); graph[3].push_back(2);
graph[2].push_back(4); graph[4].push_back(2);

vector<ll> centers = get_tree_center(n, graph);

cout << "木の中心: ";
for (ll c : centers) {
  cout << c << " ";
}
cout << endl;
// 直径パスが偶数長なら中心は1つ、奇数長なら2つ出力されます。
応用例:木の同型判定 (Tree Isomorphism)
// 部分木を括弧の文字列として表現しハッシュ化する
// 実は、このような木と括弧列の対応は全単射
// 計算量:O(V log V) ※文字列結合・ソートのオーバーヘッドを含む
string get_tree_hash(ll v, ll p, const vector<vector<ll>> &graph) {
  vector<string> child_hashes;
  for (ll next_v : graph[v]) {
    if (next_v == p) continue;
    child_hashes.push_back(get_tree_hash(next_v, v, graph));
  }
  // 順序に依存しないようソートする
  sort(child_hashes.begin(), child_hashes.end());
  
  string res = "(";
  for (const string &s : child_hashes) res += s;
  res += ")";
  return res;
}

// 2つの木が同型か判定する
// 計算量:O(V log V)
bool is_isomorphic(ll n, const vector<vector<ll>> &graph1, const vector<vector<ll>> &graph2) {
  vector<ll> centers1 = get_tree_center(n, graph1);
  vector<ll> centers2 = get_tree_center(n, graph2);
  
  // 中心の個数が異なる場合は同型ではない
  if (centers1.size() != centers2.size()) return false;
  
  // 木1の1つ目の中心を根としたハッシュ
  string hash1 = get_tree_hash(centers1[0], -1, graph1);
  
  // 木2の1つ目の中心を根としたハッシュ
  string hash2_a = get_tree_hash(centers2[0], -1, graph2);
  if (hash1 == hash2_a) return true;
  
  // 中心が2つある場合は、もう一方も試す
  if (centers2.size() == 2) {
    string hash2_b = get_tree_hash(centers2[1], -1, graph2);
    if (hash1 == hash2_b) return true;
  }
  
  return false;
}

Tree Centroid (木の重心)

用途: 頂点を取り除いたとき、残りの各連結成分のサイズが全体の半分以下になる頂点(重心)の取得

計算量: O(V)。 任意の頂点を根としてDFSを行い、各部分木のサイズを計算しながら条件を満たす頂点を探します。重心は必ず1つまたは2つ存在します。

// 木の重心を求める(戻り値は1つまたは2つの頂点)
vector<ll> get_tree_centroid(ll n, const vector<vector<ll>> &graph) {
  vector<ll> centroid;
  vector<ll> subtree_size(n, 0);

  // 再帰ラムダを用いたDFS
  auto dfs = [&](auto self, ll v, ll p) -> void {
    subtree_size[v] = 1;
    bool is_centroid = true;

    for (ll next_v : graph[v]) {
      if (next_v == p) continue;
      
      self(self, next_v, v);
      subtree_size[v] += subtree_size[next_v];
      
      // 子側の部分木サイズが全体の半分より大きい場合、重心ではない
      if (subtree_size[next_v] > n / 2) {
        is_centroid = false;
      }
    }

    // 親側の部分木サイズが全体の半分より大きい場合、重心ではない
    if (n - subtree_size[v] > n / 2) {
      is_centroid = false;
    }

    if (is_centroid) {
      centroid.push_back(v);
    }
  };

  dfs(dfs, 0, -1);
  return centroid;
}

▼ 使い方と応用パターン

基本の使い方(木の重心の取得)
ll n = 6;
vector<vector<ll>> graph(n);
// 辺の追加 (スターグラフのような構成)
graph[0].push_back(1); graph[1].push_back(0);
graph[0].push_back(2); graph[2].push_back(0);
graph[0].push_back(3); graph[3].push_back(0);
graph[0].push_back(4); graph[4].push_back(0);
graph[0].push_back(5); graph[5].push_back(0);

vector<ll> centroids = get_tree_centroid(n, graph);

cout << "木の重心: ";
for (ll c : centroids) {
  cout << c << " ";
}
cout << endl;
// この場合、頂点0を消せばすべてサイズ1の成分になるため、0が重心になります。
応用例:距離の総和の最小化
// 指定した頂点からの距離の総和をBFSで計算する
// 計算量:O(V)
ll get_dist_sum(ll start, ll n, const vector<vector<ll>> &graph) {
  ll total_dist = 0;
  queue<pair<ll, ll>> que; // {現在の頂点, 距離}
  vector<ll> visited(n, 0);
  
  que.push({start, 0});
  visited[start] = 1;
  
  while (!que.empty()) {
    auto [v, d] = que.front();
    que.pop();
    
    total_dist += d;
    
    for (ll next_v : graph[v]) {
      if (!visited[next_v]) {
        visited[next_v] = 1;
        que.push({next_v, d + 1});
      }
    }
  }
  return total_dist;
}

// 全頂点間の距離の総和が最小となる値を求める
// (重心は距離の総和を最小化する頂点である性質を利用)
// 計算量:重心取得 O(V) + BFS O(V) = O(V)
ll get_min_total_dist(ll n, const vector<vector<ll>> &graph) {
  vector<ll> centroids = get_tree_centroid(n, graph);
  
  // 重心が1つでも2つでも、重心からの距離の総和が最小となる
  return get_dist_sum(centroids[0], n, graph);
}

Centroid Decomposition (重心分解)

用途: 木を重心で再帰的に分割し、深さ O(log N) の重心分解木(Centroid Tree)を構築する。木上のパスの集計や、動的な距離クエリの高速化(分割統治法)

計算量: 構築 O(N log N)。
現在の木から重心を見つけて削除し、分裂した部分木に対して再帰的に処理を行います。どんなに偏った木でも、生成される重心分解木の深さは必ず log N 以下になります。
構造体単体で重心の探索から重心分解木の構築(根、親、子リストの保持)まで完結しています。

// 前提:木の重心
// (※この構造体内部で重心を求める処理を完結して実装しています)
struct CentroidDecomposition {
    ll n;
    vector<vector<ll>> graph;
    vector<bool> removed;
    vector<ll> subtree_size;
    
    // 取得できる情報群
    vector<ll> centroid_parent;       // 重心分解木における親(根の場合は -1)
    vector<vector<ll>> centroid_tree; // 重心分解木における子リスト(有向辺)
    ll centroid_root;                 // 重心分解木の根(一番最初の重心)

    // 構築 O(N log N)
    CentroidDecomposition(ll n, const vector<vector<ll>>& g) : n(n), graph(g) {
        removed.assign(n, false);
        subtree_size.assign(n, 0);
        centroid_parent.assign(n, -1);
        centroid_tree.assign(n, vector<ll>());
        centroid_root = -1;
        build(0, -1);
    }

    // DFSで現在の有効な部分木のサイズを計算
    void calc_size(ll v, ll p) {
        subtree_size[v] = 1;
        for (ll next_v : graph[v]) {
            if (next_v == p || removed[next_v]) continue;
            calc_size(next_v, v);
            subtree_size[v] += subtree_size[next_v];
        }
    }

    // 現在の部分木の重心を見つける
    ll get_centroid(ll v, ll p, ll total_size) {
        for (ll next_v : graph[v]) {
            if (next_v == p || removed[next_v]) continue;
            // 子のサイズが全体の半分より大きければ、そっちに重心がある
            if (subtree_size[next_v] > total_size / 2) {
                return get_centroid(next_v, v, total_size);
            }
        }
        return v;
    }

    // 重心分解木を再帰的に構築
    void build(ll v, ll p_centroid) {
        calc_size(v, -1);
        ll c = get_centroid(v, -1, subtree_size[v]);
        
        centroid_parent[c] = p_centroid;
        if (p_centroid == -1) {
            centroid_root = c; // 親がいないならそこが大元の根
        } else {
            centroid_tree[p_centroid].push_back(c); // 親の子リストに自分を追加
        }
        
        removed[c] = true;

        for (ll next_v : graph[c]) {
            if (!removed[next_v]) {
                build(next_v, c);
            }
        }
    }
};

▼ 使い方と応用パターン

基本の使い方(重心分解木の根・親・子の取得と走査)
ll n = 7;
vector<vector<ll>> graph(n);
// 0-1-3-4
//  | |
//  2 5-6 という直線的な木
graph[0].push_back(1); graph[1].push_back(0);
graph[1].push_back(2); graph[2].push_back(1);
graph[1].push_back(3); graph[3].push_back(1);
graph[3].push_back(4); graph[4].push_back(3);
graph[3].push_back(5); graph[5].push_back(3);
graph[5].push_back(6); graph[6].push_back(5);

// 構築
CentroidDecomposition cd(n, graph);

// 1. 重心分解木の根(一番最初に選ばれた全体の重心)を取得
cout << "[1] 重心分解木の根: " << cd.centroid_root << "\n\n";

// 2. 各頂点の情報をすべて取得
cout << "[2] 各頂点の重心分解木上での情報\n";
for (ll i = 0; i < n; ++i) {
    cout << "頂点 " << i << " : ";
    cout << "親 = " << cd.centroid_parent[i] << ", ";
    
    cout << "子 = { ";
    for (ll child : cd.centroid_tree[i]) {
        cout << child << " ";
    }
    cout << "}\n";
}
cout << "\n";

// 3. 応用:ある頂点(例: 頂点6)から、重心分解木を根に向かって登る
// (点更新などのクエリで頻繁に使う操作です)
ll current = 6;
cout << "[3] 頂点 " << current << " から根までのパス: ";
while (current != -1) {
    cout << current << " -> ";
    current = cd.centroid_parent[current];
}
cout << "END\n";
応用例:木上の分割統治法(構造体を利用して外付けでパス集計を解く)

事前に構築した CentroidDecomposition 構造体の情報(根と子リスト)を利用して、木上の分割統治を行う典型的なパターンです。
※「部分木」という言葉の区別に注意してください:
① 元の木上の部分木(連結成分):現在の重心 c を取り除いたことによって分断された、元のグラフ上の各エリア。
② 重心分解木上の子:上記①で分断された各エリアの中から、次に重心として選ばれた頂点。
ここでは例として「距離が K となる頂点対の数を数える」具体的な集計処理を実装しています。

// 前提: 事前に構築済みであること
// CentroidDecomposition cd(n, graph);

ll K = 3; // 例: 距離が 3 となるペアを数える
ll ans = 0;

// 分割統治の過程で「すでに重心として処理され、元の木から取り除かれた頂点」を記録する
vector<bool> used(n, false);

// 重心分解木を上から下へ辿る再帰関数
// c: 現在処理対象としている重心
auto solve = [&](auto& self, ll c) -> void {
    // 重心 c を処理済みにする(これにより、元の木が c を境に論理的に分断される)
    used[c] = true; 

    // =========================================================
    // [1] 重心 c を経由する(c をまたぐ)パスの集計処理
    // =========================================================
    
    // これまで見た「元の木上の部分木」の頂点までの距離を記録する
    // キー: 距離, バリュー: その距離になる頂点の数
    map<ll, ll> dist_count;
    dist_count[0] = 1; // c 自身との距離は 0 (1個)
    
    // c から元のグラフ(graph)を辿り、分断された各「元の木上の部分木」ごとに処理する
    for (ll next_v : graph[c]) {
        if (used[next_v]) continue; // 上位の重心として既に分断された方向へは行かない
        
        vector<ll> current_subtree_dists; // 現在注目している部分木内の頂点群の距離
        
        // 元の木上を探索し、c からの距離を列挙する DFS
        auto get_dist = [&](auto& get_dist_self, ll v, ll p, ll d) -> void {
            current_subtree_dists.push_back(d);
            for (ll nv : graph[v]) {
                if (nv == p || used[nv]) continue; // 逆流防止 & 他の重心エリアへの侵入防止
                get_dist_self(get_dist_self, nv, v, d + 1);
            }
        };
        
        // next_v を起点に、距離 1 から探索スタート
        get_dist(get_dist, next_v, c, 1);
        
        // 集計ステップ1: 今列挙した部分木内の頂点と、過去に探索した別の部分木の頂点とを掛け合わせる
        for (ll d : current_subtree_dists) {
            ll target = K - d;
            if (dist_count.count(target)) {
                ans += dist_count[target];
            }
        }
        
        // 集計ステップ2: 今列挙した部分木の距離を dist_count に合流させる(次の部分木との計算に備える)
        for (ll d : current_subtree_dists) {
            dist_count[d]++;
        }
    }
    
    // =========================================================
    // [2] 分断された各エリアの新しい重心に対して再帰
    // =========================================================
    
    // 重心分解木における c の子(c を取り除いた各連結成分の重心)へ進む
    for (ll child_centroid : cd.centroid_tree[c]) {
        self(self, child_centroid);
    }
};

// 処理全体は「重心分解木の根(一番最初の木全体の重心)」からスタートする
solve(solve, cd.centroid_root);

// ans に距離 K となるペアの総数が入る
cout << ans << endl;

Complement Graph (補グラフ上のBFS / 連結成分分解)

用途: 辺が $\mathcal{O}(N^2)$ 存在する補グラフ(与えられた辺以外のすべてに張られたグラフ)での高速な探索・成分分解

計算量: $\mathcal{O}((N + M) \log N)$ ($N$: 頂点数, $M$: 元グラフの辺数)。
未訪問の頂点を std::set で一元管理し、元グラフの隣接リストを二分探索することで、補グラフ上で辺が存在する頂点だけを効率的に抽出・削除します。二部グラフ判定の高速な矛盾チェック($\mathcal{O}(M)$)も内蔵しています。

// 補グラフ(与えられた辺「以外」に辺があるグラフ)を効率的に扱うライブラリ
// 前提: typedef long long ll; および graph は vector<vector<ll>> 型
struct ComplementGraph {
  ll n;
  vector<vector<ll>> adj;

  // コンストラクタ: 元グラフを渡し、内部で二分探索用にソートする
  ComplementGraph(ll n, const vector<vector<ll>>& graph) : n(n), adj(graph) {
    for (ll i = 0; i < n; ++i) {
      sort(adj[i].begin(), adj[i].end());
    }
  }

  // 1. 補グラフ上での始点(複数可)からの最短距離を求める
  vector<ll> get_distances(const vector<ll>& start_nodes) {
    vector<ll> dist(n, -1);
    set<ll> unvisited;
    for (ll i = 0; i < n; ++i) unvisited.insert(i);

    queue<ll> q;
    for (ll s : start_nodes) {
      dist[s] = 0;
      q.push(s);
      unvisited.erase(s);
    }

    while (!q.empty()) {
      ll u = q.front();
      q.pop();

      auto it = unvisited.begin();
      while (it != unvisited.end()) {
        ll v = *it;
        // 元グラフに辺があれば補グラフには辺がない
        if (binary_search(adj[u].begin(), adj[u].end(), v)) {
          ++it;
        } else {
          dist[v] = dist[u] + 1;
          q.push(v);
          it = unvisited.erase(it); // 訪問したら即座に候補から消す
        }
      }
    }
    return dist;
  }

  // 2. 補グラフにおける連結成分分解
  // 戻り値: 各連結成分に含まれる頂点リストのリスト
  vector<vector<ll>> get_connected_components() {
    vector<vector<ll>> comps;
    set<ll> unvisited;
    for (ll i = 0; i < n; ++i) unvisited.insert(i);

    while (!unvisited.empty()) {
      ll start = *unvisited.begin();
      unvisited.erase(unvisited.begin());

      vector<ll> comp;
      queue<ll> q;
      q.push(start);
      comp.push_back(start);

      while (!q.empty()) {
        ll u = q.front();
        q.pop();

        auto it = unvisited.begin();
        while (it != unvisited.end()) {
          ll v = *it;
          if (binary_search(adj[u].begin(), adj[u].end(), v)) {
            ++it;
          } else {
            comp.push_back(v);
            q.push(v);
            it = unvisited.erase(it);
          }
        }
      }
      comps.push_back(comp);
    }
    return comps;
  }

  // 3. 補グラフの二部グラフ判定(元グラフを2つのクリークに分割可能か等に使用)
  // 戻り値: 二部グラフなら各成分の {色0の頂点群, 色1の頂点群} のリスト。不可なら空配列
  vector<pair<vector<ll>, vector<ll>>> get_bipartite_components() {
    vector<pair<vector<ll>, vector<ll>>> comps;
    set<ll> unvisited;
    for (ll i = 0; i < n; ++i) unvisited.insert(i);
    
    vector<ll> color(n, -1);

    while (!unvisited.empty()) {
      ll start = *unvisited.begin();
      unvisited.erase(unvisited.begin());

      vector<ll> color0, color1;
      queue<ll> q;
      q.push(start);
      color[start] = 0;
      color0.push_back(start);

      vector<ll> current_comp = {start};

      while (!q.empty()) {
        ll u = q.front();
        q.pop();

        auto it = unvisited.begin();
        while (it != unvisited.end()) {
          ll v = *it;
          if (binary_search(adj[u].begin(), adj[u].end(), v)) {
            ++it;
          } else {
            color[v] = 1 - color[u];
            if (color[v] == 0) color0.push_back(v);
            else color1.push_back(v);
            
            current_comp.push_back(v);
            q.push(v);
            it = unvisited.erase(it);
          }
        }
      }

      // 矛盾チェック:同色同士に「補グラフの辺」がないか(=元グラフで全て辺が張られているか)
      for (ll u : current_comp) {
        ll same_color_count = 0;
        for (ll v : adj[u]) {
          if (color[u] == color[v]) same_color_count++;
        }
        // 自分以外の同色の頂点すべてと元グラフで繋がっていなければならない
        ll expected = (color[u] == 0 ? color0.size() : color1.size()) - 1;
        if (same_color_count < expected) return {}; // 矛盾発見(二部グラフではない)
      }
      comps.push_back({color0, color1});
    }
    return comps;
  }
};

▼ 使い方・適用例

基本の使い方(最短距離と連結成分分解)
ll n = 5;
vector<vector<ll>> graph(n);
// 元グラフの辺(0-1, 1-2 のみ。これら「以外」が補グラフの辺となる)
graph[0].push_back(1); graph[1].push_back(0);
graph[1].push_back(2); graph[2].push_back(1);

// 1. ライブラリの初期化
ComplementGraph cg(n, graph);

// 2. 補グラフ上での最短距離の取得(頂点0を始点とする)
// ※ 始点が複数ある場合は {0, 3} のように複数渡せます(多点スタートBFS)
vector<ll> dist = cg.get_distances({0});
cout << "頂点0からの補グラフ上の距離:" << endl;
for(ll i = 0; i < n; ++i) {
  cout << " 頂点" << i << ": " << dist[i] << endl;
}

// 3. 補グラフ上での連結成分分解
vector<vector<ll>> comps = cg.get_connected_components();
cout << "補グラフの連結成分数: " << comps.size() << endl;
for (ll i = 0; i < comps.size(); ++i) {
  cout << " 成分 " << i << " の頂点: ";
  for (ll v : comps[i]) cout << v << " ";
  cout << endl;
}
応用1: 最短経路DAG上のDP(経路数カウント)

補グラフで「距離 d-1 から 距離 d への遷移」をまともに数えると TLE します。
そこで、「距離 d-1 の全頂点の dp の和」から「元グラフで辺が張られている(=補グラフで遷移できない)頂点の dp の和」を引くという $\mathcal{O}(N+M)$ の天才的アプローチを取ります。

ComplementGraph cg(n, graph);
vector<ll> dist = cg.get_distances({0}); // 頂点0からの距離

// 距離ごとに頂点をバケット分類
ll max_d = 0;
for(ll d : dist) max_d = max(max_d, d);
vector<vector<ll>> nodes_by_dist(max_d + 1);
for(ll i = 0; i < n; i++) {
  if(dist[i] != -1) nodes_by_dist[dist[i]].push_back(i);
}

vector<ll> dp(n, 0);
dp[0] = 1;
ll mod = 998244353;

for (ll d = 1; d <= max_d; d++) {
  // 距離 d-1 の層の経路数の総和を求めておく
  ll prev_sum = 0;
  for (ll u : nodes_by_dist[d - 1]) {
    prev_sum = (prev_sum + dp[u]) % mod;
  }

  // 距離 d の各頂点について
  for (ll v : nodes_by_dist[d]) {
    ll invalid_sum = 0;
    // 「元グラフ」の辺を逆に辿り、距離が d-1 の頂点の分だけ除外する
    for (ll u : graph[v]) {
      if (dist[u] == d - 1) {
        invalid_sum = (invalid_sum + dp[u]) % mod;
      }
    }
    dp[v] = (prev_sum - invalid_sum + mod) % mod;
  }
}

cout << "頂点 N-1 への補グラフ上での最短経路数: " << dp[n - 1] << endl;
応用2: 連結成分サイズを用いたナップサックDP (bitset高速化)

グラフを2つのグループに分ける際、「異なるグループ間に元グラフの辺が全て張られているか」等の条件は「補グラフの連結成分を丸ごとどちらかのグループに入れる問題」に帰着します。

ComplementGraph cg(n, graph);
auto comps = cg.get_connected_components();

// N <= 100000 の部分和問題を bitset で 1/64 に定数倍高速化
bitset<100005> dp;
dp[0] = 1;

for (const auto& comp : comps) {
  ll s = comp.size();
  dp = dp | (dp << s); // 今までの状態にサイズ s を足したものを合成
}

ll K = n / 2; // 例:合計を K にできるか
if (dp[K]) {
  cout << "部分和 " << K << " は作成可能" << endl;
}
応用3: 2つのクリークへの分割 ($2^k-2$ 通りの彩色)

「与えられたグラフを2つのクリーク(完全グラフ)に分割できるか?」という問題は、「補グラフが二部グラフになるか?」と同値です。
可能であれば、連結成分の数を $k$ として $2^k$ 通りの分け方があります(空を許さない場合は $-2$ します)。

ComplementGraph cg(n, graph);
auto bipartite_comps = cg.get_bipartite_components();

if (bipartite_comps.empty()) {
  cout << "補グラフは二部グラフではないため分割不可能" << endl;
} else {
  ll k = bipartite_comps.size(); // 連結成分の数
  
  // 各連結成分は (色0=A, 色1=B) または (色0=B, 色1=A) の 2通りの選び方がある
  ll ans = 1;
  ll mod = 998244353;
  for (ll i = 0; i < k; ++i) {
    ans = (ans * 2) % mod;
  }
  
  // もし「どちらのグループも1人以上必要(空グループ不可)」という制約がある場合、
  // 全てA、全てB になる 2 パターンを除外する
  ans = (ans - 2 + mod) % mod;
  
  cout << "2つのクリークに分割する方法: " << ans << " 通り" << endl;
}
応用4: 異なるグループ間にすべて辺があるような2分割 ($2^k-2$ 通り)

「グループAとグループBの任意の頂点対間に、元グラフ上で辺が存在する」という条件は、「補グラフにおいてグループAとグループBをまたぐ辺が1本もない」ことと同値です。
つまり、補グラフ上で連結している頂点(同じ連結成分に属する頂点)は必ず同じグループに入らなければなりません。補グラフの連結成分の数を $k$ とすると、各成分をどちらのグループに入れるかで $2^k$ 通りの分け方になります。

ComplementGraph cg(n, graph);
auto comps = cg.get_connected_components();
ll k = comps.size(); // 補グラフの連結成分の数

ll ans = 1;
ll mod = 998244353;
// 連結成分ごとに A または B の 2通りの割り当てがある
for (ll i = 0; i < k; ++i) {
  ans = (ans * 2) % mod;
}

// もし「どちらのグループも1人以上必要(空グループ不可)」という制約がある場合、
// 全てA、全てB になる 2 パターンを除外する
ans = (ans - 2 + mod) % mod;

cout << "条件を満たす2グループへの分割方法: " << ans << " 通り" << endl;

Topological Sort

用途: DAG の順序付け

計算量: O(V + E)。 依存関係のある処理順を作るときに使う。DAG以外では適用不可能。

// DAG のトポロジカルソートを行う
// 戻り値: ソートされた頂点配列(閉路がある場合は空の配列を返す)
vector<ll> topological_sort(const vector<vector<ll>>& G) {
    ll n = G.size();
    vector<ll> indeg(n, 0);
    for (ll i = 0; i < n; i++) {
        for (ll to : G[i]) indeg[to]++;
    }

    queue<ll> q;
    for (ll i = 0; i < n; i++) {
        if (indeg[i] == 0) q.push(i);
    }

    vector<ll> ord;
    while (!q.empty()) {
        ll v = q.front(); q.pop();
        ord.push_back(v);
        for (ll to : G[v]) {
            if (--indeg[to] == 0) q.push(to);
        }
    }
    
    // 閉路がある場合は要素数が n にならない
    if (ord.size() != n) return {};
    return ord;
}

注意: グラフ上に「頂点 $u$ から頂点 $v$ への有向辺($u \to v$)」が存在する場合、ord 配列の中では必ず $u$ が $v$ より手前に配置される。

▼ 使い方と応用パターン

基本の使い方(依存関係の解決と閉路判定)
ll n = 4;
vector<vector<ll>> G(n);
// 依存関係: 0->1, 0->2, 1->3, 2->3
G[0].push_back(1);
G[0].push_back(2);
G[1].push_back(3);
G[2].push_back(3);

vector<ll> ord = topological_sort(G);

if (ord.empty()) {
    cout << "閉路が存在するため、ソートできません" << endl;
} else {
    for (ll v : ord) {
        cout << v << " ";
    }
    cout << endl; // 例: 0 1 2 3 または 0 2 1 3
}

Connected Components

用途: 連結成分分解(無向グラフ)

計算量: 隣接リストなら O(V + E)。 DFS/BFS を各未訪問頂点から開始して連結成分を列挙する。

// graph の連結成分を求める
// 戻り値 V[i] が i 番目の連結成分に含まれる頂点集合
vector<vector<ll>> connected_components(
    const vector<vector<ll>>& graph) {

    ll n = graph.size();

    vector<bool> seen(n, false);
    vector<vector<ll>> V;

    auto dfs = [&](auto&& self,
                   ll v,
                   vector<ll>& comp) -> void {

        seen[v] = true;
        comp.push_back(v);

        for (ll next : graph[v]) {
            if (seen[next]) continue;
            self(self, next, comp);
        }
    };

    for (ll v = 0; v < n; v++) {
        if (seen[v]) continue;

        vector<ll> comp;
        dfs(dfs, v, comp);

        V.push_back(comp);
    }

    return V;
}

注意: 無向グラフを想定。再帰DFSを使っているため、頂点数が非常に多い場合は iterative DFS や BFS に置き換えると安全。

▼ 使い方と応用パターン

基本の使い方(グラフの連結成分を列挙する)
ll n = 5;
vector<vector<ll>> graph(n);

// グループA: 0-1-2 が連結
graph[0].push_back(1); graph[1].push_back(0);
graph[1].push_back(2); graph[2].push_back(1);

// グループB: 3-4 が連結
graph[3].push_back(4); graph[4].push_back(3);

// 連結成分分解を実行
vector<vector<ll>> comps = connected_components(graph);

cout << "連結成分の数: " << comps.size() << endl; // 2

for (int i = 0; i < comps.size(); i++) {
    cout << "グループ " << i << ": ";
    for (ll v : comps[i]) {
        cout << v << " ";
    }
    cout << endl;
}
// 実行結果例:
// グループ 0: 0 1 2 
// グループ 1: 3 4

Strongly Connected Components (SCC)

用途: 有向グラフの強連結成分分解・閉路検出・DAG化・同値類判定

計算量: O(V + E)。 コサラジュのアルゴリズム(2回のDFS)を用いて有向グラフを強連結成分に分解し、成分を縮約したDAGを構築する。

// SCC (強連結成分分解) の構造体
struct SCC {
    ll n;
    vector<vector<ll>> g, rg;
    
    // 構築後に利用可能なデータ
    vector<ll> comp;          // comp[i]: 頂点 i が属するSCCのグループID (0-indexed, トポロジカル順)
    vector<vector<ll>> scc;   // scc[i]: グループ i に含まれる頂点のリスト
    vector<vector<ll>> dag;   // dag[i]: グループ i から遷移可能なグループIDのリスト (縮約後のDAG)

    vector<ll> order;
    vector<bool> used;

    // 初期化
    SCC(ll n) : n(n), g(n), rg(n), comp(n, -1), used(n, false) {}

    // 有向辺の追加 (u -> v)
    void add_edge(ll u, ll v) {
        g[u].push_back(v);
        rg[v].push_back(u);
    }

    // 1回目のDFS
    void dfs1(ll v) {
        used[v] = true;
        for (ll next : g[v]) {
            if (!used[next]) dfs1(next);
        }
        order.push_back(v);
    }

    // 2回目のDFS
    void dfs2(ll v, ll k) {
        used[v] = true;
        comp[v] = k;
        scc.back().push_back(v);
        for (ll next : rg[v]) {
            if (!used[next]) dfs2(next, k);
        }
    }

    // SCCの構築・DAGの生成 (すべての辺を追加し終えた後に呼ぶ)
    void build() {
        for (ll i = 0; i < n; i++) {
            if (!used[i]) dfs1(i);
        }
        
        used.assign(n, false);
        ll k = 0;
        for (ll i = (ll)order.size() - 1; i >= 0; i--) {
            ll v = order[i];
            if (!used[v]) {
                scc.push_back({});
                dfs2(v, k++);
            }
        }

        // DAGの構築 (SCCで縮約した後のグラフ)
        dag.resize(k);
        for (ll u = 0; u < n; u++) {
            for (ll v : g[u]) {
                ll x = comp[u];
                ll y = comp[v];
                if (x != y) {
                    dag[x].push_back(y);
                }
            }
        }
        
        // DAGの重複辺を削除
        for (ll i = 0; i < k; i++) {
            sort(dag[i].begin(), dag[i].end());
            dag[i].erase(unique(dag[i].begin(), dag[i].end()), dag[i].end());
        }
    }

    // Union-Find的な機能: 頂点 u と v が同じ強連結成分に属するか判定
    bool same(ll u, ll v) {
        return comp[u] == comp[v];
    }
};

▼ 使い方と応用パターン

基本の使い方(構築・同一グループ判定・DAGの利用)
ll N = 6;
SCC scc(N);

// 有向辺を張る
scc.add_edge(0, 1);
scc.add_edge(1, 2);
scc.add_edge(2, 0); // 0-1-2 は互いに行き来可能 (閉路)
scc.add_edge(2, 3);
scc.add_edge(3, 4);
scc.add_edge(4, 5);
scc.add_edge(5, 3); // 3-4-5 も互いに行き来可能

// 必ず最後に build() を呼ぶ
scc.build();

// 1. Union-Find 的な使い方 (互いに行き来可能か判定)
cout << (scc.same(0, 2) ? "Yes" : "No") << endl; // Yes
cout << (scc.same(0, 3) ? "Yes" : "No") << endl; // No

// 2. 頂点が属する成分番号の取得
// 番号はトポロジカルソート順(上流から下流)に振られる
// 葉からDPするときは、番号が大きい順に見れば良い
cout << "頂点3のグループID: " << scc.comp[3] << endl;

// 3. 縮約後のDAGを使った遷移 (DPなどに活用)
// scc.dag[i] には、グループiから直接行けるグループIDが入る
ll group0 = scc.comp[0];
for (ll next_group : scc.dag[group0]) {
    cout << "Group " << group0 << " -> Group " << next_group << endl;
}

Strongly Connected Orientation (強連結な向き付け)

用途: 無向グラフを強連結な有向グラフに向き付ける・可能かの判定

計算量: O(V + E)。 Lowlink(DFS)を用いて橋(bridge)を検出しつつ、木辺を親から子へ、後退辺を子から先祖へ向けることで線形時間で構築します。グラフに橋が含まれる場合や非連結な場合は不可能(false)となります。

// 強連結な向き付けの判定と構築
struct StronglyConnectedOrientation {
    ll V;
    vector<vector<ll>> adj;
    vector<pair<ll, ll>> directed_edges; // 構築された有向辺のリスト
    bool possible;

    StronglyConnectedOrientation(ll n) : V(n), adj(n), possible(true) {}

    // 無向辺の追加
    void add_edge(ll u, ll v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    // 判定と構築の実行
    bool build() {
        vector<ll> ord(V, -1), low(V, -1);
        vector<bool> visited(V, false);
        ll time = 0;
        directed_edges.clear();
        possible = true;

        // DFS
        auto dfs = [&](auto self, ll u, ll p) -> void {
            visited[u] = true;
            ord[u] = low[u] = time++;
            bool parent_skipped = false; // 多重辺対策

            for (ll v : adj[u]) {
                if (v == p && !parent_skipped) {
                    parent_skipped = true;
                    continue;
                }
                if (!visited[v]) { // 木辺
                    directed_edges.push_back({u, v});
                    self(self, v, u);
                    low[u] = min(low[u], low[v]);
                    if (low[v] > ord[u]) {
                        possible = false; // 橋が存在するため不可能
                    }
                } else if (ord[v] < ord[u]) { // 後退辺
                    directed_edges.push_back({u, v});
                    low[u] = min(low[u], ord[v]);
                }
            }
        };

        ll connected_components = 0;
        for (ll i = 0; i < V; ++i) {
            if (!visited[i]) {
                if (connected_components > 0) {
                    possible = false; // グラフが非連結の場合は全体を強連結にできない
                }
                dfs(dfs, i, -1);
                connected_components++;
            }
        }
        
        if (V <= 1) possible = true; // 頂点数1以下の場合は常に可能
        return possible;
    }
};

▼ 使い方と応用パターン

基本の使い方(判定と有向辺の取得)
ll N = 4;
StronglyConnectedOrientation sco(N);

// サイクルを持つグラフを構築(橋がないため可能)
sco.add_edge(0, 1);
sco.add_edge(1, 2);
sco.add_edge(2, 3);
sco.add_edge(3, 0);

if (sco.build()) {
    cout << "強連結な向き付けが可能です!" << endl;
    for (auto [u, v] : sco.directed_edges) {
        cout << u << " -> " << v << endl;
    }
} else {
    cout << "橋または非連結成分が存在するため、不可能です。" << endl;
}

// 橋を追加すると不可能になる
// sco.add_edge(0, 4); // (N=5にして追加した場合)

Euler Tour

用途: 木の平坦化・部分木クエリ・LCA(最小共通祖先)・2頂点間距離の高速取得・パスクエリ(括弧列)

計算量: 構築 O(V)。
木構造を配列上の1次元の区間に変換します。部分木の管理用(サイズN)、パスクエリ用の括弧列(サイズ2N)、LCAや距離計算用の一筆書き(サイズ2N-1)を同時に構築する非常に汎用的な構造体です。

struct EulerTour {
    ll n;
    // [1] 部分木クエリ用:頂点vの部分木は半開区間 [ in_time[v], out_time[v] ) に対応 (サイズ N)
    vector<ll> in_time, out_time;
    ll timer_inout;

    // [2] パスクエリ用(括弧列):頂点vに入る時刻 path_in[v] と出る時刻 path_out[v] (サイズ 2N)
    vector<ll> path_in, path_out;
    ll timer_path;

    // [3] LCA用:訪問順の頂点(tour), その深さ(depth), 頂点vがtourに最初に現れるインデックス(first_visit) (サイズ 2N-1)
    vector<ll> tour, depth, first_visit;
    
    // 距離用:根からの距離(重みなし木ならdepthと同じ。重み付きならパスの重み和)
    vector<long long> dist;

    EulerTour(const vector<vector<ll>>& graph, ll root = 0) {
        n = graph.size();
        in_time.assign(n, -1);
        out_time.assign(n, -1);
        path_in.assign(n, -1);
        path_out.assign(n, -1);
        first_visit.assign(n, -1);
        dist.assign(n, 0);
        timer_inout = 0;
        timer_path = 0;
        build(graph, root, -1, 0, 0);
    }

    void build(const vector<vector<ll>>& graph, ll v, ll p, ll d, long long dist_from_root) {
        // 部分木用 (入)
        in_time[v] = timer_inout++;
        
        // パスクエリ用 (入)
        path_in[v] = timer_path++;

        // LCA用 (入)
        first_visit[v] = tour.size();
        tour.push_back(v);
        depth.push_back(d);
        
        dist[v] = dist_from_root;

        for (ll next : graph[v]) {
            if (next == p) continue;
            // ※辺に重みがあるグラフの場合は、引数を変えて dist_from_root + cost にする
            build(graph, next, v, d + 1, dist_from_root + 1);
            
            // LCA用 (戻り)
            tour.push_back(v);
            depth.push_back(d);
        }
        
        // 部分木用 (出) ※時間は進めない
        out_time[v] = timer_inout;
        
        // パスクエリ用 (出) ※時間は進める
        path_out[v] = timer_path++;
    }
};

▼ 使い方(部分木クエリ / パスクエリ / LCA / 距離)

部分木クエリ(ある頂点以下の総和など)
// 構築 (根を0とする)
EulerTour et(graph, 0);

ll v = 2; // 部分木を調べたい頂点
ll l = et.in_time[v];
ll r = et.out_time[v];

// サイズNのセグメント木を用意して、in_timeが指す場所に値を入れていく
// l = in_time, r = out_time として、区間 [l, r) の値を処理することで頂点vの部分木全体に対する操作を O(log N) で計算できる。
パスクエリ(括弧列・根からのパスの重み和)
// 構築 (根を0とする)
EulerTour et(graph, 0);

// パスクエリ用(括弧列)のサイズは 2*N です。
// セグメント木やBIT(別途用意)をサイズ 2*N で初期化します。
BIT<long long> bit(2 * et.n);

// [更新] 頂点 v の重みを w 増加させる場合
ll v = 3;
long long w = 10;
// 入る時に +w、出る時に -w を加算することで、他の枝に入った分の値が相殺されます。
bit.add(et.path_in[v], w);
bit.add(et.path_out[v], -w);

// [取得] 根(0)から頂点 u までのパス上の頂点の重み和を取得
ll u = 5;
// 0 から path_in[u] までの累積和を取る(半開区間なら [0, path_in[u] + 1) )
long long root_to_u = bit.query(0, et.path_in[u] + 1);

// [応用] 任意の2頂点 u, v 間のパス上の重み和を求める場合 (LCAを併用)
// ll lca = ...(下のLCAのコード等で取得);
// sum = (根からu) + (根からv) - 2 * (根からlca) + (lca自身の重み);
木のLCA (最小共通祖先) 【前提:汎用スパーステーブル】
EulerTour et(graph, 0);

// 前提: 汎用スパーステーブル (SparseTable) を用いて構築
// 要素を {深さ, 頂点番号} のペアにする
using pii = pair<ll, ll>;
vector<pii> A(et.tour.size());
for (size_t i = 0; i < et.tour.size(); i++) {
    A[i] = {et.depth[i], et.tour[i]};
}

// 深さが最小となるペアを取得する関数を指定して Sparse Table を構築 (前計算 O(N log N))
auto get_min = [](pii a, pii b) { return min(a, b); };
SparseTable<pii, decltype(get_min)> st(A, get_min);

// クエリ O(1): 頂点 u と v の LCA を求める
ll u = 3, v = 5;
ll l = et.first_visit[u];
ll r = et.first_visit[v];
if (l > r) swap(l, r);

// 半開区間 [l, r+1) で区間最小値を取得
ll lca = st.query(l, r + 1).second;
木の二頂点間の距離 【前提:汎用スパーステーブル】
// (※上の LCA のコードで SparseTable st が構築されていることが前提)

ll u = 3, v = 5;
ll l = et.first_visit[u];
ll r = et.first_visit[v];
if (l > r) swap(l, r);

// LCAを求める
ll lca = st.query(l, r + 1).second;

// クエリ O(1): 2頂点間の距離 
// 距離 = (uの根からの距離) + (vの根からの距離) - 2 * (LCAの根からの距離)
long long dist = et.dist[u] + et.dist[v] - 2 * et.dist[lca];

注意:
用途に応じて、適切な配列のインデックスを使用してください。
・部分木クエリの範囲指定には in_time / out_time (サイズN)
・パスクエリの累積和には path_in / path_out (サイズ2N)
・LCAや距離を求める際のスパーステーブルへのクエリには first_visit を使用します。

XOR MST (完全グラフのXOR最小全域木)

用途: N個の非負整数において、任意の2頂点間のコストをビットごとの排他的論理和(A[i] ^ A[j])としたときの完全グラフの最小全域木(MST)を高速に構築する

計算量: O(N log N log(max A))
すべての頂点対に辺を張ると O(N^2) となるが、配列をソートしてTrie木(プレフィックス木)を構築し、Borůvka法のように分割統治とTrie木の探索(常にサイズの小さい方の集合から他方へ最小XOR辺を探す)を行うことで高速化している。要素に重複がある場合もコスト0の辺として自然に処理される。

// XOR MST 本体
// 戻り値: {MSTの重みの総和, 使用した辺(u, v)のリスト}
// ※ a[i] >= 0 を想定しています
pair<ll, vector<pair<int, int>>> xor_mst(const vector<ll>& a) {
    int n = a.size();
    if (n <= 1) return {0, {}};
    
    // 元のインデックスを保持したままソート
    vector<pair<ll, int>> val_idx(n);
    for (int i = 0; i < n; i++) val_idx[i] = {a[i], i};
    sort(val_idx.begin(), val_idx.end());
    
    struct TrieNode {
        int ch[2];
        int l, r; // このノードを通過する値の val_idx におけるインデックスの範囲 [l, r]
        TrieNode() { ch[0] = ch[1] = -1; l = r = -1; }
    };
    vector<TrieNode> trie(1);
    
    // 値の最大値から必要なビット数を判定
    int max_bit = 29;
    for (int i = 0; i < n; i++) {
        if (a[i] >= (1LL << 30)) max_bit = 59;
    }
    
    // Trie木の構築
    for (int i = 0; i < n; i++) {
        int u = 0;
        if (trie[u].l == -1) trie[u].l = i;
        trie[u].r = i;
        for (int bit = max_bit; bit >= 0; bit--) {
            int b = (val_idx[i].first >> bit) & 1;
            if (trie[u].ch[b] == -1) {
                trie[u].ch[b] = trie.size();
                trie.emplace_back();
                trie.back().l = i;
            }
            u = trie[u].ch[b];
            trie[u].r = i;
        }
    }
    
    ll total_weight = 0;
    vector<pair<int, int>> mst_edges;
    
    // 分割統治でMSTの辺を決定
    auto dfs = [&](auto& self, int u, int bit) -> void {
        if (bit < 0) {
            // 完全一致する重複要素同士はコスト0で結ぶ
            for (int i = trie[u].l; i < trie[u].r; i++) {
                mst_edges.push_back({val_idx[i].second, val_idx[i+1].second});
            }
            return;
        }
        
        int lc = trie[u].ch[0];
        int rc = trie[u].ch[1];
        
        // 子ノードの再帰処理
        if (lc != -1) self(self, lc, bit - 1);
        if (rc != -1) self(self, rc, bit - 1);
        
        // 両方の子ノードが存在する場合、2つのグループを繋ぐ最小の辺を探す
        if (lc != -1 && rc != -1) {
            ll min_w = 4e18; // 十分大きな値
            pair<int, int> best_edge = {-1, -1};
            
            int len_l = trie[lc].r - trie[lc].l + 1;
            int len_r = trie[rc].r - trie[rc].l + 1;
            
            // Trie木を辿って val との XOR が最小になる要素を探索
            auto find_min = [&](int search_u, ll val) -> pair<ll, int> {
                int curr = search_u;
                for (int b = bit - 1; b >= 0; b--) {
                    int v_bit = (val >> b) & 1;
                    if (trie[curr].ch[v_bit] != -1) {
                        curr = trie[curr].ch[v_bit];
                    } else {
                        curr = trie[curr].ch[v_bit ^ 1];
                    }
                }
                return {val ^ val_idx[trie[curr].l].first, val_idx[trie[curr].l].second};
            };
            
            // 要素数が少ない方のグループを全探索 (マージテク・Borůvka法の応用)
            if (len_l <= len_r) {
                for (int i = trie[lc].l; i <= trie[lc].r; i++) {
                    auto best = find_min(rc, val_idx[i].first);
                    if (best.first < min_w) {
                        min_w = best.first;
                        best_edge = {val_idx[i].second, best.second};
                    }
                }
            } else {
                for (int i = trie[rc].l; i <= trie[rc].r; i++) {
                    auto best = find_min(lc, val_idx[i].first);
                    if (best.first < min_w) {
                        min_w = best.first;
                        best_edge = {val_idx[i].second, best.second};
                    }
                }
            }
            
            total_weight += min_w;
            mst_edges.push_back(best_edge);
        }
    };
    
    dfs(dfs, 0, max_bit);
    return {total_weight, mst_edges};
}

▼ 使い方

基本的な使い方 (MSTの構築と重み・辺の取得)
int main() {
    // 頂点数 N
    int N = 5;
    // N個の頂点の値 A_i
    vector<ll> A = {1, 2, 3, 4, 5};

    // XOR MST を構築
    auto [mst_weight, mst_edges] = xor_mst(A);

    cout << "MSTの総コスト: " << mst_weight << "\n";
    cout << "結ばれた辺 (頂点インデックス):\n";
    for (auto [u, v] : mst_edges) {
        cout << u << " - " << v << "\n";
    }
    
    return 0;
}
重複する値が含まれる場合 (そのまま適用可能)
int main() {
    int N = 5;
    // 全く同じ値が複数含まれるケース
    // Manhattan MSTと異なり、事前処理による重複除去は不要です
    vector<ll> A = {7, 7, 7, 10, 10};

    // アルゴリズム内で重複値は自動的に「コスト0の辺」として連結されます
    auto [mst_weight, mst_edges] = xor_mst(A);

    cout << "MSTの総コスト: " << mst_weight << "\n"; // 総コストは 7^10 のみとなる
    
    // 必要なら辺もそのまま利用できます
    for (auto [u, v] : mst_edges) {
        cout << u << " - " << v << " (cost " << (A[u] ^ A[v]) << ")\n";
    }

    return 0;
}

Triangle Count (次数の偏りを利用した $K_3$ 探索)

用途: 一般の無向グラフにおいて、長さ3の閉路(三角形)の総数を高速に数え上げる。頂点数 $N, M \le 10^5$ のような巨大なグラフにも対応する。

計算量: O(M √M)
単純に3重ループで探すと O(N^3) となるが、すべての無向辺を「次数の小さい頂点から大きい頂点へ(次数が同じなら頂点番号の小さい方へ)」というルールで有向辺に変換することで、どの頂点も出次数が高々 O(√M) に抑えられるという強烈な性質(グラフの縮退度)を利用。各有向辺 u → v について、共通の行き先 w を探すことで、全体 O(M √M) で三角形を数え上げる。

// Triangle Count 本体
// 戻り値: グラフに含まれる三角形(長さ3の閉路)の総数
long long triangle_count(int n, const vector<pair<int, int>>& edges) {
    // 1. 各頂点の次数を計算
    vector<int> deg(n, 0);
    for (auto [u, v] : edges) {
        deg[u]++;
        deg[v]++;
    }

    // 2. 次数小 → 次数大 (同じなら頂点番号小 → 大) へと向き付けした隣接リストを構築
    vector<vector<int>> adj(n);
    for (auto [u, v] : edges) {
        bool dir_u_to_v = false;
        if (deg[u] < deg[v]) {
            dir_u_to_v = true;
        } else if (deg[u] == deg[v] && u < v) {
            dir_u_to_v = true;
        }

        if (dir_u_to_v) {
            adj[u].push_back(v);
        } else {
            adj[v].push_back(u);
        }
    }

    long long count = 0;
    // 頂点uの隣接頂点を O(1) で判定するための配列
    vector<bool> is_adj(n, false); 

    // 3. 各頂点を始点として三角形を探索
    for (int u = 0; u < n; u++) {
        // u の行き先をマーク
        for (int v : adj[u]) is_adj[v] = true;
        
        // u → v → w と辿り、u → w の辺があるか確認
        for (int v : adj[u]) {
            for (int w : adj[v]) {
                if (is_adj[w]) {
                    count++;
                }
            }
        }
        
        // 次の頂点に備えてマークをリセット(毎回配列を初期化するより高速)
        for (int v : adj[u]) is_adj[v] = false;
    }

    return count;
}

▼ 使い方

基本的な使い方 (三角形のカウント)
int main() {
    // 頂点数 N
    int N = 5;
    // 無向辺のリスト
    vector<pair<int, int>> edges = {
        {0, 1}, {1, 2}, {2, 0}, // 三角形1: (0, 1, 2)
        {0, 2}, {2, 3}, {3, 0}, // 三角形2: (0, 2, 3)
        {3, 4}                  // どこにも属さない辺
    };

    // 三角形の総数を数え上げる
    long long cnt = triangle_count(N, edges);

    cout << "三角形の数: " << cnt << "\n"; // 出力: 2
    
    return 0;
}
【備考】密グラフ向けの bitset による高速化 (O(N^3 / 64))

頂点数 $N$ が $2000 \sim 3000$ 程度までの場合は、隣接行列を std::bitset で持ち、AND演算を使うことで定数倍を 1/64 にする手法が非常に高速です。問題の制約によって使い分けてください。

#include <bitset>

int main() {
    int N = 5;
    vector<pair<int, int>> edges = /* ... */;

    // Nの上限が固定の場合は直接サイズを指定 (例: 2000)
    const int MAX_N = 2000;
    vector<bitset<MAX_N>> adj(N);

    // 無向グラフの隣接行列を構築
    for (auto [u, v] : edges) {
        adj[u].set(v);
        adj[v].set(u);
    }

    long long count = 0;
    for (int i = 0; i < N; i++) {
        for (int j = i + 1; j < N; j++) {
            if (adj[i][j]) {
                // i と j の両方に隣接する頂点(共通の隣接頂点)をまとめてカウント
                count += (adj[i] & adj[j]).count();
            }
        }
    }
    // 1つの三角形につき3本の辺それぞれでカウントされてしまうため3で割る
    count /= 3;

    cout << "三角形の数: " << count << "\n";
    return 0;
}

Max Flow / Min cut (atcoder::mf_graph)

用途: 最大流の算出、最小カットの復元、燃やす埋める問題(Project Selection Problem)など

計算量: Dinic法により $O(V^2 E)$(実際の競プロのケースでは理論値より非常に高速に動作する)。
内部は #include <atcoder/maxflow> と using namespace atcoder; が必要。

using ll = long long;

int main() {
    int N = 5;
    
    // 1. 宣言: 頂点数 N を指定。容量の型に ll を指定します。
    mf_graph<ll> graph(N);

    // 2. 辺の追加: add_edge(from, to, capacity)
    // 頂点は 0-indexed で指定します。戻り値として辺のID(int)が返ります。
    graph.add_edge(0, 1, 10);
    graph.add_edge(0, 2, 2);
    graph.add_edge(1, 2, 6);
    graph.add_edge(1, 3, 6);
    graph.add_edge(2, 4, 5);
    graph.add_edge(3, 4, 8);

    // 3. 最大流 (Max Flow): flow(s, t)
    // 頂点 0 から 4 への最大流量を計算して返します。
    ll max_flow = graph.flow(0, 4);

    // 4. 最小カット (Min Cut): min_cut(s)
    // 必ず flow(s, t) を実行した「後」に呼び出します。
    // 残余グラフ上で始点 s から到達可能な頂点が true (S側)、そうでない頂点が false (T側) となる配列を返します。
    vector<bool> is_S = graph.min_cut(0);

    // 5. 各辺の状態取得: edges() や get_edge(i) (任意)
    // auto edges = graph.edges();
    // for (auto e : edges) {
    //     // e.from, e.to, e.cap, e.flow にアクセスして復元等に利用できます。
    // }
}

▼ 応用テンプレート(実戦向け)

燃やす埋める問題 (Project Selection Problem)

「Aを選ぶかBを選ぶか」の2択にかかるコストや、「Aを選んだのにBを選ばないと罰金」といった制約を最小カットに帰着します。利益を最大化する場合は「あらかじめ全ての利益を受け取ったと仮定し、そこから失う最小コストを引く(最大利益 = 全利益の和 - 最小カット)」という形に変形するのが定石です。

using ll = long long;
const ll INF = 1e18; // 依存関係(絶対切れない辺)に使う無限大

int main() {
    int N = 3; // アイテム数など
    int S = N, T = N + 1; 
    mf_graph<ll> graph(N + 2); // 頂点数は S と T を含めるため +2

    // 【パターン1】単体のコスト設定
    // 頂点 i を「S側」にするとコスト A かかり、「T側」にするとコスト B かかる場合
    // ⇒ S -> i に容量 B、i -> T に容量 A を張る
    // (※ i がS側になると S->i の辺は切れないため、i->T の辺(容量A)が切断される = コストAを払う)
    ll cost_A = 10, cost_B = 20;
    graph.add_edge(S, 0, cost_B); 
    graph.add_edge(0, T, cost_A); 

    // 【パターン2】罰金 (ペナルティ)
    // i を S側、j を T側にした場合のみペナルティ P がかかる場合
    // ⇒ i -> j に容量 P を張る
    ll P = 100;
    graph.add_edge(0, 1, P);

    // 【パターン3】依存関係 (必須条件)
    // i を S側にするなら、絶対に j も S側にしなければならない場合
    // ⇒ i -> j に容量 INF を張る (罰金 INF と同義)
    graph.add_edge(1, 2, INF);
    
    // 失うコストの最小値(最小カット)が求まる
    ll min_cost = graph.flow(S, T);
    
    // 利益の最大値を求めたい場合:
    // (事前に足し合わせた得られる可能性のある全利益) - min_cost
}

Bipartite Matching (atcoder::mf_graph)

用途: 二部グラフの最大マッチング・最小頂点被覆・最大独立集合の算出および復元 (ACL利用)

計算量: 単位ネットワーク上のDinic法となるため、マッチングは $O(E \sqrt{V})$。復元は最小カット(残余グラフ上のBFS)を行うため $O(V+E)$。
内部は #include <atcoder/maxflow> と using namespace atcoder; が必要です。ACLの最大流ライブラリをラップし、復元処理をまとめた構造体ベースの実装です。

// ===== Bipartite Matching (ACL mf_graph) =====
struct BipartiteMatching {
    ll L_size, R_size;
    ll S, T;
    mf_graph<ll> graph;

    // L側の頂点は 0 ~ L-1, R側の頂点は L ~ L+R-1 に割り当て、SとTを追加
    BipartiteMatching(ll L, ll R) : L_size(L), R_size(R), S(L + R), T(L + R + 1), graph(L + R + 2) {
        // S から L 側へ容量 1 の辺を張る
        for (ll i = 0; i < L; ++i) graph.add_edge(S, i, 1);
        // R 側から T へ容量 1 の辺を張る
        for (ll j = 0; j < R; ++j) graph.add_edge(L_size + j, T, 1);
    }

    void add_edge(ll u, ll v) {
        // L側の u から R側の v へ容量 1 の辺を張る
        graph.add_edge(u, L_size + v, 1);
    }

    // 最大マッチングのサイズを返す
    ll solve() {
        return graph.flow(S, T);
    }

    // 復元処理: (最大マッチングの辺, 左の最小頂点被覆, 右の最小頂点被覆, 左の最大独立集合, 右の最大独立集合)
    // ※必ず solve() を実行した後に呼ぶこと
    tuple<vector<pair<ll, ll>>, vector<ll>, vector<ll>, vector<ll>, vector<ll>> restore() {
        vector<pair<ll, ll>> edges;
        for (auto e : graph.edges()) {
            // SやTが絡まない辺で、流量が1になっているものがマッチングに使われた辺
            if (e.from != S && e.to != T && e.flow == 1) {
                edges.push_back({e.from, e.to - L_size});
            }
        }

        // 最小カット (min_cut) を用いて頂点被覆と独立集合を復元
        vector<bool> is_S = graph.min_cut(S);
        vector<ll> mvc_l, mvc_r, mis_l, mis_r;

        for (ll i = 0; i < L_size; ++i) {
            if (!is_S[i]) mvc_l.push_back(i); // L側: 最小カットでT側に属していれば被覆
            else mis_l.push_back(i);          // L側: 最小カットでS側に属していれば独立集合
        }
        for (ll i = 0; i < R_size; ++i) {
            if (is_S[L_size + i]) mvc_r.push_back(i); // R側: 最小カットでS側に属していれば被覆
            else mis_r.push_back(i);                  // R側: 最小カットでT側に属していれば独立集合
        }
        return {edges, mvc_l, mvc_r, mis_l, mis_r};
    }
};

仕組みとコマンドの解説:
ACLには二部マッチング専用の構造体はないため、atcoder::mf_graph を用いて始点 $S$ と終点 $T$ を追加して解いています。
・graph.flow(S, T): $S$ から $T$ への最大流を流し、それが最大マッチングのサイズになります。
・graph.edges(): ACLの関数で全辺の情報を取得します。流量 (flow) が 1 になっている辺を取り出すことで、マッチングのペアを復元できます。
・graph.min_cut(S): 残余グラフ上で $S$ から到達可能な頂点 (true)、そうでない頂点 (false) を判定します。Kőnigの定理に基づいて、この配列を用いて最小頂点被覆および最大独立集合を復元しています。

▼ 使い方と応用パターン

基本の使い方(マッチングの計算と各集合の復元)
// ① 初期化: 左側の頂点数 L と 右側の頂点数 R を指定する
ll L_size = 3;
ll R_size = 4;
BipartiteMatching bm(L_size, R_size);

// ② 辺の追加: (左の頂点番号, 右の頂点番号) ※0-indexedを想定
bm.add_edge(0, 0);
bm.add_edge(0, 1);
bm.add_edge(1, 1);
bm.add_edge(1, 2);
bm.add_edge(2, 3);

// ③ 計算: 最大マッチングのサイズを取得
ll max_matching = bm.solve();
cout << "最大マッチング数: " << max_matching << "\n";

// ④ 復元: (最大マッチングの辺, Lの最小頂点被覆, Rの最小頂点被覆, Lの最大独立集合, Rの最大独立集合)
// ※ C++17以降の構造化束縛を使うと便利です
auto [edges, mvc_l, mvc_r, mis_l, mis_r] = bm.restore();

// --- 出力して確認 ---
cout << "\n[最大マッチングのペア]" << "\n";
for (auto [u, v] : edges) {
    cout << "L" << u << " - R" << v << "\n";
}

cout << "\n[最小頂点被覆] (合計 " << mvc_l.size() + mvc_r.size() << " 個)" << "\n";
cout << "L側: ";
for (ll u : mvc_l) cout << u << " ";
cout << "\nR側: ";
for (ll v : mvc_r) cout << v << " ";
cout << "\n";

cout << "\n[最大独立集合] (合計 " << mis_l.size() + mis_r.size() << " 個)" << "\n";
cout << "L側: ";
for (ll u : mis_l) cout << u << " ";
cout << "\nR側: ";
for (ll v : mis_r) cout << v << " ";
cout << "\n";

最大反鎖 / 最小パス被覆 (Dilworthの定理)

用途: 半順序集合 (DAG) の最大反鎖長 (互いに到達不可能な頂点集合の最大サイズ) の算出と復元

計算量: 内部の推移的閉包計算 (Warshall-Floyd) が $O(V^3)$、二部マッチング構築・算出が $O(V^2 \sqrt{V})$。
前提として、上述の BipartiteMatching 構造体が利用できる環境を想定しています。

// ===== 最大反鎖 / 最小パス被覆 (Dilworthの定理) =====
// 前提: BipartiteMatching
struct Dilworth {
    ll N;
    vector<vector<bool>> reachable;
    BipartiteMatching bm;

    // 頂点数 N のDAGを指定して初期化
    Dilworth(ll n) : N(n), reachable(n, vector<bool>(n, false)), bm(n, n) {}

    // u から v への有向辺(直接の遷移)を追加する
    void add_edge(ll u, ll v) {
        reachable[u][v] = true;
    }

    // 最大反鎖のサイズ(=最小パス被覆の数)を返す
    ll solve() {
        // 1. Warshall-Floyd法で推移的閉包(到達可能性)を計算
        for (ll k = 0; k < N; ++k) {
            for (ll i = 0; i < N; ++i) {
                for (ll j = 0; j < N; ++j) {
                    if (reachable[i][k] && reachable[k][j]) {
                        reachable[i][j] = true;
                    }
                }
            }
        }

        // 2. 到達可能なすべての頂点対について、二部マッチング用の辺を追加
        for (ll i = 0; i < N; ++i) {
            for (ll j = 0; j < N; ++j) {
                if (reachable[i][j]) {
                    bm.add_edge(i, j);
                }
            }
        }

        // 3. 最大マッチングを計算し、Dilworthの定理より最大反鎖サイズを求める
        return N - bm.solve();
    }

    // 最大反鎖の復元: (反鎖を構成する頂点番号のリスト)
    // ※必ず solve() を実行した後に呼ぶこと
    vector<ll> restore() {
        auto [edges, mvc_l, mvc_r, mis_l, mis_r] = bm.restore();
        
        vector<bool> in_mis_l(N, false), in_mis_r(N, false);
        for (ll u : mis_l) in_mis_l[u] = true;
        for (ll v : mis_r) in_mis_r[v] = true;

        vector<ll> antichain;
        // L側の最大独立集合 と R側の最大独立集合 の両方に含まれる頂点が、最大反鎖の要素となる
        for (ll i = 0; i < N; ++i) {
            if (in_mis_l[i] && in_mis_r[i]) {
                antichain.push_back(i);
            }
        }
        return antichain;
    }
};

仕組みと解説:
・Dilworthの定理: 有限半順序集合において、「最大反鎖の要素数(互いに到達できない頂点の最大数)」は「最小パス被覆のパス数」に等しいという定理です。
・ライブラリ内部で Warshall-Floyd法 を用いて各頂点の到達可能性(推移的閉包)を計算します。これにより、元のグラフに間接的な辺しかなくても正確に半順序集合を構築できます。
・各頂点 $i$ を 出口 $L_i$ と 入口 $R_i$ に分割し、二部グラフを作ります。$u$ から $v$ へ到達可能なら $L_u$ から $R_v$ へ辺を張り、最大マッチングを求めます。
・全体の頂点数 $V$ から最大マッチング数を引いた $V - \text{max_matching}$ が最小パス被覆(=最大反鎖のサイズ)となります。
・復元は、分割した二部グラフにおける L側とR側の両方の最大独立集合に含まれる頂点(どちらの最小頂点被覆にも属さない頂点)を抽出することで実現しています。

▼ 使い方と応用パターン

基本の使い方(辺の追加から算出・復元まで)
// ① 頂点数 N の構造体を初期化
ll N = 5;
Dilworth dil(N);

// ② 直接の有向辺(遷移)を追加する
dil.add_edge(0, 1);
dil.add_edge(1, 2);
dil.add_edge(3, 4);
// ※内部で推移的閉包を取るため、0->2 の到達可能性も自動的に計算されます

// ③ 最大反鎖サイズの計算
ll max_antichain_size = dil.solve();
cout << "最大反鎖のサイズ (最小パス被覆数): " << max_antichain_size << "\n";

// ④ 最大反鎖を構成する頂点の復元
vector<ll> antichain = dil.restore();
cout << "最大反鎖を構成する頂点: ";
for (ll v : antichain) {
    cout << v << " ";
}
cout << "\n";

General Graph Matching (Edmonds' Blossom Algorithm)

用途: 一般グラフ(奇数長の閉路を含むグラフ)の最大マッチングの算出および復元

計算量: Edmondsの花アルゴリズム(Blossom Algorithm)を用いており、計算量は $O(V^3)$ です。頂点数 $V \le 500$ 程度の制約で高速に動作します。
二部グラフとは異なり、一般グラフには奇数長の閉路が存在するためACLの最大流では解けません。この構造体は奇数閉路(花・Blossom)を縮約しながら増大道を探索し、最大マッチングを求めます。

// ===== General Graph Matching (Edmonds' Blossom Algorithm) =====
// 前提: using ll = long long; が定義されていること
struct GeneralGraphMatching {
    ll N;
    vector<vector<ll>> adj;
    vector<ll> match;
    vector<ll> p, base, q;
    vector<bool> used, blossom;

    // 頂点数 n で初期化 (0-indexed)
    GeneralGraphMatching(ll n) : N(n), adj(n), match(n, -1) {}

    // 無向辺を追加
    void add_edge(ll u, ll v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    // 共通祖先(LCA)を見つける内部関数
    ll lca(ll a, ll b) {
        bool match_a = false;
        vector<bool> trace(N, false);
        while (true) {
            a = base[a];
            trace[a] = true;
            if (match[a] == -1) break;
            a = p[match[a]];
        }
        while (true) {
            b = base[b];
            if (trace[b]) return b;
            b = p[match[b]];
        }
    }

    // 奇数長の閉路(花)をマークして縮約する内部関数
    void mark_path(ll v, ll b, ll children) {
        while (base[v] != b) {
            blossom[base[v]] = blossom[base[match[v]]] = true;
            p[v] = children;
            children = match[v];
            v = p[match[v]];
        }
    }

    // 増大道を探索する内部関数
    ll find_path(ll root) {
        used.assign(N, false);
        p.assign(N, -1);
        base.resize(N);
        for (ll i = 0; i < N; ++i) base[i] = i;
        used[root] = true;
        
        ll qh = 0, qt = 0;
        q.resize(N);
        q[qt++] = root;

        while (qh < qt) {
            ll v = q[qh++];
            for (ll to : adj[v]) {
                if (base[v] == base[to] || match[v] == to) continue;
                if (to == root || (match[to] != -1 && p[match[to]] != -1)) {
                    ll curbase = lca(v, to);
                    blossom.assign(N, false);
                    mark_path(v, curbase, to);
                    mark_path(to, curbase, v);
                    for (ll i = 0; i < N; ++i) {
                        if (blossom[base[i]]) {
                            base[i] = curbase;
                            if (!used[i]) {
                                used[i] = true;
                                q[qt++] = i;
                            }
                        }
                    }
                } else if (p[to] == -1) {
                    p[to] = v;
                    if (match[to] == -1) return to;
                    to = match[to];
                    used[to] = true;
                    q[qt++] = to;
                }
            }
        }
        return -1;
    }

    // 最大マッチングのペア数を返す
    ll solve() {
        ll ans = 0;
        for (ll i = 0; i < N; ++i) {
            if (match[i] == -1) {
                ll v = find_path(i);
                if (v != -1) {
                    ans++;
                    while (v != -1) {
                        ll pv = p[v], ppv = match[pv];
                        match[v] = pv;
                        match[pv] = v;
                        v = ppv;
                    }
                }
            }
        }
        return ans;
    }

    // マッチングに使われた辺のリストを返す
    // ※必ず solve() を実行した後に呼ぶこと
    vector<pair<ll, ll>> restore() {
        vector<pair<ll, ll>> edges;
        for (ll i = 0; i < N; ++i) {
            if (i < match[i]) {
                edges.push_back({i, match[i]});
            }
        }
        return edges;
    }
};

仕組みとコマンドの解説:
・find_path(root): 未マッチの頂点を根としてBFSを行い、増大道(交互道)を探します。途中で奇数長の閉路を発見した場合は lca() で花托(ベース)を特定し、mark_path() で閉路全体を1つの頂点のように縮約して探索を続行します。
・solve(): 全ての頂点から増大道がなくなるまで探索を繰り返し、マッチング数をインクリメントします。
・restore(): 最終的な配列 match には、各頂点 i のペアとなる頂点番号が格納されています。重複を避けるため i < match[i] のペアのみを抽出して返します。

▼ 使い方と応用パターン

基本の使い方(一般グラフのマッチングと復元)
// ① 初期化: 頂点数 N を指定する (二部グラフと異なり1つのサイズのみ)
ll N = 6;
GeneralGraphMatching gm(N);

// ② 無向辺の追加 (0-indexed)
// ※ 奇数長の閉路(0-1-2)を含む一般グラフ
gm.add_edge(0, 1);
gm.add_edge(1, 2);
gm.add_edge(2, 0);
gm.add_edge(2, 3);
gm.add_edge(3, 4);
gm.add_edge(4, 5);

// ③ 計算: 最大マッチングのサイズを取得
ll max_matching = gm.solve();
cout << "最大マッチング数: " << max_matching << "\n";

// ④ 復元: マッチングに使われた辺のリストを取得
vector<pair<ll, ll>> edges = gm.restore();

// --- 出力して確認 ---
cout << "\n[マッチングしたペア]\n";
for (auto [u, v] : edges) {
    cout << u << " - " << v << "\n";
}

Kruskal / クラスカル法 (最小/最大全域木)

用途: グラフから最小全域木(MST)または最大全域木を構築し、その隣接リストと重みの総和を出力する

計算量: O(E log E) または O(E log V)。 辺のコストによるソートが計算量の支配的な部分となる。

// 前提: UnionFind

// 辺を表す構造体
struct Edge {
    ll u, v, cost;
};

// クラスカル法 (最小/最大全域木)
// 引数 get_max を true にすると最大全域木を構築する
// 戻り値: {MSTの重みの総和, MSTを構成する隣接リスト}
pair<ll, vector<vector<pair<ll, ll>>>> kruskal(ll V, vector<Edge>& edges, bool get_max = false) {
    // 1. 辺をコスト順にソート
    sort(edges.begin(), edges.end(), [&](const Edge& a, const Edge& b) {
        if (get_max) return a.cost > b.cost; // 最大全域木: 降順
        return a.cost < b.cost;              // 最小全域木: 昇順
    });

    UnionFind uf(V);
    ll total_cost = 0;
    vector<vector<pair<ll, ll>>> mst_adj(V); // MSTの隣接リスト (to, cost)

    // 2. コストの条件に合う辺から順に確認
    for (const auto& e : edges) {
        // 閉路ができない場合のみ辺を採用 (異なるグループなら繋ぐ)
        if (uf.unite(e.u, e.v)) {
            total_cost += e.cost;
            // 無向グラフとして隣接リストに辺を追加
            mst_adj[e.u].push_back({e.v, e.cost});
            mst_adj[e.v].push_back({e.u, e.cost});
        }
    }

    return {total_cost, mst_adj};
}

▼ 使い方と応用パターン

最小全域木を求める(デフォルト)
ll V = 4; // 頂点数
// グラフの辺リスト {u, v, cost}
vector<Edge> edges = {
    {0, 1, 1}, {0, 2, 4}, {1, 2, 2}, {2, 3, 3}, {1, 3, 5}
};

// クラスカル法を実行 (最小全域木)
auto [total_cost, mst_adj] = kruskal(V, edges);

cout << "最小コスト: " << total_cost << endl; // 6 (辺0-1, 1-2, 2-3 が選ばれる)
最大全域木を求める
ll V = 4; // 頂点数
// グラフの辺リスト {u, v, cost}
vector<Edge> edges = {
    {0, 1, 1}, {0, 2, 4}, {1, 2, 2}, {2, 3, 3}, {1, 3, 5}
};

// 第3引数 get_max を true にして実行
auto [total_cost, mst_adj] = kruskal(V, edges, true);

cout << "最大コスト: " << total_cost << endl; // 12 (辺1-3, 0-2, 2-3 が選ばれる)

グラフ上の汎用ダブリング (Generic Doubling)

用途: 状態遷移(移動)の一意性がある問題において、$K$ 回移動後の行き先や、移動途中に得られる値の累積(和・最大値など)を高速に取得

計算量: 構築 O(N log K)、クエリ O(log K)。 Functional Graphに対して用いることができる。モノイドを指定することで値の累積も同時に扱える。

template <typename T = long long>
struct Doubling {
    ll n;
    ll max_log;
    vector<vector<ll>> next;
    vector<vector<T>> sum;
    T identity;
    using F = function<T(T, T)>;
    F op;
    bool has_value;

    // 移動のみ(値の累積なし)の場合のコンストラクタ
    Doubling(ll n, long long max_k) : n(n), has_value(false) {
        max_log = (max_k <= 0) ? 1 : 64 - __builtin_clzll(max_k);
        next.assign(max_log, vector<ll>(n, -1));
    }

    // 値の累積を行う場合のコンストラクタ (単位元 id と 二項演算 op を指定)
    Doubling(ll n, long long max_k, T id, F op)
        : n(n), identity(id), op(op), has_value(true) {
        max_log = (max_k <= 0) ? 1 : 64 - __builtin_clzll(max_k);
        next.assign(max_log, vector<ll>(n, -1));
        sum.assign(max_log, vector<T>(n, id));
    }

    // 1ステップの遷移を設定 (u から v へ、重み/値 w)
    void set_next(ll u, ll v, T w = T()) {
        next[0][u] = v;
        if (has_value) sum[0][u] = w;
    }

    // テーブル構築 (すべての遷移を設定した後に呼び出す)
    void build() {
        for (ll k = 0; k < max_log - 1; k++) {
            for (ll i = 0; i < n; i++) {
                if (next[k][i] != -1) {
                    next[k + 1][i] = next[k][next[k][i]];
                    if (has_value) {
                        sum[k + 1][i] = op(sum[k][i], sum[k][next[k][i]]);
                    }
                }
            }
        }
    }

    // start から k 回移動した後の {行き先, 累積値} を取得
    pair<ll, T> query(ll start, long long k) const {
        ll cur = start;
        T res = identity;
        for (ll i = 0; i < max_log; i++) {
            if ((k >> i) & 1) {
                if (cur == -1) break;
                if (has_value) res = op(res, sum[i][cur]);
                cur = next[i][cur];
            }
        }
        return {cur, res};
    }
};

▼ 使い方(目的別の初期化方法)

移動のみ($K$ 回後の行き先を求める場合)
using ll = long long;
ll N = 5;
ll K = 1000000000000LL;

// 頂点数 N, 最大移動回数 K で初期化
Doubling<ll> db(N, K);

db.set_next(0, 1);
db.set_next(1, 2);
db.set_next(2, 0);
db.set_next(3, 4);
db.set_next(4, 3);
db.build();

auto [dest, _] = db.query(0, K); // 0 から K 回移動後の頂点
値の加算(Heavy Buckets 等の水の累積和)
using ll = long long;
ll N = 3;
ll max_T = 1000000000LL;

// 単位元 0, 演算 (a + b) で初期化
auto add_op = [](ll a, ll b) { return a + b; };
Doubling<ll> db(N, max_T, 0LL, add_op);

// set_next(u, v, w): u から v に移動する際コスト w (水など) が加算される
db.set_next(0, 1, 10);
db.set_next(1, 2, 20);
db.set_next(2, 0, 30);
db.build();

auto [dest, total_water] = db.query(0, 5); // 0から5回移動後の行き先と加算合計値
区間最大値の保持(移動途中の最大コストを求める場合)
using ll = long long;
ll N = 4;
ll K = 100;

constexpr ll INF = 1e18;
// 単位元 -INF, 演算 max(a, b) で初期化
auto max_op = [](ll a, ll b) { return max(a, b); };
Doubling<ll> db(N, K, -INF, max_op);

db.set_next(0, 1, 5);
db.set_next(1, 2, 12);
db.set_next(2, 3, 3);
db.build();

auto [dest, max_cost] = db.query(0, 2); // 0から2回移動中の最大コスト -> 12

注意: set_next で全ての遷移を設定し終えてから、必ず build() を呼ぶこと。移動先が存在しない・画面外へ出るような場合は、行き先に -1 を設定しておくと query の実行中に自動的に移動を中断できる。

行列木定理 (Matrix Tree Theorem)

用途: グラフにおける全域木の個数を計算する。複雑な制約を持つ全域木の数え上げ問題において、ラプラシアン行列の行列式計算に帰着させる。

計算量: 構築 O(E)、クエリ(行列式計算) O(V^3)。 ラプラシアン行列(次数行列 $-$ 隣接行列)の任意の余因子が行列式の値(=全域木の数)に等しくなる性質を利用します。値が大きくなるため、通常は modint などの剰余体上で計算します。

template <typename T>
struct MatrixTreeTheorem {
    int n;
    vector<vector<T>> laplacian;

    MatrixTreeTheorem(int n) : n(n), laplacian(n, vector<T>(n, 0)) {}

    // 無向辺 u-v を追加 (重み w、デフォルトは 1)
    // 多重辺の場合は w に追加する辺の本数を指定する
    void add_edge(int u, int v, T w = 1) {
        laplacian[u][u] += w;
        laplacian[v][v] += w;
        laplacian[u][v] -= w;
        laplacian[v][u] -= w;
    }

    // 全域木の数を計算(0行目と0列目を取り除いた余因子を計算)
    T build() {
        if (n <= 1) return 1;
        int m = n - 1;
        vector<vector<T>> mat(m, vector<T>(m));
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < m; ++j) {
                mat[i][j] = laplacian[i + 1][j + 1];
            }
        }

        T res = 1;
        for (int i = 0; i < m; ++i) {
            int pivot = -1;
            for (int j = i; j < m; ++j) {
                if (mat[j][i] != 0) { // 浮動小数の場合は abs(mat[j][i]) > 1e-9 などで判定
                    pivot = j;
                    break;
                }
            }
            if (pivot == -1) return 0; // 行列式が0 = 全域木が存在しない
            
            if (i != pivot) {
                swap(mat[i], mat[pivot]);
                res = -res; // 行の入れ替えで符号反転
            }
            
            // 掃き出し法
            T inv = (T)1 / mat[i][i];
            for (int j = i + 1; j < m; ++j) {
                T factor = mat[j][i] * inv;
                for (int k = i; k < m; ++k) {
                    mat[j][k] -= mat[i][k] * factor;
                }
            }
            res *= mat[i][i];
        }
        return res;
    }
};

▼ 使い方(目的別の初期化方法)

基本的な無向グラフの全域木の数え上げ
#include <atcoder/modint>
using mint = atcoder::modint998244353;

int N = 4;
MatrixTreeTheorem<mint> mtt(N);

// 辺を追加 (0-1, 1-2, 2-3, 3-0, 0-2)
mtt.add_edge(0, 1);
mtt.add_edge(1, 2);
mtt.add_edge(2, 3);
mtt.add_edge(3, 0);
mtt.add_edge(0, 2);

// 全域木の個数を計算
mint ans = mtt.build();
cout << ans.val() << endl; // 出力: 8
多重辺が含まれる場合の数え上げ
#include <atcoder/modint>
using mint = atcoder::modint998244353;

int N = 3;
MatrixTreeTheorem<mint> mtt(N);

// 頂点 0 と 1 の間に 3 本の辺がある場合
mtt.add_edge(0, 1, 3);
// 頂点 1 と 2 の間に 2 本の辺がある場合
mtt.add_edge(1, 2, 2);

mint ans = mtt.build();
cout << ans.val() << endl; // 出力: 6 (0-1の選び方3通り × 1-2の選び方2通り)

注意: 上記のコードは要素が atcoder::modint などのように四則演算や逆元の割り算がオーバーロードされており、!= 0 での比較が可能であることを前提に書かれています。もし double などの実数型で用いる場合は、ゼロ判定部分を abs(mat[j][i]) > 1e-9 のような許容誤差を含んだものに変更してください。また、有向グラフの根付き木を数える場合は、「入次数からラプラシアン行列を構築し、根となる頂点に対応する行と列を取り除いて行列式を求める」ように add_edge を変更する必要があります。

区間・平面

尺取法 (Two Pointers)

用途: 条件を満たす連続部分列の探索・数え上げ

計算量: O(N)。「区間が広がるほど条件を満たしやすい(または満たしにくい)」といった単調性がある問題に適用できる。

パターン1: 左端固定・右端拡張(半開区間 [left, right)で管理する。区間の長さは right - left )

ll res = 0;
ll right = 0;
// sumやcountなどの区間の状態を管理する変数をここに用意

for (ll left = 0; left < n; ++left) {
    // rightを進めても条件を満たす間(または満たすまで)、rightを伸ばす
    while (right < n && /* 条件を満たすか判定 */) {
        // 状態の更新 (例: sum += a[right])
        ++right;
    }

    // この時点で [left, right) が確定。必要な処理を行う
    res += (right - left); // 例: 条件を満たす部分列の数を足す

    // 次のループ(left++)に向けて状態を正しく保つ
    if (right == left) {
        // 区間が空の場合は right も一緒に進める(状態は更新しない)
        ++right;
    } else {
        // 左端の要素を区間から除外 (例: sum -= a[left])
    }
}

パターン2: 右端固定・左端縮小(閉区間 [left, right]で管理する。区間の長さは right - left + 1 )

ll res = 0;
ll left = 0;
// sumやcountなどの区間の状態を管理する変数をここに用意

for (ll right = 0; right < n; ++right) {
    // まず右端の要素を問答無用で追加する
    // 状態の更新 (例: sum += a[right])

    // 条件を破ってしまったら、満たすようになるまで左端を縮める
    while (left <= right && /* 条件を満たしていないか判定 */) {
        // 左端の要素を除外 (例: sum -= a[left])
        ++left;
    }

    // この時点で閉区間 [left, right] は確実に条件を満たしている
    res += (right - left + 1); // 例: 閉区間の長さを足す
}

バグらせないために:

  • 「状態の更新(sum += a[x] など)」と「ポインタの移動(++x)」は必ずセットの順序で行う。間に別の処理を挟んだり、逆にしたりすると区間の中身を見失う。
  • 自分が今「半開区間」と「閉区間」のどちらで変数を管理しているかを意識し、区間の長さの計算(+1 が必要かどうか)を間違えないようにする。

区間スケジューリング

用途: 重複しないように選べる区間の最大数の計算

計算量: O(N log N)。 区間を「終端位置」の早い順にソートする貪欲法(Greedy)が最適となる典型問題。

// 区間 {始点, 終点} の配列が与えられたとする
// vector<pair<ll, ll>> intervals(N);

// 終端位置が早い順にソートしたいので、{終点, 始点} のペアにする
vector<pair<ll, ll>> sorted_intervals(N);
rep(i, N) {
    sorted_intervals[i] = {intervals[i].second, intervals[i].first};
}
sort(sorted_intervals.begin(), sorted_intervals.end());

ll ans = 0;
ll current_end_time = 0;

rep(i, N) {
    // 最後に選んだ区間の終端よりも、次の区間の始点が後(または同じ)なら選べる
    if (sorted_intervals[i].second >= current_end_time) {
        ans++;
        current_end_time = sorted_intervals[i].first; // 新しい終端時間を記録
    }
}

注意: 始点でのソートや、区間の短い順でのソートでは最適解が得られない反例が存在する。必ず「終点」でソートすること。

Inversion Number (転倒数)

用途: 配列内の順序が逆転しているペアの数を求める(バブルソートの交換回数など)

計算量: $O(N \log N)$
Fenwick Tree (BIT) + 座標圧縮、またはマージソートを用いることで高速に計算できます。 定数倍が軽く、AtCoder Library (ACL) の fenwick_tree と相性の良い BIT ベースの手法が最も標準的です。

// 転倒数 (Fenwick Tree + 座標圧縮ベース)
// 配列内の要素がどれだけ大きくても、負の値を含んでいても動作します

#include <vector>
#include <algorithm>

template<typename T>
long long inversion_number(const std::vector<T>& A) {
    int n = A.size();
    if (n == 0) return 0;

    // 1. 座標圧縮 (配列の値を 0-indexed のランクに変換)
    std::vector<T> sorted_A = A;
    std::sort(sorted_A.begin(), sorted_A.end());
    sorted_A.erase(std::unique(sorted_A.begin(), sorted_A.end()), sorted_A.end());
    int m = sorted_A.size();

    // 2. Fenwick Tree (BIT) の実装 (内部は 1-indexed)
    std::vector<int> bit(m + 1, 0);
    auto add = [&](int i, int x) {
        for (++i; i <= m; i += i & -i) bit[i] += x;
    };
    auto sum = [&](int i) {
        int s = 0;
        for (; i > 0; i -= i & -i) s += bit[i];
        return s;
    };

    // 3. 転倒数のカウント
    long long inv = 0;
    for (int i = 0; i < n; ++i) {
        // 圧縮後のランクを取得 (0-indexed)
        int rank = std::lower_bound(sorted_A.begin(), sorted_A.end(), A[i]) - sorted_A.begin();
        
        // これまでに出現した要素のうち、自分以下の要素の数を取得
        int count_less_equal = sum(rank + 1);
        
        // (これまでに出現した全要素数 i) - (自分以下の要素数) = (自分より真に大きい要素数)
        inv += i - count_less_equal;
        
        // 自分のランクを BIT に登録
        add(rank, 1);
    }
    return inv;
}

▼ 使い方と別解の実装パターン

具体的な使い方 (main 関数での呼び出し)

関数に vector を渡すだけでそのまま答えが返ってきます。

#include <iostream>

int main() {
    std::vector<long long> A = {40, 10, 30, 50, 20};
    
    // 転倒数を計算
    long long ans = inversion_number(A);
    
    // 出力: 4 (反転ペア: (40,10), (40,30), (40,20), (30,20))
    std::cout << ans << "\n";
    
    return 0;
}
AtCoder Library (ACL) を使う場合

ACLの fenwick_tree を使える環境であれば、よりシンプルに記述できます(座標圧縮の手順は変わりません)。

#include <atcoder/fenwicktree>

template<typename T>
long long inversion_number_acl(const std::vector<T>& A) {
    // 座標圧縮部分は同じため省略 ...
    int m = sorted_A.size();

    atcoder::fenwick_tree<int> fw(m);
    long long inv = 0;
    for (int i = 0; i < n; ++i) {
        int rank = std::lower_bound(sorted_A.begin(), sorted_A.end(), A[i]) - sorted_A.begin();
        inv += i - fw.sum(0, rank + 1);
        fw.add(rank, 1);
    }
    return inv;
}
マージソートによる実装 (座標圧縮不要)

BITを使わない別解です。要素をマージする際に、「右側の配列の要素が左側の配列の要素より先に配置される」とき、左側に残っている要素の数がそのまま転倒数として加算されます。元の配列を直接ソート(破壊的変更)するため、コピーを渡すのが一般的です。

template<typename T>
long long merge_count(std::vector<T>& A) {
    int n = A.size();
    if (n <= 1) return 0;

    std::vector<T> b(A.begin(), A.begin() + n / 2);
    std::vector<T> c(A.begin() + n / 2, A.end());
    
    // 左右それぞれで生じる転倒数
    long long res = merge_count(b) + merge_count(c);
    
    int i = 0, j = 0, k = 0;
    while (i < b.size() && j < c.size()) {
        if (b[i] <= c[j]) {
            A[k++] = b[i++];
        } else {
            // 右側の要素が左側より小さい場合、左側の残りの要素数だけ転倒が発生する
            A[k++] = c[j++];
            res += b.size() - i;
        }
    }
    while (i < b.size()) A[k++] = b[i++];
    while (j < c.size()) A[k++] = c[j++];
    
    return res;
}

// 呼び出し例:
// std::vector<int> A_copy = A;
// long long ans = merge_count(A_copy);

2つの数列を線で結んだときの交点数(転倒数)

用途: 2つの数列A, Bの上段・下段で対応する(同じ値の)要素同士を線分で結んだ際に発生する、線分の交点数を求める。共通しない要素は無視される。

計算量: O(N log N) (Nは結ばれる線の数)。
前処理として各要素の対応位置を O(N) で求めた後、BITを用いて転倒数を O(N log N) で計算する。

// 前提:Fenwick Tree(BIT), using ll = long long;
                
ll calc_intersections(const vector<ll>& A, const vector<ll>& B) {
    // Bの各要素が「Bの何番目にあるか」を記録
    // ※重複要素が含まれる場合、前から順に対応させる(交差を防ぐ)ために queue を使用
    unordered_map<ll, queue<ll>> pos_B;
    for (ll i = 0; i < (ll)B.size(); ++i) {
        pos_B[B[i]].push(i);
    }

    // Aの要素のうち、Bにも存在するものを結ぶ(向かうべきBのインデックス配列 C を作成)
    vector<ll> C;
    for (ll i = 0; i < (ll)A.size(); ++i) {
        if (!pos_B[A[i]].empty()) {
            C.push_back(pos_B[A[i]].front());
            pos_B[A[i]].pop();
        }
    }

    // 配列 C の転倒数を BIT を用いて求める
    BIT bit(B.size());
    ll intersections = 0;
    for (ll i = 0; i < (ll)C.size(); ++i) {
        // i は現在までに追加した要素の総数
        // bit.sum(C[i] + 1) は、これまでに追加した要素のうち C[i] 以下のものの個数
        // その差分が C[i] より大きい要素の個数 = 交差する線の数(転倒数)
        intersections += i - bit.sum(C[i] + 1);
        bit.add(C[i], 1);
    }

    return intersections;
}

注意: 片方の数列にしか存在しない要素は線を引けないため無視して計算されます。重複要素がある場合は queue の性質により、互いに交差しない平行な線が自動的に選択されます。

▼ 使い方と応用パターン

1. 配列Aを配列Bにする最小スワップ回数

要素群が完全に一致しているかを事前にソート等で判定してからこの関数を呼ぶことで、従来通り「隣接要素の最小スワップ回数」として利用できます。

vector<ll> A = {2, 3, 1, 4};
vector<ll> B = {4, 1, 3, 2};

// 一致判定
vector<ll> sorted_A = A, sorted_B = B;
sort(sorted_A.begin(), sorted_A.end());
sort(sorted_B.begin(), sorted_B.end());

if (sorted_A != sorted_B) {
    cout << "-1" << endl; // 構成要素が異なるため変換不能
} else {
    // 例題の配列なら 6 になる
    cout << calc_intersections(A, B) << endl; 
}
2. 重複要素を含む数列のスワップ・交点数

内部で queue を利用しているため、重複要素がある場合でも交点を増やさない(=最短スワップになる)紐付けが自動で行われます。意識せずそのまま渡すだけで利用可能です。

vector<ll> A = {1, 1, 2, 2, 3};
vector<ll> B = {3, 2, 1, 2, 1};

// 重複があっても自動で最短になるような紐付けが行われる
cout << calc_intersections(A, B) << endl; 
3. 応用:隣接要素の +1, -1 操作の最小回数

「隣接する要素を選び、片方に+1、もう片方に-1する」という操作で配列Aを配列Bに変換する最小操作回数も、この関数に帰着できます。

考え方: 各バケツ(配列のインデックス)の仕切りを仮想的な「0」、中に入っている値(ボール)を仮想的な「1」の連続とみなします。
たとえば A = {2, 0, 1} なら、ボール2個・仕切り・ボール0個・仕切り・ボール1個なので、変換後の仮想配列は {1, 1, 0, 0, 1} になります。
隣接要素の +1, -1 操作とは、実はこの「仮想的な1(ボール)と0(仕切り)の隣接スワップ」と完全に等価です。

// A = {2, 0, 1} を B = {1, 1, 1} にする最小操作回数

auto expand = [](const vector<ll>& V) {
    vector<ll> res;
    for (ll i = 0; i < (ll)V.size(); ++i) {
        for (ll j = 0; j < V[i]; ++j) res.push_back(1); // ボールを '1'
        if (i + 1 < (ll)V.size()) res.push_back(0);     // 仕切りを '0'
    }
    return res;
};

// A' = {1, 1, 0, 0, 1}
// B' = {1, 0, 1, 0, 1}
vector<ll> expanded_A = expand(A);
vector<ll> expanded_B = expand(B);

// 仮想配列同士の最小スワップ回数が、元の配列の最小操作回数と一致する
cout << calc_intersections(expanded_A, expanded_B) << endl;
4. 応用例:数直線上の移動とすれ違い(空きマスを含めた転倒数への帰着)

問題設定: N人が数直線上の相異なる座標 $S_i$ におり、$x \pm 1$ への移動(移動先に人がいれば同時に移動・実質スワップ)を繰り返して各人を目的の座標 $T_i$ に移動させる最小操作回数を求める。(参照:ABC479 - F)

解法: 人の移動(+1, -1)と、人が重なったときのスワップという操作は、「人」と「空きマス」が並んだ巨大な数列の隣接スワップ操作と完全に同一視できます。
数直線全体を配列とみなし、人のいる座標には「人のID (1〜N)」、人がいない座標にはすべて「0 (空きマス)」を配置した初期状態の配列 $A$ と、目標状態の配列 $B$ を作成します。
あとは「人 $i$ は $A$ の位置から $B$ の位置へ」「空きマスは $A$ の $i$ 番目から $B$ の $i$ 番目へ」紐付けて転倒数を求めるだけです。本ライブラリの calc_intersections は queue により重複要素(大量の 0)を交差しないよう前から順に自動で紐づけるため、生成した配列をそのまま渡すだけで答えが求まります。

// S: 初期座標の配列, T: 目標座標の配列 (1-indexed で 人1〜N とする)
// ※座標の最大値が小さい場合の概念的な実装です。
// 座標が 10^9 等と大きい場合は、空きマス(0の連続)の転倒数への寄与をまとめる等、座標圧縮の工夫が必要になります。

ll solve_number_line_moves(const vector<ll>& S, const vector<ll>& T) {
    ll n = S.size();
    
    // 座標の最大値をカバーする範囲を決定
    ll max_x = 0;
    for(ll x : S) max_x = max(max_x, x);
    for(ll x : T) max_x = max(max_x, x);

    // 数直線全体を表現する配列(0 は空きマス)
    vector<ll> A(max_x + 1, 0);
    vector<ll> B(max_x + 1, 0);

    // 人の配置(IDは1〜N)
    for(int i = 0; i < n; ++i) A[S[i]] = i + 1;
    for(int i = 0; i < n; ++i) B[T[i]] = i + 1;

    // 「人」同士、および「人と空きマス」の交点数(転倒数)を一括計算
    // 重複する '0' はライブラリ内部の queue により自動的に前から順に対応付けられる
    return calc_intersections(A, B);
}

累積和 (Cumulative Sum)

用途: 静的な配列の区間和の高速取得

計算量: 前計算 O(N)、クエリ O(1)。 途中で値の更新がない場合に、任意の区間の合計を定数時間で求める。

// 元の配列 a (サイズ N)
// vector<ll> a(N);

// 累積和配列 S を構築 (サイズは N + 1)
vector<ll> s(N + 1, 0);
rep(i, N) {
    s[i + 1] = s[i] + a[i];
}

// 区間 [left, right) の和を O(1) で求める関数
// 例: a[1] + a[2] + a[3] なら left = 1, right = 4
auto query = [&](ll left, ll right) {
    return s[right] - s[left];
};

注意: 配列のサイズを N + 1 にし、1-indexedのように管理することで、left = 0 の場合などの配列外参照(Out of bounds)を防ぎ、分岐をなくせる。

N次元累積和 / 区間和 (一般化 SOS DP)

用途: 任意の各次元サイズ D_0, D_1... を持つ N 次元配列の累積和および任意の超立方体領域の区間和を計算する

計算量: 構築 O(N K)、区間和クエリ O(2^N) (K は配列の総要素数 \(\prod D_i\))
前提: 1次元に平坦化された配列 a と、各次元のサイズを格納した vector<ll> D を受け取る。累積和の構築後、包除原理を用いて X <= p <= Y に挟まれた超立方体の要素の総和を高速に計算できる。

// 各次元のサイズ D を指定する N 次元累積和 / 逆変換 (差分)
template <typename T>
void n_dimensional_prefix_sum(vector<T>& a, const vector<ll>& D, bool invert = false) {
    ll n = a.size();
    ll N_dim = D.size();
    // ストライド (D_0, D_0*D_1, ...) を前計算
    vector<ll> stride(N_dim + 1, 1);
    for (ll d = 0; d < N_dim; d++) {
        stride[d + 1] = stride[d] * D[d];
    }
    
    if (!invert) {
        for (ll d = 0; d < N_dim; d++) {
            for (ll i = 0; i < n; i++) {
                if ((i / stride[d]) % D[d] > 0) {
                    a[i] += a[i - stride[d]];
                }
            }
        }
    } else {
        for (ll d = 0; d < N_dim; d++) {
            for (ll i = n - 1; i >= 0; i--) {
                if ((i / stride[d]) % D[d] > 0) {
                    a[i] -= a[i - stride[d]];
                }
            }
        }
    }
}

// 累積和済みの配列 a に対して、各次元で X[i] <= p[i] <= Y[i] を満たす超立方体領域の和を求める
template <typename T>
T get_nd_range_sum(const vector<T>& a, const vector<ll>& D, const vector<ll>& X, const vector<ll>& Y) {
    ll N_dim = D.size();
    vector<ll> stride(N_dim + 1, 1);
    for (ll d = 0; d < N_dim; d++) {
        stride[d + 1] = stride[d] * D[d];
    }

    T ans = 0;
    // 包除原理: 2^N_dim 個の状態 (各次元について上限 Y[i] か 下限 X[i]-1 を選ぶか) を探索
    for (ll mask = 0; mask < (1LL << N_dim); mask++) {
        ll idx = 0;
        bool out_of_bounds = false;
        ll lower_count = 0; // 下限 (X[i]-1) を採用した次元の数

        for (ll i = 0; i < N_dim; i++) {
            ll p = 0;
            if ((mask >> i) & 1) {
                p = Y[i];
            } else {
                p = X[i] - 1;
                lower_count++;
            }

            // 下限が 0 未満になった場合は、その領域の和は 0 なのでスキップ
            if (p < 0) {
                out_of_bounds = true;
                break;
            }
            if (p >= D[i]) p = D[i] - 1;
            
            idx += p * stride[i];
        }

        if (out_of_bounds) continue;

        // 下限を採用した回数が奇数なら引き、偶数なら足す
        if (lower_count % 2 == 1) {
            ans -= a[idx];
        } else {
            ans += a[idx];
        }
    }
    return ans;
}

▼ 使い方

実践的な使い方 (各次元のサイズが異なる3次元配列の場合)
// 各次元のサイズ (例: H=10, W=15, D=20 の3次元配列)
vector<ll> D = {10, 15, 20};
ll K = 10 * 15 * 20; // 総要素数 3000
vector<long long> a(K, 0);

// N次元配列の各次元の座標の vector から、1次元配列上のインデックスを計算するラムダ式
auto get_idx = [&](const vector<ll>& pos) {
    ll idx = 0;
    ll stride = 1;
    for (ll d = 0; d < D.size(); d++) {
        idx += pos[d] * stride;
        stride *= D[d];
    }
    return idx;
};

// 元の配列に値をセットする
a[get_idx({2, 3, 4})] = 10;
a[get_idx({1, 3, 4})] = 5;
a[get_idx({9, 14, 19})] = 100;

// N次元累積和を実行 (a は累積和配列になる)
n_dimensional_prefix_sum(a, D);

// 【区間和の取得例】
// x は 1~2, y は 2~4, z は 3~5 の範囲にある要素の総和を取得する
vector<ll> X = {1, 2, 3}; // 下限
vector<ll> Y = {2, 4, 5}; // 上限

long long sum = get_nd_range_sum(a, D, X, Y);
cout << "区間和: " << sum << "\n"; 
// 上記のセットされた値のうち {2,3,4} と {1,3,4} が範囲に含まれるため 15 が出力される

// 逆変換 (差分) を実行して元の配列に戻す
n_dimensional_prefix_sum(a, D, true);

いもす法 (Imos Method)

用途: 多数の区間に対する加算の一括処理・重複区間のカウント

計算量: O(N + Q)。 クエリごとに加算をシミュレートせず、端点の記録だけ行い、最後に累積和を取る手法(オフライン区間加算)。

// 長さ M のタイムラインや配列を想定
vector<ll> table(M + 1, 0);

// Q回の区間加算: 区間 [l, r) に +1 をする
rep(i, Q) {
    ll l, r;
    cin >> l >> r;
    table[l]++;     // 始点に +1
    table[r]--;     // 終点の直後(半開区間の終端)に -1
}

// 最後に累積和をとることで、各点の実際の値を復元
rep(i, M) {
    table[i + 1] += table[i];
}

// 最終的に table[i] にはインデックス i に重なっている区間の数が格納される

注意: 問題文で与えられる区間が [l, r] のような「閉区間」の場合、マイナスする位置は r + 1 になる点に注意(半開区間 [l, r+1) として扱う)。二次元に拡張することも可能。

汎用スパーステーブル (Generic Sparse Table)

用途: 静的な配列に対する、任意の区間最小値・最大値(RMQ)やGCDの高速な取得

計算量: 構築 O(N log N)、クエリ O(1)。 冪等性(idemopotency)を持つ演算($x \circ x = x$ となる演算。min, max, gcdなど)をコンストラクタで自由に指定して高速に処理できます。

template <typename T, typename F>
struct SparseTable {
    ll n;
    vector<vector<T>> table;
    vector<ll> log_table;
    F op; // 適用する二項演算 (min, max, gcd など)

    SparseTable() {}
    SparseTable(const vector<T>& v, F op) : op(op) {
        init(v);
    }

    // テーブルの初期化・構築
    void init(const vector<T>& v) {
        n = v.size();
        ll log_n = 0;
        while ((1LL << log_n) <= n) {
            log_n++;
        }
        
        table.assign(log_n, vector<T>(n));
        for (ll i = 0; i < n; i++) {
            table[0][i] = v[i];
        }
        
        for (ll i = 1; i < log_n; i++) {
            ll len = 1LL << (i - 1);
            for (ll j = 0; j + len * 2 <= n; j++) {
                table[i][j] = op(table[i - 1][j], table[i - 1][j + len]);
            }
        }
        
        log_table.assign(n + 1, 0);
        for (ll i = 2; i <= n; i++) {
            log_table[i] = log_table[i >> 1] + 1;
        }
    }

    // 半開区間 [l, r) に対するクエリ処理
    T query(ll l, ll r) {
        ll len = r - l;
        ll k = log_table[len];
        return op(table[k][l], table[k][r - (1LL << k)]);
    }
};

▼ 使い方(min / max それぞれの指定方法)

区間最小値 (Range Minimum Query) の場合
using ll = long long;
vector<ll> A = {3, 1, 4, 1, 5, 9, 2, 6};

// ラムダ式で min を指定して構築
auto get_min = [](ll a, ll b) { return min(a, b); };
SparseTable<ll, decltype(get_min)> st_min(A, get_min);

ll min_val = st_min.query(4, 8); // {5, 9, 2, 6} の最小値 -> 2
区間最大値 (Range Maximum Query) の場合
using ll = long long;
vector<ll> A = {3, 1, 4, 1, 5, 9, 2, 6};

// ラムダ式で max を指定して構築
auto get_max = [](ll a, ll b) { return max(a, b); };
SparseTable<ll, decltype(get_max)> st_max(A, get_max);

ll max_val = st_max.query(0, 4); // {3, 1, 4, 1} の最大値 -> 4

注意: クエリは半開区間 [l, r) で指定する点に注意。また、データの更新(値の書き換え)が発生する動的な問題に対しては、セグメント木(Segment Tree)など別のデータ構造を使用する必要がある。

区間管理 (Range Set)

用途: 区間の追加・削除・結合の自動管理、任意の範囲内の被覆要素数の取得、区間の走査

計算量: 追加クエリ 均し O(log Q)、取得クエリ O(K + log Q) [Kは重なる区間の数]。
setを用いて区間を管理する。重複や隣接する区間を自動的にマージし、イテレータの無効化バグを防ぐ構造体に隠蔽している。

struct RangeSet {
    set<pair<ll, ll>> st; // {右端, 左端} で保持
    ll black_count = 0;   // 全体で黒く塗られているマスの総数

    // イテレータ (range-based for ループ用)
    auto begin() const { return st.begin(); }
    auto end() const { return st.end(); }
    auto rbegin() const { return st.rbegin(); }
    auto rend() const { return st.rend(); }

    // 閉区間 [l, r] を追加し、重なる・隣接する区間をマージする
    void insert(ll l, ll r) {
        auto it = st.lower_bound({l - 1, -1});
        while (it != st.end() && it->second <= r + 1) {
            ll prev_r = it->first;
            ll prev_l = it->second;
            l = min(l, prev_l);
            r = max(r, prev_r);
            black_count -= (prev_r - prev_l + 1);
            it = st.erase(it);
        }
        st.insert({r, l});
        black_count += (r - l + 1);
    }

    // 閉区間 [l, r] の範囲内で黒く塗られているマスの個数を返す
    ll query(ll l, ll r) const {
        ll res = 0;
        // l以上の右端を持つ最初の区間を検索
        auto it = st.lower_bound({l, -1});
        // 区間の左端が r 以下である限りループ
        while (it != st.end() && it->second <= r) {
            ll cur_r = min(r, it->first);
            ll cur_l = max(l, it->second);
            res += (cur_r - cur_l + 1);
            it++;
        }
        return res;
    }
};

▼ 使い方

基本的な使い方 (追加、取得、forでの走査)
RangeSet rs;

rs.insert(1, 3); 
rs.insert(6, 10);

// [2, 7] の範囲の黒マスの数
cout << rs.query(2, 7) << "\n"; // 4

// すべての区間を for ループで順に走査する
// ※内部で {右端, 左端} の順で保持しているため、構造化バインディングで [r, l] で受け取ると便利です。
for (auto [r, l] : rs) {
    cout << "[" << l << ", " << r << "]\n";
}
// 出力:
// [1, 3]
// [6, 10]

注意: query 関数は重なっている区間の数を $K$ とすると $O(K + \log Q)$ かかる。std::set のみで管理しているため、取得クエリが大量にある場合はセグメント木などへの変更を検討すること。

長方形管理 (面積・周長・最大重なり数)

用途: 複数個の長方形群における和集合の面積、全体の周長、および最大重なり数の計算

計算量: 構築および全計算 $O(N \log N)$ [$N$は長方形の数]。
平面走査 (Plane Sweep) と、連結成分数や最大被覆数を管理する拡張セグメント木を用いて高速に計算します。X軸方向とY軸方向の2回走査を行うことで、計算量のオーダーを落とすことなく複雑な周長の計算も正確に行います。

struct RectangleUnion {
    struct Rect { long long l, r, d, u; };
    vector<Rect> rects;

    // 半開区間 [l, r) × [d, u) として長方形を追加 (l <= x < r, d <= y < u)
    void add_rectangle(long long l, long long r, long long d, long long u) {
        if (l < r && d < u) {
            rects.push_back({l, r, d, u});
        }
    }

    struct Result {
        long long area;
        long long perimeter;
        int max_overlap;
    };

    // 追加された長方形群の和集合の面積、周長、最大重なり数を計算する
    Result calculate() const {
        if (rects.empty()) return {0, 0, 0};

        // 平面走査を実行するラムダ式 (X方向・Y方向を切り替えて再利用)
        auto solve_scan = [](const vector<Rect>& rs, bool is_x) -> Result {
            struct Event {
                long long pos, l, r;
                int val;
                bool operator<(const Event& other) const {
                    return pos < other.pos;
                }
            };
            vector<Event> events;
            vector<long long> coords;
            for (const auto& r : rs) {
                long long pos1 = is_x ? r.l : r.d;
                long long pos2 = is_x ? r.r : r.u;
                long long range1 = is_x ? r.d : r.l;
                long long range2 = is_x ? r.u : r.r;
                events.push_back({pos1, range1, range2, 1});
                events.push_back({pos2, range1, range2, -1});
                coords.push_back(range1);
                coords.push_back(range2);
            }

            sort(coords.begin(), coords.end());
            coords.erase(unique(coords.begin(), coords.end()), coords.end());
            int n = coords.size() - 1;
            if (n <= 0) return {0, 0, 0};

            // セグメント木のデータ構造
            vector<int> count(4 * n, 0);       // 区間全体の被覆回数
            vector<long long> length(4 * n, 0); // 被覆されている長さ
            vector<int> comp(4 * n, 0);        // 被覆されている連結成分数
            vector<bool> l_act(4 * n, false);  // 左端が被覆されているか
            vector<bool> r_act(4 * n, false);  // 右端が被覆されているか
            vector<int> max_val(4 * n, 0);     // 区間内の最大被覆数

            auto update_node = [&](int node, int nl, int nr) {
                if (count[node] > 0) {
                    length[node] = coords[nr] - coords[nl];
                    comp[node] = 1;
                    l_act[node] = true;
                    r_act[node] = true;
                    max_val[node] = count[node];
                    if (nr - nl > 1) {
                        max_val[node] += max(max_val[2 * node + 1], max_val[2 * node + 2]);
                    }
                } else {
                    if (nr - nl == 1) {
                        length[node] = 0;
                        comp[node] = 0;
                        l_act[node] = false;
                        r_act[node] = false;
                        max_val[node] = 0;
                    } else {
                        length[node] = length[2 * node + 1] + length[2 * node + 2];
                        comp[node] = comp[2 * node + 1] + comp[2 * node + 2];
                        if (r_act[2 * node + 1] && l_act[2 * node + 2]) {
                            comp[node]--; // 境界で連結成分が繋がる場合
                        }
                        l_act[node] = l_act[2 * node + 1];
                        r_act[node] = r_act[2 * node + 2];
                        max_val[node] = max(max_val[2 * node + 1], max_val[2 * node + 2]);
                    }
                }
            };

            auto apply = [&](auto& self, int l, int r, int val, int node, int nl, int nr) -> void {
                if (l >= nr || r <= nl) return;
                if (l <= nl && nr <= r) {
                    count[node] += val;
                } else {
                    int mid = nl + (nr - nl) / 2;
                    self(self, l, r, val, 2 * node + 1, nl, mid);
                    self(self, l, r, val, 2 * node + 2, mid, nr);
                }
                update_node(node, nl, nr);
            };

            sort(events.begin(), events.end());
            auto get_idx = [&](long long v) {
                return lower_bound(coords.begin(), coords.end(), v) - coords.begin();
            };

            long long area = 0;
            long long perimeter_part = 0;
            int max_overlap = 0;
            long long prev_pos = events[0].pos;

            for (size_t i = 0; i < events.size(); ) {
                long long curr_pos = events[i].pos;
                long long delta = curr_pos - prev_pos;
                
                area += length[0] * delta;
                perimeter_part += (long long)comp[0] * 2LL * delta; // 進行方向と平行な境界線の長さ
                
                while (i < events.size() && events[i].pos == curr_pos) {
                    int l_idx = get_idx(events[i].l);
                    int r_idx = get_idx(events[i].r);
                    if (l_idx < r_idx) {
                        apply(apply, l_idx, r_idx, events[i].val, 0, 0, n);
                    }
                    i++;
                }
                max_overlap = max(max_overlap, max_val[0]);
                prev_pos = curr_pos;
            }
            return Result{area, perimeter_part, max_overlap};
        };

        Result res_x = solve_scan(rects, true);  // X軸方向走査 (面積, 横枠の周長, 最大重なり)
        Result res_y = solve_scan(rects, false); // Y軸方向走査 (面積, 縦枠の周長, 最大重なり)

        return Result{
            res_x.area, 
            res_x.perimeter + res_y.perimeter, // 縦横の境界線の和が全体の周長
            res_x.max_overlap
        };
    }
};

▼ 使い方

基本的な使い方 (追加、面積・周長・最大重なりの計算)
RectangleUnion ru;

// 長方形 [l, r) × [d, u) を追加 (l <= x < r, d <= y < u)
ru.add_rectangle(0, 5, 0, 5); 
ru.add_rectangle(2, 7, 2, 7);

auto res = ru.calculate();

// すべての長方形の計算結果を出力
cout << "Area: " << res.area << "\n";               // 出力: 41
cout << "Perimeter: " << res.perimeter << "\n";     // 出力: 28
cout << "Max Overlap: " << res.max_overlap << "\n"; // 出力: 2

注意: 追加する長方形の境界は [l, r](閉区間)ではなく、[l, r)(半開区間)として扱われます($l \le x \lt r$、$d \le y \lt u$)。点の被覆数ではなく「実際の空間の面積や長さ」を計算する仕様になっているため、長方形の幅 r - l と高さ u - d がそのまま評価されます。

Cartesian Tree

用途: 区間最小値/最大値による配列の二分木化、ヒストグラム内の最大長方形、区間に関する木DP

計算量: 構築 O(N)。
配列を「最小値(最大値)で左右に分割する」という構造を、スタックを用いて線形時間で構築します。

// Cartesian Tree の構造体
struct CartesianTree {
    ll root;             // 根となるインデックス
    vector<ll> p;        // p[i] は i の親(親がない場合は -1)
    vector<ll> l, r;     // l[i], r[i] はそれぞれ i の左の子、右の子(ない場合は -1)

    // 構造体の初期化 O(N)
    // is_min = true の場合: 区間の最小値が根になる(Min-Cartesian Tree)
    // is_min = false の場合: 区間の最大値が根になる(Max-Cartesian Tree)
    CartesianTree(const vector<ll>& a, bool is_min = true) {
        ll n = a.size();
        p.assign(n, -1);
        l.assign(n, -1);
        r.assign(n, -1);
        
        vector<ll> st; // 右側のスパイン(根から右の子を辿ったパス)を管理するスタック
        for (ll i = 0; i < n; ++i) {
            ll last = -1;
            // Min木なら a[st.back()] > a[i] の間ポップ
            // Max木なら a[st.back()] < a[i] の間ポップ
            while (!st.empty() && (is_min ? a[st.back()] > a[i] : a[st.back()] < a[i])) {
                last = st.back();
                st.pop_back();
            }
            if (!st.empty()) {
                r[st.back()] = i;
                p[i] = st.back();
            }
            if (last != -1) {
                l[i] = last;
                p[last] = i;
            }
            st.push_back(i);
        }
        root = st.empty() ? -1 : st[0];
    }
};

▼ 使い方と応用パターン

基本の使い方(最小値バージョン・最大値バージョン)
vector<ll> A = {9, 3, 7, 1, 8, 12, 10, 20, 15, 18, 5};

// 1. Min-Cartesian Tree (デフォルト)
CartesianTree min_ct(A); // または CartesianTree min_ct(A, true);
cout << "Min Tree Root: " << min_ct.root << endl; // 1 (A[3] = 1 なのでインデックス3が根)
cout << "Left child of root: " << min_ct.l[min_ct.root] << endl; // 3より左の最小値のインデックス

// 2. Max-Cartesian Tree
CartesianTree max_ct(A, false);
cout << "Max Tree Root: " << max_ct.root << endl; // 20 (A[7] = 20 なのでインデックス7が根)
Tree DP (木DP) への応用例の骨組み
vector<ll> A = { ... };
CartesianTree ct(A);

// DFSで部分木(=ある最小値を分割点としたときの区間)に関する情報を計算する
auto dfs = [&](auto& dfs, ll v) -> ll {
    if (v == -1) return 0;
    
    ll left_val = dfs(dfs, ct.l[v]);
    ll right_val = dfs(dfs, ct.r[v]);
    
    // v を根とする部分木(元の配列でのある連続区間に対応)の答えをマージ
    return left_val + right_val + A[v]; 
};

ll ans = dfs(dfs, ct.root);

ヒストグラムの凸包 (多機能版 / Convex Hull Trick 対応)

用途: 逐次追加されるヒストグラムの凸包を管理し、直結点の取得や面積計算、二分探索による最大値クエリ(CHT)をサポートする

計算量: 点の追加はならし $O(1)$。最大値クエリは二分探索により $O(\log K)$ ($K$ は凸包の頂点数)。
単なる凸包の取得にとどまらず、DPの最適化(Convex Hull Trick)や幾何的な性質の動的計算にすぐ使える実戦的な実装です。

#include <vector>
#include <optional>
#include <algorithm>

using namespace std;

struct Point {
    long long x, y;
};

class AdvancedHistogramConvexHull {
private:
    vector<Point> upper_hull;

    // 3点の外積。負なら右回り(時計回り)、正なら左回り(反時計回り)
    long long cross_product(Point p1, Point p2, Point p3) const {
        return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
    }

public:
    // 点を追加し、直結した(削除されずに残った最も右の)点を返す
    // 最初の点の追加時など、直結点がない場合は std::nullopt を返す
    std::optional<Point> add_point(long long x, long long h) {
        Point p = {x, h};
        
        while (upper_hull.size() >= 2) {
            Point p1 = upper_hull[upper_hull.size() - 2];
            Point p2 = upper_hull[upper_hull.size() - 1];
            if (cross_product(p1, p2, p) >= 0) {
                upper_hull.pop_back(); // 凹んでいるので削除
            } else {
                break;
            }
        }
        
        std::optional<Point> connected_point = std::nullopt;
        if (!upper_hull.empty()) {
            connected_point = upper_hull.back(); // 削除されずに残った最も右の点
        }
        
        upper_hull.push_back(p);
        return connected_point;
    }

    // 上側凸包を構成する点の一覧(左から右への順序)を取得
    const vector<Point>& get_upper_hull() const {
        return upper_hull;
    }

    // 全体の凸包を構成する点の一覧を反時計回り(CCW)で取得
    vector<Point> get_full_hull() const {
        if (upper_hull.empty()) return {};
        
        vector<Point> hull;
        // 内部では上側の点しか管理していないので、底面 (y=0) をここで補完する
        hull.push_back({upper_hull.front().x, 0}); // 左下の点
        if (upper_hull.back().x != upper_hull.front().x) {
            hull.push_back({upper_hull.back().x, 0}); // 右下の点
        }
        
        // 上側凸包を右から左へ辿ることで、反時計回りに閉じる
        for (int i = upper_hull.size() - 1; i >= 0; --i) {
            hull.push_back(upper_hull[i]);
        }
        return hull;
    }

    // 便利機能1: 現在の凸包の面積の 2倍 を返す (整数のまま計算するため)
    // 実面積が必要な場合は 2.0 で割る
    long long get_area_x2() const {
        vector<Point> hull = get_full_hull();
        long long area = 0;
        int n = hull.size();
        for (int i = 0; i < n; ++i) {
            area += (hull[i].x * hull[(i + 1) % n].y - hull[(i + 1) % n].x * hull[i].y);
        }
        return abs(area);
    }

    // 便利機能2: 傾き `slope` の直線 y = slope * x + b が凸包と接するときの最大切片 b を求める
    // Convex Hull Trick (CHT) における最大値検索 (二分探索により O(log N))
    long long get_max_intercept(long long slope) const {
        if (upper_hull.empty()) return 0;
        
        int left = 0, right = upper_hull.size() - 1;
        while (right - left >= 3) {
            int m1 = left + (right - left) / 3;
            int m2 = right - (right - left) / 3;
            long long v1 = upper_hull[m1].y - slope * upper_hull[m1].x;
            long long v2 = upper_hull[m2].y - slope * upper_hull[m2].x;
            if (v1 < v2) {
                left = m1;
            } else {
                right = m2;
            }
        }
        
        long long max_val = upper_hull[left].y - slope * upper_hull[left].x;
        for (int i = left + 1; i <= right; ++i) {
            max_val = max(max_val, upper_hull[i].y - slope * upper_hull[i].x);
        }
        return max_val;
    }
};

▼ 使い方 (拡張機能と点の一覧確認)

直結点の取得、点の一覧確認、便利機能の呼び出し方
AdvancedHistogramConvexHull hc;

// 1. 点を追加し、直前の頂点(直結した点)を取得する
auto prev1 = hc.add_point(1, 2); // std::nullopt
auto prev2 = hc.add_point(2, 1); // prev2.value() は (1, 2)
auto prev3 = hc.add_point(3, 5); // (2, 1)が削除され、prev3.value() は (1, 2)

// 2. 凸包を構成する点の一覧を確認する
// 【パターンA】上側の輪郭だけが必要な場合
vector<Point> upper = hc.get_upper_hull();
cout << "--- 上側の点 (左から右) ---\n";
for (const auto& p : upper) {
    cout << "(" << p.x << ", " << p.y << ")\n";
}

// 【パターンB】底面 (y=0) を含めた多角形全体が必要な場合
vector<Point> full = hc.get_full_hull();
cout << "--- 全体の点 (反時計回り) ---\n";
for (const auto& p : full) {
    cout << "(" << p.x << ", " << p.y << ")\n";
}

// 3. 面積の計算
long long area_x2 = hc.get_area_x2();
cout << "実際の面積: " << area_x2 / 2.0 << "\n";

// 4. Convex Hull Trick クエリ
long long max_b = hc.get_max_intercept(2);
cout << "傾き2での最大切片: " << max_b << "\n";

ヒストグラムの最大長方形 (Largest Rectangle in a Histogram)

用途: 与えられたヒストグラム(棒グラフ)に内接する長方形の最大面積を高速に求める

計算量: 時間計算量 $O(N)$。
スタックを用いて各棒が左右にどこまで伸ばせるかを管理することで、線形時間で最大面積を計算する。

long long largestRectangleArea(const vector<long long>& h) {
    int n = h.size();
    stack<pair<long long, int>> st; // {高さ, 左端の位置}
    long long max_area = 0;

    for (int i = 0; i < n; ++i) {
        int left_pos = i;
        // スタックのトップにある長方形の高さが現在の高さ以上なら、それ以上右に伸ばせない
        while (!st.empty() && st.top().first >= h[i]) {
            auto [ph, pos] = st.top();
            st.pop();
            max_area = max(max_area, ph * (i - pos));
            left_pos = pos;
        }
        st.push({h[i], left_pos});
    }

    // スタックに残っている要素(右端まで伸ばせる要素)の処理
    while (!st.empty()) {
        auto [ph, pos] = st.top();
        st.pop();
        max_area = max(max_area, ph * (n - pos));
    }

    return max_area;
}

▼ 使い方

基本的な使い方 (配列の定義と関数の呼び出し)
// ヒストグラムの各棒の高さを表す配列
vector<long long> h = {2, 1, 5, 6, 2, 3};

// 最大面積を計算
long long max_area = largestRectangleArea(h);

cout << max_area << "\n"; 
// 出力: 10 (高さ5と6の棒からなる 幅2 × 高さ5 の長方形)

注意: 配列 h の末尾に番兵として高さ 0 を追加しておくと、ループ後のスタックの残存処理を省略できコードをシンプルにできる。この問題は、0と1のグリッドから最大長方形を見つける問題(最大長方形部分行列)など、二次元の最大面積問題を解く際の基礎アルゴリズムとして広く応用される。

グリッドの最大長方形 (Maximal Rectangle)

用途: .(空マス)と #(障害物)で構成された 2 次元グリッドにおいて、. のみで構成される長方形の最大面積を求める

計算量: 時間計算量 $O(H \times W)$ [$H$ は高さ, $W$ は幅]。
各行を底辺としたヒストグラム(上方向に連続する . の高さ)を動的に更新しながら、「ヒストグラムの最大長方形」アルゴリズム(単調スタック)を適用することで全体を線形時間で計算する。

long long maximalRectangle(const vector<string>& grid) {
    if (grid.empty() || grid[0].empty()) return 0;
    int h = grid.size();
    int w = grid[0].size();

    // 1次元ヒストグラムの最大長方形を求めるヘルパー関数 (O(W))
    auto largestRectangleArea = [](const vector<long long>& height) {
        int n = height.size();
        stack<pair<long long, int>> st; // {高さ, 左端の位置}
        long long max_area = 0;

        for (int i = 0; i < n; ++i) {
            int left_pos = i;
            while (!st.empty() && st.top().first >= height[i]) {
                auto [ph, pos] = st.top();
                st.pop();
                max_area = max(max_area, ph * (i - pos));
                left_pos = pos;
            }
            st.push({height[i], left_pos});
        }
        while (!st.empty()) {
            auto [ph, pos] = st.top();
            st.pop();
            max_area = max(max_area, ph * (n - pos));
        }
        return max_area;
    };

    vector<long long> hist(w, 0);
    long long max_area = 0;

    // 行ごとにヒストグラムを更新しながら最大値を求める
    for (int r = 0; r < h; ++r) {
        for (int c = 0; c < w; ++c) {
            if (grid[r][c] == '.') {
                hist[c]++;
            } else {
                hist[c] = 0; // 障害物でリセット
            }
        }
        max_area = max(max_area, largestRectangleArea(hist));
    }

    return max_area;
}

▼ 使い方

基本的な使い方 (グリッドの定義と関数の呼び出し)
// グリッドの入力(. が空マス、# が障害物)
vector<string> grid = {
    "...#.",
    ".#...",
    ".#..#",
    ".#..."
};

// 最大面積を計算
long long max_area = maximalRectangle(grid);

cout << max_area << "\n";
// 出力: 6 (右下の 3×2 の範囲などで構成される長方形)

注意: 入力が vector<vector<char>> や数値グリッド(0/1)の場合は、文字判定部分(grid[r][c] == '.')を問題の条件に合わせて適宜変更すること。マス数が大きくなっても $O(H \times W)$ で動作するため、$1000 \times 1000$ 程度のグリッドであれば十分高速に実行できる。

Manhattan MST (マンハッタン距離の最小全域木)

用途: 2次元平面上のN点において、任意の2点間のコストをマンハッタン距離(|x1 - x2| + |y1 - y2|)としたときの最小全域木(MST)を高速に構築する

計算量: O(N log N)
すべての頂点対に辺を張ると O(N^2) となるが、各頂点から特定の4方向について最も近い点のみに辺を張ることで、候補となる辺の数を O(N) 本に削減できる。抽出した辺に対してKruskal法(Union-Find)を適用しMSTを構築する。内部で atcoder::segtree を使用する。

// 前提 : UnionFind, atcoder::segtree

// セグメント木に乗せる型:{値(x+y), 頂点番号}
using S = pair<ll, int>;
S op(S a, S b) { return min(a, b); }
S e() { return {4e18, -1}; } // 無限大と無効な頂点番号

// Manhattan MST の辺の候補 (O(N)本) を列挙する関数
vector<pair<ll, pair<int, int>>> manhattan_mst_edges(vector<pair<ll, ll>> ps) {
    int n = ps.size();
    vector<int> id(n);
    iota(id.begin(), id.end(), 0);
    vector<pair<ll, pair<int, int>>> edges;

    // 対称性を利用して4方向の最隣接点を探索
    for (int s = 0; s < 2; s++) {
        for (int t = 0; t < 2; t++) {
            auto cmp = [&](int i, int j) {
                return ps[i] > ps[j];
            };
            sort(id.begin(), id.end(), cmp);

            // y - x で座標圧縮
            vector<ll> ys(n);
            for (int i = 0; i < n; i++) ys[i] = ps[i].second - ps[i].first;
            sort(ys.begin(), ys.end());
            ys.erase(unique(ys.begin(), ys.end()), ys.end());

            atcoder::segtree<S, op, e> seg(ys.size());
            for (int i : id) {
                int y_idx = lower_bound(ys.begin(), ys.end(), ps[i].second - ps[i].first) - ys.begin();
                S res = seg.prod(y_idx, ys.size());
                if (res.second != -1) {
                    ll w = abs(ps[i].first - ps[res.second].first) + abs(ps[i].second - ps[res.second].second);
                    edges.push_back({w, {i, res.second}});
                }
                seg.set(y_idx, min(seg.get(y_idx), {ps[i].first + ps[i].second, i}));
            }
            // y軸反転
            for (int i = 0; i < n; i++) ps[i].second = -ps[i].second;
        }
        // x軸とy軸を入れ替え
        for (int i = 0; i < n; i++) swap(ps[i].first, ps[i].second);
    }
    return edges;
}

// Manhattan MST本体
// 返り値: {MSTの重みの総和, 使用した辺(u, v)のリスト}
pair<ll, vector<pair<int, int>>> manhattan_mst(const vector<pair<ll, ll>>& ps) {
    auto edges = manhattan_mst_edges(ps);
    // 重みの昇順にソート (Kruskal法)
    sort(edges.begin(), edges.end());
    
    int n = ps.size();
    UnionFind uf(n); // 前提としているUnionFindを使用
    ll total_weight = 0;
    vector<pair<int, int>> mst_edges;
    
    for (auto& edge : edges) {
        ll w = edge.first;
        int u = edge.second.first;
        int v = edge.second.second;
        // 異なるグループなら繋ぐ
        if (uf.unite(u, v)) {
            total_weight += w;
            mst_edges.push_back({u, v});
        }
    }
    return {total_weight, mst_edges};
}

▼ 使い方

基本的な使い方 (MSTの構築と重み・辺の取得)
int main() {
    int N = 5;
    // N個の点の座標 (x, y)
    vector<pair<ll, ll>> points = {
        {0, 0},
        {2, 2},
        {3, 1},
        {1, 4},
        {5, 3}
    };

    // Manhattan MST を構築
    auto [mst_weight, mst_edges] = manhattan_mst(points);

    cout << "MSTの総コスト: " << mst_weight << "\n";
    cout << "結ばれた辺 (頂点インデックス):\n";
    for (auto [u, v] : mst_edges) {
        cout << u << " - " << v << "\n";
    }
    
    return 0;
}
重複する点が含まれる場合の使い方 (前処理で対応)
#include <map>

int main() {
    int N = 6;
    // 重複する点を含む N 個の座標 (例: インデックス 0と2、1と4が同じ座標)
    vector<pair<ll, ll>> points = {
        {0, 0}, {2, 2}, {0, 0}, {1, 4}, {2, 2}, {5, 3}
    };

    // 1. 重複を取り除き、元のインデックスとの対応を作る
    map<pair<ll, ll>, int> unique_pts;
    vector<pair<ll, ll>> distinct_points;
    vector<pair<int, int>> zero_cost_edges; // コスト0で結ばれる辺(重複点)

    for (int i = 0; i < N; ++i) {
        if (unique_pts.count(points[i])) {
            // 既に同じ座標の点があれば、代表点との間にコスト0の辺を張る扱いにする
            zero_cost_edges.push_back({i, unique_pts[points[i]]});
        } else {
            // 初めて登場した座標
            unique_pts[points[i]] = i;
            distinct_points.push_back(points[i]);
        }
    }

    // 2. ユニークな点集合で Manhattan MST を構築
    auto [mst_weight, mst_edges_distinct] = manhattan_mst(distinct_points);

    cout << "MSTの総コスト: " << mst_weight << "\n";
    cout << "結ばれた辺 (元の頂点インデックス):\n";

    // 3. 辺のインデックスを元の頂点インデックスに復元して出力
    for (auto [u, v] : mst_edges_distinct) {
        int orig_u = unique_pts[distinct_points[u]];
        int orig_v = unique_pts[distinct_points[v]];
        cout << orig_u << " - " << orig_v << "\n";
    }
    
    // 重複点の辺も出力 (コスト0)
    for (auto [u, v] : zero_cost_edges) {
        cout << u << " - " << v << " (cost 0)\n";
    }

    return 0;
}

Euclidean MST (ユークリッド距離の最小全域木)

用途: 2次元平面上のN点において、任意の2点間のコストをユークリッド距離(√((x1 - x2)² + (y1 - y2)²))としたときの最小全域木(MST)を高速に構築する

計算量: 期待値 O(N log N)
すべての頂点対に辺を張ると O(N^2) となるが、ドロネー三角形分割(Delaunay Triangulation)を構築することで、MSTの候補となる辺の数を O(N) 本に削減できる。本実装では History DAG を用いた Randomized Bowyer-Watson アルゴリズムにより O(N log N) でドロネー三角形分割を構築し、抽出した辺に対してKruskal法を適用する。座標や外接円判定はすべて整数演算で処理される。

// 前提 : UnionFind (uf.unite(u, v) などが実装されていること)
// 前提 : 座標はすべて相異なるものとします (重複がある場合は事前に取り除く)

namespace Delaunay {
    using lll = __int128_t; // オーバーフロー防止用の128bit整数

    struct Point {
        ll x, y;
        int id;
    };

    lll cross(Point a, Point b, Point c) {
        return (lll)(b.x - a.x) * (c.y - a.y) - (lll)(b.y - a.y) * (c.x - a.x);
    }

    // pがa,b,cの外接円の内部にあるか判定
    bool inCircle(Point a, Point b, Point c, Point p) {
        lll adx = a.x - p.x, ady = a.y - p.y;
        lll bdx = b.x - p.x, bdy = b.y - p.y;
        lll cdx = c.x - p.x, cdy = c.y - p.y;
        lll abdet = adx * bdy - bdx * ady;
        lll bcdet = bdx * cdy - cdx * bdy;
        lll cadet = cdx * ady - adx * cdy;
        lll alift = adx * adx + ady * ady;
        lll blift = bdx * bdx + bdy * bdy;
        lll clift = cdx * cdx + cdy * cdy;
        return alift * bcdet + blift * cadet + clift * abdet > 0;
    }

    struct Triangle {
        int p[3];
        int adj[3]; // adj[i] は p[i] の対辺を共有する三角形のID
        vector<int> children;
        bool dead;
        Triangle() {}
        Triangle(int a, int b, int c) {
            p[0] = a; p[1] = b; p[2] = c;
            adj[0] = adj[1] = adj[2] = -1;
            dead = false;
        }
    };

    vector<Point> pts;
    vector<Triangle> tr;

    bool contains(int tid, int pid) {
        for (int i = 0; i < 3; ++i) {
            if (cross(pts[tr[tid].p[i]], pts[tr[tid].p[(i + 1) % 3]], pts[pid]) < 0) return false;
        }
        return true;
    }

    vector<pair<int, int>> build_delaunay(const vector<pair<ll, ll>>& points) {
        int n = points.size();
        if (n <= 1) return {};
        pts.clear();
        for (int i = 0; i < n; ++i) pts.push_back({points[i].first, points[i].second, i});
        
        // 全点を包含する巨大なスーパー三角形を配置
        ll INF = 3e9; // 座標の最大値が10^9程度であることを想定
        pts.push_back({0, INF, n});
        pts.push_back({-INF, -INF, n + 1});
        pts.push_back({INF, -INF, n + 2});
        
        tr.clear();
        tr.push_back(Triangle(n, n + 1, n + 2));
        
        vector<int> perm(n);
        iota(perm.begin(), perm.end(), 0);
        mt19937 rand_engine(42);
        shuffle(perm.begin(), perm.end(), rand_engine);

        vector<int> visited_gen;
        
        for (int i = 0; i < n; ++i) {
            int pid = perm[i];
            
            // 1. History DAG を用いて包含する葉三角形を検索 (期待値 O(log N))
            int curr = 0;
            while (tr[curr].children.size() > 0) {
                bool found = false;
                for (int child : tr[curr].children) {
                    if (!tr[child].dead && contains(child, pid)) {
                        curr = child; found = true; break;
                    }
                }
                // 丸め誤差等へのフォールバック
                if (!found) {
                    for (int child : tr[curr].children) {
                        if (contains(child, pid)) { curr = child; found = true; break; }
                    }
                    if (!found) curr = tr[curr].children[0];
                }
            }
            
            // 2. 新たな点が外接円に含まれる三角形 (bad triangles) をBFSで探索
            vector<int> bad;
            vector<int> q;
            q.push_back(curr);
            
            if (visited_gen.size() < tr.size() + 100) {
                visited_gen.resize(max((int)tr.size() + 1000, 100000), 0);
            }
            int gen = i + 1;
            visited_gen[curr] = gen;
            
            int head = 0;
            while (head < (int)q.size()) {
                int t = q[head++];
                bad.push_back(t);
                for (int j = 0; j < 3; ++j) {
                    int nxt = tr[t].adj[j];
                    if (nxt != -1 && visited_gen[nxt] != gen) {
                        visited_gen[nxt] = gen;
                        if (inCircle(pts[tr[nxt].p[0]], pts[tr[nxt].p[1]], pts[tr[nxt].p[2]], pts[pid])) {
                            q.push_back(nxt);
                        } else {
                            visited_gen[nxt] = 0; // 条件外なら訪問フラグをリセット
                        }
                    }
                }
            }
            
            // 3. 空洞の境界エッジを抽出
            struct BoundaryEdge { int u, v, out_tri, old_bad; };
            vector<BoundaryEdge> bnd;
            for (int t : bad) {
                for (int j = 0; j < 3; ++j) {
                    int nxt = tr[t].adj[j];
                    if (nxt == -1 || visited_gen[nxt] != gen) {
                        bnd.push_back({tr[t].p[(j + 1) % 3], tr[t].p[(j + 2) % 3], nxt, t});
                    }
                }
            }
            
            // 4. 空洞を新たな三角形群で再分割
            int first_new = tr.size();
            for (int j = 0; j < (int)bnd.size(); ++j) {
                Triangle nt(pid, bnd[j].u, bnd[j].v);
                nt.adj[0] = bnd[j].out_tri;
                // 外部の三角形からの隣接リンクを更新
                if (bnd[j].out_tri != -1) {
                    Triangle& out = tr[bnd[j].out_tri];
                    for (int k = 0; k < 3; ++k) {
                        if (out.adj[k] == bnd[j].old_bad) {
                            out.adj[k] = first_new + j;
                            break;
                        }
                    }
                }
                // 新しい三角形同士を内部でリンク
                for (int k = 0; k < (int)bnd.size(); ++k) {
                    if (bnd[k].u == bnd[j].v) nt.adj[1] = first_new + k;
                    if (bnd[k].v == bnd[j].u) nt.adj[2] = first_new + k;
                }
                tr.push_back(nt);
            }
            
            // 5. History DAG の更新
            for (int t : bad) {
                tr[t].dead = true;
                for (int j = 0; j < (int)bnd.size(); ++j) {
                    tr[t].children.push_back(first_new + j);
                }
            }
        }
        
        // 最終的なドロネー網のエッジを抽出
        vector<pair<int, int>> edges;
        for (auto& t : tr) {
            if (!t.dead) {
                for (int i = 0; i < 3; ++i) {
                    int u = t.p[i], v = t.p[(i + 1) % 3];
                    if (u < n && v < n && u < v) {
                        edges.push_back({u, v});
                    }
                }
            }
        }
        sort(edges.begin(), edges.end());
        edges.erase(unique(edges.begin(), edges.end()), edges.end());
        return edges;
    }
}

// Euclidean MST本体
// 返り値: {MSTの重みの総和, 使用した辺(u, v)のリスト}
pair<double, vector<pair<int, int>>> euclidean_mst(const vector<pair<ll, ll>>& ps) {
    int n = ps.size();
    if (n <= 1) return {0.0, {}};
    
    // ドロネー三角形分割を用いて辺候補を O(N) 本に削減
    auto edges = Delaunay::build_delaunay(ps);
    
    // Kruskal法: 重みは平方で正確にソート (浮動小数点誤差を防ぐ)
    vector<pair<ll, pair<int, int>>> weighted_edges;
    for (auto& edge : edges) {
        int u = edge.first;
        int v = edge.second;
        ll dx = ps[u].first - ps[v].first;
        ll dy = ps[u].second - ps[v].second;
        ll w2 = dx * dx + dy * dy;
        weighted_edges.push_back({w2, {u, v}});
    }
    
    sort(weighted_edges.begin(), weighted_edges.end());

    UnionFind uf(n); // 前提としているUnionFindを使用
    double total_weight = 0;
    vector<pair<int, int>> mst_edges;

    for (auto& edge : weighted_edges) {
        ll w2 = edge.first;
        int u = edge.second.first;
        int v = edge.second.second;
        // 異なるグループなら繋ぐ
        if (uf.unite(u, v)) {
            total_weight += sqrt((double)w2);
            mst_edges.push_back({u, v});
        }
    }
    return {total_weight, mst_edges};
}

▼ 使い方

基本的な使い方 (MSTの構築と重み・辺の取得)
int main() {
    int N = 5;
    // N個の点の座標 (x, y) ※座標はすべて相異なること
    vector<pair<ll, ll>> points = {
        {0, 0},
        {2, 2},
        {3, 1},
        {1, 4},
        {5, 3}
    };

    // Euclidean MST を構築
    auto [mst_weight, mst_edges] = euclidean_mst(points);

    cout << "MSTの総コスト: " << mst_weight << "\n";
    cout << "結ばれた辺 (頂点インデックス):\n";
    for (auto [u, v] : mst_edges) {
        cout << u << " - " << v << "\n";
    }

    return 0;
}
重複する点が含まれる場合の使い方 (前処理で対応)
#include <map>

int main() {
    int N = 6;
    // 重複する点を含む N 個の座標 (例: インデックス 0と2、1と4が同じ座標)
    vector<pair<ll, ll>> points = {
        {0, 0}, {2, 2}, {0, 0}, {1, 4}, {2, 2}, {5, 3}
    };

    // 1. 重複を取り除き、元のインデックスとの対応を作る
    map<pair<ll, ll>, int> unique_pts;
    vector<pair<ll, ll>> distinct_points;
    vector<pair<int, int>> zero_cost_edges; // コスト0で結ばれる辺(重複点)

    for (int i = 0; i < N; ++i) {
        if (unique_pts.count(points[i])) {
            // 既に同じ座標の点があれば、代表点との間にコスト0の辺を張る扱いにする
            zero_cost_edges.push_back({i, unique_pts[points[i]]});
        } else {
            // 初めて登場した座標
            unique_pts[points[i]] = i;
            distinct_points.push_back(points[i]);
        }
    }

    // 2. ユニークな点集合で Euclidean MST を構築
    auto [mst_weight, mst_edges_distinct] = euclidean_mst(distinct_points);

    cout << "MSTの総コスト: " << mst_weight << "\n";
    cout << "結ばれた辺 (元の頂点インデックス):\n";

    // 3. 辺のインデックスを元の頂点インデックスに復元して出力
    for (auto [u, v] : mst_edges_distinct) {
        int orig_u = unique_pts[distinct_points[u]];
        int orig_v = unique_pts[distinct_points[v]];
        cout << orig_u << " - " << orig_v << "\n";
    }
    
    // 重複点の辺も出力 (コスト0)
    for (auto [u, v] : zero_cost_edges) {
        cout << u << " - " << v << " (cost 0)\n";
    }

    return 0;
}

グリッド一筆書き & 最大マッチング構築

用途: N × N グリッド上の全マスを通る一筆書き経路の取得、最大マッチングの構築、および任意マスのペア相手の高速検索

計算量: 経路取得・マッチング構築・テーブル構築 すべて O(N^2)。
フローアルゴリズムを使わず、グリッド上のスネーク走査(一筆書き)を生成し、それを2マスずつ区切ることで高速にマッチングを構築する。一度テーブルを作れば、以降は任意の箇所のマッチング先を O(1) で参照できる。

#include <utility>

struct GridPathMatching {
    using Cell = std::pair<int, int>;
    using Edge = std::pair<Cell, Cell>;

    // N×N グリッドの全マスを通る一筆書き経路(Nが偶数なら元のマスに戻れる閉路)を返す
    static std::vector<Cell> get_hamiltonian_path(int N) {
        std::vector<Cell> path;
        if (N <= 0) return path;
        path.reserve(N * N);
        
        if (N % 2 == 0) {
            for (int c = 0; c < N; ++c) path.push_back({0, c});
            for (int r = 1; r < N; ++r) {
                if (r % 2 == 1) for (int c = N - 1; c >= 1; --c) path.push_back({r, c});
                else            for (int c = 1; c < N; ++c) path.push_back({r, c});
            }
            for (int r = N - 1; r >= 1; --r) path.push_back({r, 0});
        } else {
            for (int r = 0; r < N; ++r) {
                if (r % 2 == 0) for (int c = 0; c < N; ++c) path.push_back({r, c});
                else            for (int c = N - 1; c >= 0; --c) path.push_back({r, c});
            }
        }
        return path;
    }

    // N×N グリッドの最大マッチング(ペアのリスト)を返す
    // missing_r, missing_c を指定すると、その1マスを除外したマッチングを構築
    static std::vector<Edge> get_max_matching(int N, int missing_r = -1, int missing_c = -1) {
        std::vector<Edge> res;
        if (N <= 0) return res;
        auto path = get_hamiltonian_path(N);
        int total = N * N;

        if (missing_r < 0 || missing_c < 0) {
            for (int i = 0; i + 1 < total; i += 2) res.push_back({path[i], path[i + 1]});
        } else {
            int idx = -1;
            for (int i = 0; i < total; ++i) {
                if (path[i].first == missing_r && path[i].second == missing_c) {
                    idx = i; break;
                }
            }
            if (idx != -1) {
                if (N % 2 == 0) {
                    for (int i = 0; i < total - 2; i += 2) {
                        res.push_back({path[(idx + 1 + i) % total], path[(idx + 2 + i) % total]});
                    }
                } else {
                    for (int i = 0; i + 1 < idx; i += 2) res.push_back({path[i], path[i + 1]});
                    for (int i = idx + 1; i + 1 < total; i += 2) res.push_back({path[i], path[i + 1]});
                }
            }
        }
        return res;
    }

    // 各マスの「マッチング先のマス」をO(1)で引ける二次元配列 (N×N) を作成して返す
    // ペアが存在しないマス(欠損マスや余ったマス)には {-1, -1} が入る
    static std::vector<std::vector<Cell>> get_matching_table(int N, int missing_r = -1, int missing_c = -1) {
        std::vector<std::vector<Cell>> table(N, std::vector<Cell>(N, {-1, -1}));
        auto edges = get_max_matching(N, missing_r, missing_c);
        for (auto [u, v] : edges) {
            table[u.first][u.second] = v;
            table[v.first][v.second] = u;
        }
        return table;
    }
};

▼ 使い方

一筆書き(ハミルトンパス/閉路)の取得 & マッチング相手の参照
// ====================================================
// 1. 一筆書き経路の取得 (ハミルトンパス / ハミルトン閉路)
// ====================================================

// 【Nが偶数の場合】 -> ハミルトン閉路 (Hamiltonian Cycle)
// 全マスを1度ずつ通り、終点 (1, 0) から始点 (0, 0) へ戻れる閉路(ループ)を形成します。
auto cycle = GridPathMatching::get_hamiltonian_path(4); // 4×4 グリッド
// 経路: {0,0} -> {0,1} -> ... -> {1,0} (※終点 {1,0} は始点 {0,0} と隣接しているため輪になる)


// 【Nが奇数の場合】 -> ハミルトンパス (Hamiltonian Path)
// 全マスを1度ずつ通り、(0,0) から (N-1, N-1) へ至る1本の経路(パス)を形成します。
auto path = GridPathMatching::get_hamiltonian_path(3);  // 3×3 グリッド
// 返り値: {{0,0}, {0,1}, {0,2}, {1,2}, {1,1}, {1,0}, {2,0}, {2,1}, {2,2}}


// ====================================================
// 2. マッチングテーブルの構築と O(1) でのマッチング相手参照
// ====================================================

// 【パターンA: 欠損なし (3×3)】
auto table1 = GridPathMatching::get_matching_table(3);

// マス (0, 0) のペア相手を取得
auto [r1, c1] = table1[0][0]; // {0, 1} が返る

// Nが奇数の場合、最後に余るマス (2, 2) のペア相手を取得
auto [er, ec] = table1[2][2]; // {-1, -1} が返る (ペアが存在しない)


// 【パターンB: 1マス欠け (3×3 グリッドから (1,1) を除外)】
auto table2 = GridPathMatching::get_matching_table(3, 1, 1);

// 除外したマス (1, 1) のペア相手を取得
auto [mr, mc] = table2[1][1]; // {-1, -1} が返る

// 除外マスの影響を受けたマス (1, 0) のペア相手を取得
auto [r2, c2] = table2[1][0]; // {2, 0} が返る

障害物回避DP (First-Hit DP)

用途: $H\times W$ の巨大グリッドにおいて、$K$ 個の障害物を回避して左上から右下へ到達する経路数の計算(包除原理)

計算量: O(K2) (K は障害物の数)。 事前に O(H+W) など最大移動ステップ分の階乗前計算(init_nCr等)が完了していることが前提。

// 前提:階乗および組み合わせの mod 計算 (init_nCr, nCr) が定義・初期化されていること
using ll = long long;

struct Point {
    ll r, c;
    bool operator<(const Point& other) const {
        if (r != other.r) return r < other.r;
        return c < other.c;
    }
};

// 2点間の経路数 (障害物無視)
ll count_paths(Point from, Point to, long long mod) {
    if (from.r > to.r || from.c > to.c) return 0;
    ll dr = to.r - from.r;
    ll dc = to.c - from.c;
    return nCr(dr + dc, dr, mod);
}

// 障害物回避DPのメイン処理
// start: スタート座標, goal: ゴール座標, obstacles: 障害物リスト
ll obstacle_avoidance_dp(Point start, Point goal, vector<Point> obstacles, long long mod) {
    // 障害物を座標順 (行 -> 列) にソート
    sort(obstacles.begin(), obstacles.end());

    // ゴールを最終目的地として障害物リストの最後に追加
    obstacles.push_back(goal);
    int K = obstacles.size();

    // dp[i]: 他の障害物を一切踏まずに、初めて障害物 i に到達する経路数
    vector<long long> dp(K, 0);

    for (int i = 0; i < K; i++) {
        // スタートから障害物 i への全経路数
        dp[i] = count_paths(start, obstacles[i], mod);

        // 途中で他の障害物 j にぶつかった場合を引く (包除原理)
        for (int j = 0; j < i; j++) {
            // j から i へ到達可能か判定 (ソート済みなので r_j <= r_i は保証。列のみ確認)
            if (obstacles[j].c <= obstacles[i].c) {
                long long invalid_paths = dp[j] * count_paths(obstacles[j], obstacles[i], mod) % mod;
                dp[i] = (dp[i] - invalid_paths + mod) % mod;
            }
        }
    }

    // ゴール (K-1番目) への合法な経路数が答え
    return dp[K - 1];
}

▼ 使い方と応用パターン

基本の使い方(巨大なグリッドでの経路探索)
int main() {
    long long mod = 1000000007;
    
    // 1. 盤面サイズ H=10^9, W=10^9 のような巨大グリッドを想定
    // 最大移動回数は H+W なので、それ以上の余裕を持たせて前計算
    // ※ H+W が 10^9 以上の場合は nCr を都度計算する等工夫が必要。
    // ※ ここでは H+W < 200000 程度の問題か、十分なメモリがある前提。
    init_nCr(200005, mod);

    Point start = {0, 0};
    Point goal = {100000, 100000};
    
    // 障害物の座標 (少数を想定)
    vector<Point> obstacles = {
        {10, 20},
        {500, 600},
        {80000, 90000}
    };

    long long ans = obstacle_avoidance_dp(start, goal, obstacles, mod);
    cout << "有効な経路数: " << ans << endl;

    return 0;
}

注意: この手法は、障害物の数 $K$ が $K \le 3000$ 程度であれば高速に動作しますが、グリッド全体の移動回数($r + c$)に基づく組み合わせ計算 nCr が定数時間で返ってくることが前提です。もし $H, W$ が大きすぎて fact の前計算配列がメモリ制限に引っかかる場合は、階乗前計算の代わりに $O(R)$ かかる乗算で nCr を都度計算する等の調整が必要です。

DP

bitDP

用途: 集合の探索、巡回セールスマン問題(TSP)など

計算量: O(2^N × N^2) など
設計: 訪問済みの集合を整数(ビット列)で管理し、現在の状態とともに保持する。

// 巡回セールスマン問題の遷移例
vector<vector<long long>> dp(1 << N, vector<long long>(N, INF));
dp[1][0] = 0; // 開始点
for (ll S = 1; S < (1 << N); S++) {
    for (ll v = 0; v < N; v++) {
        if (dp[S][v] == INF) continue;
        for (ll u = 0; u < N; u++) {
            if (!(S & (1 << u))) { // uが未訪問の場合
                dp[S | (1 << u)][u] = min(dp[S | (1 << u)][u], dp[S][v] + cost[v][u]);
            }
        }
    }
}

注意: 制約 N が 15〜20 程度と小さい場合に検討する。ビット演算の優先順位(括弧の付与)に注意。

部分集合bitDP (3^N bitDP)

用途: 集合の2分割、最適グループ分け問題、グラフの彩色数・集合被覆問題など

計算量: O(3^N)
設計: 各集合 $S$ に対して、その部分集合 $sub$ をすべて列挙し、$S \setminus sub$ と $sub$ への分割(あるいは $sub$ から $S$ への遷移)を行う。全集合に対する部分集合の総数が $\sum_{k=0}^N \binom{N}{k} 2^k = 3^N$ となるため全体の計算量は $O(3^N)$ となる。

// 集合 S を部分集合 sub と (S ^ sub) に分割して最適化する例
vector<long long> dp(1LL << N, INF);
dp[0] = 0; // 初期値

for (long long S = 1; S < (1LL << N); S++) {
    // S の非空部分集合 sub を降順にすべて走査
    for (long long sub = S; sub > 0; sub = (sub - 1) & S) {
        dp[S] = min(dp[S], dp[S ^ sub] + cost[sub]);
    }
}

注意: 制約 N が 12〜15 程度($3^{15} \approx 1.4 \times 10^7$)の場合に検討する。N = 16 ($3^{16} \approx 4.3 \times 10^7$)あたりが実行時間の限界目安。空集合(sub == 0)を含める必要があるかは問題の遷移条件に応じて調整すること。

桁DP

用途: ある値以下の整数のうち、特定の条件を満たすものの数を数え上げる問題など

計算量: O(D × state) (Dは桁数)など
設計: dp[決定した桁数][未満フラグ(smaller)][リーディングゼロフラグ(leading_zero)][その他の状態] を持ち、上の桁から順に遷移していく。

// N以下の条件を満たす数を数える桁DPの遷移例
// 桁DPの配列はサイズに変数が使えないため、関数の外(グローバル)で定数サイズで宣言する
const ll MAX_D = 100005; // 想定される最大桁数
const ll MAX_J = 100;    // 状態jの最大値
ll dp[MAX_D][2][2][MAX_J]; // dp[i][smaller][leading_zero][j]

void solve() {
    string N; // 桁数が大きい場合は文字列として受け取る
    ll D = N.size();

    // 複数テストケースなどで初期化が必要な場合は memset を使う
    memset(dp, 0, sizeof(dp));
    dp[0][0][1][0] = 1;

    for (ll i = 0; i < D; i++) {
        ll Nd = N[i] - '0';

        for (ll smaller = 0; smaller < 2; smaller++) {
            for (ll leading_zero = 0; leading_zero < 2; leading_zero++) {
                for (ll j = 0; j < MAX_J; j++) {
                    if (dp[i][smaller][leading_zero][j] == 0) continue;

                    ll limit = smaller ? 9 : Nd;
                    for (ll d = 0; d <= limit; d++) {
                        ll next_smaller = smaller || (d < Nd);
                        ll next_leading_zero = leading_zero && (d == 0);

                        // leading_zero を考慮して状態更新
                        ll next_j = /* jとdから次の状態を計算 */ j;

                        dp[i + 1][next_smaller][next_leading_zero][next_j]
                            += dp[i][smaller][leading_zero][j];
                    }
                }
            }
        }
    }
}

注意: 上限値 N は非常に大きな数(例:10^100000)にも対応できるよう string として受け取るのが一般的。 また、リーディングゼロを「まだ何も数字を置いていない状態」として区別したい場合は、 leading_zero フラグを持たせると、桁和・桁積・使用数字などの状態管理がしやすい。

木DP (Tree DP)

用途: 木構造上の最適化問題(最大独立集合、部分木サイズ計算、頂点・辺選択の最適化など)

計算量: O(N)。
任意の頂点を根としてDFSを行い、帰りがけ順(子の計算が完了した後)にボトムアップで親ノードへDPの値を計算・集約する。

// v: 現在の頂点, p: 親頂点, G: グラフ, dp: DPテーブル
void dfs(ll v, ll p, const vvl& G, vvl& dp) {
    // 初期化(頂点 v 単体の状態を設定)
    dp[v][0] = 0; // 例: 頂点 v を選ばない場合
    dp[v][1] = 1; // 例: 頂点 v を選ぶ場合

    for (ll u : G[v]) {
        if (u == p) continue; // 親ノードへの逆走防止

        // 行きがけ: 子ノード u の DP を先に再帰計算
        dfs(u, v, G, dp);

        // 帰りがけ処理(遷移式: 子 u の結果をもとに親 v の DP を更新)
        dp[v][0] += max(dp[u][0], dp[u][1]);
        dp[v][1] += dp[u][0];
    }
}

▼ 使い方

基本的な使い方 (最大独立集合の計算)
int main() {
    ll N = 5;
    vvl G(N);

    // 辺の追加 (無向グラフ)
    G[0].push_back(1); G[1].push_back(0);
    G[0].push_back(2); G[2].push_back(0);
    G[1].push_back(3); G[3].push_back(1);
    G[1].push_back(4); G[4].push_back(1);

    // dp[頂点][状態] のサイズ確保
    vvl dp(N, vl(2, 0));

    // 根を 0 として DFS 実行 (親は存在しないため -1)
    dfs(0, -1, G, dp);

    // 根(0)における「選ぶ」「選ばない」の大きい方が答え
    ll ans = max(dp[0][0], dp[0][1]);
    cout << ans << "\n";

    return 0;
}

注意: 無向グラフとして引数 G を受け取り、if (u == p) continue; で親への逆走を防ぐ。N が大きい場合($N \ge 10^5$)は再帰スタックのオーバーフローに注意すること。

全方位木DP (2回DFS方式)

用途: 構造体を使わず、2回のDFS(ボトムアップとトップダウン)を用いて全頂点を根としたときの結果を $O(N)$ で計算する

計算量: O(N)。
設計: dp1[v] に頂点 $v$ を根とする部分木のDP値、dp2[v] に頂点 $v$ の親方向(外側)からのDP値を保持する。dfs2 内で「左からの累積」と「右からの累積」を順に掛けることで、自分以外の部分木からの寄与を省メモリかつ $O(N)$ で計算する。

// 1回目のDFS: 頂点 v を根とする「部分木」の DP 値をボトムアップに計算
void dfs1(ll v, ll p, const vvl& G, vvl& dp1) {
    dp1[v][0] = 1; // 初期化

    for (ll u : G[v]) {
        if (u == p) continue; // 親ノードへの逆走防止

        dfs1(u, v, G, dp1);

        // 【帰りがけ遷移】子 u の部分木結果を親 v に集約
        dp1[v][0] = (dp1[v][0] * (dp1[u][0] + 1)) % M;
    }
}

// 2回目のDFS: 親方向(部分木の外側)からの DP 値をトップダウンに伝播
void dfs2(ll v, ll p, const vvl& G, const vvl& dp1, vvl& dp2) {
    ll prod = 1;
    vl children;

    // 前向き走査: 左(手前)からの累積値を計算しながら各子へ掛ける
    for (ll u : G[v]) {
        if (u == p) continue;
        dp2[u][0] = (dp2[u][0] * prod) % M;
        prod = (prod * (dp1[u][0] + 1)) % M;
        children.push_back(u);
    }

    // 順序を反転して後ろ向き走査の準備
    reverse(children.begin(), children.end());

    // 親からの DP 値(dp2[v])を含めた右(奥)からの累積値
    prod = (dp2[v][0] + 1) % M;

    for (ll u : children) {
        dp2[u][0] = (dp2[u][0] * prod) % M;
        prod = (prod * (dp1[u][0] + 1)) % M;

        // 子ノード u へ再帰伝播
        dfs2(u, v, G, dp1, dp2);
    }
}

▼ 使い方

基本的な使い方 (全頂点に対する部分木組み合わせの計算)
void solve() {
    ll N = 5;
    M = 1000000007;
    vvl G(N);

    // 辺の追加 (無向グラフ)
    G[0].push_back(1); G[1].push_back(0);
    G[0].push_back(2); G[2].push_back(0);
    G[1].push_back(3); G[3].push_back(1);
    G[1].push_back(4); G[4].push_back(1);

    // dp1[i]: 0を根としたときの頂点 i の部分木での値
    vvl dp1(N, vl(1, 1));
    // dp2[i]: 頂点 i から見た親方向(外側)の値
    vvl dp2(N, vl(1, 1));
    dp2[0][0] = 0; // 根 0 の外側値は初期設定

    // 1. 部分木 DP の計算
    dfs1(0, -1, G, dp1);

    // 2. 親方向 DP の伝播
    dfs2(0, -1, G, dp1, dp2);

    // 3. 各頂点 i を根としたときの最終結果を集約
    for (ll i = 0; i < N; i++) {
        ll ans = (dp1[i][0] * (dp2[i][0] + 1)) % M;
        cout << "頂点 " << i << " の答え: " << ans << "\n";
    }
}

注意: `dfs2` のループ内で「自分以外の兄弟の累積値」を計算する際、積(乗算)の場合はこのように「前向き・後ろ向き」の2回走査で求めることができる。和(加算)や最大値(`max`)などの場合は演算子に応じて `prod` の結合ロジックを調整すること。

最長増加部分列 (LIS) と復元

用途: 配列から順序を崩さずに要素を取り出したとき、それらが単調増加となる部分列(LIS)の最大長さや、実際の部分列(要素)を求める問題など

計算量: O(N log N)
設計: dp[i] に「長さ i + 1 の増加部分列の末尾となる要素の最小値」を保持する。各要素に対して二分探索を用いて配列 dp を更新しながら、subseq_idx[i] に A[i] が配置されたインデックスを記録する。すべての処理完了後、配列を後ろから走査することで LIS の構成要素を復元する。

// 最長増加部分列(LIS)の長さと復元
ll N;
// Aは入力配列
vector<ll> A(N);

vector<ll> dp;
vector<ll> subseq_idx(N); // 各要素が LIS 内のどの位置(0-indexed)に対応するかを保持

for (ll i = 0; i < N; i++) {
    // 二分探索で挿入位置を特定する
    auto it = lower_bound(dp.begin(), dp.end(), A[i]);
    ll idx = distance(dp.begin(), it);
    if (it == dp.end()) {
        dp.push_back(A[i]);
    } else {
        *it = A[i];
    }
    subseq_idx[i] = idx;
}

ll ans = dp.size(); // LIS の長さ

// LIS の復元(後ろから走査)
vector<ll> lis(ans);
ll cur_len = ans - 1;
for (ll i = N - 1; i >= 0; i--) {
    if (subseq_idx[i] == cur_len) {
        lis[cur_len] = A[i];
        cur_len--;
    }
}

注意: 問題が「広義単調増加(同じ値が連続してもよい)」を求める場合は、二分探索の部分を lower_bound ではなく upper_bound に変更する必要がある。

重み付き最長増加部分列 (Weighted LIS) + 個数カウント

用途: 配列から順序を崩さずに要素を取り出したとき、それらが単調増加となる部分列における、要素の重み(価値など)の総和の最大値を求める問題、およびその最大値を達成する組み合わせの数を数え上げる問題など

計算量: O(N log M) (M は値の最大値。座標圧縮を併用すれば O(N log N))
設計: セグメント木に乗せるデータを pair<最大重み, その個数> に拡張します。二項演算では重みが大きい方を採用し、同じ重みの場合は個数を足し合わせる(適宜 Modulo を取る)ことで、区間内の最大重みとそのパターン数を同時に管理・取得します。

// セグメント木に乗せる型: pair<最大重み, 組み合わせの数>
using S = pair<ll, ll>;
const ll MOD = 998244353; // 個数を数える際の法(問題に合わせて変更)

// 二項演算: 重みが大きい方を採用。同じなら個数を足し合わせる
S op(S a, S b) {
    if (a.first > b.first) return a;
    if (a.first < b.first) return b;
    return {a.first, (a.second + b.second) % MOD};
}

// 単位元
S e() {
    return {-1e18, 0};
}

void solve() {
    ll N;
    cin >> N;
    // Hは要素の値(あらかじめ 1 以上 N 以下に座標圧縮されているとする)
    // Aはそれぞれの重み(価値)
    vector<ll> H(N), A(N);
    for (int i = 0; i < N; i++) cin >> H[i];
    for (int i = 0; i < N; i++) cin >> A[i];

    // 値の最大値に合わせて初期化(H[i] が N 以下の前提)
    segtree<S, op, e> seg(N + 1);
    
    // 初期状態:長さ0の増加部分列(重み0、個数1)を仮想的に値0の位置に置く
    seg.set(0, {0, 1});

    for (int i = 0; i < N; i++) {
        // 自身より小さい値(0 〜 H[i] - 1)の範囲の最大重みと個数を取得
        S mx = seg.prod(0, H[i]);
        
        ll new_weight = mx.first + A[i];
        ll new_count = mx.second;

        // 現在の H[i] での(最大重み, 個数)を取得し、新しい候補とマージして更新
        S cur = seg.get(H[i]);
        seg.set(H[i], op(cur, {new_weight, new_count}));
    }
    
    // 全体の最大重みとその個数を取得
    S ans = seg.prod(0, N + 1);
    cout << "最大重み: " << ans.first << endl;
    cout << "個数: " << ans.second << endl;
}

注意: seg.set(H[i], mx + A[i]) のように単純に上書きするのではなく、一度 seg.get(H[i]) で現在の値を取得し、op 関数を通してマージすることで、同じ値 H[i] で終わる別パターンの LIS の個数を正しく合算しています。要素の値 H[i] は 1 以上に座標圧縮されていることを前提としています。

▼ 使い方と応用パターン

基本の使い方(通常のLIS長の最大値と、そのパターンの数え上げ)
// 全ての重みを 1 とみなせば、通常の LIS の長さとその個数を求められる
ll N = 4;
// 配列 [2, 1, 3, 3] を考える
vector<ll> H = {2, 1, 3, 3}; 
vector<ll> A = {1, 1, 1, 1}; // すべて重み1

segtree<S, op, e> seg(N + 1);
seg.set(0, {0, 1}); // 基準点

for (int i = 0; i < N; i++) {
    S mx = seg.prod(0, H[i]); 
    ll new_weight = mx.first + A[i];
    ll new_count = mx.second;
    S cur = seg.get(H[i]);
    seg.set(H[i], op(cur, {new_weight, new_count}));
}

S ans = seg.prod(0, N + 1);
cout << "LISの長さ: " << ans.first << endl; // 2
// (2, 3) が2パターン、(1, 3) が2パターンで、計 4 通り
cout << "そのパターンの数: " << ans.second << endl; // 4

2次元最長増加部分列 (2次元LIS)

用途: 2次元平面上の点 (x, y) の集合から、x と y がともに厳密に増加するような部分列の最大長さを求める問題

計算量: O(N log N)
設計: 要素を「x の昇順、x が等しい場合は y の降順」でソートする。その後、y の値のみを取り出して通常の1次元LISを適用する。

// 2次元最長増加部分列の長さを求める
ll N;
vector<pair<ll, ll>> points(N); // {x, y}

// xの昇順、xが等しい場合はyの降順にソートする
sort(points.begin(), points.end(), [](const pair<ll, ll>& a, const pair<ll, ll>& b) {
    if (a.first != b.first) return a.first < b.first;
    return a.second > b.second;
});

vector<ll> dp;
for (ll i = 0; i < N; i++) {
    ll y = points[i].second;
    auto it = lower_bound(dp.begin(), dp.end(), y);
    if (it == dp.end()) {
        dp.push_back(y);
    } else {
        *it = y;
    }
}
ll ans = dp.size();

注意: x が等しいときに y を「降順」にソートすることが重要である。これにより、同じ x を持つ複数の要素が同じLISに含まれてしまうのを防ぐことができる。

ナップサック問題 (基本構造と派生形)

用途: 容量制限がある中での価値の最大化(アイテムごとに重さと価値が存在する問題)を解く

計算量: 基本の重さDPは O(NW)、価値DPは O(NV)。
制約(Wが大きいか、Vの合計が大きいか)に応じた基本の使い分けに加え、同じ品物を選ぶ条件(個数制限あり/なし)に応じたカスタマイズコードを適用します。

// 【基本構造】0-1ナップサック問題

// 1. 重さDP (Wが小さい場合: W <= 10^5 程度)
// dp[i] : 重さの合計が i 以下のときの価値の最大値
ll knapsack_weight_dp(const vector<ll>& weights, const vector<ll>& values, ll W) {
    int n = weights.size();
    vector<ll> dp(W + 1, 0);
    for (int i = 0; i < n; ++i) {
        // 重さが大きい方から更新する(同じ品物を複数回選ばないため)
        for (ll w = W; w >= weights[i]; --w) {
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }
    return dp[W];
}

// 2. 価値DP (Vの合計が小さい場合: sum(V) <= 10^5 程度)
// dp[i] : 価値の合計が i のときの重さの最小値
ll knapsack_value_dp(const vector<ll>& weights, const vector<ll>& values, ll W) {
    int n = weights.size();
    ll max_v_sum = 0;
    for (ll v : values) max_v_sum += v;

    const ll INF = 1e18; // 十分大きい値
    vector<ll> dp(max_v_sum + 1, INF);
    dp[0] = 0;

    for (int i = 0; i < n; ++i) {
        for (ll v = max_v_sum; v >= values[i]; --v) {
            if (dp[v - values[i]] != INF) {
                dp[v] = min(dp[v], dp[v - values[i]] + weights[i]);
            }
        }
    }

    ll res = 0;
    for (ll v = 0; v <= max_v_sum; ++v) {
        if (dp[v] <= W) res = max(res, v);
    }
    return res;
}

▼ 使い方と応用パターン(カスタマイズ実装)

基本の使い方 (0-1ナップサック 重さDP / 価値DP)
// 【重さDP の使い方】容量 W が小さい場合の基本
ll N = 4;
ll W = 10; 
vector<ll> weights = {2, 1, 3, 2};
vector<ll> values = {3, 2, 4, 2};

ll ans1 = knapsack_weight_dp(weights, values, W);
cout << "最大価値 (重さDP): " << ans1 << endl;

// 【価値DP の使い方】重さ W_i が大きく、価値 V_i が小さい場合
ll W_large = 1000000000; 
vector<ll> weights_large = {200000000, 100000000, 300000000, 200000000};
// 価値は上の values を流用

ll ans2 = knapsack_value_dp(weights_large, values, W_large);
cout << "最大価値 (価値DP): " << ans2 << endl;
【派生】個数制限付きナップサック (Bounded Knapsack) の実装と使い方
// 各品物に「最大K個まで」という個数制限がある場合 (O(NW log K))
// 2のべき乗に分割し、0-1ナップサックに帰着させるカスタマイズ
ll bounded_knapsack(const vector<ll>& weights, const vector<ll>& values, const vector<ll>& counts, ll W) {
    vector<ll> w_01, v_01;
    int n = weights.size();
    
    // 品物を2のべき乗個に分割
    for (int i = 0; i < n; ++i) {
        ll rem = counts[i];
        for (ll k = 1; rem > 0; k <<= 1) {
            ll take = min(k, rem);
            w_01.push_back(weights[i] * take);
            v_01.push_back(values[i] * take);
            rem -= take;
        }
    }

    // 分割した品物で通常の重さDP(0-1 ナップサック)を実行
    vector<ll> dp(W + 1, 0);
    for (size_t i = 0; i < w_01.size(); ++i) {
        for (ll w = W; w >= w_01[i]; --w) {
            dp[w] = max(dp[w], dp[w - w_01[i]] + v_01[i]);
        }
    }
    return dp[W];
}

// ----------------------
// 使い方
// ----------------------
/*
ll W = 10;
vector<ll> weights = {3, 4, 2};
vector<ll> values = {4, 5, 3};
vector<ll> counts = {2, 1, 3}; // 選べる上限個数

ll ans = bounded_knapsack(weights, values, counts, W);
cout << "最大価値 (個数制限あり): " << ans << endl; 
*/
【派生】個数制限なしナップサック (Unbounded Knapsack) の実装と使い方
// 各品物を「何個でも」選んでよい場合 (O(NW))
// DPの遷移(ループ方向)を逆にして複数回選択を許容するカスタマイズ
ll unbounded_knapsack(const vector<ll>& weights, const vector<ll>& values, ll W) {
    int n = weights.size();
    vector<ll> dp(W + 1, 0);
    
    for (int i = 0; i < n; ++i) {
        // 0-1ナップサックとは異なり、「前から」更新する
        for (ll w = weights[i]; w <= W; ++w) {
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }
    return dp[W];
}

// ----------------------
// 使い方
// ----------------------
/*
ll W = 10;
vector<ll> weights = {3, 4, 2};
vector<ll> values = {4, 5, 3};

ll ans = unbounded_knapsack(weights, values, W);
cout << "最大価値 (制限なし): " << ans << endl; 
*/
【派生】選ぶ品数が K 個「以下」のナップサック問題
// 使う品数を K 個「以下」に制限したナップサック問題 (O(NKW))
// dp[k][w] : ちょうど k 個選んで重さ合計が w のときの価値の最大値
ll knapsack_at_most_k_items(const vector<ll>& weights, const vector<ll>& values, ll W, int K) {
    int n = weights.size();
    const ll MIN_INF = -1e18; // 到達不可能な状態
    // 2次元DP配列 (K+1 x W+1)
    vector<vector<ll>> dp(K + 1, vector<ll>(W + 1, MIN_INF));
    dp[0][0] = 0;

    for (int i = 0; i < n; ++i) {
        // 同じ品物を複数回使わないため、個数(k)も重さ(w)も後ろから更新
        for (int k = K; k >= 1; --k) {
            for (ll w = W; w >= weights[i]; --w) {
                if (dp[k - 1][w - weights[i]] != MIN_INF) {
                    dp[k][w] = max(dp[k][w], dp[k - 1][w - weights[i]] + values[i]);
                }
            }
        }
    }

    ll max_val = 0; // 0個選ぶ場合(価値0)が保証されるため初期値は0
    // 0個からK個までのすべての状態から最大値を探索
    for (int k = 0; k <= K; ++k) {
        for (ll w = 0; w <= W; ++w) {
            max_val = max(max_val, dp[k][w]);
        }
    }
    return max_val;
}
【派生】選ぶ品数が「ちょうど」 K 個のナップサック問題
// 使う品数を「ちょうど」 K 個に制限したナップサック問題 (O(NKW))
// 状態遷移の構造は「K個以下」と同様ですが、最終的な集計対象が限定されます
ll knapsack_exact_k_items(const vector<ll>& weights, const vector<ll>& values, ll W, int K) {
    int n = weights.size();
    const ll MIN_INF = -1e18;
    vector<vector<ll>> dp(K + 1, vector<ll>(W + 1, MIN_INF));
    dp[0][0] = 0;

    for (int i = 0; i < n; ++i) {
        for (int k = K; k >= 1; --k) {
            for (ll w = W; w >= weights[i]; --w) {
                if (dp[k - 1][w - weights[i]] != MIN_INF) {
                    dp[k][w] = max(dp[k][w], dp[k - 1][w - weights[i]] + values[i]);
                }
            }
        }
    }

    ll max_val = MIN_INF;
    // ちょうど K 個選んだ状態 (dp[K][*]) のみから最大値を探索
    for (ll w = 0; w <= W; ++w) {
        max_val = max(max_val, dp[K][w]);
    }
    
    // ちょうどK個選んで重さW以下に収める方法が存在しない場合は -1 などを返す
    return (max_val == MIN_INF) ? -1 : max_val;
}
【派生】重みが超増加数列の場合 (O(N))
// 重みが超増加数列(w[i] > w[0] + ... + w[i-1])を満たすナップサック問題
// 重さ順にソート済み(昇順)であることを前提とします。
// 価値(V)が任意に設定されていても、O(N) で厳密解を求めることができます。
ll knapsack_superincreasing(const vector<ll>& weights, const vector<ll>& values, ll W) {
    int n = weights.size();
    if (n == 0) return 0;

    // 価値の累積和を前計算(i-1 番目以下を「すべて選んだ」ときの価値を O(1) で出すため)
    vector<ll> pref_v(n, 0);
    pref_v[0] = values[0];
    for (int i = 1; i < n; ++i) {
        pref_v[i] = pref_v[i - 1] + values[i];
    }

    // 再帰的に後ろ(重い方)から評価する
    auto dfs = [&](auto self, int i, ll r) -> ll {
        if (i < 0) return 0;
        
        // 荷物 i が容量オーバーで入らない場合は、選ばない一択
        if (r < weights[i]) {
            return self(self, i - 1, r);
        }
        
        // 【荷物 i が入る場合】
        // 1. i を「選ばない」場合
        // 超増加数列の性質より r >= weights[i] > (0番目〜i-1番目の重さの総和) となるため、
        // i-1番目以下の荷物を「すべて」入れることができる。
        ll not_pick = (i > 0) ? pref_v[i - 1] : 0;
        
        // 2. i を「選ぶ」場合
        ll pick = values[i] + self(self, i - 1, r - weights[i]);
        
        return max(not_pick, pick);
    };

    return dfs(dfs, n - 1, W);
}
【派生】半分全列挙 / Meet in the middle (O(2^(N/2) N))
// N <= 40 程度で、重さ W も価値 V も巨大な場合の 0-1 ナップサック。
// 全体を半分に分け、それぞれの部分集合の全探索結果を二分探索で結合する。
ll knapsack_meet_in_the_middle(const vector& weights, const vector& values, ll W) {
    int n = weights.size();
    int n1 = n / 2;
    int n2 = n - n1;

    // 1. 前半の全列挙 (最大 2^20 通り)
    vector> half1;
    for (int bit = 0; bit < (1 << n1); ++bit) {
        ll w = 0, v = 0;
        for (int i = 0; i < n1; ++i) {
            if ((bit >> i) & 1) { w += weights[i]; v += values[i]; }
        }
        half1.push_back({w, v});
    }

    // 2. 後半の全列挙 (最大 2^20 通り)
    vector> half2;
    for (int bit = 0; bit < (1 << n2); ++bit) {
        ll w = 0, v = 0;
        for (int i = 0; i < n2; ++i) {
            if ((bit >> i) & 1) { w += weights[n1 + i]; v += values[n1 + i]; }
        }
        half2.push_back({w, v});
    }

    // 3. 後半を重さでソートし、「重いのに価値が低い」無駄な状態を排除して単調増加にする
    sort(half2.begin(), half2.end());
    vector> filtered_half2;
    ll max_v = -1;
    for (auto p : half2) {
        if (max_v < p.second) {
            filtered_half2.push_back(p);
            max_v = p.second;
        }
    }

    // 4. 前半の各状態に対して、残りの容量で最適な後半の状態を二分探索
    ll ans = 0;
    for (auto p : half1) {
        ll w1 = p.first;
        ll v1 = p.second;
        if (w1 > W) continue;

        ll rem_w = W - w1;
        // rem_w 以下の最大の重さを持つ要素を検索
        auto it = upper_bound(filtered_half2.begin(), filtered_half2.end(), make_pair(rem_w, (ll)2e18));
        if (it != filtered_half2.begin()) {
            --it; // 条件を満たす最大の要素
            ans = max(ans, v1 + it->second);
        }
    }
    return ans;
}
【派生】重みの種類が少ない場合の 半分全列挙 / Meet in the middle
// 重みの種類数 K が少ない(4〜6種類程度)が、全体の個数 N や容量 W が大きく、
// DP が使えない場合に非常に有効なアルゴリズム。
// 同じ重さのアイテムを価値降順で累積和を取り、種類を半分に分けて全列挙し、二分探索で結合します。

// w : 各種類の重さ (サイズ K)
// v : 各種類のアイテムの価値リスト (サイズ K、v[i] は種類 i のアイテムの価値配列)
ll knapsack_few_weights_mitm(const vector& w, vector>& v, ll W) {
    int K = w.size();
    if (K == 0) return 0;

    // 1. 各種類ごとに価値を降順ソートし、累積和を計算
    vector> pref_v(K);
    for (int i = 0; i < K; ++i) {
        sort(v[i].rbegin(), v[i].rend());
        pref_v[i].assign(v[i].size() + 1, 0);
        for (size_t j = 0; j < v[i].size(); ++j) {
            pref_v[i][j + 1] = pref_v[i][j] + v[i][j];
        }
    }

    // 状態を列挙する再帰関数 (DFS)
    auto dfs = [&](auto self, int idx, int end_idx, ll current_w, ll current_v, vector>& res) -> void {
        if (idx == end_idx) {
            res.push_back({current_w, current_v});
            return;
        }
        // 種類 idx を j 個選ぶ場合を全て試す
        for (size_t j = 0; j < pref_v[idx].size(); ++j) {
            if (current_w + w[idx] * j > W) break; // 容量オーバーなら枝刈り
            self(self, idx + 1, end_idx, current_w + w[idx] * j, current_v + pref_v[idx][j], res);
        }
    };

    // 2. 前半と後半に分けて状態を全列挙
    int mid = K / 2;
    vector> half1, half2;
    dfs(dfs, 0, mid, 0, 0, half1);
    dfs(dfs, mid, K, 0, 0, half2);

    // 3. 後半を重さで昇順ソートし、「重いのに価値が低い」無駄な状態を排除 (パレート最適)
    sort(half2.begin(), half2.end());
    vector> filtered_half2;
    ll max_v = -1;
    for (auto& p : half2) {
        if (max_v < p.second) {
            filtered_half2.push_back(p);
            max_v = p.second;
        }
    }

    // 4. 前半の各状態に対して、残りの容量で最適な後半の状態を二分探索
    ll ans = 0;
    for (auto& p : half1) {
        ll w1 = p.first;
        ll v1 = p.second;
        if (w1 > W) continue;

        ll rem_w = W - w1;
        // rem_w 以下の最大の重さを持つ要素を検索
        auto it = upper_bound(filtered_half2.begin(), filtered_half2.end(), make_pair(rem_w, (ll)3e18));
        if (it != filtered_half2.begin()) {
            --it; // 条件を満たす最大の要素
            ans = max(ans, v1 + it->second);
        }
    }
    return ans;
}
【派生】重みの幅が非常に狭い場合 (C <= w_i <= C + D)
// すべての重さが C ≦ w_i ≦ C + D (Dは5など非常に小さい) の場合。 (O(N^3 D))
// 「選んだ個数 k」と「基準値 C からのはみ出した重さの合計 s」を状態に持ちます。
// 重さが 10^9 のように巨大で、かつ DP が使えない場合に有効です。
ll knapsack_narrow_weight_range(const vector<ll>& weights, const vector<ll>& values, ll W) {
    int n = weights.size();
    if (n == 0) return 0;
    
    ll C = *min_element(weights.begin(), weights.end());
    ll max_w = *max_element(weights.begin(), weights.end());
    ll D = max_w - C; // 重みの幅 (例: 5 程度を想定)
    
    // dp[k][s] : ちょうど k 個選んで、(重さの合計 - k*C) が s となるときの最大価値
    int max_s = n * D;
    const ll MIN_INF = -1e18;
    vector<vector<ll>> dp(n + 1, vector<ll>(max_s + 1, MIN_INF));
    dp[0][0] = 0;
    
    for (int i = 0; i < n; ++i) {
        ll d = weights[i] - C; // はみ出した重さ (0 〜 D)
        // 同じものを複数回選ばないよう、k も s も後ろから更新
        for (int k = i; k >= 0; --k) {
            for (int s = max_s; s >= d; --s) {
                if (dp[k][s - d] != MIN_INF) {
                    dp[k + 1][s] = max(dp[k + 1][s], dp[k][s - d] + values[i]);
                }
            }
        }
    }
    
    ll ans = 0;
    for (int k = 0; k <= n; ++k) {
        for (int s = 0; s <= max_s; ++s) {
            if (dp[k][s] != MIN_INF) {
                ll total_weight = (ll)k * C + s;
                if (total_weight <= W) {
                    ans = max(ans, dp[k][s]);
                }
            }
        }
    }
    return ans;
}

// ----------------------
// 使い方
// ----------------------
/*
ll W = 3000000008; // 巨大な容量
vector<ll> weights = {1000000000, 1000000002, 1000000005, 1000000003}; // 幅 D = 5
vector<ll> values = {10, 15, 25, 20};

ll ans = knapsack_narrow_weight_range(weights, values, W);
cout << "最大価値 (幅が狭い): " << ans << endl;
*/
【派生】Wが極めて大きい個数制限なしナップサック (鳩の巣原理による微調整)
// W が巨大 (例: 10^18) だが、重さの最大値 w_max が小さい (例: 1000 以下) 場合 (O(N log N + N * w_max^2))
// 鳩の巣原理により、大半の容量をコスパ最強の品物で貪欲に埋め、残り (w_max^2 程度) をDPで微調整します。
ll unbounded_knapsack_huge_w(const vector<ll>& weights, const vector<ll>& values, ll W) {
    int n = weights.size();
    if (n == 0) return 0;

    ll max_w = 0;
    int best_idx = 0;
    for (int i = 0; i < n; ++i) {
        max_w = max(max_w, weights[i]);
        // コスパ比較 (割り算の誤差を避けるため乗算で比較): v[i]/w[i] > v[best]/w[best]
        ll lhs = values[i] * weights[best_idx];
        ll rhs = values[best_idx] * weights[i];
        if (lhs > rhs || (lhs == rhs && weights[i] < weights[best_idx])) {
            best_idx = i; // コスパが同じなら重さが小さい方を優先
        }
    }

    // 鳩の巣原理に基づく微調整の上限 (w_max * w_max)
    ll threshold = max_w * max_w;
    ll base_val = 0;
    ll rem_w = W;

    // 容量 W が閾値より十分大きい場合、W - threshold 分をコスパ最強品物で埋める
    if (W > threshold) {
        ll take_cnt = (W - threshold) / weights[best_idx];
        base_val = take_cnt * values[best_idx];
        rem_w = W - take_cnt * weights[best_idx];
    }

    // 残りの容量 rem_w (最大でも threshold + w_max 程度) で通常の個数制限なしDP
    vector<ll> dp(rem_w + 1, 0);
    for (int i = 0; i < n; ++i) {
        for (ll w = weights[i]; w <= rem_w; ++w) {
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }

    return base_val + dp[rem_w];
}

// ----------------------
// 使い方
// ----------------------
/*
ll W = 1000000000000000000LL; // 10^18
vector<ll> weights = {400, 500, 300}; // w_max = 500
vector<ll> values = {401, 500, 299};

ll ans = unbounded_knapsack_huge_w(weights, values, W);
cout << "最大価値 (W巨大): " << ans << endl;
*/
【派生】重みが倍数制約を満たす個数制限なしナップサック (完全貪欲法)
// 品物の重さが「w_1 は w_2 の約数、w_2 は w_3 の約数…」という倍数関係にある場合 (O(N log N))
// DPすら不要で、コスパ(価値 / 重さ)順の貪欲法が常に数学的な最適解になります。
// (※ここでは何個でも選べる「個数制限なし」を前提としています)

struct DivisibleItem { ll w, v; };

ll knapsack_divisible(const vector<ll>& weights, const vector<ll>& values, ll W) {
    int n = weights.size();
    vector<DivisibleItem> items(n);
    for (int i = 0; i < n; ++i) {
        items[i] = {weights[i], values[i]};
    }

    // コスパ(価値 / 重さ)の降順にソート
    sort(items.begin(), items.end(), [](const DivisibleItem& a, const DivisibleItem& b) {
        ll lhs = a.v * b.w;
        ll rhs = b.v * a.w;
        if (lhs != rhs) return lhs > rhs;
        return a.w > b.w; // コスパが同じなら重さが大きい方を優先
    });

    ll ans = 0;
    ll rem_w = W;

    for (int i = 0; i < n; ++i) {
        if (rem_w == 0) break;
        
        // 入るだけ限界まで貪欲に選ぶ
        ll take = rem_w / items[i].w;
        ans += take * items[i].v;
        rem_w -= take * items[i].w;
    }

    return ans;
}

// ----------------------
// 使い方
// ----------------------
/*
ll W = 100;
// 重みが 1, 2, 4, 8... などの倍数関係を満たしていること
vector<ll> weights = {1, 2, 4, 8};
vector<ll> values = {2, 5, 9, 20};

ll ans = knapsack_divisible(weights, values, W);
cout << "最大価値 (倍数制約): " << ans << endl;
*/

部分和問題 (基本と高速化)

用途: 与えられた数値の集合からいくつかを選び、目標の和(S)を作れるか判定する

計算量: 基本は O(NS)。
【応用】要素の総和が S 程度に抑えられている場合は、種類数に注目したダブリング処理により O(S √S) に高速化可能。さらに std::bitset を用いることで O(S √S / 64) まで定数倍高速化できます。また、同じ要素の複数回使用を許可する場合のバリエーションも存在します。

// 【基本構造】通常の部分和問題 (O(NS))
// dp[i] : 和 i を作れるかどうかの真偽値
vector<bool> subset_sum_basic(const vector<ll>& A, ll S) {
    int n = A.size();
    vector<bool> dp(S + 1, false);
    dp[0] = true; // 0は何も選ばなければ作れる

    for (int i = 0; i < n; ++i) {
        // 同じ要素を複数回選ばないよう、後ろから更新する
        for (ll j = S; j >= A[i]; --j) {
            if (dp[j - A[i]]) {
                dp[j] = true;
            }
        }
    }
    return dp;
}

▼ 使い方と応用パターン(カスタマイズ実装)

基本の使い方 (普通の部分和問題)
ll S = 10;
vector<ll> A = {2, 3, 7};

// DPテーブルを取得
vector<bool> dp = subset_sum_basic(A, S);

if (dp[S]) {
    cout << S << " は作れます!" << endl;
} else {
    cout << S << " は作れません。" << endl;
}
【応用】通常の部分和問題の bitset による高速化 (O(NS / 64))
// 通常の部分和問題を std::bitset で定数倍高速化するテンプレート
// ※和の最大値 MAX_S がコンパイル時に定数として決まっている必要があります。
template<size_t MAX_S>
bitset<MAX_S> subset_sum_bitset(const vector<ll>& A) {
    bitset<MAX_S> dp;
    dp[0] = 1; // 0は何も選ばなければ作れる

    for (ll a : A) {
        // 現在作れる和の集合を a だけずらして(足して)統合する
        dp |= (dp << a);
    }
    return dp;
}

// ----------------------
// 使い方
// ----------------------
/*
const int MAX_S = 100005;
vector<ll> A = {2, 3, 7};

// DP状態(bitset)を取得
bitset<MAX_S> dp = subset_sum_bitset<MAX_S>(A);

ll target = 10;
if (target < MAX_S && dp[target]) {
    cout << target << " は作れます!" << endl;
} else {
    cout << target << " は作れません。" << endl;
}
*/
【応用】個数制限なし部分和問題 (同じ要素を何度でも使える)
// 各要素を何個でも選べる部分和問題 (O(NS))
vector<bool> subset_sum_unbounded(const vector<ll>& A, ll S) {
    int n = A.size();
    vector<bool> dp(S + 1, false);
    dp[0] = true;

    for (int i = 0; i < n; ++i) {
        // 同じ要素を何度でも選べるよう、前から(昇順に)更新する
        for (ll j = A[i]; j <= S; ++j) {
            if (dp[j - A[i]]) {
                dp[j] = true;
            }
        }
    }
    return dp;
}
【応用】個数制限あり部分和問題 (各要素に使える上限回数がある)
// 各要素 a が m 個まで選べる部分和問題 (O(NS))
// A : 要素の値の配列、M : 各要素の使える上限個数の配列
vector<bool> subset_sum_bounded(const vector<ll>& A, const vector<ll>& M, ll S) {
    int n = A.size();
    vector<bool> dp(S + 1, false);
    dp[0] = true;

    for (int i = 0; i < n; ++i) {
        // 現在の要素 i を使った個数を記録する配列
        vector<ll> count(S + 1, 0);
        
        for (ll j = A[i]; j <= S; ++j) {
            // j がまだ作れておらず、j - A[i] が作れていて、かつ要素 i の使用回数が M[i] 未満なら使う
            if (!dp[j] && dp[j - A[i]] && count[j - A[i]] < M[i]) {
                dp[j] = true;
                count[j] = count[j - A[i]] + 1;
            }
        }
    }
    return dp;
}
【応用】選ぶ要素数が K 個「以下」の部分和問題
// 使う要素数を K 個「以下」に制限した部分和問題 (O(NS))
// 真偽値ではなく「和 i を作るために必要な最小の要素数」を DP で管理する
bool subset_sum_max_k_items(const vector<ll>& A, ll S, ll K) {
    const ll INF = 1e18;
    vector<ll> dp(S + 1, INF);
    dp[0] = 0; // 0を作るための要素数は0

    for (ll a : A) {
        // 同じ要素を複数回使わないため後ろから更新
        for (ll j = S; j >= a; --j) {
            if (dp[j - a] != INF) {
                dp[j] = min(dp[j], dp[j - a] + 1);
            }
        }
    }
    // S を作るための最小要素数が K 以下であれば作れる
    return dp[S] <= K;
}
【応用】選ぶ要素数が「ちょうど」 K 個の部分和問題
// 使う要素数を「ちょうど」 K 個に制限した部分和問題 (O(NSK))
// dp[i][k] : 和 i をちょうど k 個の要素で作れるかどうかの真偽値
bool subset_sum_exact_k_items(const vector<ll>& A, ll S, int K) {
    // 2次元配列で管理
    vector<vector<bool>> dp(S + 1, vector<bool>(K + 1, false));
    dp[0][0] = true;

    for (ll a : A) {
        // 同じ要素を複数回使わないため、和も個数も後ろから更新
        for (ll i = S; i >= a; --i) {
            for (int k = K; k >= 1; --k) {
                if (dp[i - a][k - 1]) {
                    dp[i][k] = true;
                }
            }
        }
    }
    // 和 S をちょうど K 個で作れるか
    return dp[S][K];
}
【応用】要素の総和が抑えられた場合の高速化 (O(S √S))
// 与えられる要素群 A の中に同じ数値が大量に含まれており、
// かつ A の要素の総和が S 付近である場合に極めて有効
vector<bool> subset_sum_bounded_total(const vector<ll>& A, ll S) {
    map<ll, ll> counts;
    for (ll a : A) {
        if (a <= S) counts[a]++;
    }

    vector<ll> w_01;
    for (auto& p : counts) {
        ll val = p.first;
        ll rem = p.second;
        // 個数制限付きナップサック問題のダブリングテクニック
        for (ll k = 1; rem > 0; k <<= 1) {
            ll take = min(k, rem);
            w_01.push_back(val * take);
            rem -= take;
        }
    }

    vector<bool> dp(S + 1, false);
    dp[0] = true;
    for (ll w : w_01) {
        for (ll i = S; i >= w; --i) {
            if (dp[i - w]) dp[i] = true;
        }
    }
    return dp;
}

// ----------------------
// 使い方
// ----------------------
/*
ll S = 15;
vector<ll> A = {1, 1, 1, 1, 1, 2, 2, 3, 3}; 

vector<bool> dp = subset_sum_bounded_total(A, S);
ll target = 10;
if (target <= S && dp[target]) cout << "Yes" << endl;
*/
【応用】bitset でさらなる定数倍高速化 (O(S √S / 64))
// 部分和問題は std::bitset を用いると劇的に定数倍高速化できます。
// ※定数 S_MAX がコンパイル時に決まっている(マクロ等で定義されている)必要があります。

/*
const int S_MAX = 100005;

// DP更新部分を以下に置き換える (O(NS/64) または O(S√S/64))
bitset<S_MAX> dp;
dp[0] = 1;

// 通常の部分和なら A、ダブリング済みなら w_01 を回す
for (ll w : A) { 
    // 現在作れる状態をまるごと左シフトして OR を取るだけ
    dp |= (dp << w);
}

// 判定時は dp[target] が 1 かどうかを見る
*/

メモ化再帰 (Memoized Recursion)

基本的なメモ化再帰

用途: トップダウンアプローチによる状態遷移の効率化。一度計算した状態を配列に記録し、再利用する。

計算量: O(状態数 × 遷移数)
設計: 再帰関数の中で、既に計算済み(メモ配列に有効な値がある)なら即座にその値を返す。未計算なら計算してメモ配列に記録してから返す。

vector<long long> memo(n + 1, -1);

long long fib(ll n) {
    if (n <= 1) return n;
    if (memo[n] != -1) return memo[n];
    return memo[n] = fib(n - 1) + fib(n - 2);
}

注意: メモ配列の初期値(未訪問を示す値)は、あり得る計算結果と被らない値(-1 や INF)にする。再帰の深さが深くなる場合、スタックオーバーフローに注意する。

グリッド・グラフ上のメモ化再帰

用途: DAG(有向非巡回グラフ)上の最長経路問題や、グリッドの移動経路数・最小コスト計算など。

計算量: O(H × W) または O(V + E)
設計: 各マスや頂点における最適値を保持するメモ配列を用意する。ベースケース(ゴールや場外)を適切に処理し、各方向への再帰呼び出しの結果を集計する。

ll H, W;
vector<vector<ll>> grid;
vector<vector<long long>> memo;

long long dfs(ll r, ll c) {
    if (r == H - 1 && c == W - 1) return 1; // ゴールに到達
    if (memo[r][c] != -1) return memo[r][c];
    
    long long res = 0;
    if (c + 1 < W) res += dfs(r, c + 1); // 右へ移動
    if (r + 1 < H) res += dfs(r + 1, c); // 下へ移動
    
    return memo[r][c] = res;
}

注意: 探索グラフに閉路(サイクル)が含まれている場合、無限ループになるため、DAG(有向非巡回グラフ)であることを確認するか、訪問中フラグ(行きがけ順・帰りがけ順の管理)を導入する。

mapを用いた動的メモ化再帰

用途: 状態のインデックスが巨大(例: N ≤ 10^9)な場合や、状態の組み合わせが疎(スパース)で配列を確保できない場合。

計算量: O(状態数 × log(状態数) × 遷移数)
設計: メモの管理に配列ではなく std::map や std::unordered_map を使用する。count() や find() で計算済みかを判定する。

map<long long, long long> memo;

long long solve(long long n) {
    if (n == 0) return 0;
    if (memo.count(n)) return memo[n];
    
    // 遷移例: solve(n) = solve(n / 2) + solve(n / 3) + n
    long long res = solve(n / 2) + solve(n / 3) + n;
    return memo[n] = res;
}

注意: std::map を使用するとアクセスごとに対数時間(O(log M))がかかるため、通常の配列よりも定数倍が重くなる。時間制限が厳しい場合は std::unordered_map の使用や、座標圧縮を検討する。

畳み込み

数論変換 (NTT: Number Theoretic Transform)

用途: 剰余環上での畳み込みを O(N log N) で高速に行う

計算量: O((N + M) log (N + M))
前提: #include <atcoder/all> が必要。
(任意modの畳み込みが求められる場合は、ACLのconvolution_llの使用も検討するべき。)

#include <atcoder/all>
using namespace std;
using namespace atcoder;
using mint = modint998244353;

▼ 使い方

基本的な使い方 (ACLの利用)
vector<mint> a = {1, 2, 3}; // 1 + 2x + 3x^2
vector<mint> b = {4, 5, 6}; // 4 + 5x + 6x^2

// ACL の convolution を呼び出すだけで畳み込みが完了
vector<mint> c = convolution(a, b);

// 結果の出力
for (int i = 0; i < c.size(); i++) {
    cout << c[i].val() << (i == c.size() - 1 ? "" : " ");
}
// 出力: 4 13 28 27 18

高速フーリエ変換 (FFT: Fast Fourier Transform)

用途: 浮動小数点数(実数・複素数)での畳み込みを O(N log N) で行う

計算量: O((N + M) log (N + M))
前提: <complex>

#include <complex>
#include <cmath>
using C = complex<double>;

void fft(vector<C>& a, bool invert) {
    int n = a.size();
    for (int i = 1, j = 0; i < n; i++) {
        int bit = n >> 1;
        for (; j & bit; bit >>= 1) j ^= bit;
        j ^= bit;
        if (i < j) swap(a[i], a[j]);
    }
    for (int len = 2; len <= n; len <<= 1) {
        double ang = 2 * M_PI / len * (invert ? -1 : 1);
        C wlen(cos(ang), sin(ang));
        for (int i = 0; i < n; i += len) {
            C w(1);
            for (int j = 0; j < len / 2; j++) {
                C u = a[i + j];
                C v = a[i + j + len / 2] * w;
                a[i + j] = u + v;
                a[i + j + len / 2] = u - v;
                w *= wlen;
            }
        }
    }
    if (invert) {
        for (C& x : a) x /= n;
    }
}

vector<double> convolution_fft(const vector<double>& a, const vector<double>& b) {
    vector<C> fa(a.begin(), a.end()), fb(b.begin(), b.end());
    int n = 1;
    while (n < a.size() + b.size()) n <<= 1;
    fa.resize(n); fb.resize(n);
    
    fft(fa, false);
    fft(fb, false);
    for (int i = 0; i < n; i++) fa[i] *= fb[i];
    fft(fa, true);
    
    vector<double> res(n);
    for (int i = 0; i < n; i++) res[i] = fa[i].real();
    return res;
}

▼ 使い方

基本的な使い方 (実数の畳み込み)
vector<double> a = {1.0, 2.0, 3.0};
vector<double> b = {4.0, 5.0, 6.0};

// FFTを用いた畳み込み
vector<double> c = convolution_fft(a, b);

// 出力 (浮動小数点による誤差が含まれるため四捨五入などで補正するとよい)
for (int i = 0; i < a.size() + b.size() - 1; i++) {
    cout << round(c[i]) << (i == a.size() + b.size() - 2 ? "" : " ");
}
// 出力: 4 13 28 27 18

高速ゼータ変換・メビウス変換 (FZT / FMT)

用途: 集合の包含関係に基づく Bitwise OR / AND 畳み込み・包除原理・モノイド累積

計算量: $O(N 2^N)$ (配列サイズ $K = 2^N$ のとき $O(K \log K)$)
前提: 配列のサイズは 2 のべき乗である必要がある。ゼータ変換は可換モノイド、メビウス変換は可換群、畳み込みは分配法則が成り立つ環構造で動作します。

// 下位集合に関する高速ゼータ変換 (OR畳み込みの前処理)
// g[S] = sum_{T ⊆ S} f[T]
// 【モノイド拡張】可換モノイド op(a, b) で置き換え可能 (例: max, min など)
template <typename T>
void fast_zeta_transform_or(vector<T>& a) {
    int n = a.size();
    for (int i = 1; i < n; i <<= 1) {
        for (int j = 0; j < n; j++) {
            if (!(j & i)) {
                // --- 変更ポイント ---
                a[j | i] += a[j];
                // 可換モノイド拡張例 (max): a[j | i] = max(a[j | i], a[j]);
                // --------------------
            }
        }
    }
}

// 下位集合に関する高速メビウス変換 (OR畳み込みの後処理 / 逆変換)
// 【群拡張】逆演算 inv_op(a, b) が必要 (min/max 等は逆演算がないため不可)
template <typename T>
void fast_mobius_transform_or(vector<T>& a) {
    int n = a.size();
    for (int i = 1; i < n; i <<= 1) {
        for (int j = 0; j < n; j++) {
            if (!(j & i)) {
                // --- 変更ポイント ---
                a[j | i] -= a[j];
                // 逆演算拡張例: a[j | i] = inv_op(a[j | i], a[j]);
                // --------------------
            }
        }
    }
}

// Bitwise OR 畳み込み: c[k] = sum_{i | j = k} a[i] * b[j]
// 【環拡張】外側演算(ゼータ/メビウス)と内側演算(積)に分配法則が必要
template <typename T>
vector<T> bitwise_or_convolution(vector<T> a, vector<T> b) {
    fast_zeta_transform_or(a);
    fast_zeta_transform_or(b);
    for (size_t i = 0; i < a.size(); i++) {
        // --- 変更ポイント ---
        a[i] *= b[i];
        // 独自の積演算: a[i] = mul(a[i], b[i]);
        // --------------------
    }
    fast_mobius_transform_or(a);
    return a;
}

// 上位集合に関する高速ゼータ変換 (AND畳み込みの前処理)
// g[S] = sum_{S ⊆ T} f[T]
// 【モノイド拡張】可換モノイド op(a, b) で置き換え可能
template <typename T>
void fast_zeta_transform_and(vector<T>& a) {
    int n = a.size();
    for (int i = 1; i < n; i <<= 1) {
        for (int j = 0; j < n; j++) {
            if (!(j & i)) {
                // --- 変更ポイント ---
                a[j] += a[j | i];
                // 可換モノイド拡張例 (min): a[j] = min(a[j], a[j | i]);
                // --------------------
            }
        }
    }
}

// 上位集合に関する高速メビウス変換 (AND畳み込みの後処理 / 包除原理)
// 【群拡張】逆演算 inv_op(a, b) が必要
template <typename T>
void fast_mobius_transform_and(vector<T>& a) {
    int n = a.size();
    for (int i = 1; i < n; i <<= 1) {
        for (int j = 0; j < n; j++) {
            if (!(j & i)) {
                // --- 変更ポイント ---
                a[j] -= a[j | i];
                // 逆演算拡張例: a[j] = inv_op(a[j], a[j | i]);
                // --------------------
            }
        }
    }
}

// Bitwise AND 畳み込み: c[k] = sum_{i & j = k} a[i] * b[j]
template <typename T>
vector<T> bitwise_and_convolution(vector<T> a, vector<T> b) {
    fast_zeta_transform_and(a);
    fast_zeta_transform_and(b);
    for (size_t i = 0; i < a.size(); i++) {
        // --- 変更ポイント ---
        a[i] *= b[i];
        // --------------------
    }
    fast_mobius_transform_and(a);
    return a;
}

▼ 使い方・適用例

1. 【基礎】下位集合 (OR側) : 部分集合の総和と復元
// f[S] を「ちょうど集合 S」の値とする (サイズは 2^N)
vector<long long> f_sub = {1, 2, 3, 4}; // N=2 (S=0, 1, 2, 3)

// --- 高速ゼータ変換 ---
// g[S] = sum_{T ⊆ S} f[T] (部分集合の総和)
fast_zeta_transform_or(f_sub);
// 変換後: f_sub[3] (集合 {0,1}) = f[0] + f[1] + f[2] + f[3] = 10

// --- 高速メビウス変換 ---
// ゼータ変換後の配列を元の f[S] ("ちょうど集合 S" の値) に復元
fast_mobius_transform_or(f_sub);
// 復元後: f_sub[3] = 4 に戻る
2. 【基礎】上位集合 (AND側) : 包除原理・超集合の総和
// f_sup[S] を「少なくとも集合 S を含む条件を満たす通り数」として前計算
vector<long long> f_sup = {100, 50, 40, 10}; // N=2

// --- 高速メビウス変換 ---
// "少なくとも S" から "ちょうど S" の通り数を求める (包除原理)
fast_mobius_transform_and(f_sup);
// 変換後: f_sup[S] には「"ちょうど" 集合 S を満たす値」が入る

// ※ 逆に "ちょうど S" から "少なくとも S" を作る場合はゼータ変換を使用
// fast_zeta_transform_and(f_sup);
3. 【基礎】Bitwise OR 畳み込みの使い方
#include <atcoder/modint>
using mint = atcoder::modint998244353;

// サイズは 2 の冪乗にする (例: ビット数 2 -> サイズ 4)
vector<mint> a = {1, 2, 0, 3}; 
vector<mint> b = {4, 1, 2, 0};

// Bitwise OR 畳み込みの実行
// c[k] = sum_{i | j = k} a[i] * b[j]
vector<mint> c = bitwise_or_convolution(a, b);

for (int i = 0; i < c.size(); i++) {
    cout << "c[" << i << "] = " << c[i].val() << "\n";
}
4. 【基礎】Bitwise AND 畳み込みの使い方
#include <atcoder/modint>
using mint = atcoder::modint998244353;

vector<mint> a = {1, 2, 0, 3}; 
vector<mint> b = {4, 1, 2, 0};

// Bitwise AND 畳み込みの実行
// c[k] = sum_{i & j = k} a[i] * b[j]
vector<mint> c = bitwise_and_convolution(a, b);

for (int i = 0; i < c.size(); i++) {
    cout << "c[" << i << "] = " << c[i].val() << "\n";
}
5. 【応用】可換モノイド単体利用(例: 部分集合における max 累積)
// fast_zeta_transform_or 内の `a[j | i] += a[j]` を
// `a[j | i] = max(a[j | i], a[j])` に変更する

vector<long long> f = {3, 1, 4, 1, 5, 9, 2, 6}; // N=3

// 変更後のゼータ変換を実行すると:
// f[S] は max_{T ⊆ S} f[T] (部分集合における最大値) に更新される
// 例: f[3] (集合 {0,1}) は max(f[0], f[1], f[2], f[3]) = max(3, 1, 4, 1) = 4 となる
fast_zeta_transform_or_max(f);
6. 【応用】抽象化した汎用 Bitwise OR 畳み込み関数(カスタム演算を渡す例)
// 演算を加法・乗法以外にカスタムしたい場合の関数設計例
template <typename T, typename Op, typename InvOp, typename Mul>
vector<T> custom_or_convolution(vector<T> a, vector<T> b, Op op, InvOp inv_op, Mul mul) {
    int n = a.size();

    // 自前ゼータ (OR)
    auto zeta = [&](vector<T>& v) {
        for (int i = 1; i < n; i <<= 1) {
            for (int j = 0; j < n; j++) {
                if (!(j & i)) v[j | i] = op(v[j | i], v[j]);
            }
        }
    };

    // 自前メビウス (OR)
    auto mobius = [&](vector<T>& v) {
        for (int i = 1; i < n; i <<= 1) {
            for (int j = 0; j < n; j++) {
                if (!(j & i)) v[j | i] = inv_op(v[j | i], v[j]);
            }
        }
    };

    zeta(a); zeta(b);
    for (size_t i = 0; i < a.size(); i++) a[i] = mul(a[i], b[i]);
    mobius(a);

    return a;
}

約数・倍数ゼータ/メビウス変換 (GCD/LCM畳み込み)

用途: 添字のGCDやLCMを用いた畳み込み・包除原理・モノイド累積

計算量: $O(N \log \log N)$
前提: 1-indexed配列。ゼータ変換は可換モノイド、メビウス変換は可換群、畳み込みは分配法則が成り立つ環構造で動作します。

// 約数ゼータ変換: g[i] = sum_{j | i} f[j]
// 【モノイド拡張】可換モノイド op(a, b) で置き換え可能 (例: max, min, gcd など)
template <typename T>
void divisor_zeta_transform(vector<T>& a) {
    int n = a.size() - 1;
    vector<bool> is_prime(n + 1, true);
    for (int p = 2; p <= n; ++p) {
        if (!is_prime[p]) continue;
        for (int i = 1; i * p <= n; ++i) {
            is_prime[i * p] = false;
            // --- 変更ポイント ---
            a[i * p] += a[i]; 
            // 可換モノイド拡張例 (max): a[i * p] = max(a[i * p], a[i]);
            // --------------------
        }
    }
}

// 約数メビウス変換 (ゼータ変換の逆変換)
// 【群拡張】逆演算 inv_op(a, b) が必要 (min/max 等は逆演算がないため不可)
template <typename T>
void divisor_mobius_transform(vector<T>& a) {
    int n = a.size() - 1;
    vector<bool> is_prime(n + 1, true);
    for (int p = 2; p <= n; ++p) {
        if (!is_prime[p]) continue;
        for (int i = n / p; i >= 1; --i) {
            is_prime[i * p] = false;
            // --- 変更ポイント ---
            a[i * p] -= a[i]; 
            // 逆演算拡張例: a[i * p] = inv_op(a[i * p], a[i]);
            // --------------------
        }
    }
}

// LCM畳み込み: c[k] = sum_{lcm(i, j) = k} a[i] * b[j]
// 【環拡張】外側演算(ゼータ/メビウス)と内側演算(積)に分配法則が必要
template <typename T>
vector<T> lcm_convolution(vector<T> a, vector<T> b) {
    divisor_zeta_transform(a);
    divisor_zeta_transform(b);
    for (size_t i = 1; i < a.size(); ++i) {
        // --- 変更ポイント ---
        a[i] *= b[i]; 
        // 独自の積演算: a[i] = mul(a[i], b[i]);
        // --------------------
    }
    divisor_mobius_transform(a);
    return a;
}

// 倍数ゼータ変換: g[i] = sum_{i | j} f[j]
// 【モノイド拡張】可換モノイド op(a, b) で置き換え可能
template <typename T>
void multiple_zeta_transform(vector<T>& a) {
    int n = a.size() - 1;
    vector<bool> is_prime(n + 1, true);
    for (int p = 2; p <= n; ++p) {
        if (!is_prime[p]) continue;
        for (int i = n / p; i >= 1; --i) {
            is_prime[i * p] = false;
            // --- 変更ポイント ---
            a[i] += a[i * p];
            // 可換モノイド拡張例 (gcd): a[i] = std::gcd(a[i], a[i * p]);
            // --------------------
        }
    }
}

// 倍数メビウス変換 (ゼータ変換の逆変換)
// 【群拡張】逆演算 inv_op(a, b) が必要
template <typename T>
void multiple_mobius_transform(vector<T>& a) {
    int n = a.size() - 1;
    vector<bool> is_prime(n + 1, true);
    for (int p = 2; p <= n; ++p) {
        if (!is_prime[p]) continue;
        for (int i = 1; i * p <= n; ++i) {
            is_prime[i * p] = false;
            // --- 変更ポイント ---
            a[i] -= a[i * p];
            // 逆演算拡張例: a[i] = inv_op(a[i], a[i * p]);
            // --------------------
        }
    }
}

// GCD畳み込み: c[k] = sum_{gcd(i, j) = k} a[i] * b[j]
template <typename T>
vector<T> gcd_convolution(vector<T> a, vector<T> b) {
    multiple_zeta_transform(a);
    multiple_zeta_transform(b);
    for (size_t i = 1; i < a.size(); ++i) {
        // --- 変更ポイント ---
        a[i] *= b[i];
        // --------------------
    }
    multiple_mobius_transform(a);
    return a;
}

▼ 使い方・適用例

1. 【基礎】約数ゼータ・メビウス変換(約数の和の計算と復元)
// f[i] を「要素 i の個数や値」とする (1-indexed, サイズ N+1)
// 例: N=6 で f = [0, 1, 2, 3, 4, 5, 6]
vector<long long> f = {0, 1, 2, 3, 4, 5, 6};

// --- 約数ゼータ変換 ---
// g[i] = sum_{j | i} f[j] (i の約数 j における f[j] の総和)
divisor_zeta_transform(f);
// 変換後: f[6] = f[1] + f[2] + f[3] + f[6] = 1 + 2 + 3 + 6 = 12

// --- 約数メビウス変換 ---
// ゼータ変換後の配列をもとの f[i] に復元する
divisor_mobius_transform(f);
// 復元後: f[6] = 6 に戻る
2. 【基礎】倍数ゼータ・メビウス変換(倍数和の計算と包除原理)
// 競プロ典型: 「gcd(a, b) = k となる組の数」の算出

// 1. 「k の倍数になる条件を満たすペア数」を前計算して f[k] に格納 (倍数和)
vector<long long> f = {0, 36, 9, 4, 1, 1, 1}; // N=6 の例

// 2. 倍数メビウス変換を行うと、「ちょうど gcd が k」の値に変換される (包除原理)
multiple_mobius_transform(f);
// 変換後: f[k] には gcd(a, b) == k となる具体的な個数が入る

// ※ 逆に「ちょうど k」から「k の倍数の和」を作る場合は倍数ゼータ変換を使用
// multiple_zeta_transform(f);
3. 【基礎】GCD 畳み込みの計算例
#include <atcoder/modint>
using mint = atcoder::modint998244353;

// 配列 a, b (1-indexed)
// a[1]=2, a[2]=3, a[3]=1
// b[1]=1, b[2]=4, b[3]=2
vector<mint> a = {0, 2, 3, 1};
vector<mint> b = {0, 1, 4, 2};

// c[k] = sum_{gcd(i, j) = k} a[i] * b[j] を計算
vector<mint> c = gcd_convolution(a, b);

// 計算例:
// c[1] = a[1]*b[1] + a[1]*b[2] + a[1]*b[3] + a[2]*b[1] + a[2]*b[3] + a[3]*b[1] + a[3]*b[2]
// c[2] = a[2]*b[2] (gcd(2,2)=2)
// c[3] = a[3]*b[3] (gcd(3,3)=3)
for (int i = 1; i < c.size(); i++) {
    cout << "c[" << i << "] = " << c[i].val() << "\n";
}
4. 【基礎】LCM 畳み込みの計算例
#include <atcoder/modint>
using mint = atcoder::modint998244353;

vector<mint> a = {0, 1, 2, 3, 4}; // 1-indexed, N=4
vector<mint> b = {0, 2, 1, 0, 3};

// c[k] = sum_{lcm(i, j) = k} a[i] * b[j]
// ※ lcm(i, j) > N となるインデックスの積は切り捨てられます
vector<mint> c = lcm_convolution(a, b);

for (int i = 1; i < c.size(); i++) {
    cout << "c[" << i << "] = " << c[i].val() << "\n";
}
5. 【応用】可換モノイド単体利用(例: 約数における max 累積)
// 約数ゼータ変換の `a[i * p] += a[i]` を `a[i * p] = max(a[i * p], a[i])` に変更
// g[i] = max_{j | i} f[j] (i の約数 j における f[j] の最大値) を求める

vector<long long> f = {0, 3, 1, 4, 1, 5, 9}; // 1-indexed (N=6)

// 変更後の約数ゼータ変換を実行すると:
// f[6] は f[1], f[2], f[3], f[6] の最大値 (max(3, 1, 4, 9) = 9) に更新される
divisor_zeta_transform_max(f);
6. 【応用】抽象化した汎用畳み込み関数(カスタム演算を渡す例)
// 演算を加法・乗法以外(例: XOR演算など)にカスタムしたい場合の関数設計例
template <typename T, typename Op, typename InvOp, typename Mul>
vector<T> custom_gcd_convolution(vector<T> a, vector<T> b, Op op, InvOp inv_op, Mul mul) {
    int n = a.size() - 1;
    
    // 自前ゼータ (倍数)
    auto zeta = [&](vector<T>& v) {
        vector<bool> is_prime(n + 1, true);
        for (int p = 2; p <= n; ++p) {
            if (!is_prime[p]) continue;
            for (int i = n / p; i >= 1; --i) {
                is_prime[i * p] = false;
                v[i] = op(v[i], v[i * p]);
            }
        }
    };
    
    // 自前メビウス (倍数)
    auto mobius = [&](vector<T>& v) {
        vector<bool> is_prime(n + 1, true);
        for (int p = 2; p <= n; ++p) {
            if (!is_prime[p]) continue;
            for (int i = 1; i * p <= n; ++i) {
                is_prime[i * p] = false;
                v[i] = inv_op(v[i], v[i * p]);
            }
        }
    };

    zeta(a); zeta(b);
    for (size_t i = 1; i < a.size(); ++i) a[i] = mul(a[i], b[i]);
    mobius(a);
    
    return a;
}

高速アダマール変換 (FWHT: Fast Walsh-Hadamard Transform)

用途: Bitwise XOR 畳み込みを O(N 2^N) で行う

計算量: O(N 2^N) (配列サイズ K = 2^N のとき O(K log K))
前提: 配列のサイズは 2 のべき乗である必要がある。アダマール行列を用いて点ごとの積に変換し、逆変換をかけることで Bitwise XOR 畳み込みを高速に計算する。

// 高速アダマール変換 / 逆変換
template <typename T>
void fast_walsh_hadamard_transform(vector<T>& a, bool invert = false) {
    int n = a.size();
    for (int len = 1; len < n; len <<= 1) {
        for (int i = 0; i < n; i += 2 * len) {
            for (int j = 0; j < len; j++) {
                T u = a[i + j];
                T v = a[i + j + len];
                a[i + j] = u + v;
                a[i + j + len] = u - v;
            }
        }
    }
    if (invert) {
        T inv_n = T(1) / T(n);
        for (auto& x : a) x *= inv_n;
    }
}

// Bitwise XOR 畳み込み: c[k] = sum_{i ^ j = k} a[i] * b[j]
template <typename T>
vector<T> bitwise_xor_convolution(vector<T> a, vector<T> b) {
    fast_walsh_hadamard_transform(a);
    fast_walsh_hadamard_transform(b);
    for (size_t i = 0; i < a.size(); i++) a[i] *= b[i];
    fast_walsh_hadamard_transform(a, true);
    return a;
}

▼ 使い方

基本的な使い方 (XOR 畳み込み)
#include <atcoder/modint>
using mint = atcoder::modint998244353;

// サイズは 2 の冪乗にする (例: ビット数 3 -> サイズ 8)
vector<mint> a = {1, 0, 2, 1, 0, 0, 0, 0};
vector<mint> b = {0, 1, 1, 0, 0, 0, 0, 0};

// Bitwise XOR 畳み込みの実行
vector<mint> c = bitwise_xor_convolution(a, b);

// 結果の出力 (c[k] = sum_{i ^ j = k} a[i] * b[j])
for (int i = 0; i < c.size(); i++) {
    cout << "c[" << i << "] = " << c[i].val() << "\n";
}

形式的冪級数 (FPS: Formal Power Series)

用途: O(N log N) での高度な多項式演算を一通り完結させる

計算量: 加減算や微積分は O(N)。乗算、inv、exp、log、pow はいずれも O(N log N) です。
前提: #include <atcoder/all> が必要です。ダブリングによるニュートン法で実装しています。

template <typename mint>
struct FPS : std::vector<mint> {
    using std::vector<mint>::vector;
    FPS(const std::vector<mint>& v) { this->assign(v.begin(), v.end()); }

    // 指定した次数までで切り打ち(リサイズ)
    FPS pre(ll deg) const {
        FPS res(*this);
        res.resize(deg);
        return res;
    }

    FPS& operator+=(const FPS& r) {
        if (r.size() > this->size()) this->resize(r.size());
        for (ll i = 0; i < (ll)r.size(); i++) (*this)[i] += r[i];
        return *this;
    }

    FPS& operator-=(const FPS& r) {
        if (r.size() > this->size()) this->resize(r.size());
        for (ll i = 0; i < (ll)r.size(); i++) (*this)[i] -= r[i];
        return *this;
    }

    // ACL の convolution を使った乗算
    FPS& operator*=(const FPS& r) {
        if (this->empty() || r.empty()) {
            this->clear();
            return *this;
        }
        auto res = atcoder::convolution(*this, r);
        this->assign(res.begin(), res.end());
        return *this;
    }

    FPS operator+(const FPS& r) const { return FPS(*this) += r; }
    FPS operator-(const FPS& r) const { return FPS(*this) -= r; }
    FPS operator*(const FPS& r) const { return FPS(*this) *= r; }

    FPS operator*(mint v) const {
        FPS res(*this);
        for (ll i = 0; i < (ll)res.size(); i++) res[i] *= v;
        return res;
    }
    FPS& operator*=(mint v) {
        for (ll i = 0; i < (ll)this->size(); i++) (*this)[i] *= v;
        return *this;
    }

    // 微分
    FPS deriv() const {
        if (this->empty()) return FPS();
        FPS res(this->size() - 1);
        for (ll i = 1; i < (ll)this->size(); i++) res[i - 1] = (*this)[i] * i;
        return res;
    }

    // 積分
    FPS integral() const {
        if (this->empty()) return FPS();
        FPS res(this->size() + 1);
        res[0] = 0;
        for (ll i = 0; i < (ll)this->size(); i++) res[i + 1] = (*this)[i] / (i + 1);
        return res;
    }

    // 逆元 (Inverse)
    FPS inv(ll deg = -1) const {
        if (deg == -1) deg = this->size();
        FPS res{(*this)[0].inv()};
        for (ll d = 1; d < deg; d *= 2) {
            FPS f = pre(2 * d);
            FPS g = res;
            f *= g;
            f.resize(2 * d);
            for (ll i = 0; i < 2 * d; i++) f[i] = -f[i];
            f[0] += 2;
            f *= g;
            f.resize(2 * d);
            res = f;
        }
        return res.pre(deg);
    }

    // 対数 (Log) : F[0] == 1 が必須
    FPS log(ll deg = -1) const {
        if (deg == -1) deg = this->size();
        FPS f = deriv();
        FPS g = inv(deg);
        f *= g;
        return f.integral().pre(deg);
    }

    // 指数 (Exp) : F[0] == 0 が必須
    FPS exp(ll deg = -1) const {
        if (deg == -1) deg = this->size();
        FPS res{1};
        for (ll d = 1; d < deg; d *= 2) {
            FPS f = res.log(2 * d);
            f[0] -= 1;
            for (ll i = 0; i < 2 * d; i++) f[i] = -f[i];
            FPS g = pre(2 * d);
            f += g;
            res *= f;
            res.resize(2 * d);
        }
        return res.pre(deg);
    }

    // 累乗 (Pow) : 定数項が0の場合(先行ゼロ)にも対応
    FPS pow(long long k, ll deg = -1) const {
        if (deg == -1) deg = this->size();
        if (k == 0) {
            FPS res(deg, 0);
            if (deg > 0) res[0] = 1;
            return res;
        }
        for (ll i = 0; i < (ll)this->size(); i++) {
            if ((*this)[i] != 0) {
                mint rev = (*this)[i].inv();
                FPS f(*this);
                for (ll j = 0; j < (ll)f.size(); j++) f[j] *= rev;
                f = f.pre(deg);
                f.erase(f.begin(), f.begin() + i);
                f = f.log(deg);
                for (ll j = 0; j < (ll)f.size(); j++) f[j] *= k;
                f = f.exp(deg);
                for (ll j = 0; j < (ll)f.size(); j++) f[j] *= (*this)[i].pow(k);
                
                FPS res(deg, 0);
                long long shift = i * k;
                if (shift >= deg) return res;
                for (ll j = 0; j < (ll)f.size() && j + shift < deg; j++) {
                    res[j + shift] = f[j];
                }
                return res;
            }
        }
        return FPS(deg, 0);
    }
};

using mint = modint998244353;
using fps = FPS<mint>;

▼ 使い方

基本的な使い方 (初期化と基本演算)
fps F = {1, 2, 3}; 
fps G = {4, 5};
// F(x) = 1 + 2x + 3x^2
// G(x) = 4 + 5x


// 四則演算
fps add = F + G;
fps sub = F - G;
fps mul = F * G;


// N 次未満に切り詰める
ll N = 10;
fps pre_mul = (F * G).pre(N);


// 逆元
// F(x) * I(x) = 1 (mod x^N)
// F[0] != 0 である必要があります
fps inv = F.inv(N);


// 微分と積分
fps deriv = F.deriv();
fps integral = F.integral();


// 対数と指数
// log は F[0] == 1 が必須
fps log_F = F.log(N);

// exp は F[0] == 0 が必須
fps exp_F = F.exp(N);


// 累乗
// F(x)^K (mod x^N) を計算
// 定数項が 0 の場合も内部で処理されます
long long K = 1000000007;
fps pow_F = F.pow(K, N);


// 要素へのアクセス
cout << F[0].val() << "\n";

注意: pre(N) を使うと x^N 以上の項を切り捨てることができる。逆元・対数・指数・累乗などの計算では、 必要な次数 N を引数として指定する。 また、inv は定数項が 0 では計算できず、 log は定数項が 1、 exp は定数項が 0 である必要がある。

ヒューリスティック

Random Number (mt19937)

用途: 乱数の生成・配列のシャッフル・乱択アルゴリズム

特徴: C++標準の rand() は精度が低く(最大値が小さい・周期が短い)、競プロでは非推奨です。 代わりにメルセンヌ・ツイスター (std::mt19937_64) を使用します。デバッグ時に結果を再現できるよう、固定シードを渡せるようにしておくのが実用的です。

// 乱数生成を扱いやすくする構造体
struct Random {
    mt19937_64 mt; // 64ビット版メルセンヌ・ツイスター

    // コンストラクタ:引数なしなら現在時刻、引数ありならその値をシードにする
    Random(uint64_t seed = chrono::steady_clock::now().time_since_epoch().count()) : mt(seed) {}

    // 1. nextInt: [L, R] の閉区間の整数を等確率でランダムに返す
    ll nextInt(ll L, ll R) {
        uniform_int_distribution<ll> dist(L, R);
        return dist(mt);
    }

    // 2. nextDouble: [0.0, 1.0) の実数を一様分布でランダムに返す
    double nextDouble() {
        uniform_real_distribution<double> dist(0.0, 1.0);
        return dist(mt);
    }
};

▼ 使い方と応用パターン

基本の使い方(初期化・シード固定・配列のシャッフル)
// `Random rnd;` は、設計図(struct)から実際に使える道具(オブジェクト)を
// 作成する「初期化(インスタンス化)」の処理です。

// パターンA: 本番用(引数なし=現在時刻シード。毎回結果が変わる)
Random rnd;

// パターンB: デバッグ用(引数あり=固定シード。毎回同じ乱数が出るのでバグ探しに便利)
Random rnd_fixed(12345);

// [L, R] の範囲でランダムな整数を生成
ll dice = rnd.nextInt(1, 6);
cout << "サイコロの目: " << dice << endl;

// 確率的な処理 (例: 30% の確率で実行)
if (rnd.nextDouble() < 0.3) {
    cout << "30%の確率で当たり!" << endl;
}

// 配列をランダムにシャッフルする
// std::shuffle の第3引数には乱数生成器の本体 (mt) を直接渡す
vector<ll> A = {1, 2, 3, 4, 5};
shuffle(A.begin(), A.end(), rnd.mt);

cout << "シャッフル後: ";
for(ll x : A) cout << x << " ";
cout << endl;

雑多に便利なもの

pair / tuple / struct / structured binding

用途: 複数値をまとめて持つ・辞書順でソートする

計算量: O(1)。 グラフの辺や座標、複数返り値の受け取り、イベントソートのキュー管理で多用する。

// pair の定義
pair<ll, ll> p = {1, 2};
auto q = make_pair(3, 4);

// 要素の読み出し
cout << p.first << " " << p.second << "\n";

// tuple の定義
tuple<ll, ll, ll> t = {1, 2, 3};

// tuple の要素取得
cout << get<0>(t) << " " << get<1>(t) << " " << get<2>(t) << "\n";

// {時間, イベント種類, ID} の順
using Event = tuple<ll, int, int>;

// structの定義と宇宙船演算子 (C++20: tupleの代用として推奨)
struct Event {
    ll time;
    int type;
    int id;
    // メンバ変数の上から順に辞書式比較を自動生成
    auto operator<=>(const Event&) const = default;
};
Event ev = {10, 0, 5};

// structured binding (C++17) : pair, tuple, struct 全てに対応
auto [x, y] = p;
auto [a, b, c] = t;
auto [time, type, id] = ev;

注意: pairやtupleは自動的に辞書順で比較される。C++20以降は struct 内に auto operator<=>(const 型名&) const = default; を1行書くだけで全く同じように辞書順比較が有効になるため、要素名が明確な struct の利用がおすすめ。

accumulate / iota / unique

用途: 合計・連番生成・重複除去の基本

計算量: いずれも O(N)。 「書けるのに手で書くのが面倒」な場面で強い。

long long s = accumulate(v.begin(), v.end(), 0LL);

vector<ll> id(n);
iota(id.begin(), id.end(), 0);

sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());

min_element / max_element / clamp

用途: 最小値・最大値・範囲制限

計算量: O(N)。 「要素全体を見る」基本操作を安全に書ける。

auto it = min_element(v.begin(), v.end());
auto jt = max_element(v.begin(), v.end());

ll y = clamp(x, 0, 100);

注意: 空配列に対して min_element や max_element を呼ばない。

lambda

用途: その場で関数を定義する

計算量: 関数そのものは O(1)、中身に依存。 再帰・比較関数・探索の局所補助でよく使う。

auto f = [&](ll x) {
    return x * x;
};

safe_math (安全な算術演算)

用途: 64bit整数(ll)のオーバーフローを自動検知・クリップし、掛け算・LCM・累乗などを安全に計算する

計算量: safe_add・safe_mul O(1)、safe_lcm O(log(min(a, b)))、safe_pow O(log(exp))。空間計算量はいずれも O(1)。 GCC/Clang の組み込み関数を利用し、オーバーフローを高速に判定します。限界値(SAFE_INF / SAFE_MIN_INF)に到達した場合は値が飽和(クリップ)し、伝搬する設計です。

// 安全な算術演算(オーバーフロー対策)
// namespaceを使わず、関数名に safe_ の接頭辞をつけています

constexpr long long SAFE_INF = std::numeric_limits<long long>::max();
constexpr long long SAFE_MIN_INF = std::numeric_limits<long long>::min();

// 内部判定用ラップ(GCC/Clang固有のビルトイン関数を利用)
inline bool safe_check_add(long long a, long long b, long long& res) {
    return __builtin_add_overflow(a, b, &res);
}
inline bool safe_check_mul(long long a, long long b, long long& res) {
    return __builtin_mul_overflow(a, b, &res);
}

// 1. 安全な足し算(a + b)
long long safe_add(long long a, long long b) {
    if (a == SAFE_INF || b == SAFE_INF) return SAFE_INF;
    if (a == SAFE_MIN_INF || b == SAFE_MIN_INF) return SAFE_MIN_INF;
    long long res;
    if (safe_check_add(a, b, res)) {
        return (a > 0) ? SAFE_INF : SAFE_MIN_INF;
    }
    return res;
}

// 2. 安全な引き算(a - b)
long long safe_sub(long long a, long long b) {
    if (b == SAFE_MIN_INF) return (a >= 0) ? SAFE_INF : a - b; // 厳密な極値回避
    return safe_add(a, -b);
}

// 3. 安全な掛け算(a * b)
long long safe_mul(long long a, long long b) {
    // どちらかが 0 なら 0
    if (a == 0 || b == 0) return 0;
    // どちらかが INF / MIN_INF の場合の伝搬
    if (a == SAFE_INF || b == SAFE_INF || a == SAFE_MIN_INF || b == SAFE_MIN_INF) {
        return ((a > 0) == (b > 0)) ? SAFE_INF : SAFE_MIN_INF;
    }
    long long res;
    if (safe_check_mul(a, b, res)) {
        return ((a ^ b) >= 0) ? SAFE_INF : SAFE_MIN_INF;
    }
    return res;
}

// 4. 安全な最小公倍数(lcm(a, b))
long long safe_lcm(long long a, long long b) {
    if (a == 0 || b == 0) return 0;
    a = std::abs(a);
    b = std::abs(b);
    long long g = std::gcd(a, b);
    // a / g は割り切れるのでオーバーフローしない、その結果と b を掛ける
    return safe_mul(a / g, b);
}

// 5. 安全な累乗(base ^ exp)
long long safe_pow(long long base, long long exp) {
    if (exp < 0) return 0; // 整数範囲での負の乗数は基本 0(1^-1 など例外は割愛)
    if (exp == 0) return 1;
    long long res = 1;
    long long a = base;
    while (exp > 0) {
        if (exp & 1) res = safe_mul(res, a);
        exp >>= 1;
        if (exp > 0) a = safe_mul(a, a);
    }
    return res;
}

// (参考)MSVCなど、__builtin_mul_overflow がない環境向けのフォールバック判定
/*
bool portable_check_mul(long long a, long long b, long long& res) {
    if (a != 0 && b != 0) {
        if (a > 0 && b > 0 && a > SAFE_INF / b) return true;
        if (a > 0 && b < 0 && b < SAFE_MIN_INF / a) return true;
        if (a < 0 && b > 0 && a < SAFE_MIN_INF / b) return true;
        if (a < 0 && b < 0 && a < SAFE_INF / b) return true;
    }
    res = a * b;
    return false;
}
*/

▼ 使い方と応用パターン

基本の使い方(掛け算と LCM)
long long a = 1000000000000000000LL; // 10^18
long long b = 10LL;

// そのまま掛けるとオーバーフローして負の値などになるが、
// safe_mul() を使えば LLONG_MAX にクリップされる
long long ans_mul = safe_mul(a, b);
if (ans_mul == SAFE_INF) {
    cout << "Overflow detected in multiplication!" << endl;
}

long long c = 1000000000000000000LL; // 10^18
long long d = 999999999999999999LL;  // 10^18 - 1
// 互いに素な巨大な値のLCMをとっても安全
long long ans_lcm = safe_lcm(c, d);
if (ans_lcm == SAFE_INF) {
    cout << "Overflow detected in LCM!" << endl;
}
累乗(pow)による上限判定と二分探索での活用
// 例:「N ^ K が M を超えるか?」を調べたいとき
long long N = 1000000;
long long K = 4;
long long M = 1000000000000000000LL;

// そのまま pow(N, K) を計算すると確実にオーバーフローするが、
// safe_pow なら SAFE_INF で止まるため、安全に大小比較が可能。
long long val = safe_pow(N, K);

if (val > M) {
    cout << "N^K is strictly greater than M." << endl;
}

// 二分探索内で「mid * 判定係数 <= X」を調べる際にも
// safe_mul(mid, factor) <= X のように書くだけで
// オーバーフローによるバグ(負数に反転して条件を満たしてしまう等)を防げる。
応用:INF 値のカスタマイズ(閾値を下げる)
// 競技プログラミングなどで、「10^18 を超えたら実質 INF 扱い」としたい場合は、
// ラッパー関数で制限をかけます。

const long long LIMIT = 1000000000000000000LL; // 10^18

long long custom_mul(long long a, long long b) {
    long long res = safe_mul(a, b);
    if (res > LIMIT) return LIMIT + 1; // 上限を設ける
    return res;
}

注意: この実装は __builtin_add_overflow などの GCC/Clang 専用のビルトイン関数に依存しています。AtCoder をはじめとするほとんどの競技プログラミング環境(GCC環境)でそのまま動作しますが、MSVC(Visual Studio)などの環境でコンパイルエラーになる場合は、コメントアウトされている portable_check_mul などの代替ロジックを利用してください。

alphabet index (a-z)

用途: 小文字アルファベットの何番目かを取得

計算量: O(1)。 文字をASCII値から直接計算する。

// a=1, b=2, ..., z=26
ll alphabetIndexLower(char c) {
    if ('a' <= c && c <= 'z') {
        return c - 'a' + 1;
    }
    return -1; // 範囲外
}

// 使用例
char c = 'c';
cout << alphabetIndexLower(c) << "\n"; // 3

注意: 小文字専用のため、大文字は無効扱い。

alphabet index (A-Z)

用途: 大文字アルファベットの何番目かを取得

計算量: O(1)。 文字をASCII値から直接計算する。

// A=1, B=2, ..., Z=26
ll alphabetIndexUpper(char c) {
    if ('A' <= c && c <= 'Z') {
        return c - 'A' + 1;
    }
    return -1; // 範囲外
}

// 使用例
char c = 'C';
cout << alphabetIndexUpper(c) << "\n"; // 3

注意: 大文字専用のため、小文字は無効扱い。

座標圧縮

用途: 大きい値域を連番に変換し、配列・BIT・セグメント木などで効率的に扱う

計算量: 構築 O(N log N)、座標 ID の取得・境界検索 O(log N)、 圧縮後の値の取得 O(1)。空間計算量は O(N)。 重複値の除去、元の値への復元、区間検索にも対応する。

// 座標圧縮
template <class T>
struct CoordinateCompression {
    vector<T> xs;  // 圧縮対象の値(構築後は昇順・重複なし)
    bool built = false;

    // 1. 値を 1 つ追加
    void add(const T& x) {
        xs.push_back(x);
        built = false;
    }

    // 2. 配列などの範囲をまとめて追加
    template <class It>
    void add_all(It first, It last) {
        xs.insert(xs.end(), first, last);
        built = false;
    }

    // 3. ソートして重複を除去し、圧縮を確定
    void build() {
        sort(xs.begin(), xs.end());
        xs.erase(unique(xs.begin(), xs.end()), xs.end());
        built = true;
    }

    // 4. 圧縮後の座標数
    int size() const {
        assert(built);
        return (int)xs.size();
    }

    // 5. 圧縮後の座標が空かどうか
    bool empty() const {
        assert(built);
        return xs.empty();
    }

    // 6. 値 x の圧縮 ID を取得
    //    登録済みなら 0-indexed の ID、未登録なら -1
    int id(const T& x) const {
        assert(built);

        auto it = lower_bound(xs.begin(), xs.end(), x);
        if (it == xs.end() || *it != x) return -1;

        return (int)(it - xs.begin());
    }

    // 7. x 以上となる最初の座標の位置
    //    つまり、x 未満の異なる座標の個数
    int lower(const T& x) const {
        assert(built);
        return (int)(
            lower_bound(xs.begin(), xs.end(), x) - xs.begin()
        );
    }

    // 8. x より大きい最初の座標の位置
    //    つまり、x 以下の異なる座標の個数
    int upper(const T& x) const {
        assert(built);
        return (int)(
            upper_bound(xs.begin(), xs.end(), x) - xs.begin()
        );
    }

    // 9. 値 x が圧縮対象に登録されているか
    bool contains(const T& x) const {
        return id(x) != -1;
    }

    // 10. 圧縮 ID から元の値を取得
    const T& value(int i) const {
        assert(built);
        return xs.at(i);
    }

    // 11. 元の値が [l, r] に含まれる座標の位置範囲
    //     戻り値は半開区間 [first, second)
    //     l や r 自体が登録されていなくても使用可能
    pair<int, int> range(const T& l, const T& r) const {
        assert(built);

        if (r < l) {
            int p = lower(l);
            return {p, p};
        }

        return {lower(l), upper(r)};
    }
};

▼ 使い方と応用パターン

基本の使い方(圧縮 ID の取得・元の値への復元・未登録判定)
// 圧縮したい配列
vector<long long> a = {
    1000000000LL, -5, 100, -5, 42
};

// 座標圧縮を構築
CoordinateCompression<long long> cc;
cc.add_all(a.begin(), a.end());
cc.build();

// 座標は昇順に {-5, 42, 100, 1000000000}
// 元の値が同じなら、圧縮 ID も同じになる
for (long long x : a) {
    cout << cc.id(x) << " ";
}
// 出力: 3 0 2 0 1

// 圧縮 ID から元の値を復元
cout << cc.value(0) << endl; // -5
cout << cc.value(2) << endl; // 100

// 登録されていない値は -1
cout << cc.id(777) << endl; // -1

// 登録済みかどうかを判定
cout << cc.contains(42) << endl;  // 1 (true)
cout << cc.contains(777) << endl; // 0 (false)

// 圧縮後の座標数
cout << cc.size() << endl; // 4
区間 [L, R] の座標を取得する(lower・upper・range)
CoordinateCompression<long long> cc;

// 座標として使う値を登録
vector<long long> xs = {10, 100, 1000, 10000};
cc.add_all(xs.begin(), xs.end());
cc.build();

// x 未満の異なる座標の個数
cout << cc.lower(100) << endl; // 1

// x 以下の異なる座標の個数
cout << cc.upper(100) << endl; // 2

// 元の値が [50, 1000] に含まれる座標の範囲
auto p = cc.range(50, 1000);
int left = p.first;
int right = p.second;

// 圧縮 ID の範囲 [left, right) を走査
// 100, 1000 が出力される
for (int i = left; i < right; ++i) {
    cout << cc.value(i) << " ";
}
cout << endl;

// left と right は半開区間なので、
// 対応する座標の個数は right - left
応用:頻度配列と累積和で区間内の要素数を数える
vector<long long> a = {2, 5, 2, 10, 7, 5};

CoordinateCompression<long long> cc;
cc.add_all(a.begin(), a.end());
cc.build();

// 圧縮座標ごとの出現回数
vector<long long> freq(cc.size(), 0);

for (long long x : a) {
    ++freq[cc.id(x)];
}

// 累積和(pref[i] は ID が i 未満の要素数)
vector<long long> pref(cc.size() + 1, 0);

for (int i = 0; i < cc.size(); ++i) {
    pref[i + 1] = pref[i] + freq[i];
}

// 元の値が [L, R] に含まれる要素数
long long L = 3, R = 8;
auto p = cc.range(L, R);

int left = p.first;
int right = p.second;

long long count = pref[right] - pref[left];
cout << count << endl; // 3(5, 7, 5)
応用:BIT・セグメント木と組み合わせる
// 座標圧縮後の ID を BIT やセグメント木の添字として使う
vector<long long> a = {1000000000LL, 10, 500, 10};

CoordinateCompression<long long> cc;
cc.add_all(a.begin(), a.end());
cc.build();

// 圧縮後の座標数だけデータ構造を用意
// FenwickTree<long long> bit(cc.size()); など
//
// 値 x を更新するとき:
// int i = cc.id(x);
// bit.add(i, delta);
//
// 元の値が x 以下の要素を集計するとき、
// 対象の ID 範囲は [0, cc.upper(x))。
//
// BIT が 1-indexed の場合は、更新や集計の添字を
// データ構造の仕様に合わせて調整する。

注意: なるべくまとめて座標圧縮したほうが実装が楽になる。

値をすべて登録してから build() を呼ぶこと。 構築後に add() や add_all() を呼んだ場合も、 再度 build() が必要。 id(x) は登録済みの値に対してのみ有効な ID を返し、 未登録の値には -1 を返す。 一方、lower()・upper()・range() は、 登録されていない区間の境界値にも使用できる。 また、range(l, r) の返り値は [first, second) の半開区間である。

ランレングス圧縮 (RLE)

用途: 連続する同じ値をまとめて扱う

計算量: O(N)。 文字列や配列の連続している区間を圧縮。

vector<pair<char, long long>> rle;

for (ll i = 0; i < N; ) {
    ll j = i;
    while (j < N && S[j] == S[i]) j++;

    rle.push_back({S[i], j - i});
    i = j;
}

例: aaabbcccc → {('a',3), ('b',2), ('c',4)}

mex (Minimum Excluded value)

用途: 集合に含まれない最小の非負整数を求める

計算量: O(N)。 配列に含まれない 0 以上の最小の整数を見つける。

long long calc_mex(const vector<long long>& A) {
    ll n = A.size();
    vector<bool> exists(n + 1, false);
    
    for (auto a : A) {
        if (a >= 0 && a <= n) exists[a] = true;
    }
    
    for (ll i = 0; i <= n; i++) {
        if (!exists[i]) return i;
    }
    
    return 0;
}

例: {0, 1, 3, 4} → 2