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;
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" の繰り返し)
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
計算量: 挿入・検索ともに O(|S|)
(|S| は文字列の長さ)。
文字列の追加順(ID)や、ノードごとの任意の重み・関数値を持てるように設計された汎用実戦モデル。
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
// char_size: 文字の種類数 (英小文字のみなら26)
// base: 基準となる文字 ('a', 'A', '0' など)
template <int char_size = 26, int base = 'a'>
struct Trie {
struct Node {
vector<int> next; // 子ノードのインデックス
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) を追加
}
// 文字列の挿入 (同時にIDや関数の値を登録できる)
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; // 用途に応じて +=, max(), = などに変更可能
}
// 文字列の完全一致検索 (見つかれば終端ノードの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;
}
};
【Trieを構成する構造と変数】
・nodes
: ツリーを構成する全ノードの配列。インデックス
0
が根(Root)になります。
【各Nodeが持つ変数】
・next
: 子ノードのインデックス配列(道がない場合は
-1
)。
・accept_count
: このノードを終端とする文字列の数(完全一致)。
・pass_count
:
このノードを通過(または終端)した文字列の総数(接頭辞として共有している数)。
・accept_ids
:
【拡張】同じ文字列が複数追加される事も考慮し、追加時のラベル(indexなど)を保持する配列。
・val
:
【拡張】ノードごとに持たせたい関数の値、重み、DPのスコアなど。
▼ 応用テンプレート(コピペ&即使用用)
1. 文字列の追加とラベル・値の取得
Trie<26, 'a'> trie;
// insert(文字列, id, val)
trie.insert("apple", 101, 50);
trie.insert("apple", 102, 30); // 同じ文字列を別のIDで追加
trie.insert("app", 103, 10);
int node_id = trie.search("apple");
if (node_id != -1) {
// node_id は "apple" の終端ノード
// trie.nodes[node_id].accept_ids -> {101, 102}
// trie.nodes[node_id].val -> 80 (50 + 30)
}
2. Prefix (接頭辞) の出現回数カウント
Trie<26, 'a'> trie;
trie.insert("cat");
trie.insert("cap");
trie.insert("camera");
// "ca" から始まる文字列の数を O(|S|) で取得
int cnt = trie.count_prefix("ca"); // -> 3 が返る
int cnt2 = trie.count_prefix("cap"); // -> 1 が返る
3. 辞書順の走査 (DFS)
Trie<26, 'a'> trie;
// 挿入処理...
// 辞書順に文字列を復元しながら探索するDFS
auto dfs = [&](auto self, int u, string current_str) -> void {
// このノードで終わる文字列がある場合
if (trie.nodes[u].accept_count > 0) {
// current_str が構築された文字列
}
// 辞書順に子ノードへ潜る
for (int i = 0; i < 26; i++) {
int v = trie.nodes[u].next[i];
if (v != -1) {
self(self, v, current_str + char('a' + i));
}
}
};
dfs(dfs, 0, ""); // 根(0)から空文字列でスタート
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();
注意: 未ソート配列に使ってはいけない。
next_permutation
用途: 順列全探索、辞書順で次の並びを生成する
計算量: 1回あたり O(N)。 全列挙は N! なので、サイズには強く注意。
sort(v.begin(), v.end());
do {
// v を1通り処理
} while (next_permutation(v.begin(), v.end()));
注意: 出力はboolで、一回実行するたびにvの中身が次の順列になる。
多次元 vector
用途: 2次元・3次元配列を定義する
計算量: 初期化は要素数に比例して O(HW) や O(XYZ)。 DPの土台として頻出。
vector<vector<ll>> a(h, vector<ll>(w, 0));
vector<vector<long long>> dp(n, vector<long long>(m, INF));
vector<vector<vector<ll>>> g(x, vector<vector<ll>>(y, vector<ll>(z)));
グリッドの回転
用途: 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計算を適宜有効にしてください。
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);
}
//使用例
ll 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。
割り進めることで探索範囲も縮むため、
実際の計算量はかなり軽い。
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)。 N ≤ 107 程度なら高速に動作。
struct Sieve {
vector<ll> min_factor;
vector<ll> primes;
Sieve(ll n) : min_factor(n + 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);
}
}
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;
}
};
ll main() {
// 1. まず、探索したい最大値(N)を指定して、篩(構造体)を初期化する
// ※この時点で、1から1000000までの「最小の素因数(SPF)」がすべて計算されます(前処理)
Sieve sieve(1000000);
// 2. 素数判定 (is_prime) の使い方
cout << sieve.is_prime(53) << endl; // 1 (true)
cout << sieve.is_prime(100) << endl; // 0 (false)
// 3. 高速素因数分解 (factor) の使い方
// 例として「24」を素因数分解する (24 = 2^3 * 3^1)
auto res1 = sieve.factor(24);
cout << "24の素因数分解: ";
for (auto p : res1) {
cout << p.first << "^" << p.second << " ";
}
cout << endl; // 出力: 2^3 3^1
// 例として「9973(素数)」を素因数分解する
auto res2 = sieve.factor(9973);
cout << "9973の素因数分解: ";
for (auto p : res2) {
cout << p.first << "^" << p.second << " ";
}
cout << endl; // 出力: 9973^1
// 4. 列挙された素数リスト (primes) の使い方
// 1000000以下の素数の個数を出力してみる
cout << "1000000以下の素数の数: " << sieve.primes.size() << endl;
return 0;
}
注意:
単なる素数列挙だけでなく、各数の「最小の素因数(SPF)」を配列に記録しておくことで、前処理後は各クエリ
O(log X)
での高速素因数分解が可能になる。
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;
}
幾何
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";
}
}
探索・二分探索・前計算
二分探索
用途: 単調な判定関数の境界値を探す
計算量: 判定関数を O(F) とすると全体 O(F log R)
パターン1: 最小値を求める(ng < ok)
long long ng = -1;
long long ok = 1e18; // 条件を満たす十分大きな値
while (abs(ok - ng) > 1) { // ok - ng > 1 でも可
long long mid = ng + (ok - ng) / 2;
if (check(mid))
ok = mid;
else
ng = mid;
}
パターン2: 最大値を求める(ok < ng)
long long ok = -1; // 条件を満たす十分小さな値
long long ng = 1e18;
while (abs(ok - ng) > 1) { // ng - ok > 1 でも可
long long mid = ok + (ng - ok) / 2;
if (check(mid))
ok = mid;
else
ng = mid;
}
用途例: 最小値探索、最大値探索、答えを二分探索する問題
注意:
「ok は条件を満たす」「ng
は条件を満たさない」を常に維持する。
while文の条件式を
abs(ok - ng) > 1
に統一しておくと、okとngの初期値の大小関係が変わってもループ条件を書き換えずに済むため安全です。
三分探索
用途: 凸関数の極値(最小値または最大値)を探す
計算量: 評価関数を O(F) とすると全体 O(F log R) またはループ回数分の定数倍
パターン1: 整数での三分探索(下に凸の最小値を求める)
ll low = 0;
ll high = 1e18; // 探索範囲
while (high - low > 2) {
ll m1 = low + (high - low) / 3;
ll m2 = high - (high - low) / 3;
if (f(m1) < f(m2)) // 上に凸(最大値探索)の場合は不等号を逆にする
high = m2;
else
low = m1;
}
ll ans = low;
for (ll i = low + 1; i <= high; i++) {
if (f(i) < f(ans)) ans = i; // 絞り込んだ範囲から全探索で最小を探す
}
パターン2: 実数での三分探索(下に凸の最小値を求める)
ld low = 0.0;
ld high = 1e18; // 探索範囲
ll cnt = 100; // 許容誤差に応じた十分なループ回数
while (cnt--) {
ld m1 = low + (high - low) / 3.0;
ld m2 = high - (high - low) / 3.0;
if (f(m1) < f(m2)) // 上に凸(最大値探索)の場合は不等号を逆にする
high = m2;
else
low = m1;
}
// low または high が極値のx座標(必要なら f(low) が極値)
用途例: 独立な変数の変化に対して凸関数になるコストの最小化、放物線などの極値探索
注意:
対象の関数が「狭義の凸関数」でない(=平坦な部分がある)場合、正しく極値を求めることができません。
また、実数の場合は while (high - low > EPS) のような誤差による終了判定よりも、cnt を用いてループ回数を固定する(例: 100〜200回)方が安全でバグりにくくなります。
bit全探索
用途: 部分集合をすべて列挙して、条件を満たす組み合わせを探す
計算量: O(2^N × N)
for (ll mask = 0; mask < (1 << N); mask++) {
for (ll i = 0; i < N; i++) {
if (mask & (1 << i)) {
// 判定・計算をする
}
}
}
用途例: 全列挙、部分集合和、組み合わせ探索、ナップサックの小規模版
注意:
1 << N は
N >= 31 で危険なので、 必要なら
1LL << N を使う。
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);
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
}
順列木 (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)を省いているため、必要に応じて適宜メモリ解放の処理を追加するか、競技プログラミング等の使い捨ての用途に限定すること。
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);
区間和 & 区間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);
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()) {
if (d + G[m][i].second < dist[G[m][i].first]) {
dist[G[m][i].first] = d + G[m][i].second;
todo.push({d + G[m][i].second, G[m][i].first});
}
}
}
return dist;
}
注意:
典型的に「古い状態を無視」する実装になる。if (d != dist[v]) continue;
はほぼ必須。
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をするより高速。
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;
}
注意: 出力する長さはパスの長さ = 辺の個数であることに注意!グラフが複数の独立した木(非連結グラフ)で構成されている場合でも正しく動作する。
Topological Sort
用途: DAG の順序付け
計算量: O(V + E)。 依存関係のある処理順を作るときに使う。DAG以外では適用不可能。
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);
}
}
注意: グラフ上に「頂点 $u$ から頂点 $v$ への有向辺($u \to v$)」が存在する場合、ord 配列の中では必ず $u$ が $v$ より手前に配置される。
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 に置き換えると安全。
Strongly Connected Components (SCC)
用途: 有向グラフの強連結成分分解・閉路検出・DAG化
計算量: O(V + E)。 2回のDFS(コサラジュのアルゴリズム)を用いて有向グラフを強連結成分に分解する。
// 1回目のDFS: 帰りがけ順(バックトラック時)に頂点を記録
void SCC1(const vvl &graph, vb &seen, vl &V, ll v) {
seen[v] = true;
for (ll next : graph[v]) {
if (seen[next] == true) {
continue;
}
SCC1(graph, seen, V, next);
}
V.push_back(v);
}
// 2回目のDFS: 逆方向グラフを用いて成分ごとに頂点を回収
void SCC2(const vvl &graph, vb &seen, ll v, vl &SCC_part) {
seen[v] = true;
for (ll next : graph[v]) {
if (seen[next] == true) {
continue;
}
SCC2(graph, seen, next, SCC_part);
}
SCC_part.push_back(v);
}
// =====ここから使い方=====
vvl G1(N), G2(N); // G1は順方向、G2は逆方向の隣接リスト
vb seen1(N, false);
vl V;
rep(i, N) {
if (seen1[i] == false) {
SCC1(G1, seen1, V, i);
}
}
reverse(V.begin(), V.end());
vvl SCC;
vb seen2(N, false);
rep(i, N) {
if (seen2[V[i]] == false) {
vl SCC_part;
SCC2(G2, seen2, V[i], SCC_part);
SCC.push_back(SCC_part);
}
}
// SCCのi行目がi番目の強連結成分の要素
注意:
二次元配列
SCC
に格納されるグループの順序は、自動的に「トポロジカルソート順(上流から下流)」になる。成分を潰してDAGとしてDPを回す際は、そのまま
0 から順に遷移すればよい。
Euler Tour
用途: 木の平坦化・部分木クエリ・LCA(最小共通祖先)・2頂点間距離の高速取得
計算量: 構築 O(V)。
木構造を配列上の1次元の区間に変換します。部分木の管理用(入出時刻)と、LCAや距離計算用のオイラーツアー(行き来の記録)を同時に構築する汎用的な構造体です。
struct EulerTour {
int n;
// 部分木クエリ用:頂点vの部分木は半開区間 [ in_time[v], out_time[v] ) に対応
vector<int> in_time, out_time;
// LCA用:訪問順の頂点(tour), その深さ(depth), 頂点vがtourに最初に現れるインデックス(first_visit)
vector<int> tour, depth, first_visit;
// 距離用:根からの距離(重みなし木ならdepthと同じ。重み付きならパスの重み和)
vector<long long> dist;
int timer_inout;
EulerTour(const vector<vector<int>>& graph, int root = 0) {
n = graph.size();
in_time.assign(n, -1);
out_time.assign(n, -1);
first_visit.assign(n, -1);
dist.assign(n, 0);
timer_inout = 0;
build(graph, root, -1, 0, 0);
}
void build(const vector<vector<int>>& graph, int v, int p, int d, long long dist_from_root) {
in_time[v] = timer_inout++;
first_visit[v] = tour.size();
tour.push_back(v);
depth.push_back(d);
dist[v] = dist_from_root;
for (int next : graph[v]) {
if (next == p) continue;
// ※辺に重みがあるグラフの場合は、引数を変えて dist_from_root + cost にする
build(graph, next, v, d + 1, dist_from_root + 1);
tour.push_back(v);
depth.push_back(d);
}
out_time[v] = timer_inout;
}
};
▼ 使い方(部分木クエリ / LCA / 距離)
シンプルな使い方(部分木クエリ)
// 構築 (根を0とする)
EulerTour et(graph, 0);
int v = 2; // 部分木を調べたい頂点
int l = et.in_time[v];
int r = et.out_time[v];
// セグメント木やBIT(別途用意)に対して、配列上の区間 [l, r) の和を求めたり、
// 区間 [l, r) に値を足したりすることで、頂点vの部分木全体に対する操作を O(log N) で処理できます。
// 例: long long sum = segtree.query(l, r);
木のLCA (最小共通祖先) 【前提:汎用スパーステーブル】
EulerTour et(graph, 0);
// 前提: 汎用スパーステーブル (SparseTable) を用いて構築
// 要素を {深さ, 頂点番号} のペアにする
using pii = pair<int, int>;
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 を求める
int u = 3, v = 5;
int l = et.first_visit[u];
int r = et.first_visit[v];
if (l > r) swap(l, r);
// 半開区間 [l, r+1) で区間最小値を取得
int lca = st.query(l, r + 1).second;
木の二頂点間の距離 【前提:汎用スパーステーブル】
// (※上の LCA のコードで SparseTable st が構築されていることが前提)
int u = 3, v = 5;
int l = et.first_visit[u];
int r = et.first_visit[v];
if (l > r) swap(l, r);
// LCAを求める
int 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 と、LCA探索用の first_visit を混同しないように注意してください。LCAや距離を求める際のスパーステーブルへのクエリは、必ず first_visit のインデックスを使用します。
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};
}
};
int main() {
// ① 初期化: 左側の頂点数 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";
return 0;
}
仕組みとコマンドの解説:
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の定理に基づいて、この配列を用いて最小頂点被覆および最大独立集合を復元しています。
Kruskal / クラスカル法 (最小全域木)
用途: グラフから最小全域木(MST)を構築し、その隣接リストを出力する
計算量: O(E log E) または O(E log V)。 辺のコストによるソートが計算量の支配的な部分となる。
// Union-Find の構造体
struct UnionFind {
vector<ll> parent;
vector<ll> siz;
UnionFind(ll n) {
parent.assign(n, -1);
siz.assign(n, 1);
}
ll find(ll x) {
if (parent[x] == -1) return x;
return parent[x] = find(parent[x]);
}
bool unite(ll x, ll y) {
ll root_x = find(x);
ll root_y = find(y);
if (root_x == root_y) return false;
if (siz[root_x] < siz[root_y]) swap(root_x, root_y);
parent[root_y] = root_x;
siz[root_x] += siz[root_y];
return true;
}
bool same(ll x, ll y) {
return find(x) == find(y);
}
ll size(ll x) {
return siz[find(x)];
}
};
// 辺を表す構造体
struct Edge {
ll u, v, cost;
// コストの昇順にソートするための比較関数
bool operator<(const Edge& o) const {
return cost < o.cost;
}
};
// クラスカル法 (最小全域木)
// 戻り値: {MSTの重みの総和, MSTを構成する隣接リスト}
pair<ll, vector<vector<pair<ll, ll>>>> kruskal(ll V, vector<Edge>& edges) {
// 1. 辺をコストの昇順にソート
sort(edges.begin(), edges.end());
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 main() {
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 << "MSTの総コスト: " << total_cost << "\n\n";
cout << "--- MSTの隣接リスト ---\n";
for (ll i = 0; i < V; ++i) {
cout << "頂点 " << i << ": ";
for (auto& edge : mst_adj[i]) {
cout << "(" << edge.first << ", コスト:" << edge.second << ") ";
}
cout << "\n";
}
return 0;
}
グラフ上の汎用ダブリング (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)$
ナイーブに2重ループで数えると $O(N^2)$ かかりますが、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);
累積和 (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; // 全体で黒く塗られているマスの総数
// 閉区間 [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) {
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;
}
};
▼ 使い方
基本的な使い方 (区間の追加と特定範囲の取得)
RangeSet rs;
rs.insert(1, 3);
rs.insert(6, 10);
// 現在の区間: [1, 3], [6, 10]
// 全体の黒マスの数
cout << rs.black_count << "\n"; // 3 + 5 = 8
// [2, 7] の範囲の黒マスの数
// [1, 3] との重なり: [2, 3] (長さ2)
// [6, 10] との重なり: [6, 7] (長さ2)
cout << rs.query(2, 7) << "\n"; // 4
注意:
query 関数は重なっている区間の数を $K$
とすると $O(K + \log Q)$ かかる。std::set
のみで管理しているため、取得クエリが大量にある場合はセグメント木などへの変更を検討すること。
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
程度と小さい場合に検討する。ビット演算の優先順位(括弧の付与)に注意。
桁DP
用途: ある値以下の整数のうち、特定の条件を満たすものの数を数え上げる問題など
計算量: O(D × state)
(Dは桁数)など
設計:
dp[決定した桁数][未満フラグ(smaller)][リーディングゼロフラグ(leading_zero)][その他の状態]
を持ち、上の桁から順に遷移していく。
// N以下の条件を満たす数を数える桁DPの遷移例
string N; // 桁数が大きい場合は文字列として受け取る
ll D = N.size();
ll dp[D + 1][2][2][MAX_J] = {};
// dp[i][smaller][leading_zero][j]
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
フラグを持たせると、桁和・桁積・使用数字などの状態管理がしやすい。
最長増加部分列 (LIS)
用途: 配列から順序を崩さずに要素を取り出したとき、それらが単調増加となる部分列(LIS)の最大長さを求める問題など
計算量: O(N log N)
設計:
dp[i] に「長さ i + 1
の増加部分列の末尾となる要素の最小値」を保持する。各要素に対して二分探索を用いて配列
dp を更新していく。
// 最長増加部分列(LIS)の長さを求める
ll N;
// Aは入力配列
vector<ll> A(N);
vector<ll> dp;
for (ll i = 0; i < N; i++) {
// 二分探索で挿入位置を特定する
auto it = lower_bound(dp.begin(), dp.end(), A[i]);
if (it == dp.end()) {
dp.push_back(A[i]);
} else {
*it = A[i];
}
}
ll ans = dp.size();
注意:
問題が「広義単調増加(同じ値が連続してもよい)」を求める場合は、二分探索の部分を
lower_bound ではなく
upper_bound に変更する必要がある。
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に含まれてしまうのを防ぐことができる。
メモ化再帰 (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
のべき乗である必要がある。ゼータ変換で上位/下位集合の和をとり、要素ごとの積を計算した後にメビウス変換で復元することで
Bitwise OR / AND の畳み込みを行う。
// 下位集合に関する高速ゼータ変換 (OR畳み込みの前処理)
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];
}
}
}
// 下位集合に関する高速メビウス変換 (OR畳み込みの後処理)
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];
}
}
}
// 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];
fast_mobius_transform_or(a);
return a;
}
▼ 使い方
基本的な使い方 (OR 畳み込み)
#include <atcoder/modint>
using mint = atcoder::modint998244353;
// サイズは 2 の冪乗にする (例: ビット数 2 -> サイズ 4)
vector<mint> a = {1, 2, 0, 3}; // a[0], a[1], a[2], a[3]
vector<mint> b = {4, 1, 2, 0};
// Bitwise OR 畳み込みの実行
vector<mint> c = bitwise_or_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";
}
高速アダマール変換 (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;
Time-Bounded Search (Timer & Random Search)
用途: 制限時間ギリギリまでの乱択探索・山登り法・焼きなまし法
特徴: std::chrono を用いて経過時間を正確に計測し、指定した時間(例:1.8秒)に達するまで解の探索・改善を高速に回し続けます。ヒューリスティック(最適化)問題において、スコアを最大化・最小化するための必須テクニックです。
#include <chrono>
// Randomの導入が必要
// 実行時間を計測するための構造体
struct Timer {
chrono::steady_clock::time_point start_time;
// コンストラクタで生成時の時間を記録
Timer() : start_time(chrono::steady_clock::now()) {}
// 経過時間を秒単位の実数 (double) で返す
double elapsed() const {
return chrono::duration_cast<chrono::duration<double>>(
chrono::steady_clock::now() - start_time).count();
}
};
// 時間制限付き乱択アルゴリズムのテンプレート
void solve() {
Timer timer;
Random rnd;
// TLEを避けるため、実際の制限時間より少し短めに設定 (例: 2.0秒制限なら1.8〜1.9秒)
const double TIME_LIMIT = 1.8;
long long best_score = -1; // 最小化問題の場合は 1e18 などにする
// ここに最良の解(状態)を保持する変数を定義する (例: vector<int> best_state;)
int loop_count = 0; // 性能評価用(何回ループを回せたか)
// 制限時間になるまでひたすらループを回す
while (timer.elapsed() < TIME_LIMIT) {
loop_count++;
// 1. ランダムに解の候補を生成、または現在の解を少し変化させる(近傍遷移)
// 例: int v = rnd.nextInt(0, N - 1);
// 2. その解のスコアを計算する
long long current_score = /* 評価関数の結果 */;
// 3. スコアが改善されていたら、ベストスコアとベスト状態を更新する
if (current_score > best_score) {
best_score = current_score;
// best_state = current_state; などの状態保存処理
}
}
// デバッグ用: ループ回数や最終スコアを出力(本番では消すか cerr のままにする)
// cerr << "Loop Count: " << loop_count << endl;
// cerr << "Best Score: " << best_score << endl;
// 最後に最良の解を出力する
// print(best_state);
}
▼ 使い方と応用パターン
応用: 山登り法 (Hill Climbing) のベース
// 完全なランダムではなく、「今の状態から1箇所だけランダムに変更する」と
// 効率よく解を改善できることが多いです(山登り法)。
// 初期状態を生成
vector<int> state = generate_initial_state();
long long current_score = calc_score(state);
while (timer.elapsed() < TIME_LIMIT) {
// 乱数を使って「変更する場所」をランダムに選ぶ
int target_idx = rnd.nextInt(0, N - 1);
int old_val = state[target_idx];
int new_val = rnd.nextInt(1, M); // ランダムな新しい値
// 状態を仮に変更する
state[target_idx] = new_val;
// 変更後のスコアを計算(差分計算ができるとループが高速になり有利!)
long long next_score = calc_score(state);
// 改善していれば採用、悪化していれば元に戻す (山登り)
if (next_score >= current_score) {
current_score = next_score;
} else {
state[target_idx] = old_val; // 元に戻す(棄却)
}
}
雑多に便利なもの
pair / tuple / structured binding
用途: 複数値をまとめて持つ
計算量: O(1)。 グラフの辺や座標、複数返り値の受け取りで多用する。
// pair の定義
pair<ll, ll> p = {1, 2};
auto q = make_pair(3, 4);
// 要素の読み出し
cout << p.first << " " << p.second << "\n";
// structured binding (C++17)
auto [x, y] = p;
cout << x << " " << y << "\n";
// tuple の定義
tuple<ll, ll, ll> t = {1, 2, 3};
// tuple の分解
auto [a, b, c] = t;
// tuple の要素取得
cout << get<0>(t) << " " << get<1>(t) << " " << get<2>(t) << "\n";
注意: 比較可能な型なら pair/tuple は辞書順で比較される。
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;
};
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
注意: 大文字専用のため、小文字は無効扱い。
座標圧縮
用途: 大きい値域を連番に潰して扱う
計算量: ソート O(N log N)、各値の変換 O(log N) または O(1)。 区間や点を扱う問題で強い。
vector<long long> xs = {10, 100, 1000};
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();
};
for (auto &x : xs)
x = get_id(x);
注意:
unique の前にソートが必要。
ランレングス圧縮 (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)}