グラフ理論
動的計画法
基本アルゴリズム
1 ソート
1.1 クイックソートアルゴリズムテンプレート
—— テンプレート問題 AcWing 785. クイックソート
1
2
3
4
5
6
7
8
9
10
11
12
13
void quick_sort ( int q [], int l , int r )
{
if ( l >= r ) return ;
int i = l - 1 , j = r + 1 , x = q [ l + r >> 1 ];
while ( i < j )
{
do i ++ ; while ( q [ i ] < x );
do j -- ; while ( q [ j ] > x );
if ( i < j ) swap ( q [ i ], q [ j ]);
}
quick_sort ( q , l , j ), quick_sort ( q , j + 1 , r );
}
1.2 マージソートアルゴリズムテンプレート
—— テンプレート問題 AcWing 787. マージソート
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void merge_sort ( int q [], int l , int r )
{
if ( l >= r ) return ;
int mid = l + r >> 1 ;
merge_sort ( q , l , mid );
merge_sort ( q , mid + 1 , r );
int k = 0 , i = l , j = mid + 1 ;
while ( i <= mid && j <= r )
if ( q [ i ] <= q [ j ]) tmp [ k ++ ] = q [ i ++ ];
else tmp [ k ++ ] = q [ j ++ ];
while ( i <= mid ) tmp [ k ++ ] = q [ i ++ ];
while ( j <= r ) tmp [ k ++ ] = q [ j ++ ];
for ( i = l , j = 0 ; i <= r ; i ++ , j ++ ) q [ i ] = tmp [ j ];
}
2 二分探索
2.1 整数二分探索アルゴリズムテンプレート
—— テンプレート問題 AcWing 789. 数の範囲
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool check ( int x ) { /* ... */ } // xがある性質を満たすかどうかをチェック
// 区間[l, r]が[l, mid]と[mid + 1, r]に分割される場合:
int bsearch_1 ( int l , int r )
{
while ( l < r )
{
int mid = l + r >> 1 ;
if ( check ( mid )) r = mid ; // check()はmidが性質を満たすかどうかを判断
else l = mid + 1 ;
}
return l ;
}
// 区間[l, r]が[l, mid - 1]と[mid, r]に分割される場合:
int bsearch_2 ( int l , int r )
{
while ( l < r )
{
int mid = l + r + 1 >> 1 ;
if ( check ( mid )) l = mid ;
else r = mid - 1 ;
}
return l ;
}
2.2 浮動小数点数二分探索アルゴリズムテンプレート
—— テンプレート問題 AcWing 790. 数の三乗根
1
2
3
4
5
6
7
8
9
10
11
12
13
bool check ( double x ) { /* ... */ } // xがある性質を満たすかどうかをチェック
double bsearch_3 ( double l , double r )
{
const double eps = 1e-6 ; // epsは精度を示し、問題の精度要求に依存
while ( r - l > eps )
{
double mid = ( l + r ) / 2 ;
if ( check ( mid )) r = mid ;
else l = mid ;
}
return l ;
}
3 高精度
3.1 高精度加算
—— テンプレート問題 AcWing 791. 高精度加算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// C = A + B, A >= 0, B >= 0
vector < int > add ( vector < int > & A , vector < int > & B )
{
if ( A . size () < B . size ()) return add ( B , A );
vector < int > C ;
int t = 0 ;
for ( int i = 0 ; i < A . size (); i ++ )
{
t += A [ i ];
if ( i < B . size ()) t += B [ i ];
C . push_back ( t % 10 );
t /= 10 ;
}
if ( t ) C . push_back ( t );
return C ;
}
3.2 高精度減算
—— テンプレート問題 [AcWing 792. 高精度減算]https://www.acwing.com/problem/content/794/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// C = A - B, 満たすA >= B, A >= 0, B >= 0
vector < int > sub ( vector < int > & A , vector < int > & B )
{
vector < int > C ;
for ( int i = 0 , t = 0 ; i < A . size (); i ++ )
{
t = A [ i ] - t ;
if ( i < B . size ()) t -= B [ i ];
C . push_back (( t + 10 ) % 10 );
if ( t < 0 ) t = 1 ;
else t = 0 ;
}
while ( C . size () > 1 && C . back () == 0 ) C . pop_back ();
return C ;
}
3.3 高精度乗低精度
—— テンプレート問題 AcWing 793. 高精度乗算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// C = A * b, A >= 0, b >= 0
vector < int > mul ( vector < int > & A , int b )
{
vector < int > C ;
int t = 0 ;
for ( int i = 0 ; i < A . size () || t ; i ++ )
{
if ( i < A . size ()) t += A [ i ] * b ;
C . push_back ( t % 10 );
t /= 10 ;
}
while ( C . size () > 1 && C . back () == 0 ) C . pop_back ();
return C ;
}
3.4 高精度除以低精度
—— テンプレート問題 AcWing 794. 高精度除算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// A / b = C ... r, A >= 0, b > 0
vector < int > div ( vector < int > & A , int b , int & r )
{
vector < int > C ;
r = 0 ;
for ( int i = A . size () - 1 ; i >= 0 ; i -- )
{
r = r * 10 + A [ i ];
C . push_back ( r / b );
r %= b ;
}
reverse ( C . begin (), C . end ());
while ( C . size () > 1 && C . back () == 0 ) C . pop_back ();
return C ;
}
4 前置和と差分
前置:ある区間の和を素早く求める
差分:ある区間に+cを素早く行う、前置と逆演算
4.1 1次元前置和
—— テンプレート問題 AcWing 795. 前置和
1
2
S [ i ] = a [ 1 ] + a [ 2 ] + ... a [ i ]
a [ l ] + ... + a [ r ] = S [ r ] - S [ l - 1 ]
4.2 2次元前置和
—— テンプレート問題 AcWing 796. 子行列の和
1
2
3
S [ i , j ] = 第 i行j列の左上部分の全ての要素の和
( x1 , y1 ) を左上角、 ( x2 , y2 ) を右下角とする子行列の和は:
S [ x2 , y2 ] - S [ x1 - 1 , y2 ] - S [ x2 , y1 - 1 ] + S [ x1 - 1 , y1 - 1 ]
4.3 1次元差分
—— テンプレート問題 AcWing 797. 差分
区間[l, r]の各数にcを加える:B[l] += c, B[r + 1] -= c
4.4 2次元差分
—— テンプレート問題 AcWing 798. 差分行列
1
2
( x1 , y1 ) を左上角、 ( x2 , y2 ) を右下角とする子行列の全ての要素に cを加える :
S [ x1 , y1 ] += c , S [ x2 + 1 , y1 ] -= c , S [ x1 , y2 + 1 ] -= c , S [ x2 + 1 , y2 + 1 ] += c
5 ビット演算
—— テンプレート問題 AcWing 801. 二進数中の1の個数
1
2
nの第k位の数字を求める : n >> k & 1
nの最後の1を返す : lowbit ( n ) = n & - n
6 双ポインタアルゴリズム
—— テンプレート問題 AcWIng 799. 最長連続非重複部分列 , AcWing 800. 配列要素の目標和
1
2
3
4
5
6
7
8
9
for ( int i = 0 , j = 0 ; i < n ; i ++ )
{
while ( j < i && check ( i , j )) j ++ ;
// 具体的な問題のロジック
}
一般的な問題の分類:
( 1 ) あるシーケンスに対して、 2 つのポインタで区間を維持
( 2 ) 2 つのシーケンスに対して、ある種の順序を維持、例えばマージソートで 2 つのソート済みシーケンスをマージする操作
7 離散化
—— テンプレート問題 AcWing 802. 区間和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vector < int > alls ; // 全ての離散化する値を格納
sort ( alls . begin (), alls . end ()); // 全ての値をソート
alls . erase ( unique ( alls . begin (), alls . end ()), alls . end ()); // 重複要素を削除
// 二分探索でxに対応する離散化された値を求める
int find ( int x ) // x以上の最小の位置を見つける
{
int l = 0 , r = alls . size () - 1 ;
while ( l < r )
{
int mid = l + r >> 1 ;
if ( alls [ mid ] >= x ) r = mid ;
else l = mid + 1 ;
}
return r + 1 ; // 1, 2, ...nにマッピング
}
8 区間マージ
—— テンプレート問題 AcWing 803. 区間マージ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 全ての交差する区間をマージ
void merge ( vector < PII > & segs )
{
vector < PII > res ;
sort ( segs . begin (), segs . end ());
int st = - 2e9 , ed = - 2e9 ;
for ( auto seg : segs )
if ( ed < seg . first )
{
if ( st != - 2e9 ) res . push_back ({ st , ed });
st = seg . first , ed = seg . second ;
}
else ed = max ( ed , seg . second );
if ( st != - 2e9 ) res . push_back ({ st , ed });
segs = res ;
}
データ構造
1 リンクリストと隣接リスト:木とグラフのストレージ
1.1 単一リンクリスト
—— テンプレート問題 AcWing 826. 単一リンクリスト
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// headはリンクリストの頭を保存し、e[]はノードの値を保存し、ne[]はノードのnextポインタを保存し、idxは現在どのノードを使用しているかを示す
int head , e [ N ], ne [ N ], idx ;
// 初期化
void init ()
{
head = - 1 ;
idx = 0 ;
}
// リンクリストの頭に数aを挿入
void insert ( int a )
{
e [ idx ] = a , ne [ idx ] = head , head = idx ++ ;
}
// 頭ノードを削除、頭ノードが存在することを保証する必要がある
void remove ()
{
head = ne [ head ];
}
1.2 二重リンクリスト
—— テンプレート問題 AcWing 827. 二重リンクリスト
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// e[]はノードの値を示し、l[]はノードの左ポインタを示し、r[]はノードの右ポインタを示し、idxは現在どのノードを使用しているかを示す
int e [ N ], l [ N ], r [ N ], idx ;
// 初期化
void init ()
{
//0は左端点、1は右端点
r [ 0 ] = 1 , l [ 1 ] = 0 ;
idx = 2 ;
}
// ノードaの右側に数xを挿入
void insert ( int a , int x )
{
e [ idx ] = x ;
l [ idx ] = a , r [ idx ] = r [ a ];
l [ r [ a ]] = idx , r [ a ] = idx ++ ;
}
// ノードaを削除
void remove ( int a )
{
l [ r [ a ]] = l [ a ];
r [ l [ a ]] = r [ a ];
}
2 スタックとキュー:単調キュー、単調スタック
2.1 スタック
—— テンプレート問題 AcWing 828. スタックのシミュレーション
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ttはスタックトップを示す
int stk [ N ], tt = 0 ;
// スタックトップに数を挿入
stk [ ++ tt ] = x ;
// スタックトップから数をポップ
tt -- ;
// スタックトップの値
stk [ tt ];
// スタックが空かどうかを判断、tt > 0の場合、空でないことを示す
if ( tt > 0 )
{
}
2.2 キュー
—— テンプレート問題 AcWing 829. キューのシミュレーション
通常のキュー:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// hhはキューヘッドを示し、ttはキューの末尾を示す
int q [ N ], hh = 0 , tt = - 1 ;
// キューの末尾に数を挿入
q [ ++ tt ] = x ;
// キューヘッドから数をポップ
hh ++ ;
// キューヘッドの値
q [ hh ];
// キューが空かどうかを判断、hh <= ttの場合、空でないことを示す
if ( hh <= tt )
{
}
循環キュー
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// hhはキューヘッドを示し、ttはキューの末尾の次の位置を示す
int q [ N ], hh = 0 , tt = 0 ;
// キューの末尾に数を挿入
q [ tt ++ ] = x ;
if ( tt == N ) tt = 0 ;
// キューヘッドから数をポップ
hh ++ ;
if ( hh == N ) hh = 0 ;
// キューヘッドの値
q [ hh ];
// キューが空かどうかを判断、hh != ttの場合、空でないことを示す
if ( hh != tt )
{
}
2.3 単調スタック
—— テンプレート問題 AcWing 830. 単調スタック
一般的なモデル:各数の左側でそれに最も近い大きい/小さい数を見つける
1
2
3
4
5
6
int tt = 0 ;
for ( int i = 1 ; i <= n ; i ++ )
{
while ( tt && check ( stk [ tt ], i )) tt -- ;
stk [ ++ tt ] = i ;
}
2.4 単調キュー
—— テンプレート問題 AcWing 154. スライディングウィンドウ
一般的なモデル:スライディングウィンドウ内の最大値/最小値を見つける
1
2
3
4
5
6
7
int hh = 0 , tt = - 1 ;
for ( int i = 0 ; i < n ; i ++ )
{
while ( hh <= tt && check_out ( q [ hh ])) hh ++ ; // キューヘッドがウィンドウから滑り出たかどうかを判断
while ( hh <= tt && check ( q [ tt ], i )) tt -- ;
q [ ++ tt ] = i ;
}
3 KMP
—— テンプレート問題 AcWing 831. KMP文字列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// s[]は長いテキスト、p[]はパターン文字列、nはsの長さ、mはpの長さ
パターン文字列の Next配列を求める :
for ( int i = 2 , j = 0 ; i <= m ; i ++ )
{
while ( j && p [ i ] != p [ j + 1 ]) j = ne [ j ];
if ( p [ i ] == p [ j + 1 ]) j ++ ;
ne [ i ] = j ;
}
// マッチング
for ( int i = 1 , j = 0 ; i <= n ; i ++ )
{
while ( j && s [ i ] != p [ j + 1 ]) j = ne [ j ];
if ( s [ i ] == p [ j + 1 ]) j ++ ;
if ( j == m )
{
j = ne [ j ];
// マッチング成功後のロジック
}
} //Cのライブラリ関数 strstr()も同様
4 Trie木
—— テンプレート問題 AcWing 835. Trie文字列統計
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int son [ N ][ 26 ], cnt [ N ], idx ;
// 0番目の点は根ノードであり、空ノードでもある
// son[][]は木の各ノードの子ノードを保存
// cnt[]は各ノードで終わる単語の数を保存
// 文字列を挿入
void insert ( char * str )
{
int p = 0 ;
for ( int i = 0 ; str [ i ]; i ++ )
{
int u = str [ i ] - 'a' ;
if ( ! son [ p ][ u ]) son [ p ][ u ] = ++ idx ;
p = son [ p ][ u ];
}
cnt [ p ] ++ ;
}
// 文字列の出現回数を検索
int query ( char * str )
{
int p = 0 ;
for ( int i = 0 ; str [ i ]; i ++ )
{
int u = str [ i ] - 'a' ;
if ( ! son [ p ][ u ]) return 0 ;
p = son [ p ][ u ];
}
return cnt [ p ];
} //map<string,int>を使うこともできるが、時間は2倍以上かかる
5 併合集合
—— テンプレート問題[ AcWing 836. 集合の併合](https://www.acwing.com/problem/content/838/ , AcWing 837. 連結ブロック中の点の数
(1)素朴な併合集合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int p [ N ]; //各点の祖先ノードを保存
// xの祖先ノードを返す
int find ( int x )
{
if ( p [ x ] != x ) p [ x ] = find ( p [ x ]);
return p [ x ];
}
// 初期化、ノード番号は1~nと仮定
for ( int i = 1 ; i <= n ; i ++ ) p [ i ] = i ;
// aとbが属する2つの集合を併合:
p [ find ( a )] = find ( b );
(2)サイズを維持する併合集合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int p [ N ], size [ N ];
//p[]は各点の祖先ノードを保存し、size[]は祖先ノードのみに意味があり、祖先ノードが属する集合中の点の数を示す
// xの祖先ノードを返す
int find ( int x )
{
if ( p [ x ] != x ) p [ x ] = find ( p [ x ]);
return p [ x ];
}
// 初期化、ノード番号は1~nと仮定
for ( int i = 1 ; i <= n ; i ++ )
{
p [ i ] = i ;
size [ i ] = 1 ;
}
// aとbが属する2つの集合を併合:
size [ find ( b )] += size [ find ( a )];
p [ find ( a )] = find ( b );
(3)祖先ノードへの距離を維持する併合集合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int p [ N ], d [ N ];
//p[]は各点の祖先ノードを保存し、d[x]はxからp[x]への距離を保存
// xの祖先ノードを返す
int find ( int x )
{
if ( p [ x ] != x )
{
int u = find ( p [ x ]);
d [ x ] += d [ p [ x ]];
p [ x ] = u ;
}
return p [ x ];
}
// 初期化、ノード番号は1~nと仮定
for ( int i = 1 ; i <= n ; i ++ )
{
p [ i ] = i ;
d [ i ] = 0 ;
}
// aとbが属する2つの集合を併合:
p [ find ( a )] = find ( b );
d [ find ( a )] = distance ; // 具体的な問題に基づいて、find(a)のオフセットを初期化
6 ヒープ
—— テンプレート問題[ AcWing 838. ヒープソート](https://www.acwing.com/problem/content/840/ , AcWing 839. ヒープのシミュレーション
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// h[N]はヒープ中の値を保存し、h[1]はヒープトップであり、xの左子は2x、右子は2x + 1
// ph[k]はk番目に挿入された点がヒープ中のどの位置にあるかを保存
// hp[k]はヒープ中のインデックスがkの点が何番目に挿入されたかを保存
int h [ N ], ph [ N ], hp [ N ], size ;
// 2つの点とそのマッピング関係を交換
void heap_swap ( int a , int b )
{
swap ( ph [ hp [ a ]], ph [ hp [ b ]]);
swap ( hp [ a ], hp [ b ]);
swap ( h [ a ], h [ b ]);
}
void down ( int u )
{
int t = u ;
if ( u * 2 <= size && h [ u * 2 ] < h [ t ]) t = u * 2 ;
if ( u * 2 + 1 <= size && h [ u * 2 + 1 ] < h [ t ]) t = u * 2 + 1 ;
if ( u != t )
{
heap_swap ( u , t );
down ( t );
}
}
void up ( int u )
{
while ( u / 2 && h [ u ] < h [ u / 2 ])
{
heap_swap ( u , u / 2 );
u >>= 1 ;
}
}
// O(n)でヒープを構築
for ( int i = n / 2 ; i ; i -- ) down ( i );
7 ハッシュ
7.1 一般的なハッシュ
—— テンプレート問題 AcWing 840. ハッシュテーブルのシミュレーション
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
( 1 ) チェイン法
int h [ N ], e [ N ], ne [ N ], idx ;
// ハッシュテーブルに数を挿入
void insert ( int x )
{
int k = ( x % N + N ) % N ;
e [ idx ] = x ;
ne [ idx ] = h [ k ];
h [ k ] = idx ++ ;
}
// ハッシュテーブルに特定の数が存在するかどうかを検索
bool find ( int x )
{
int k = ( x % N + N ) % N ;
for ( int i = h [ k ]; i != - 1 ; i = ne [ i ])
if ( e [ i ] == x )
return true ;
return false ;
}
( 2 ) オープンアドレス法
int h [ N ];
// xがハッシュテーブルに存在する場合、xのインデックスを返す。xがハッシュテーブルに存在しない場合、xを挿入すべき位置を返す
int find ( int x )
{
int t = ( x % N + N ) % N ;
while ( h [ t ] != null && h [ t ] != x )
{
t ++ ;
if ( t == N ) t = 0 ;
}
return t ;
}
7.2 文字列ハッシュ
—— テンプレート問題 AcWing 841. 文字列ハッシュ
コアの考え方:文字列をP進数と見なし、Pの経験値は131または13331であり、これらの値の衝突確率は低い
小技:モジュロの数を2^64にし、unsigned long longで直接保存し、オーバーフローの結果がモジュロの結果になる
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef unsigned long long ULL ;
ULL h [ N ], p [ N ]; // h[k]は文字列の最初のk文字のハッシュ値を保存し、p[k]は P^k mod 2^64を保存
// 初期化
p [ 0 ] = 1 ;
for ( int i = 1 ; i <= n ; i ++ )
{
h [ i ] = h [ i - 1 ] * P + str [ i ];
p [ i ] = p [ i - 1 ] * P ;
}
// 部分文字列 str[l ~ r] のハッシュ値を計算
ULL get ( int l , int r )
{
return h [ r ] - h [ l - 1 ] * p [ r - l + 1 ];
}
7.3 C++ STL概要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
vector , 可変長配列、倍増の考え方
size () 要素数を返す
empty () 空かどうかを返す
clear () クリア
front () / back ()
push_back () / pop_back ()
begin () / end ()
[]
辞書順で比較演算をサポート
pair < int , int >
first , 最初の要素
second , 2 番目の要素
比較演算をサポートし、 firstを第1キー 、 secondを第2キー (辞書順)として使用
string , 文字列
size () / length () 文字列の長さを返す
empty ()
clear ()
substr ( 開始インデックス、 ( 部分文字列の長さ )) 部分文字列を返す
c_str () 文字列が格納されている文字配列の開始アドレスを返す
queue , キュー
size ()
empty ()
push () キューの末尾に要素を挿入
front () キューヘッドの要素を返す
back () キューの末尾の要素を返す
pop () キューヘッドの要素をポップ
priority_queue , 優先度キュー、デフォルトは大根ヒープ
size ()
empty ()
push () 要素を挿入
top () ヒープトップの要素を返す
pop () ヒープトップの要素をポップ
小根ヒープとして定義する方法: priority_queue < int , vector < int > , greater < int >> q ;
stack , スタック
size ()
empty ()
push () スタックトップに要素を挿入
top () スタックトップの要素を返す
pop () スタックトップの要素をポップ
deque , 両端キュー
size ()
empty ()
clear ()
front () / back ()
push_back () / pop_back ()
push_front () / pop_front ()
begin () / end ()
[]
set , map , multiset , multimap , 平衡二分木(赤黒木)に基づき、有序列を動的に維持
size ()
empty ()
clear ()
begin () / end ()
++ , -- 前駆と後継を返し、時間複雑度は O ( logn )
set / multiset
insert () 数を挿入
find () 数を検索
count () 特定の数の個数を返す
erase ()
( 1 ) 入力が数 xの場合 、全ての xを削除 O ( k + logn )
( 2 ) 入力がイテレータの場合、そのイテレータを削除
lower_bound () / upper_bound ()
lower_bound ( x ) x以上の最小の数のイテレータを返す
upper_bound ( x ) xより大きい最小の数のイテレータを返す
map / multimap
insert () 挿入する数は pair
erase () 入力は pairまたはイテレータ
find ()
[] 注意: multimapはこの操作をサポートしない 。 時間複雑度は O ( logn )
lower_bound () / upper_bound ()
unordered_set , unordered_map , unordered_multiset , unordered_multimap , ハッシュテーブル
上記と類似し、増減改削の時間複雑度は O ( 1 )
lower_bound () / upper_bound () 、イテレータの ++ 、 -- をサポートしない
bitset , 圧縮
bitset < 10000 > s ;
~ , & , | , ^
>> , <<
== , !=
[]
count () 1 の数を返す
any () 少なくとも 1 つの 1 があるかどうかを判断
none () 全て 0 かどうかを判断
set () 全ての位置を 1 にする
set ( k , v ) 第 k位をvにする
reset () 全ての位置を 0 にする
flip () ~ と等価
flip ( k ) 第 k位を反転
探索とグラフ理論
1 DFSとBFS
1.1 木とグラフのストレージ
木は特殊なグラフであり、グラフのストレージ方法と同じです。
無向グラフの辺abに対して、2つの有向辺a->b, b->aを保存します。
したがって、有向グラフのストレージのみを考えればよいです。
(1) 隣接行列:g[a][b]
は辺a->bを保存
(2) 隣接リスト:
1
2
3
4
5
6
7
8
9
10
11
12
// 各点kに対して、単一リンクリストを開き、kから到達可能な全ての点を保存します。h[k]はこの単一リンクリストの頭ノードを保存します
int h [ N ], e [ N ], ne [ N ], idx ;
// 辺a->bを追加
void add ( int a , int b )
{
e [ idx ] = b , ne [ idx ] = h [ a ], h [ a ] = idx ++ ;
}
// 初期化
idx = 0 ;
memset ( h , - 1 , sizeof h );
1.2 木とグラフの遍歴
時間複雑度 O(n+m), nは点数を示し、m は辺数を示します
(1) 深さ優先遍歴 —— テンプレート問題 AcWing 846. 木の重心
1
2
3
4
5
6
7
8
9
10
int dfs ( int u )
{
st [ u ] = true ; // st[u] は点uが既に遍歴されたかどうかを示します
for ( int i = h [ u ]; i != - 1 ; i = ne [ i ])
{
int j = e [ i ];
if ( ! st [ j ]) dfs ( j );
}
}
(2) 幅優先遍歴 —— テンプレート問題 AcWing 847. グラフ中の点のレベル
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
queue < int > q ;
st [ 1 ] = true ; // 1番目の点が既に遍歴されたことを示します
q . push ( 1 );
while ( q . size ())
{
int t = q . front ();
q . pop ();
for ( int i = h [ t ]; i != - 1 ; i = ne [ i ])
{
int j = e [ i ];
if ( ! st [ j ])
{
st [ j ] = true ; // 点jが既に遍歴されたことを示します
q . push ( j );
}
}
}
2 木とグラフの遍歴 トポロジカルソート
—— テンプレート問題 AcWing 848. 有向グラフのトポロジカルシーケンス
時間複雑度 O(n+m), n は点数を示し、mは辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool topsort ()
{
int hh = 0 , tt = - 1 ;
// d[i] は点iの入次数を保存します
for ( int i = 1 ; i <= n ; i ++ )
if ( ! d [ i ])
q [ ++ tt ] = i ;
while ( hh <= tt )
{
int t = q [ hh ++ ];
for ( int i = h [ t ]; i != - 1 ; i = ne [ i ])
{
int j = e [ i ];
if ( -- d [ j ] == 0 )
q [ ++ tt ] = j ;
}
}
// 全ての点がキューに入った場合、トポロジカルシーケンスが存在することを示します。そうでない場合、トポロジカルシーケンスは存在しません。
return tt == n - 1 ;
}
3 最短経路
3.1 素朴なdijkstraアルゴリズム
—— テンプレート問題 AcWing 849. Dijkstraで最短経路を求める I
時間複雑度は O(n2+m), n は点数を示し、mは辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int g [ N ][ N ]; // 全ての辺を保存
int dist [ N ]; // 1番目の点から各点への最短距離を保存
bool st [ N ]; // 各点の最短経路が既に確定したかどうかを保存
// 1番目の点からn番目の点への最短経路を求めます。存在しない場合は-1を返します
int dijkstra ()
{
memset ( dist , 0x3f , sizeof dist );
dist [ 1 ] = 0 ;
for ( int i = 0 ; i < n - 1 ; i ++ )
{
int t = - 1 ; // まだ最短経路が確定していない点の中で、距離が最小の点を探します
for ( int j = 1 ; j <= n ; j ++ )
if ( ! st [ j ] && ( t == - 1 || dist [ t ] > dist [ j ]))
t = j ;
// tを用いて他の点の距離を更新します
for ( int j = 1 ; j <= n ; j ++ )
dist [ j ] = min ( dist [ j ], dist [ t ] + g [ t ][ j ]);
st [ t ] = true ;
}
if ( dist [ n ] == 0x3f3f3f3f ) return - 1 ;
return dist [ n ];
}
3.2 ヒープ最適化版dijkstra
—— テンプレート問題 AcWing 850. Dijkstraで最短経路を求める II
時間複雑度 O(mlogn), n は点数を示し、mは辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
typedef pair < int , int > PII ;
int n ; // 点の数
int h [ N ], w [ N ], e [ N ], ne [ N ], idx ; // 隣接リストで全ての辺を保存
int dist [ N ]; // 各点から1番目の点への距離を保存
bool st [ N ]; // 各点の最短距離が既に確定したかどうかを保存
// 1番目の点からn番目の点への最短距離を求めます。1番目の点からn番目の点に到達できない場合は-1を返します
int dijkstra ()
{
memset ( dist , 0x3f , sizeof dist );
dist [ 1 ] = 0 ;
priority_queue < PII , vector < PII > , greater < PII >> heap ;
heap . push ({ 0 , 1 }); // firstは距離を保存し、secondはノード番号を保存します
while ( heap . size ())
{
auto t = heap . top ();
heap . pop ();
int ver = t . second , distance = t . first ;
if ( st [ ver ]) continue ;
st [ ver ] = true ;
for ( int i = h [ ver ]; i != - 1 ; i = ne [ i ])
{
int j = e [ i ];
if ( dist [ j ] > distance + w [ i ])
{
dist [ j ] = distance + w [ i ];
heap . push ({ dist [ j ], j });
}
}
}
if ( dist [ n ] == 0x3f3f3f3f ) return - 1 ;
return dist [ n ];
}
3.3 Bellman-Fordアルゴリズム
—— テンプレート問題 AcWing 853. 辺数制限のある最短経路
時間複雑度 O(nm), nは点数を示し、mは辺数を示します
注意:テンプレート問題では、以下のテンプレートを少し修正し、バックアップ配列を追加する必要があります。詳細はテンプレート問題を参照してください。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int n , m ; // nは点数を示し、mは辺数を示します
int dist [ N ]; // dist[x]は1からxへの最短経路距離を保存
struct Edge // 辺、aは出発点、bは到達点、wは辺の重みを示します
{
int a , b , w ;
} edges [ M ];
// 1からnへの最短経路距離を求めます。1からnに到達できない場合は-1を返します。
int bellman_ford ()
{
memset ( dist , 0x3f , sizeof dist );
dist [ 1 ] = 0 ;
// n回目の反復でも三角不等式が緩和される場合、長さがn+1の最短経路が存在することを示し、抽斗原理により、経路中に少なくとも2つの同じ点が存在するため、負の重みのサイクルが存在することを示します。
for ( int i = 0 ; i < n ; i ++ )
{
for ( int j = 0 ; j < m ; j ++ )
{
int a = edges [ j ]. a , b = edges [ j ]. b , w = edges [ j ]. w ;
if ( dist [ b ] > dist [ a ] + w )
dist [ b ] = dist [ a ] + w ;
}
}
if ( dist [ n ] > 0x3f3f3f3f / 2 ) return - 1 ;
return dist [ n ];
}
3.4 spfa アルゴリズム(キュー最適化されたBellman-Fordアルゴリズム)
—— テンプレート問題 AcWing 851. spfaで最短経路を求める
時間複雑度 平均的な場合は O(m)、最悪の場合は O(nm), nは点数を示し、mは辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int n ; // 総点数
int h [ N ], w [ N ], e [ N ], ne [ N ], idx ; // 隣接リストで全ての辺を保存
int dist [ N ]; // 各点から1番目の点への最短距離を保存
bool st [ N ]; // 各点がキューに入っているかどうかを保存
// 1番目の点からn番目の点への最短経路距離を求めます。1番目の点からn番目の点に到達できない場合は-1を返します
int spfa ()
{
memset ( dist , 0x3f , sizeof dist );
dist [ 1 ] = 0 ;
queue < int > q ;
q . push ( 1 );
st [ 1 ] = true ;
while ( q . size ())
{
auto t = q . front ();
q . pop ();
st [ t ] = false ;
for ( int i = h [ t ]; i != - 1 ; i = ne [ i ])
{
int j = e [ i ];
if ( dist [ j ] > dist [ t ] + w [ i ])
{
dist [ j ] = dist [ t ] + w [ i ];
if ( ! st [ j ]) // jが既にキューに存在する場合、jを重複して挿入する必要はありません
{
q . push ( j );
st [ j ] = true ;
}
}
}
}
if ( dist [ n ] == 0x3f3f3f3f ) return - 1 ;
return dist [ n ];
}
3.5 spfaでグラフに負のサイクルが存在するかどうかを判断
—— テンプレート問題 AcWing 852. spfaで負のサイクルを判断
時間複雑度は O(nm), nは点数を示し、mは辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int n ; // 総点数
int h [ N ], w [ N ], e [ N ], ne [ N ], idx ; // 隣接リストで全ての辺を保存
int dist [ N ], cnt [ N ]; // dist[x]は1番目の点からxへの最短距離を保存し、cnt[x]は1からxへの最短経路に含まれる点数を保存
bool st [ N ]; // 各点がキューに入っているかどうかを保存
// 負のサイクルが存在する場合はtrueを返し、そうでない場合はfalseを返します。
bool spfa ()
{
// dist配列を初期化する必要はありません
// 原理:ある最短経路にn個の点がある場合(自分を除く)、自分を加えるとn+1個の点があり、抽斗原理により、少なくとも2つの点が同じであるため、サイクルが存在することを示します。
queue < int > q ;
for ( int i = 1 ; i <= n ; i ++ )
{
q . push ( i );
st [ i ] = true ;
}
while ( q . size ())
{
auto t = q . front ();
q . pop ();
st [ t ] = false ;
for ( int i = h [ t ]; i != - 1 ; i = ne [ i ])
{
int j = e [ i ];
if ( dist [ j ] > dist [ t ] + w [ i ])
{
dist [ j ] = dist [ t ] + w [ i ];
cnt [ j ] = cnt [ t ] + 1 ;
if ( cnt [ j ] >= n ) return true ; // 1番目の点からxへの最短経路に少なくともn個の点が含まれる場合(自分を除く)、サイクルが存在することを示します
if ( ! st [ j ])
{
q . push ( j );
st [ j ] = true ;
}
}
}
}
return false ;
}
3.6 floydアルゴリズム
—— テンプレート問題 AcWing 854. Floydで最短経路を求める
時間複雑度は O(n3), n は点数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
初期化:
for ( int i = 1 ; i <= n ; i ++ )
for ( int j = 1 ; j <= n ; j ++ )
if ( i == j ) d [ i ][ j ] = 0 ;
else d [ i ][ j ] = INF ;
// アルゴリズム終了後、d[a][b]はaからbへの最短距離を示します
void floyd ()
{
for ( int k = 1 ; k <= n ; k ++ )
for ( int i = 1 ; i <= n ; i ++ )
for ( int j = 1 ; j <= n ; j ++ )
d [ i ][ j ] = min ( d [ i ][ j ], d [ i ][ k ] + d [ k ][ j ]);
}
4 最小生成木
4.1 素朴版primアルゴリズム
—— テンプレート問題 AcWing 858. Primアルゴリズムで最小生成木を求める
時間複雑度は O(n2+m), n は点数を示し、mは辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int n ; // nは点数を示します
int g [ N ][ N ]; // 隣接行列で全ての辺を保存
int dist [ N ]; // 他の点から現在の最小生成木への距離を保存
bool st [ N ]; // 各点が既に生成木に含まれているかどうかを保存
// グラフが連結でない場合、INF(値は0x3f3f3f3f)を返し、そうでない場合は最小生成木の辺の重みの和を返します
int prim ()
{
memset ( dist , 0x3f , sizeof dist );
int res = 0 ;
for ( int i = 0 ; i < n ; i ++ )
{
int t = - 1 ;
for ( int j = 1 ; j <= n ; j ++ )
if ( ! st [ j ] && ( t == - 1 || dist [ t ] > dist [ j ]))
t = j ;
if ( i && dist [ t ] == INF ) return INF ;
if ( i ) res += dist [ t ];
st [ t ] = true ;
for ( int j = 1 ; j <= n ; j ++ ) dist [ j ] = min ( dist [ j ], g [ t ][ j ]);
}
return res ;
}
4.2 Kruskalアルゴリズム
—— テンプレート問題 AcWing 859. Kruskalアルゴリズムで最小生成木を求める
時間複雑度は O(mlogm), nは点数を示し、mは辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int n , m ; // nは点数を示し、mは辺数を示します
int p [ N ]; // 併合集合の親ノード配列
struct Edge // 辺を保存
{
int a , b , w ;
bool operator < ( const Edge & W ) const
{
return w < W . w ;
}
} edges [ M ];
int find ( int x ) // 併合集合のコア操作
{
if ( p [ x ] != x ) p [ x ] = find ( p [ x ]);
return p [ x ];
}
int kruskal ()
{
sort ( edges , edges + m );
for ( int i = 1 ; i <= n ; i ++ ) p [ i ] = i ; // 併合集合を初期化
int res = 0 , cnt = 0 ;
for ( int i = 0 ; i < m ; i ++ )
{
int a = edges [ i ]. a , b = edges [ i ]. b , w = edges [ i ]. w ;
a = find ( a ), b = find ( b );
if ( a != b ) // 2つの連結ブロックが連結していない場合、これらの連結ブロックを併合します
{
p [ a ] = b ;
res += w ;
cnt ++ ;
}
}
if ( cnt < n - 1 ) return INF ;
return res ;
}
5 二部グラフ
5.1 染色法で二部グラフを判定
—— テンプレート問題 AcWing 860. 染色法で二部グラフを判定
時間複雑度は O(n+m), nは点数を示し、mは辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int n ; // nは点数を示します
int h [ N ], e [ M ], ne [ M ], idx ; // 隣接リストでグラフを保存
int color [ N ]; // 各点の色を示し、-1は未染色、0は白色、1は黒色を示します
// パラメータ:uは現在のノードを示し、cは現在の点の色を示します
bool dfs ( int u , int c )
{
color [ u ] = c ;
for ( int i = h [ u ]; i != - 1 ; i = ne [ i ])
{
int j = e [ i ];
if ( color [ j ] == - 1 )
{
if ( ! dfs ( j , ! c )) return false ;
}
else if ( color [ j ] == c ) return false ;
}
return true ;
}
bool check ()
{
memset ( color , - 1 , sizeof color );
bool flag = true ;
for ( int i = 1 ; i <= n ; i ++ )
if ( color [ i ] == - 1 )
if ( ! dfs ( i , 0 ))
{
flag = false ;
break ;
}
return flag ;
}
5.2 ハンガリーアルゴリズム
—— テンプレート問題 AcWing 861. 二部グラフの最大マッチング
時間複雑度は O(nm), nは点数を示し、m は辺数を示します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int n1 , n2 ; // n1は第1集合中の点数を示し、n2は第2集合中の点数を示します
int h [ N ], e [ M ], ne [ M ], idx ; // 隣接リストで全ての辺を保存し、ハンガリーアルゴリズムでは第1集合から第2集合への辺のみを使用するため、ここでは1方向の辺のみを保存します
int match [ N ]; // 第2集合中の各点が現在マッチングしている第1集合中の点を保存
bool st [ N ]; // 第2集合中の各点が既に遍歴されたかどうかを保存
bool find ( int x )
{
for ( int i = h [ x ]; i != - 1 ; i = ne [ i ])
{
int j = e [ i ];
if ( ! st [ j ])
{
st [ j ] = true ;
if ( match [ j ] == 0 || find ( match [ j ]))
{
match [ j ] = x ;
return true ;
}
}
}
return false ;
}
// 最大マッチング数を求め、第1集合中の各点が第2集合中の点とマッチングできるかどうかを順次列挙
int res = 0 ;
for ( int i = 1 ; i <= n1 ; i ++ )
{
memset ( st , false , sizeof st );
if ( find ( i )) res ++ ;
}
数学的知識
1 素数
1.1 試し割り法で素数を判定
—— テンプレート問題 AcWing 866. 試し割り法で素数を判定
1
2
3
4
5
6
7
8
bool is_prime ( int x )
{
if ( x < 2 ) return false ;
for ( int i = 2 ; i <= x / i ; i ++ )
if ( x % i == 0 )
return false ;
return true ;
}
1.2 試し割り法で素因数を分解
—— テンプレート問題 AcWing 867. 素因数を分解
1
2
3
4
5
6
7
8
9
10
11
12
void divide ( int x )
{
for ( int i = 2 ; i <= x / i ; i ++ )
if ( x % i == 0 )
{
int s = 0 ;
while ( x % i == 0 ) x /= i , s ++ ;
cout << i << ' ' << s << endl ;
}
if ( x > 1 ) cout << x << ' ' << 1 << endl ;
cout << endl ;
}
1.3 素朴なエラトステネスの篩法で素数を求める
—— テンプレート問題 AcWing 868. 素数を篩う
1
2
3
4
5
6
7
8
9
10
11
12
13
int primes [ N ], cnt ; // 全ての素数を保存
bool st [ N ]; // st[x]はxが篩われたかどうかを保存
void get_primes ( int n )
{
for ( int i = 2 ; i <= n ; i ++ )
{
if ( st [ i ]) continue ;
primes [ cnt ++ ] = i ;
for ( int j = i + i ; j <= n ; j += i )
st [ j ] = true ;
}
}
1.4 線形篩法で素数を求める
—— テンプレート問題 AcWing 868. 素数を篩う
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int primes [ N ], cnt ; // 全ての素数を保存
bool st [ N ]; // st[x]はxが篩われたかどうかを保存
void get_primes ( int n )
{
for ( int i = 2 ; i <= n ; i ++ )
{
if ( ! st [ i ]) primes [ cnt ++ ] = i ;
for ( int j = 0 ; primes [ j ] <= n / i ; j ++ )
{
st [ primes [ j ] * i ] = true ;
if ( i % primes [ j ] == 0 ) break ;
}
}
}
2 約数
2.1 試し割り法で全ての約数を求める
—— テンプレート問題 AcWing 869. 試し割り法で約数を求める
1
2
3
4
5
6
7
8
9
10
11
12
vector < int > get_divisors ( int x )
{
vector < int > res ;
for ( int i = 1 ; i <= x / i ; i ++ )
if ( x % i == 0 )
{
res . push_back ( i );
if ( i != x / i ) res . push_back ( x / i );
}
sort ( res . begin (), res . end ());
return res ;
}
2.2 約数の個数と約数の和
—— テンプレート問題 [AcWing 870. 約数の個数](https://www.acwing.com/problem/content/872/ , AcWing 871. 約数の和
1
2
3
もし N = p1 ^ c1 * p2 ^ c2 * ... * pk ^ ck
約数の個数: ( c1 + 1 ) * ( c2 + 1 ) * ... * ( ck + 1 )
約数の和: ( p1 ^ 0 + p1 ^ 1 + ... + p1 ^ c1 ) * ... * ( pk ^ 0 + pk ^ 1 + ... + pk ^ ck )
2.3 ユークリッドの互除法
—— テンプレート問題 AcWing 872. 最大公約数
1
2
3
4
int gcd ( int a , int b )
{
return b ? gcd ( b , a % b ) : a ;
}
3 オイラー関数
3.1 オイラー関数を求める
—— テンプレート問題 AcWing 873. オイラー関数
1
2
3
4
5
6
7
8
9
10
11
12
13
int phi ( int x )
{
int res = x ;
for ( int i = 2 ; i <= x / i ; i ++ )
if ( x % i == 0 )
{
res = res / i * ( i - 1 );
while ( x % i == 0 ) x /= i ;
}
if ( x > 1 ) res = res / x * ( x - 1 );
return res ;
}
3.2 篩法でオイラー関数を求める
—— テンプレート問題 AcWing 874. 篩法でオイラー関数を求める
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int primes [ N ], cnt ; // 全ての素数を保存
int euler [ N ]; // 各数のオイラー関数を保存
bool st [ N ]; // st[x]はxが篩われたかどうかを保存
void get_eulers ( int n )
{
euler [ 1 ] = 1 ;
for ( int i = 2 ; i <= n ; i ++ )
{
if ( ! st [ i ])
{
primes [ cnt ++ ] = i ;
euler [ i ] = i - 1 ;
}
for ( int j = 0 ; primes [ j ] <= n / i ; j ++ )
{
int t = primes [ j ] * i ;
st [ t ] = true ;
if ( i % primes [ j ] == 0 )
{
euler [ t ] = euler [ i ] * primes [ j ];
break ;
}
euler [ t ] = euler [ i ] * ( primes [ j ] - 1 );
}
}
}
4 高速累乗
—— テンプレート問題 AcWing 875. 高速累乗
m^k mod pを求め、時間複雑度は O(logk)。
1
2
3
4
5
6
7
8
9
10
11
int qmi ( int m , int k , int p )
{
int res = 1 % p , t = m ;
while ( k )
{
if ( k & 1 ) res = res * t % p ;
t = t * t % p ;
k >>= 1 ;
}
return res ;
}
5 拡張ユークリッドの互除法
—— テンプレート問題 AcWing 877. 拡張ユークリッドの互除法
1
2
3
4
5
6
7
8
9
10
11
12
// x, yを求め、ax + by = gcd(a, b)を満たす
int exgcd ( int a , int b , int & x , int & y )
{
if ( ! b )
{
x = 1 ; y = 0 ;
return a ;
}
int d = exgcd ( b , a % b , y , x );
y -= ( a / b ) * x ;
return d ;
}
6 ガウス消去法
—— テンプレート問題 AcWing 883. ガウス消去法で線形方程式を解く
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// a[N][N]は拡大行列
int gauss ()
{
int c , r ;
for ( c = 0 , r = 0 ; c < n ; c ++ )
{
int t = r ;
for ( int i = r ; i < n ; i ++ ) // 絶対値が最大の行を見つける
if ( fabs ( a [ i ][ c ]) > fabs ( a [ t ][ c ]))
t = i ;
if ( fabs ( a [ t ][ c ]) < eps ) continue ;
for ( int i = c ; i <= n ; i ++ ) swap ( a [ t ][ i ], a [ r ][ i ]); // 絶対値が最大の行を最上部に交換
for ( int i = n ; i >= c ; i -- ) a [ r ][ i ] /= a [ r ][ c ]; // 現在の行の先頭を1にする
for ( int i = r + 1 ; i < n ; i ++ ) // 現在の行を用いて下の全ての列を0にする
if ( fabs ( a [ i ][ c ]) > eps )
for ( int j = n ; j >= c ; j -- )
a [ i ][ j ] -= a [ r ][ j ] * a [ i ][ c ];
r ++ ;
}
if ( r < n )
{
for ( int i = r ; i < n ; i ++ )
if ( fabs ( a [ i ][ n ]) > eps )
return 2 ; // 解なし
return 1 ; // 無限に多くの解がある
}
for ( int i = n - 1 ; i >= 0 ; i -- )
for ( int j = i + 1 ; j < n ; j ++ )
a [ i ][ n ] -= a [ i ][ j ] * a [ j ][ n ];
return 0 ; // 唯一の解がある
}
7 組み合わせ計算
7.1 再帰法で組み合わせ数を求める
—— テンプレート問題 AcWing 885. 組み合わせ数 Iを求める
1
2
3
4
5
// c[a][b] はa個のリンゴからb個を選ぶ方法の数を示します
for ( int i = 0 ; i < N ; i ++ )
for ( int j = 0 ; j <= i ; j ++ )
if ( ! j ) c [ i ][ j ] = 1 ;
else c [ i ][ j ] = ( c [ i - 1 ][ j ] + c [ i - 1 ][ j - 1 ]) % mod ;
7.2 逆元を事前処理する方法で組み合わせ数を求める
—— テンプレート問題 AcWing 886. 組み合わせ数 IIを求める
まず、全ての階乗のモジュロの余数fact[N]
と全ての階乗の逆元の余数infact[N]
を事前処理します
モジュロの数が素数の場合、フェルマーの小定理を用いて逆元を求めることができます
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int qmi ( int a , int k , int p ) // 高速累乗のテンプレート
{
int res = 1 ;
while ( k )
{
if ( k & 1 ) res = ( LL ) res * a % p ;
a = ( LL ) a * a % p ;
k >>= 1 ;
}
return res ;
}
// 階乗の余数と階乗逆元の余数を事前処理
fact [ 0 ] = infact [ 0 ] = 1 ;
for ( int i = 1 ; i < N ; i ++ )
{
fact [ i ] = ( LL ) fact [ i - 1 ] * i % mod ;
infact [ i ] = ( LL ) infact [ i - 1 ] * qmi ( i , mod - 2 , mod ) % mod ;
}
7.3 Lucasの定理
—— テンプレート問題 AcWing 887. 組み合わせ数 IIIを求める
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pが素数の場合 、任意の整数 1 <= m <= n に対して、次の式が成り立ちます:
C ( n , m ) = C ( n % p , m % p ) * C ( n / p , m / p ) ( mod p )
int qmi ( int a , int k , int p ) // 高速累乗のテンプレート
{
int res = 1 % p ;
while ( k )
{
if ( k & 1 ) res = ( LL ) res * a % p ;
a = ( LL ) a * a % p ;
k >>= 1 ;
}
return res ;
}
int C ( int a , int b , int p ) // 定理を用いて組み合わせ数C(a, b)を求める
{
if ( a < b ) return 0 ;
LL x = 1 , y = 1 ; // xは分子、yは分母
for ( int i = a , j = 1 ; j <= b ; i -- , j ++ )
{
x = ( LL ) x * i % p ;
y = ( LL ) y * j % p ;
}
return x * ( LL ) qmi ( y , p - 2 , p ) % p ;
}
int lucas ( LL a , LL b , int p )
{
if ( a < p && b < p ) return C ( a , b , p );
return ( LL ) C ( a % p , b % p , p ) * lucas ( a / p , b / p , p ) % p ;
}
7.4 素因数分解法で組み合わせ数を求める
—— テンプレート問題 AcWing 888. 組み合わせ数 IVを求める
組み合わせ数の実際の値を求める必要がある場合、特定の数の余数ではなく、素因数分解の方法が便利です:
1. 篩法で範囲内の全ての素数を求める
2. C(a, b) = a! / b! / (a - b)! という公式を用いて各素因数の次数を求める。 n! 中のpの次数は n / p + n / p^2 + n / p^3 + …
3. 高精度乗算を用いて全ての素因数を掛け合わせる
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int primes [ N ], cnt ; // 全ての素数を保存
int sum [ N ]; // 各素数の次数を保存
bool st [ N ]; // 各数が篩われたかどうかを保存
void get_primes ( int n ) // 線形篩法で素数を求める
{
for ( int i = 2 ; i <= n ; i ++ )
{
if ( ! st [ i ]) primes [ cnt ++ ] = i ;
for ( int j = 0 ; primes [ j ] <= n / i ; j ++ )
{
st [ primes [ j ] * i ] = true ;
if ( i % primes [ j ] == 0 ) break ;
}
}
}
int get ( int n , int p ) // n!中の次数を求める
{
int res = 0 ;
while ( n )
{
res += n / p ;
n /= p ;
}
return res ;
}
vector < int > mul ( vector < int > a , int b ) // 高精度乗低精度のテンプレート
{
vector < int > c ;
int t =