このライブラリについて
関連リンク
- CTF関連はこちらにも書いています.
各事項
- 動作は保証しません.
- issueを出してくださると直すかもしれません.
- ❌がついている項目は未実装です.
- ⚠️がついている項目は未実装なところが一部あります.
- CTF関係のプログラムのみpython,他は全てC++です.
- ドキュメントの充実さよりも項目を増やすことの方が優先度が高いので説明文は結構適当です.
- 全てのソースコードを私shibaken28が書いたわけではないです
- 自分が書いていないコードはCC0ライセンスで公開されているものを使用しています.
- 余談ですが,勉強のため,できるだけ自分で一度は実装するようにしています.
メモ
git subtree push --prefix docs/book/ origin gh-pages
C++の仕様や便利機能
参考になった資料
Makefile
オプションをつけないとC++17でコンパイルされない.構造体束縛とかが使えなくて泣く. 毎回オプションを付けるのは面倒なので,Makefileを作っておくと便利.
#Makefile
CC=g++
CFLAGS=-Wall -std=c++17
.SUFFIXES = .cpp
objs:=$(wildcard *.cpp)
targets:=$(objs:.cpp= )
.PHONY:all
all: $(targets)
.cpp:
$(CC) $(CFLAGS) -o $@ $<
構造体束縛
C++日本語リファレンスよりコードを引用
std::pair<int, std::string> f()
{
return {3, "Hello"};
}
// 関数f()の戻り値である整数と文字列の組を分解する。
// pairのfirstをid変数に代入し、secondをmessage変数に代入する。
// id変数の型はfirstの型(int)となり、message変数の型はsecondの型(string)となる。
auto [id, message] = f();
これにより,pair型などで受け取って中身をいちいち別の変数に取り出す手間がなくなる.
map
の値の取り出しでも重宝する.
map<int,int> mp;
for(auto [key,value]:mp){
//key,valueを使う
cout<<key<<" "<<value<<endl;
}
ラムダ式
関数に関数を渡すときに便利.構文は次の通り.
[キャプチャする変数](引数){ 関数の本体 }
型名はよくわかんないのでauto
に任しておく.
auto pow = [](int a){return a*a;}; //引数aを受け取ってa*aを返す関数
cout<<pow(4)<<endl; //16
ラムダ式の中で外の変数を使いたい場合はキャプチャする.
int x=4;
auto mul = [x](int a){return x*a;};
cout<<mul(3)<<endl; //12
&
を付けると参照キャプチャになる.他にもキャプチャの種類があるがここでは紹介しない.
int x=4;
auto mul = [&x](int a){return x*a;};
cout<<mul(3)<<endl; //12
cin
とcout
を定義
vector
とかpair
,set
をcout
できるようにすると便利(これをまとめて書く方法を知っている方がいれば教えてください).
//vector
template<class T> std::ostream &operator<<(std::ostream &out, const vector<T> &A){
for(const T &a:A){
cout<<a<<" ";
}
return out;
}
//pair
template<class T1,class T2> std::ostream &operator<<(std::ostream &out, const pair<T1,T2> &A){
cout<<"{"<<A.first<<","<<A.second<<"}";
return out;
}
//map
template<class T1,class T2> std::ostream &operator<<(std::ostream &out, const map<T1,T2> &M){
for(const auto&A:M){
cout<<"{"<<A.first<<","<<A.second<<"}";
}
return out;
}
//set
template<class T1> std::ostream &operator<<(std::ostream &out, const set<T1> &M){
cout<<"{";
for(const auto&A:M){
cout<<A<<", ";
}
cout<<"}"<<endl;
return out;
}
また,cout<<a<<" "<<b<<" "<<c<<" "<<d<<endl
は次の定義によってprint(a,b,c,d)
で書ける(厳密には余計な空白が入るので注意).
void print() { cout << endl; }
template <typename Head, typename... Tail>
void print(Head H, Tail... T) {
cout << H << " ";
print(T...);
}
ちょっとしたコーディングテクニック
C++の仕様や便利機能に対し,こちらは言語を問わずに使えるコーディングテクニックを紹介する.
円状に並ぶ要素
同じ数列を$2$つ連結することで数列の先頭と末尾が繋がって処理しやすくなる
vector
以外のデータ構造
queue
やstack
,deque
を用いると,vector
よりも簡潔に書けることがある.
- 幅優先探索(
queue
) - 深さ優先探索(
stack
) - 括弧列の検証(
stack
) - 逆ポーランド記法(
stack
) - ヒストグラムの最大面積(
stack
) - 01BFS(
deque
)
番兵,余分な要素
例外処理を完結にするために,番兵と呼ばれる特殊な値を配列の先頭や末尾に挿入しておく.
C言語だと,文字列におけるナル文字\0
がその例である.
大きな数を使う手法
問題を言い換えると,
- $N$個の正の整数$a_1, a_2, \dots, a_N$が与えられる($0\leq a_i\leq 10^9$).
- $N$個の整数のうち,偶奇が同じ$2$つの数を取り出して和を考えるとき,その最大値を求めよ.
- 存在しない場合はその旨を出力せよ.
奇数と偶数で分けて,それぞれ大きい方から$2$つ和のうち大きい方を出力すれば良い. ただし,奇数が$1$つしかない場合や偶数が$1$つしかない場合を考慮する必要がある. このとき,奇数や偶数の配列に$-10^{12}$を$2$つずつ入れておくことで,答えがマイナスになった場合はそのような数が存在しないことを表すことができる(実装例).
迷路を壁で囲む
グリッドで表される迷路を探索するような問題で,盤面を囲むような壁を設置することで,グリッドの端に到達したときの処理を簡潔にすることができる.
天井関数
int ceil(int a, int b) {
return (a + b - 1) / b + 1;
}
を用いると,$a/b$以上の最大の整数を求めることができる.
C
言語では,ceil
関数が標準ライブラリに含まれているが,浮動小数点の計算を信用していない人はこれを使うと良いだろう.
浮動小数点型を使わない
浮動小数点型を使わないことで,計算誤差を防ぐことができる. 例えば,$\frac{a}{b}$と$\frac{c}{d}$の値の比較を行う場合,$a/b<c/d$という式を書かずに,$a\cdot d<c\cdot b$という式を書くことで,誤差を防ぐことができる(オーバーフローには注意).
他にも,ルートがつかないように変形したり,logの真数の比較であったり,整数の範囲でできる場合はしたほうが良い.
グリッドの移動
グリッドで$(x,y)$から上下左右の移動をするとき,$(x+1,y),(x,y+1),(x-1,y),(x,y-1)$の処理を行うが,これを配列に格納しておくと,for
文を用いて簡潔に書くことができる.
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 1, 0, -1};
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
// (nx, ny)に対する処理
}
これは$8$方向のときや,移動できる方向が制限されている場合にも応用できる.
超頂点
グラフの頂点に番号がふってあり,同じ番号同士であれば行き来できるようなグラフを考える.
よくある考察
メタ読み
- 制約が$N=20$のときは,$2^N$通りのbit全探索
- 制約が$N=40$くらいのときは半分全列挙
- これは半分全列挙感がすごいABC271 F - XOR on Grid Path
線形変換
- 線形漸化式
- 行列を使って表すことができる
- dpの遷移が線形変換で表せるときは高速化ができる
- BasyStepGiantStepが使えるパターン(ABC270 G - Sequence in mod P)
- セグメントツリーに乗るARC008 D - タコヤキオイシクナール
よくあるバグ
前提
- コンパイルするファイルが間違っている
- 実行するファイルが間違っている
- 実はコンパイルに失敗していて,最後にコンパイルが成功したファイルを実行している
コンパイルできない
- ヘッダがない
- 人のコードパクってきたときに
funcional
ヘッダがなかった
- 人のコードパクってきたときに
using namespace
がない- 変数名と
define
の重複 vector<vector<long>> A(N,vector<int>(0))
みたいに型が一致していない<Templete T>
の書き忘れ
コーナーケース
N=1,K=0
などの極端な値A=B
のとき- 同じクエリが複数回来るかもしれない
- 自己ループ,同じ辺
- 0の0乗,0の階乗の定義
実行が終わらない
- 入力を受け取りすぎている
- クエリの数
Q
と要素数N
が混同してしまう
- クエリの数
- n重for文でインクリメントする変数を間違える
- n重for文で変数名が重複
- 計算量が多い
- setに要素を大量に入れている可能性
セグフォ
- cinを忘れている
- 二項係数ライブラリとかの前計算した配列の範囲外参照
- 文字列の操作中に空文字が登場する
- 0,1-indexedの混同
- セグ木とかに多い
その他ハマり
modを正しく取れていない
- modを取ってない
- mod(法)の値が間違っている(998244353,1e9+7)
- modの取り方を間違っている
- 法が素数でない,逆元が存在しない
- mod 3とかでの二項係数はルーカスの定理を使う
- modを取りながら累積和したときに負の数がでる
(A[r]-A[l-1]+mod)%mod
みたいな配慮modint
構造体を使おう
- 最後にマイナス1などをしている(0のときに-1が出力されてしまう)
セグ木
- 実はモノイドではなかった
- 単位元が間違っている
- 更新クエリの単位元を$0$にすると壊れる場合がある
- ビルドができていない
二分探索
- 初期値で候補から外れている値が実は答え
- 答えの上限値が
10^9
なのに二分探索の上限の初期値を10^9
にしている
- 答えの上限値が
未分類
- 改行(flush)しないと標準出力にでてこない
cin>>a,b;
みたいなことをしている- エラーが出ないことがあるのでハマりやすい
- switch文でbreak忘れ
make clean
を試す- 型のパース,intをlongの計算にそのまま突っ込むのは良くない
- 調和級数の計算量勘違い
- コンストラクタの名前を間違えている
- 閉区間,半開区間などの違い
- 累積和で区間和を出す場合に注意
set
やmap
でstd::lower_bound
などは使えない- メソッドとして用意されている
- 01ナップザックで昇順にdpを回してしまった
- それは個数制限なし
- bool型をint型だと思って
- bool値に2以上を入れてもサイレントに1になる
- ソート後の配列をソート前の状態として扱ってしまった
- 格子点とグリッドマス目の関係
- 角の座標の扱い
- 不等式における累乗の注意
- 偶数乗した場合負の数が正になる
- ビットシフト演算子の計算順序
- 四則演算のほうが早い(
1<<2+2
は1<<4
で16
になる)
- 四則演算のほうが早い(
long
型リテラルにしわすれてオーバーフロー1<<40
はオーバーフローする.1L<<40
が正しい.
データ構造
C++が用意しているデータ構造
vector
- 配列.
# include < vector >
ヘッダをインクルードすることで使用できる.
初期化
vector<int> A(10,0); // 10個の要素を持つint型の配列.初期値は0.
vector<int> B = {3,1,4,1,5}; //要素を指定して初期化.
vector<int> C;// 空の配列.
要素の取得
ランダムアクセスが$O(1)$で可能.
vector<int> A = {3,1,4,1,5};
cout << A[0] << endl; // 3
cout << A[1] << endl; // 1
cout << A[2] << endl; // 4
cout << A[3] << endl; // 1
cout << A[4] << endl; // 5
cout << A.size() << endl; // 要素数を返す.
要素の追加と削除
vector<int> C;// 空の配列.
C.push_back(2); // Cの末尾に2を追加.
C.pop_back(); // Cの末尾の要素を削除.
ソート
$O(N\log N)$かかる.
vector<int> B = {3,1,4,1,5};
sort(B.begin(),B.end()); // Bを昇順に並び替え.
sort(B.begin(),B.end(),greater<int>()); // Bを降順に並び替え.
set
- 重複なしの集合.
# include < set >
ヘッダをインクルードすることで使用できる.
初期化
set<int> A = {3,1,4,1,5}; //要素を指定して初期化.
set<int> B;// 空の集合.
要素の取得
- ランダムアクセスはできない.
- range based for loopが使える.
- 要素が含まれているかは$O(\log N)$で
count
メンバ関数で判定できる($0$ならば含まれていない,$1$ならば含まれている). - イテレータの使用して次に大きい要素や次に小さい要素を取得することもできる(詳細).
set<int> A = {3,1,4,1,5};
cout << A.size() << endl; //4.要素数を返す.
for(auto x:A){
cout << x << endl; // 1,3,4,5の順に出力される.
//勝手に昇順になっている.要素は重複しない.
}
cout << A.count(1) << endl; // 1がAに含まれているか.
要素の追加と削除
set<int> B;// 空の集合.
B.insert(2); // Bに2を追加.
B.erase(2); // Bから2を削除.
map
- キーと値のペアを格納する.
# include < map >
ヘッダをインクルードすることで使用できる.
初期化
map<int,int> A = {{1,2},{3,4},{5,6}}; //要素を指定して初期化.
map<int,int> B;// 空のmap.
要素の取得
- ランダムアクセスはできない.
- range based for loopが使える(キーと値が
pair
型でもらえる). - イテレータの使用して次に大きい要素や次に小さい要素を取得することもできる(詳細).
map<int,int> A = {{1,2},{3,4},{5,6}};
cout << A.size() << endl; //3.要素数を返す.
cout << A[1] << endl; // 2
cout << A[3] << endl; // 4
cout << A[5] << endl; // 6
for(auto x:A){
cout << x.first << " " << x.second << endl; // 1 2,3 4,5 6の順に出力される.
//勝手にキーの昇順になっている.
}
queue
- LIFO(First In First Out)のデータ構造.
# include < queue >
ヘッダをインクルードすることで使用できる.
初期化
queue<int> A;// 空のqueue.
要素の操作
queue<int> A;
A.push(1); // Aの末尾に1を追加.
A.push(2); // Aの末尾に2を追加.
A.push(3); // Aの末尾に3を追加.
cout << A[0] << endl; // 1
cout << A.front() << endl; // 1
A.pop(); // Aの先頭の要素を削除.
cout << A.front() << endl; // 2
A.pop(); // Aの先頭の要素を削除.
cout << A.front() << endl; // 3
A.pop(); // Aの先頭の要素を削除.
stack
FIFO(First In Last Out)のデータ構造.
初期化
stack<int> A;// 空のstack.
要素の操作
stack<int> A;
A.push(1); // Aの末尾に1を追加.
A.push(2); // Aの末尾に2を追加.
A.push(3); // Aの末尾に3を追加.
cout << A.top() << endl; // 3
A.pop(); // Aの末尾の要素を削除.
cout << A.top() << endl; // 2
A.pop(); // Aの末尾の要素を削除.
cout << A.top() << endl; // 1
A.pop(); // Aの末尾の要素を削除.
deque
queue
とstack
のキメラ.ランダムアクセスも可能である.# include < deque >
ヘッダをインクルードすることで使用できる.
初期化
deque<int> A;// 空のdeque.
要素の操作
deque<int> A;
A.push_back(1); // Aの末尾に1を追加.
A.push_back(2); // Aの末尾に2を追加.
A.push_back(3); // Aの末尾に3を追加.
A.push_front(4); // Aの先頭に4を追加.
A.push_front(5); // Aの先頭に5を追加.
A.push_front(6); // Aの先頭に6を追加.
cout << A[0] << endl; // 6
cout << A[1] << endl; // 5
cout << A[2] << endl; // 4
cout << A[3] << endl; // 1
cout << A[4] << endl; // 2
cout << A[5] << endl; // 3
A.pop_back(); // Aの末尾の要素を削除.
A.pop_front(); // Aの先頭の要素を削除.
cout << A[0] << endl; // 5
cout << A[1] << endl; // 4
cout << A[2] << endl; // 1
cout << A[3] << endl; // 2
priority_queue
- 優先度付きキュー.
# include < queue >
ヘッダをインクルードすることで使用できる.
初期化
priority_queue<int> A;// 空のpriority_queue.
要素の操作
大きい方から出てくる.
priority_queue<int> A;
A.push(2); // Aの末尾に2を追加.
A.push(3); // Aの末尾に3を追加.
A.push(1); // Aの末尾に1を追加.
cout << A.top() << endl; // 3
A.pop(); // Aの末尾の要素を削除.
cout << A.top() << endl; // 2
A.pop(); // Aの末尾の要素を削除.
cout << A.top() << endl; // 1
A.pop(); // Aの末尾の要素を削除.
list
- 双方向連結リスト.
# include < list >
ヘッダをインクルードすることで使用できる.
初期化
list<int> A;// 空のlist.
要素の操作
list<int> ls;
ls.push_back(1);
ls.push_back(2);
ls.push_front(3);
ls.push_front(4);
for_each(ls.begin(),ls.end(),[](int a){
cout<<a<<" "; // 4 3 1 2
});
cout<<endl;
auto it = ls.begin();
it++;
ls.insert(it,5);
for_each(ls.begin(),ls.end(),[](int a){
cout<<a<<" "; // 4 5 3 1 2
});
cout<<endl;
UnionFind
概要
UnionFindは、グラフの連結成分を管理するデータ構造. 次の操作がそれぞれ高速(ほぼ定数時間)に行える.
root(x)
:x
の根を返す.unite(u,v)
:u
,v
をマージするsame(u,v)
:u
,v
が同じ連結成分に属するかを判定する.getGroup(x)
: 現在の連結成分のリストを返す.getWeight(x)
:x
が属する連結成分の重みを返す.changeWeight(x)
: 頂点x
の重みを変更する.
ソースコード
struct UnionFind{
public:
vector<int> par;//親
vector<long> weight;//重み
vector<long> weightAlone;//単体の重み
UnionFind(int n):par(n),weight(n),weightAlone(n){
for(int i=0;i<n;i++){
par[i]=i; //親は自分自身にしておく
weight[i] = weightAlone[i] = 1;
}
}
//途中で実行すると壊れます
void setWeight(int i,long w){
weight[i] = weightAlone[i] = w;
}
//途中で実行しても大丈夫
void changeWeight(int i,long w){
long pre = weightAlone[i];
weightAlone[i] = w;
weight[root(i)] += w-pre;
}
int root(int x){
if(par[x]==x){
return x;
}else{
int r = root(par[x]);
par[x]=r;
return r;
}
}
void unite(int x,int y){
int rx=root(x);
int ry=root(y);
if(rx==ry){
return;
}
par[rx]=ry;
weight[ry] += weight[rx];
}
bool same(int x,int y){
int rx=root(x);
int ry=root(y);
return rx==ry;
}
long getWeight(int x){
return weight[root(x)];
}
vector<vector<int>> getGroups(){
vector<vector<int>> res;
map<int,vector<int>> mp;
for(int i=0;i<(int)par.size();i++){
mp[root(i)].push_back(i);
}
for(auto&[k,v]:mp){
(void)k; //使いません
res.push_back(v);
}
return res;
}
};
重みは途中で変更できる.
int main(void){
UnionFind uf(3);
//初期の全ての頂点の重みは1
uf.unite(1,2);
cout<<uf.getWeight(1)<<endl; // 2(=1+1)
uf.changeWeight(1,10);
cout<<uf.getWeight(1)<<endl; // 11(=10+1)
uf.unite(0,1);
cout<<uf.getWeight(1)<<endl; // 12(=10+1+1)
}
Segment Tree
はじめに
さっそく抽象化すると(僕が)よくわからんので、例を挙げていく.
Range Minimum Query
長さ$N$の数列($A_0,A_1,A_2,\cdots,A_{N-1}$)について,次のクエリを$O(\log N)$で行う
- $a_i$を$x$に更新する
- 区間の最小値を求める
template<typename T>
struct RMQ{
int n;
vector<T>dat;
const T INF;
RMQ(int n_,T INF_):INF(INF_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,INF);
}
void update(int k,T a){
k+=n-1;// i番目は、配列上では n-1+i 番目に格納されている
dat[k]=a;// 葉の更新
while(k>0){
k=(k-1)/2; //親のインデックス
// 子の内小さい方を選ぶ
dat[k]=min(dat[k*2+1],dat[k*2+2]);
}
}
// [a,b)の最小値を求める,頂点kは[l,r)に対応している
T query(int a,int b,int k,int l,int r){
if(r<=a||b<=l)return INF;//範囲外
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
T vl=query(a,b,k*2+1,l,(l+r)/2);
T vr=query(a,b,k*2+2,(l+r)/2,r);
return min(vl,vr);
}
}
//[a,b)の最小値を求める
T query(int a,int b){
return query(a,b,0,0,n);
}
};
Range Sum Query
- $A_i$に$x$を加算する.
- 区間の総和を求める
template<typename T>
struct RSQ{
int n;
vector<T>dat;
const T ZERO;
RSQ(int n_,T ZERO_):ZERO(ZERO_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,ZERO);
}
void update(int k,T a){
k+=n-1;// i番目は、配列上では n-1+i 番目に格納されている
dat[k]+=a;// 葉の更新
while(k>0){
k=(k-1)/2; //親のインデックス
// 子の和を計算
dat[k]=dat[k*2+1]+dat[k*2+2];
}
}
T query(int a,int b,int k,int l,int r){
if(r<=a||b<=l)return ZERO;//範囲外
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
T vl=query(a,b,k*2+1,l,(l+r)/2);
T vr=query(a,b,k*2+2,(l+r)/2,r);
return vl+vr;
}
}
//[a,b)の総和を求める
T query(int a,int b){
return query(a,b,0,0,n);
}
};
抽象化
RMQとRSQを比較すると,min
と+
,INF
とZERO
が違うだけで,ほとんど同じ.
これらを抽象化する.
min
と+
に当たるものをFX
と定義し,INF
とZERO
に当たるものをex
と定義する.
また,新たに
- 葉の値を取得する関数
get
- 更新をせずに値を入れる関数
set
set
した後に実行したい構築処理build
を追加した.
template<typename T>
struct SegTree{
using FX = function<T(T,T)>; // TとTの演算結果Tを返す
FX fx;
int n;
vector<T>dat;
const T ex;//単位元(こいつとxを演算をしてもxになる)
SegTree(int n_,T ex_,FX fx_):ex(ex_),fx(fx_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,ex);
}
void set(int k,T a){
k+=n-1;
dat[k]=a;
}
void build(){
for(int k=n-2;k>=0;k--){
dat[k]=fx(dat[k*2+1],dat[k*2+2]);
}
}
void update(int k,T a){
k+=n-1;// i番目は、配列上では n-1+i 番目に格納されている
dat[k] = a;// 葉の更新
while(k>0){
k=(k-1)/2; //親のインデックス
// 子の和を計算
dat[k]=fx(dat[k*2+1],dat[k*2+2]);
}
}
T query(int a,int b,int k,int l,int r){
if(r<=a||b<=l)return ex;//範囲外なので単位元を返す
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
T vl=query(a,b,k*2+1,l,(l+r)/2);
T vr=query(a,b,k*2+2,(l+r)/2,r);
return fx(vl,vr);
}
}
//[a,b)のfx(A_a,A_a+1,...,A_b-1)を返す
T query(int a,int b){
return query(a,b,0,0,n);
}
T get(int k){
return dat[k+n-1];
}
};
RMQにしたいときは,次のように単位元と関数を与える.
SegTree<int>st(n,2147483647,[](int a,int b){return min(a,b);});
先程のRSQにしたいときは,次のように単位元と関数を与える. ただし,$x$を加算するという操作が先程の設計だとできないので.$A_i+x$に更新すると言い換える必要がある.あるいはコードを変更する.
SegTree<int>st(n,0,[](int a,int b){return a+b;});
モノイドであればセグ木に乗せることができると知られている.
モノイドとは次の性質を満たす集合$M$と演算$\circ$($M\times M\longmapsto M$)の組である.
- $M$の任意の元$a,b,c$に対して,$(a\circ b)\circ c=a\circ (b\circ c)$を満たす.
- $M$の任意の元$a$に対して,$a\circ e=e\circ a=a$を満たす$M$の単位元$e$が存在する.
RMQで考えると,$M=\mathbb{N}$,$a\circ b=\textrm{min}(a,b)$,$e=\infty$であり,次が成り立つ.
- $\textrm{min}(\textrm{min}(a,c),b)=\textrm{min}(a,\textrm{min}(b,c))$
- $\textrm{min}(a,\infty)=\textrm{min}(\infty,a)=a$
モノイドであり,セグ木に乗せられることが確認できる.
Range GCD Query
亜種として,区間の最大公約数を求める問題について考える.
この問題は,好きな$A_i$を一つ選んでそれを削除したときの,$A_1,A_2,...,A_{N-1}$の最大公約数の最大値を求める問題だと言い換えることができる.
- 演算を$\textrm{GCD}$とする.
- $\textrm{GCD}(x,0)=x$と定義すると単位元は$0$である.
$\textrm{GCD}$は計算順序は関係なく,モノイドの条件をみたすため,セグ木にのせられることがわかる.
int GCD(int a,int b){
if(a<b)return GCD(b,a);
if(b==0)return a;
return GCD(b,a%b);
}
int main(void){
int n;cin>>n;
SegTree<int>st(n,0,[](int a,int b){return GCD(a,b);});
for(int i=0;i<n;i++){
int a;
cin>>a;
st.set(i,a);
}
st.build();
int ans = 0;
for(int i=0;i<n;i++){
int l = st.query(0,i);
int r = st.query(i+1,n);
chmax(ans,GCD(l,r));
}
cout<<ans<<endl;
}
なお,この問題は左からと右からの累積GCDを使って解くことができるのでセグ木はオーバーキル解法である(しかもこっちのほうが遅い)が,新たに書くコードの少なくバグりづらいので殴れると何かと嬉しい.
タコヤキオイシクナール
問題リンク 試験管橙diffだが,今出題されれば水~青レベルだと思う(本当?).
線形変換を行っていることに注目し,行列を考える. 美味しさ$x$のたこ焼きを$(a,b)$の機械に通すことを行列で表すと次のようになる. $$ \begin{pmatrix} x & 1 \\ \end{pmatrix} \begin{pmatrix} a & 0 \\ b & 1 \\ \end{pmatrix} = \begin{pmatrix} ax+b & 1\\ \end{pmatrix} $$ さらに,$(c,d)$の機械に通したものは次のように表現できる. $$ \begin{pmatrix} x & 1 \\ \end{pmatrix} \begin{pmatrix} a & 0 \\ b & 1 \\ \end{pmatrix} \begin{pmatrix} c & 0 \\ d & 1 \\ \end{pmatrix} = \begin{pmatrix} ? & 1\\ \end{pmatrix} $$ また,
- $\mathbb{R}^{2\times 2}$の任意の元$A,B,C$で$(AB)C=A(BC)$
- $\mathbb{R}^{2\times 2}$の任意の元$A$で,単位行列$I$を用意すると$AI=IA=A$
であり,2次の正方行列の集合$\mathbb{R}^{2\times 2}$に対して,演算を行列の積とすると,モノイドであることがわかる.よって,セグ木に乗る. なお,解くためには座標圧縮する必要がある. 提出リンク
行列+セグ木+座圧のライブラリてんこ盛り問題.
セグ木上の二分探索
未実装
Lazy Segment Tree
はじめに
遅延評価セグメント木は,抽象化してるやつを持っておいて,コンテストで出題されたらペタってやろうと企んでいる人がいる.しかし,意外と罠が多く,バグ取りに時間がかかってしまうことが多い.これは体験談.やはり,具体例を実際に実装して,その中で注意点を理解しておくのが良いと思う.
Range Minimum Query and Range Update Query
長さ$N$の数列($A_0,A_1,A_2,\cdots,A_{N-1}$)について,次のクエリを$O(\log N)$で行う.
- 区間の値を$x$に更新
- 区間の最小値を取得
なお,AOJにあるRangeUpdateQueryという問題は幅1のクエリだと考えれば解ける.
/*lazy segment tree*/
template<class T> class RMUQ{
public:
int n;
vector<T>dat,lazy;
const int INF;
RMUQ(int n_,T INF_):INF(INF_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,INF);
lazy.assign(2*n-1,INF);
}
void update(int a,int b,T x,int k,int l,int r){
eval(k);
if(a <= l && r <= b){
lazy[k]=x;
eval(k);
}else if(a < r && l < b){
update(a,b,x,k*2+1,l,(l+r)/2);
update(a,b,x,k*2+2,(l+r)/2,r);
dat[k]=min(dat[k*2+1],dat[k*2+2]);
}
}
void update(int k,T a){
update(k,k+1,a,0,0,n);
}
void update(int a,int b,T x){
update(a,b,x,0,0,n);
}
void eval(int k){
if(lazy[k]==INF)return;
if(k<n-1){
lazy[k*2+1]=lazy[k];
lazy[k*2+2]=lazy[k];
}
dat[k]=lazy[k];
lazy[k]=INF;
}
// [a,b)の最小値を求める,頂点kは[l,r)に対応している
T query(int a,int b,int k,int l,int r){
eval(k);
if(r<=a||b<=l)return INF;//範囲外
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
T vl=query(a,b,k*2+1,l,(l+r)/2);
T vr=query(a,b,k*2+2,(l+r)/2,r);
return min(vl,vr);
}
}
//[a,b)の最小値を求める
T query(int a,int b){
return query(a,b,0,0,n);
}
T get(int k){
return query(k,k+1);
}
};
Range Add Query and Range Sum Query
- 区間の値に$x$を加算
- 区間の和を取得
実装で注意しなければならないのが区間に$x$を加算するときは,$x\times (区間の幅)$をdat
に加算する必要があること.
template<class T> class RSAQ{
public:
int n;
vector<T>dat,lazy;
const int ZERO;
RSAQ(int n_,T ZERO_):ZERO(ZERO_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,ZERO);
lazy.assign(2*n-1,ZERO);
}
void update(int a,int b,T x,int k,int l,int r){
eval(k,r-l);
if(a <= l && r <= b){
lazy[k]+=x;
eval(k,r-l);
}else if(a < r && l < b){
update(a,b,x,k*2+1,l,(l+r)/2);
update(a,b,x,k*2+2,(l+r)/2,r);
dat[k]=dat[k*2+1]+dat[k*2+2];
}
}
void update(int k,T a){
update(k,k+1,a,0,0,n);
}
void update(int a,int b,T x){
update(a,b,x,0,0,n);
}
void eval(int k,int len){
if(lazy[k]==ZERO)return;
if(k<n-1){
lazy[k*2+1]+=lazy[k];
lazy[k*2+2]+=lazy[k];
}
dat[k]=dat[k] + lazy[k]*len;
lazy[k]=ZERO;
}
// [a,b)の和を求める,頂点kは[l,r)に対応している
T query(int a,int b,int k,int l,int r){
eval(k,r-l);
if(r<=a||b<=l)return ZERO;//範囲外
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
T vl=query(a,b,k*2+1,l,(l+r)/2);
T vr=query(a,b,k*2+2,(l+r)/2,r);
return vl+vr;
}
}
//[a,b)の和
T query(int a,int b){
return query(a,b,0,0,n);
}
T get(int k){
return query(k,k+1);
}
};
Range Minimum Query and Range Add Query
- 区間の値に$x$を加算
- 区間の最小値を取得
注意する点は,演算の種類が,加算と$\textrm{min}$の2種類ある点である.
この場合,どちらも単位元が異なり,加算は$0$で,$\textrm{min}$は$\infty$である.
また,lazy
をdat
に反映させるときはdat = dat + lazy
となる(なぜなら,区間に$x$が加算されたらその区間の最小値も$x$増えるから).
template<class T> class RMAQ{
public:
int n;
vector<T>dat,lazy;
const int ZERO,INF;
RMAQ(int n_,T ZERO_,T INF_):ZERO(ZERO_),INF(INF_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,ZERO);
lazy.assign(2*n-1,ZERO);
}
void update(int a,int b,T x,int k,int l,int r){
eval(k,r-l);
if(a <= l && r <= b){
lazy[k]+=x;
eval(k,r-l);
}else if(a < r && l < b){
update(a,b,x,k*2+1,l,(l+r)/2);
update(a,b,x,k*2+2,(l+r)/2,r);
dat[k]=min(dat[k*2+1],dat[k*2+2]);
}
}
void update(int k,T a){
update(k,k+1,a,0,0,n);
}
void update(int a,int b,T x){
update(a,b,x,0,0,n);
}
void eval(int k,int len){
if(lazy[k]==ZERO)return;
if(k<n-1){
lazy[k*2+1]+=lazy[k];
lazy[k*2+2]+=lazy[k];
}
dat[k]=dat[k]+lazy[k];
lazy[k]=ZERO;
}
// [a,b)の和を求める,頂点kは[l,r)に対応している
T query(int a,int b,int k,int l,int r){
eval(k,r-l);
if(r<=a||b<=l)return INF;//範囲外
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
T vl=query(a,b,k*2+1,l,(l+r)/2);
T vr=query(a,b,k*2+2,(l+r)/2,r);
return min(vl,vr);
}
}
//[a,b)の和
T query(int a,int b){
return query(a,b,0,0,n);
}
T get(int k){
return query(k,k+1);
}
};
Range Sum Query and Range Update Query
- 区間の値を$x$に更新
- 区間の和を取得
注意するのは今までlazy
を$0$のときはdat
に反映させないということをしていたが,今回はlazy
が$0$のときもdat
に反映させる必要があるということである.今回は$0$の代わりにNONE
を使っている.この値はあり得ない値に設定してあるが,場合によってはこれも変える必要がでてくる.
抽象化の準備として,NONE
を更新クエリの単位元と見ることとする.
template<class T> class RSUQ{
public:
int n;
vector<T>dat,lazy;
const int ZERO;
const int NONE = 2147483647;
RSUQ(int n_,T ZERO_):ZERO(ZERO_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,ZERO);
lazy.assign(2*n-1,NONE);
}
void update(int a,int b,T x,int k,int l,int r){
eval(k,r-l);
if(a <= l && r <= b){
lazy[k]=x;
eval(k,r-l);
}else if(a < r && l < b){
update(a,b,x,k*2+1,l,(l+r)/2);
update(a,b,x,k*2+2,(l+r)/2,r);
dat[k]=dat[k*2+1]+dat[k*2+2];
}
}
void update(int k,T a){
update(k,k+1,a,0,0,n);
}
void update(int a,int b,T x){
update(a,b,x,0,0,n);
}
void eval(int k,int len){
if(lazy[k]==NONE)return;
if(k<n-1){
lazy[k*2+1]=lazy[k];
lazy[k*2+2]=lazy[k];
}
dat[k]=lazy[k]*len;
lazy[k]=NONE;
}
// [a,b)の和を求める,頂点kは[l,r)に対応している
T query(int a,int b,int k,int l,int r){
eval(k,r-l);
if(r<=a||b<=l)return ZERO;//範囲外
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
T vl=query(a,b,k*2+1,l,(l+r)/2);
T vr=query(a,b,k*2+2,(l+r)/2,r);
return vl+vr;
}
}
//[a,b)の和
T query(int a,int b){
return query(a,b,0,0,n);
}
T get(int k){
return query(k,k+1);
}
};
抽象化
今まで見てきた通り,抽象化するときは,ただのsegment treeと比べてユーザーが決める要素が多い. 遅延セグ木に乗せられる条件を列挙する(参考).写像$p$を$p(m,n):=m*m*\cdots *m$($m$の$n$回の積)
- $X$と二項演算$\circ$がモノイド
- $M$と二項演算$\bullet$がモノイド
- $(x_1\circ x_2)* p(m,n) = (x_1*p(m,n/2))\circ(x_2*p(m,n/2)) (x_1,x_2\in X)$
- 子に伝播するときに半分ずつ伝播させるようなことができるか
- $(x* m_1)*m_2 = x*(m_1\times m_2)$
- これが何なのか正直わからん(思考停止)
なお,$p(m,n)$は高速に計算できる必要がある.
モノイドについてはSegTreeを参照.
template<typename X,typename M> struct LazySegmentTree{
public:
using FX = function<X(X, X)>;
using FA = function<X(X, M)>;
using FM = function<M(M, M)>;
using FP = function<M(M, int)>;
int n;
FX fx;
FA fa;
FM fm;
FP fp;
const X ex;
const M em;
vector<X> dat;
vector<M> lazy;
LazySegmentTree(int n_,FX fx_,FA fa_,FM fm_,FP fp_,X ex_,M em_):n(n_),fx(fx_),fa(fa_),fm(fm_),fp(fp_),ex(ex_),em(em_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,ex);
lazy.assign(2*n-1,em);
}
void set(int k,X x){
dat[k+n-1]=x;
}
void build(){
for(int k=n-2;k>=0;k--){
dat[k]=fx(dat[2*k+1],dat[2*k+2]);
}
}
void update(int a,int b,M x,int k,int l,int r){
eval(k,r-l);
if(a <= l && r <= b){
lazy[k]= fm(lazy[k],x);
eval(k,r-l);
}else if(a < r && l < b){
update(a,b,x,k*2+1,l,(l+r)/2);
update(a,b,x,k*2+2,(l+r)/2,r);
dat[k]=fx(dat[k*2+1],dat[k*2+2]);
}
}
void update(int k,M a){
update(k,k+1,a,0,0,n);
}
void update(int a,int b,M x){
update(a,b,x,0,0,n);
}
void eval(int k,int len){
if(lazy[k]==em)return;
if(k<n-1){
lazy[k*2+1]= fm(lazy[k*2+1],lazy[k]);
lazy[k*2+2] = fm(lazy[k*2+2],lazy[k]);
}
dat[k]=fa(dat[k],fp(lazy[k],len));
lazy[k]=em;
}
X query(int a,int b,int k,int l,int r){
eval(k,r-l);
if(r<=a||b<=l)return ex;//範囲外
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
X vl=query(a,b,k*2+1,l,(l+r)/2);
X vr=query(a,b,k*2+2,(l+r)/2,r);
return fx(vl,vr);
}
}
X query(int a,int b){
return query(a,b,0,0,n);
}
X get(int k){
return query(k,k+1);
}
};
各パラメータのイメージは以下の通り.
using X = long; // 持つデータの型
using M = long; // 遅延評価(クエリ)の型
auto fx = [](X a,X b){return min(a,b);}; // データの二項演算
auto fa = [](X a,M b){return b;}; // データにクエリを適用する
auto fm = [](M a,M b){return b;}; // 遅延評価の二項演算
auto fp = [](M a,int b){return a;}; // 遅延評価を幅bに適用した場合の遅延評価
X ex = (1L<<31)-1; //持つデータの演算の単位元
M em = 1e17; //遅延評価の演算の単位元
LazySegmentTree<X,M> seg(N,fx,fa,fm,fp,ex,em);
例を示す.
Range Minimum Query and Range Update Query
using X = long;
using M = long;
auto fx = [](X a,X b){return min(a,b);};
auto fa = [](X a,M b){return b;};
auto fm = [](M a,M b){return b;};
auto fp = [](M a,int b){return a;};
X ex = (1L<<31)-1; //minの単位元
M em = 1e17; //更新クエリの単位元(ありえない数値)
LazySegmentTree<X,M> seg(N,fx,fa,fm,fp,ex,em);
Range Sum Query and Range Add Query
using X = long;
using M = long;
auto fx = [](X a,X b){return a+b;};
auto fa = [](X a,M b){return a+b;};
auto fm = [](M a,M b){return a+b;};
auto fp = [](M a,int b){return a*b;};
X ex = 0; //sumの単位元
M em = 0; //加算クエリ+の単位元
LazySegmentTree<X,M> seg(N,fx,fa,fm,fp,ex,em);
Range Minimum Query and Range Add Query
初期値に注意.
using X = long;
using M = long;
auto fx = [](X a,X b){return min(a,b);};
auto fa = [](X a,M b){return a+b;};
auto fm = [](M a,M b){return a+b;};
auto fp = [](M a,int b){return a;};
X ex = 1e17; //minの単位元
M em = 0; //加算クエリ+の単位元
LazySegmentTree<X,M> seg(N,fx,fa,fm,fp,ex,em);
for(int i=0;i<N;i++){
seg.set(i,0);//初期化
}
seg.build();
Range Sum Query and Range Update Query
更新クエリの単位元はありえない数値(クエリに現れない数)にする.
using X = long;
using M = long;
auto fx = [](X a,X b){return a+b;};
auto fa = [](X a,M b){return b;};
auto fm = [](M a,M b){return b;};
auto fp = [](M a,int b){return a*(long)b;};
X ex = 0; //加算の単位元
M em = 1237615273123L; //更新クエリの単位元
LazySegmentTree<X,M> seg(N,fx,fa,fm,fp,ex,em);
for(int i=0;i<N;i++){
seg.set(i,0);//初期化
}
seg.build();
以下,応用例.
Range XOR Query
黒と白を$0,1$とすると,オセロをひっくり返す操作は,1とのXORを取ることと等価である. なお,ひっくり返す過程を出力する必要はないので,imos法とかで偶奇を見る方法でも解けるし,こちらのほうが早い.オーバーキルである.
using X = int;
using M = int;
auto fx = [](X a,X b){return a^b;};
auto fa = [](X a,M b){return a^b;};
auto fm = [](M a,M b){return a^b;};
auto fp = [](M a,int b){return a;};
X ex = 0; //xorの単位元
M em = 0; //xorの単位元
LazySegmentTree<X,M> seg(N,fx,fa,fm,fp,ex,em);
Range add linear Query
区間に等差数列を加算する.
Binary Indexed Tree
概要
一点加算と区間和を $O(\log N)$ で行うデータ構造.
なお,上位互換のsegment treeもこの機能を要しており,わざわざBITを使う必要はない(実装が簡単,セグ木に対して定数倍早い,といった利点がある).
ソースコード
1-indexedなことに注意.
template<class T> std::istream &operator>>(std::istream &in,vector<T>&A){
for(T&a:A){
std::cin>>a;
}
return in;
}
template <typename T>
struct BIT{
int n;
vector<T> bit;
BIT(int n_):n(n_){
bit.resize(n+1);
}
void add(int i,T x){
i++;
while(i<=n){
bit[i]+=x;
i+=i&-i;
}
}
T sum(int i){
i++;
T s=0;
while(i>0){
s+=bit[i];
i-=i&-i;
}
return s;
}
};
列挙
順列列挙
概要
次を列挙できる
- $0,1,2,3,...,N-1$の要素数$N$個の順列,要素の重複なし
- $0,1,2,3,...,N-1$のから$U$個選んだ順列,要素の重複なし
- $0,1,2,3,...,N-1$のから$U$個選んだ順列,要素の重複あり
- $0,1,2,3,...,N-1$のから$U$個選んだ組み合わせ,要素の重複なし
- $0,1,2,3,...,N-1$のから$U$個選んだ組み合わせ,要素の重複あり
計算量
よくわからん
ソースコード
vector<vector<int>> permutations(int N){
vector<int> array(N);
vector<vector<int>> A(0);
for(int i=0;i<N;i++)array[i]=i;
do{
A.push_back(array);
}while(next_permutation(array.begin(),array.end()));
return A;
}
vector<vector<int>> permutation(int N,int U,bool h=false){
//0,1,2,3,...,N-1のN個からU個選ぶ順列
vector<vector<int>> A(0);
auto fun = [&h,&A,&N,&U](auto &fun,vector<int> &B)->void{
if((int)B.size()==U){
auto C=B;
do{
A.push_back(C);
}while(next_permutation(C.begin(),C.end()));
return;
}
int s = (h?0:-1);
if(!B.empty())s = B.back();
for(int x=s+(h?0:1);x<N;x++){
B.push_back(x);
fun(fun,B);
B.pop_back();
}
};
vector<int> C={};
fun(fun,C);
return A;
}
vector<vector<int>> combination(int N,int U,bool h=false){
//0,1,2,3,...,N-1のN個からU個選ぶ組み合わせ順列
vector<vector<int>> A(0);
auto fun = [&h,&A,&N,&U](auto &fun,vector<int> &B)->void{
if((int)B.size()==U){
auto C=B;
A.push_back(B);
return;
}
int s = (h?0:-1);
if(!B.empty())s = B.back();
for(int x=s+(h?0:1);x<N;x++){
B.push_back(x);
fun(fun,B);
B.pop_back();
}
};
vector<int> C={};
fun(fun,C);
return A;
}
int main(void){
cout<<permutation(5,3,false)<<endl;//重複なし順列
cout<<permutation(5,3,true)<<endl; //重複あり順列
cout<<combination(5,3,false)<<endl;//重複なし組み合わせ
cout<<combination(5,3,true)<<endl; //重複あり組み合わせ
}
ビットの部分集合列挙
概要
ある非負整数$x$があるとき,$x|a =x$となるような$a$を列挙する
ソースコード
vector<long> bitsub(long x){
vector<long> A(0);
//xの部分集合
for(long T=x; ; T=(T-1)&x) {
if(T==0)break;
A.push_back(T);
}
return A;
}
整数
素数判定
概要
素数かどうかを判定する.
計算量
$O(\sqrt{N})$
ソースコード
bool isPrime(long N){
if(N<=1)return false;
for(long i=2;i*i<=N;i++){
if(N%i==0){
return false;
}
}
return true;
}
素数篩
概要
エラトステネスの篩.
計算量
$1$から$N$までの全ての整数を素数判定する(篩の処理)場合は$\sqrt{N}\log \sqrt{N}$. この処理をすると,
- $N$以下の数の素因数分解が$O(\log N)$
$N$以下の数の約数列挙が$O(\log N)$(未実装)
で可能になる.
ソースコード
struct primeSieve{
vector<long> mfactor;
//既知の素数(随時追加される)
vector<long> primes;
set<long> primeSet;
long N;
primeSieve(){
N = 0;
calc();
}
primeSieve(long N){
this-> N = N;
calc();
}
/*
素数篩
O(sqrt(N))
*/
void calc(){
primes.clear();
mfactor.resize(N+1);
fill(mfactor.begin(),mfactor.end(),-1);
mfactor[0] = 0;
mfactor[1] = 1;
for(long i=2;i<=N;i++){
if(mfactor[i] == -1){
for(long j=i;j<=N;j+=i){
mfactor[j] = i;
}
}
}
for(long i=2;i<=N;i++){
if(mfactor[i] == i){
primes.push_back(i);
primeSet.insert(i);
}
}
}
/*
素数判定
O(1)
*/
bool isPrime(long x){
if(x==1)return false;
if(x<=N) return x == mfactor[x];
if(primeSet.count(x))return true;
return isPrimeNaive(x);
}
/*
Naive素数判定
O(sqrt(N))
*/
bool isPrimeNaive(long x){
if(x<=1)return false;
for(long i=2;i*i<=x;i++){
if(x%i==0){
return false;
}
}
primes.push_back(x);
primeSet.insert(x);
return true;
}
/*
素因数分解
O(log N)
*/
vector<long> factorization(long x){
if(x>N){
return factorizationNaive(x);
}
vector<long> A(0);
if(x==1){
A.push_back(1);
return A;
}
while(x>1){
A.push_back(mfactor[x]);
x /= mfactor[x];
}
reverse(A.begin(),A.end());
return A;
}
/*
Naive素因数分解
O(sqrt N)
*/
vector<long> factorizationNaive(long x){
vector<long> A(0);
for(long i=2;i*i<=x;){
if(x%i==0){
x/=i;
A.push_back(i);
}else{
i++;
}
}
if(x>1){
A.push_back(x);
}
return A;
}
}
約数列挙
概要
正の整数$N$の約数を列挙する
計算量
$O(\sqrt{N})$
ソースコード
//約数列挙
vector<long> divisor(long x){
vector<long> f(0);
for(long i=1;i*i<=x;i++){
if(x%i==0){
f.push_back(i);
if(i!=x/i)f.push_back(x/i);
}
}
sort(f.begin(),f.end());
return f;
}
素因数分解
概要
$2$以上の正の整数$N$の素因数分解する
計算量
$O(\sqrt{N})$
ソースコード
//素因数分解
vector<long> factor(long x){
vector<long> f(0);
for(long i=2;i*i<=x;i++){
if(x%i==0){
f.push_back(i);
x/=i;
i--;
}
}
if(x>1)f.push_back(x);
return f;
}
//素因数分解2
// (素数,指数) のpair
vector<pair<long,long>> factor2(long x){
auto f = factor(x);
vector<pair<long,long>> f2(0);
for(auto a:f){
if(f2.empty()){
f2.push_back({a,1});
}else if(f2.back().first==a){
f2.back().second ++;
}else{
f2.push_back({a,1});
}
}
return f2;
}
累乗根
概要
$y,n$から$x^n\leq y$を満たすような最大の整数$x$を求める.
計算量
$O(\log N)$
ソースコード
long iroot(long y,int n){
//x^n <= yとなる最大のn
long ok = 0;
long ng = y+1;
while(abs(ok-ng)>1){
long mid = (ok+ng)/2;
long x = 1;
bool inf = false;
for(int i=0;i<n;i++){
if(((long)1e18)/x < mid)inf = true;
x *= mid;
}
if(inf||x>y){
ng = mid;
}else{
ok = mid;
}
}
return ok;
}
2次方程式
概要
$x$に関する方程式$ax^2+bx+c=0$を整数の範囲で代数的に解く.
ソースコード
long iroot(long y,int n){
//x^n <= yとなる最大のn
long ok = 0;
long ng = y+1;
while(abs(ok-ng)>1){
long mid = (ok+ng)/2;
long x = 1;
bool inf = false;
for(int i=0;i<n;i++){
if(((long)1e18)/x < mid)inf = true;
x *= mid;
}
if(inf||x>y){
ng = mid;
}else{
ok = mid;
}
}
return ok;
}
//整数の範囲でax^2+bx+c=0を解く
vector<long> quad(long a,long b,long c){
long d=b*b-4*a*c;
if(d<0)return {}; //解なし
long sqrtd=iroot(d,2);
if(sqrtd*sqrtd!=d)return {}; //整数解はない
if((sqrtd-b)%2!=0)return {}; //整数解はない
if(sqrtd==0)return {(-b)/(2*a)}; //重解
return {(-b+sqrtd)/(2*a),(-b-sqrtd)/(2*a)};
}
int main(void){
auto x=quad(1,2,1);//x^2+2x+1=0
cout<<x<<endl;// {-1}
x=quad(1,2,2);//x^2+2x+2=0
cout<<x<<endl;// {}
x=quad(2,-12,-182);//2x^2-12x-182=0
cout<<x<<endl;//{13,-7}
}
パスカルの3角形
概要
パスカルの三角形によって二項係数${}_a \mathrm{C}_b (0\leq a,b\leq N )$を求める.
計算量
前計算$O(N^2)$,クエリ$O(1)$
ソースコード
struct Pascal{
//tri[a][b] = aCb
vector<vector<long>> tri;
int N;
Pascal(int N){
this->N = N;
init();
}
void init(){
tri.clear();
tri.push_back({1});
for(int i=1;i<N;i++){
vector<long> add(0);
add.push_back(1);
for(int k=0;k<i-1;k++){
add.push_back(tri.back()[k]+tri.back()[k+1]);
}
add.push_back(1);
tri.push_back(add);
}
}
long com(int a,int b){
if(a<b)return 0;
return tri[a][b];
}
};
modint構造体
参考
https://github.com/drken1215/algorithm/blob/master/MathCombinatorics/mod.cpp
ソースコード
// modint: mod 計算を int を扱うように扱える構造体
template<int MOD> struct Fp {
long long val;
constexpr Fp(long long v = 0) noexcept : val(v % MOD) {
if (val < 0) val += MOD;
}
constexpr int getmod() { return MOD; }
constexpr Fp operator - () const noexcept {
return val ? MOD - val : 0;
}
constexpr Fp operator + (const Fp& r) const noexcept { return Fp(*this) += r; }
constexpr Fp operator - (const Fp& r) const noexcept { return Fp(*this) -= r; }
constexpr Fp operator * (const Fp& r) const noexcept { return Fp(*this) *= r; }
constexpr Fp operator / (const Fp& r) const noexcept { return Fp(*this) /= r; }
constexpr Fp& operator += (const Fp& r) noexcept {
val += r.val;
if (val >= MOD) val -= MOD;
return *this;
}
constexpr Fp& operator -= (const Fp& r) noexcept {
val -= r.val;
if (val < 0) val += MOD;
return *this;
}
constexpr Fp& operator *= (const Fp& r) noexcept {
val = val * r.val % MOD;
return *this;
}
constexpr Fp& operator /= (const Fp& r) noexcept {
long long a = r.val, b = MOD, u = 1, v = 0;
while (b) {
long long t = a / b;
a -= t * b; swap(a, b);
u -= t * v; swap(u, v);
}
val = val * u % MOD;
if (val < 0) val += MOD;
return *this;
}
constexpr bool operator == (const Fp& r) const noexcept {
return this->val == r.val;
}
constexpr bool operator != (const Fp& r) const noexcept {
return this->val != r.val;
}
friend constexpr ostream& operator << (ostream &os, const Fp<MOD>& x) noexcept {
return os << x.val;
}
friend constexpr Fp<MOD> modpow(const Fp<MOD> &a, long long n) noexcept {
if (n == 0) return 1;
auto t = modpow(a, n / 2);
t = t * t;
if (n & 1) t = t * a;
return t;
}
};
実行時に法が決まるmodint構造体
参考
https://github.com/drken1215/algorithm/blob/master/MathCombinatorics/mod_runtime.cpp
ソースコード
// modint: mod 計算を int を扱うように扱える構造体
template<int MOD> struct Fp {
long long val;
constexpr Fp(long long v = 0) noexcept : val(v % MOD) {
if (val < 0) val += MOD;
}
constexpr int getmod() { return MOD; }
constexpr Fp operator - () const noexcept {
return val ? MOD - val : 0;
}
constexpr Fp operator + (const Fp& r) const noexcept { return Fp(*this) += r; }
constexpr Fp operator - (const Fp& r) const noexcept { return Fp(*this) -= r; }
constexpr Fp operator * (const Fp& r) const noexcept { return Fp(*this) *= r; }
constexpr Fp operator / (const Fp& r) const noexcept { return Fp(*this) /= r; }
constexpr Fp& operator += (const Fp& r) noexcept {
val += r.val;
if (val >= MOD) val -= MOD;
return *this;
}
constexpr Fp& operator -= (const Fp& r) noexcept {
val -= r.val;
if (val < 0) val += MOD;
return *this;
}
constexpr Fp& operator *= (const Fp& r) noexcept {
val = val * r.val % MOD;
return *this;
}
constexpr Fp& operator /= (const Fp& r) noexcept {
long long a = r.val, b = MOD, u = 1, v = 0;
while (b) {
long long t = a / b;
a -= t * b; swap(a, b);
u -= t * v; swap(u, v);
}
val = val * u % MOD;
if (val < 0) val += MOD;
return *this;
}
constexpr bool operator == (const Fp& r) const noexcept {
return this->val == r.val;
}
constexpr bool operator != (const Fp& r) const noexcept {
return this->val != r.val;
}
friend constexpr ostream& operator << (ostream &os, const Fp<MOD>& x) noexcept {
return os << x.val;
}
friend constexpr Fp<MOD> modpow(const Fp<MOD> &a, long long n) noexcept {
if (n == 0) return 1;
auto t = modpow(a, n / 2);
t = t * t;
if (n & 1) t = t * a;
return t;
}
};
二項係数(mod)
概要
二項係数${}_a \mathrm{C}_b (0\leq a,b\leq N )$を素数$P$で割った余りを求める.
計算量
前計算$O(N\log P)$,クエリ$O(1)$
ソースコード
template<class T> struct Combination{
int N;
vector<T> fac;//階乗
vector<T> finv;//階乗の逆元
Combination(int N){
this->N = N;
init();
}
void init(){
fac.resize(N);
finv.resize(N);
fac[0] = fac[1] = 1;
finv[0] = finv[1] = 1;
for(int i=2;i<N;i++){
fac[i] = fac[i-1] * i;
finv[i] = finv[i-1] / i;
}
}
/*aCbの計算*/
T com(int a,int b){
if(a < b)return 0;
return fac[a] * finv[b] * finv[a-b];
}
};
ルーカスの定理による二項係数
概要
ルーカスの定理によって二項係数を素数$P$で割った余りを計算する.
計算量
前計算$O(P^2)$,二項係数計算に$O(\log P) $
ソースコード
template<int MOD> struct Lucas{
Pascal pas{MOD};
Lucas(){
}
Fp<MOD> com(long a,long b){
if(a<b)return 0;
Fp<MOD> ret{1};
while(a>0){
ret *=pas.com(a%MOD,b%MOD);
a/=MOD;
b/=MOD;
}
return ret;
}
};
2元1次不定方程式
概要
非負整数$a,b$に対して$ax + by = 1$ を解く.一般解が求められる.
ソースコード
// 返り値: a と b の最大公約数
// ax + by = gcd(a, b) を満たす (x, y) が格納される
long extGCD(long a, long b, long &x, long &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
long d = extGCD(b, a%b, y, x);
y -= a/b * x;
return d;
}
/*
ax + by = c の一般解
x = alpha t + beta
y = gamma t + delta
*/
bool Bezout(long a,long b,long c,long &alpha,long &beta,long &gamma,long &delta){
long x=0,y=0;
long gcd=extGCD(a,b,x,y);
if(c%gcd!=0){
return false;
}
x *= c/gcd;
y *= c/gcd;
gamma = a/gcd;
delta = y;
alpha = -b/gcd;
beta = x;
return true;
}
LCD,GCD
概要
最大公約数(GCD),最小公倍数(LCM)を求める
計算量
ソースコード
long GCD(long a,long b){
if(a<b)return GCD(b,a);
if(b==0)return a;
return GCD(b,a%b);
}
long GCD(vector<long>&A){
long gcd = A.front();
for(auto&a:A)gcd = GCD(gcd,a);
return gcd;
}
long LCM(long a,long b){
return (a/GCD(a,b))*b;
}
long LCM(vector<long>&A){
long lcm = 1;
for(auto&a:A)lcm = LCM(lcm,a);
return lcm;
}
中国剰余定理
概要
$a$で割った余りが$b$,$c$で割った余りが$d$を満たす数を求める.
ソースコード
// 返り値: a と b の最大公約数
// ax + by = gcd(a, b) を満たす (x, y) が格納される
long extGCD(long a, long b, long &x, long &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
long d = extGCD(b, a%b, y, x);
y -= a/b * x;
return d;
}
long mod(long a,long m){
if(a>=0)return a%m;
return (m-(-a)%m)%m;
}
/*
中国剰余定理
*/
bool CRT(long b1, long m1, long b2, long m2,long &r,long &m) {
long p, q;
long d = extGCD(m1, m2, p, q);
if ((b2 - b1) % d != 0) return false;
m = m1 * (m2/d);
long tmp = (b2 - b1) / d * p % (m2/d);
r = mod(b1 + m1 * tmp, m);
return true;
}
bool CRT(const vector<pair<long,long>> &X,long &r,long &m) {
int s = X.size();
r = X.front().first;
m = X.front().second;
bool ok = true;
for(int i=1;i<s;i++){
ok = CRT(r,m,X[i].first,X[i].second,r,m);
if(!ok){
break;
}
}
return ok;
}
baby step giant step
概要
素数$p$と$0\leq a,b< p $について,$a^x = b \mod p$となる$x$を求める.
計算量
$O(\sqrt{p} \log \sqrt{p})$
ソースコード
long extgcd(long a,long b,long & x,long & y){
long d = a;
if(b != 0) {
d = extgcd(b,a%b,y,x);
y -= (a/b) * x;
}else{
x = 1; y = 0;
}
return d;
}
long mod_inverse(long a,long m){
long x,y;
extgcd(a,m,x,y);
return (m + x % m) % m;
}
int modPow(long a, long n, long p) {
if (n == 0) return 1; // 0乗にも対応する場合
if (n == 1) return a % p;
if (n % 2 == 1) return (a * modPow(a, n - 1, p)) % p;
long t = modPow(a, n / 2, p);
return (t * t) % p;
}
long iroot(long y,int n){
//x^n <= yとなる最大のn
long ok = 0;
long ng = y+1;
while(abs(ok-ng)>1){
long mid = (ok+ng)/2;
long x = 1;
bool inf = false;
for(int i=0;i<n;i++){
if(((long)1e18)/x < mid)inf = true;
x *= mid;
}
if(inf||x>y){
ng = mid;
}else{
ok = mid;
}
}
return ok;
}
long BSGS(long a,long b,long p){
/* a^x = b mod p となるようなxを求める */
long x = -1;
long m = iroot(p,2)+1;
map<long,long> mp;
long ax = 1;
for(long i=0;i<=m;i++){
mp[ax] = i;
ax = (ax*a) % p;
}
/*
a^(im+j) = b
a^j = b(a^-m)^i
*/
long am = modPow(mod_inverse(a,p),m,p);
for(long i=0;i<=m;i++){
long d = (modPow(am,i,p)*b) % p;
if(mp.count(d)){
if(i==0&&mp[d]==0)continue;
x = i*m + mp[d];
break;
}
}
return x;
}
❌中国剰余定理(Garner's algorithm)
❌Cipolla's algorithm
線形代数
3次元ベクトル構造体
概要
3次元ベクトルの加算減算,外積が計算できる
ソースコード
template<class T> struct Vector3D{
T x{0};
T y{0};
T z{0};
constexpr Vector3D(T x,T y,T z){
this -> x = x;
this -> y = y;
this -> z = z;
}
constexpr Vector3D(T x1,T y1,T z1,T x2,T y2,T z2){
this -> x = x2 - x1;
this -> y = y2 - y1;
this -> z = z2 - z1;
}
constexpr Vector3D cross(const Vector3D &a)const{
return Vector3D(this->y * a.z - this->z * a.y, this->z * a.x - this->x * a.z, this->x * a.y - this->y * a.x);
}
constexpr Vector3D operator-(const Vector3D &r)const{
return Vector3D(*this)-=r;
}
constexpr Vector3D operator-=(const Vector3D &r){
this->x -= r.x;
this->y -= r.y;
this->z -= r.z;
return *this;
}
constexpr Vector3D operator+(const Vector3D &r){
return Vector3D(*this)+=r;
}
constexpr Vector3D operator+=(const Vector3D &r){
this->x += r.x;
this->y += r.y;
this->z += r.z;
return *this;
}
constexpr bool operator==(const Vector3D &r) const{
return this->x == r.x && this->y == r.y && this->z == r.z;
}
friend constexpr std::ostream &operator<<(std::ostream &out,const Vector3D &a){
out<<"{"<<a.x<<", "<<a.y<<", "<<a.z<<"}";
return out;
}
};
行列
概要
ソースコード
// matrix
template<class T> struct Matrix {
vector<vector<T> > val;
Matrix(int n = 1, int m = 1, T v = 0) : val(n, vector<T>(m, v)) {}
void init(int n, int m, T v = 0) {val.assign(n, vector<T>(m, v));}
void resize(int n, int m) {
val.resize(n);
for (int i = 0; i < n; ++i) val[i].resize(m);
}
Matrix<T>& operator = (const Matrix<T> &A) {
val = A.val;
return *this;
}
size_t size() const {return val.size();}
vector<T>& operator [] (int i) {return val[i];}
const vector<T>& operator [] (int i) const {return val[i];}
friend ostream& operator << (ostream& s, const Matrix<T>& M) {
s << endl;
for (int i = 0; i < (int)M.size(); ++i) s << M[i] << endl;
return s;
}
};
template<class T> Matrix<T> operator * (const Matrix<T> &A, const Matrix<T> &B) {
Matrix<T> R(A.size(), B[0].size());
for (int i = 0; i < A.size(); ++i)
for (int j = 0; j < B[0].size(); ++j)
for (int k = 0; k < B.size(); ++k)
R[i][j] += A[i][k] * B[k][j];
return R;
}
template<class T> Matrix<T> pow(const Matrix<T> &A, long long n) {
Matrix<T> R(A.size(), A.size());
auto B = A;
for (int i = 0; i < A.size(); ++i) R[i][i] = 1;
while (n > 0) {
if (n & 1) R = R * B;
B = B * B;
n >>= 1;
}
return R;
}
template<class T> vector<T> operator * (const Matrix<T> &A, const vector<T> &B) {
vector<T> v(A.size());
for (int i = 0; i < A.size(); ++i)
for (int k = 0; k < B.size(); ++k)
v[i] += A[i][k] * B[k];
return v;
}
template<class T> Matrix<T> operator + (const Matrix<T> &A, const Matrix<T> &B) {
Matrix<T> R(A.size(), A[0].size());
for (int i = 0; i < A.size(); ++i)
for (int j = 0; j < A[0].size(); ++j)
R[i][j] = A[i][j] + B[i][j];
return R;
}
template<class T> Matrix<T> operator - (const Matrix<T> &A, const Matrix<T> &B) {
Matrix<T> R(A.size(), A[0].size());
for (int i = 0; i < A.size(); ++i)
for (int j = 0; j < A[0].size(); ++j)
R[i][j] = A[i][j] - B[i][j];
return R;
}
❌分数
幾何
❌円同士の当たり判定
線分の交差判定
概要
線分同士が共通する点を持つかを判定.
ソースコード
inline bool rangeInRange(long l0,long r0,long l1,long r1){
if(l0>r0)swap(l0,r0);
if(l1>r1)swap(l1,r1);
if(l1<l0&&r1<l0)return false;
if(r0<l1&&r0<r1)return false;
return true;
}
inline long getSign(long a){
if(a>0)return 1;
else if(a<0)return -1;
return 0;
}
bool isIntersects(long x1,long y1,long x2,long y2,long x3,long y3,long x4,long y4){
using Vec3 = Vector3D<long>;
Vec3 A{x1,y1,0};
Vec3 B{x2,y2,0};
Vec3 C{x3,y3,0};
Vec3 D{x4,y4,0};
auto AB=B-A;
auto AC=C-A;
auto AD=D-A;
auto CD=D-C;
auto CA=A-C;
auto CB=B-C;
long a = getSign(AB.cross(AC).z);
long b = getSign(AB.cross(AD).z);
long c = getSign(CD.cross(CA).z);
long d = getSign(CD.cross(CB).z);
bool intersects = (a*b<=0&&c*d<=0);
if(a==0&&b==0){
intersects = rangeInRange(A.x,B.x,C.x,D.x) && rangeInRange(A.y,B.y,C.y,D.y);
}
return intersects;
}
❌凸包
数列
等差数列
概要
$O(1)$で等差数列の次の項目を求める.
- 第$n$項までの和
- 第$n$項はいくつか
- 第$l$項目から第$r$項目までの和
- $b$は何項目であるか
ソースコード
//初項s公差d第n項までの和
long arithSum(long a,long d,long n){
return n*(2*a+(n-1)*d)/2;
}
//初項s公差dの第n項
long arith(long a,long d,long n){
return a + d*(n-1);
}
//初項a公差d第l項から第r項までの和
long arithSumRange(long a,long d,long l,long r){
//第l項が初項となるような数列を作る.
return arithSum(arith(a,d,l),d,r-l+1);
}
//初項aで公差dのときbは第何項であるか.
long arithWhere(long a,long d,long b){
if(d==0){
if(a==b)return 1;
return -1;
}
// a+d*(n-1) = bとなるようなnが存在するか
// a + dn - d = b
// n = (b + d - a)/d
if((b+d-a)%d==0 && (b+d-a)/d > 0 )return (b+d-a)/d;
return -1;
}
//
実行例
int main(void){
long a,d;
/*
a = 2
d = 3
2,5,8,11,14,17,20,...
*/
a = 2,d = 3;
cout<<arith(a,d,4)<<endl;
cout<<arithSum(a,d,5)<<endl;
cout<<arithSumRange(a,d,3,6)<<endl;
cout<<arithWhere(a,d,20)<<endl;
/*
a = 10
d = -5
10,5,0,-5,-10,-15,...
*/
a = 10,d = -5;
cout<<arith(a,d,4)<<endl;
cout<<arithSum(a,d,5)<<endl;
cout<<arithSumRange(a,d,3,6)<<endl;
cout<<arithWhere(a,d,15)<<endl;
deque<int> qu
}
実行結果
11
40
50
7
-5
0
-30
-1
等比数列
階差数列
多項式
畳み込み
概要
多項式の掛け算が高速にできる.
ソースコード
❌形式的冪級数
文字列
Rolling Hash
概要
文字列$S$で,連続した部分文字列のハッシュ値を計算する.
計算量
- 前計算: $O(|S|)$
- ハッシュ値計算: $O(1)$
ソースコード
/*HashRolling*/
template<class T> class RollingHash{
public:
vector<T> hash,pow;
RollingHash(vector<T> &A,T base){
int N = (int)A.size();
hash.resize(N+1);
pow.resize(N+1);
pow[0]=1;
for(int i=0;i<N;i++){
hash[i+1]=hash[i]*base+A[i];
pow[i+1]=pow[i]*base;
}
}
T get(int l,int r){
return hash[r]-hash[l]*pow[r-l];
}
};
Z Algorithm
概要
文字列$S$の各位置$i$について,「$S$」と「$S$の$i$文字目以降の文字列」の最長共通接頭辞の長さを求めるアルゴリズム.
計算量
$O(|S|)$
ソースコード
/*Z-Algorithm*/
vector<int> Z_algorithm(string S){
int N=S.size();
vector<int> A(N);
A[0]=N;
int i=1,j=0;
while(i<N){
while(i+j<N&&S[j]==S[i+j])j++;
A[i]=j;
if(j==0){
i++;
continue;
}
int k=1;
while(i+k<N&&k+A[k]<j){
A[i+k]=A[k];
k++;
}
i+=k;
j-=k;
}
return A;
};
❌sufiix array
グラフ理論
ダイクストラ法
概要
重みが非負の辺のみで構成されるグラフで,2頂点間のパスの内の辺の重みの総和が最小となる値を求めるアルゴリズム
計算量
頂点数を$V$,辺の数を$E$とすると,$O(E+V\log V)$ である.
各辺は高々1回までしか通らず,$V$個の頂点についてプライオリティーキューで高々$V$個の頂点を管理しているため.
ソースコード
struct Edge{
int to;
long cost;
};
struct WeightedVertex{
int v;
long cost;
};
using Graph = vector<vector<Edge>>;
void dijkstra(int s,Graph &G,vector<long>&D){
auto comp = [](WeightedVertex &l,WeightedVertex &r){return l.cost > r.cost;};
priority_queue <
WeightedVertex,
vector<WeightedVertex>,
function<bool(WeightedVertex&,WeightedVertex&)>
> qu (comp);
D.resize(G.size());
fill(D.begin(),D.end(),-1);
D[s] = 0;
qu.push({s,0});
while(!qu.empty()){
auto a = qu.top(); qu.pop();
int from = a.v;
for(auto&e:G[from]){
if(D[e.to] == -1 || D[e.to] > D[from] + e.cost){
D[e.to] = D[from] + e.cost;
qu.push({e.to,D[e.to]});
}
}
}
}
ベルマンフォード法
概要
2頂点間のパスの内の辺の重みの総和が最小となる値を求めるアルゴリズム
ダイクストラと違って負の辺がある場合も正しく動作,負の閉路の検出ができる.
計算量
頂点数を$V$,辺の数を$E$とすると,$O(VE)$ である.
全ての頂点から全ての辺を見ているため.
ソースコード
struct Edge{
int from;
int to;
long cost;
};
struct WeightedVertex{
int v;
long cost;
};
using Graph = vector<vector<Edge>>; //隣接グラフ
const long INF = 1e17;
/*
重みはlong型
始点s,グラフGの点をDに
戻り値は **sからgの経路** を作るときに重みが負の無限になるか
*/
bool bellmanFord(int s,int g,Graph &G,vector<long>&D){
const int N = G.size();
vector<Edge>edges;
for(auto A:G){
for(Edge& a:A){
edges.push_back(a);
}
}
fill(D.begin(),D.end(),INF);
D[s] = 0;
for(int i=0;i<=N;i++){
for(auto&e:edges){
long d = D[e.from] + e.cost;
if(D[e.from] < INF && D[e.to] > d){
D[e.to] = d;
if(i==N&&e.to==g){
return true;
}
}
}
}
return false;
}
ワーシャルフロイド法
概要
全ての頂点の組の最短距離が求められる.
計算量
頂点数を$V$とすると$O(V^3)$.
注意
行列でグラフを表現するが,同じ頂点同士の距離は0,辺が存在しない場合は距離を$\infty$とする.
ソースコード
/*
Gは隣接行列である必要があり,辺がない場合はINF,自己ループ辺は0
*/
const long INF = 1e17;
vector<vector<long>> floydWarshall(vector<vector<long>> &G){
const int N = G.size();
auto H = G;
for(int a=0;a<N;a++){
for(int b=0;b<N;b++){
for(int c=0;c<N;c++){
long d = H[b][a] + H[a][c];
if(H[b][a]==INF||H[a][c]==INF)d = INF;
if(H[b][c] > d){
H[b][c] = d;
}
}
}
}
return H;
}
二次元マップをグラフに起こすやつ
概要
2次元#.
マップをグラフに起こします.
注意
calc()
関数内を適切に書き換えて使用してください.
ソースコード
struct GridGraph{
int H,W;
Graph G;
vector<vector<int>> A;
vector<long> D;
GridGraph(int h,int w){
H = h;
W = w;
init();
}
void init(){
D.clear();
G.clear();
A.clear();
G.resize(H*W);
A.resize(H);
for(int i=0;i<H;i++){
A[i].resize(W);
}
}
void in(){
map<char,int> mp = {{'.',0},{'#',1}};
for(int i=0;i<H;i++){
string S;cin>>S;
for(int j=0;j<W;j++){
A[i][j] = mp[(S[j])];
}
}
}
void build(){
static vector<int> dx = {1,0};
static vector<int> dy = {0,1};
for(int i=0;i<H;i++){
for(int j=0;j<W;j++){
for(int a=0;a<(int)dx.size();a++){
int h = i + dy[a];
int w = j + dx[a];
if(h<0||w<0||h>=H||w>=W)continue;
if(A[h][w]==A[i][j]&&A[h][w]==0){
int x = toInt(i,j);
int y = toInt(h,w);
G[x].push_back({y,1});
G[y].push_back({x,1});
}
}
}
}
}
void calc(int h,int w){
//dijkstra(toInt(h,w),G,D);
}
int toInt(int h,int w){
return W*h + w;
}
long getDis(int h,int w){
return D[toInt(h,w)];
}
};
2部グラフ判定
概要
2部グラフであるかを判定する.
アルゴリズム
BFSで隣り合う頂点を異なる2色で塗っていき,最後に全ての隣り合う頂点が異なる色で塗られているかを判定する.計算量は$O(V+E)$
ソースコード
struct Edge{
int to;
};
using Graph = vector<vector<Edge>>; //隣接グラフ
/*
ダイクストラ法
重みはlong型
始点s,グラフGの点をDに
*/
void bfs(int n,int pre,Graph &G,vector<int>&C){
for(auto&e:G[n]){
if(C[e.to]!=-1)continue;
C[e.to] = 1-C[n];
bfs(e.to,n,G,C);
}
}
int main(){
int N,M;cin>>N>>M;
Graph G(N);
for(int i=0;i<M;i++){
int u,v;
cin>>u>>v;
u--;v--;
G[u].push_back({v});
G[v].push_back({u});
}
vector<int>C(N,-1);
for(int i=0;i<N;i++){
if(C[i] != -1)continue;
C[i]=0;
bfs(i,-1,G,C);
}
bool ok = true;
for(int i=0;i<N;i++){
//cout<<i<<" "<<C[i]<<endl;
for(auto&e:G[i]){
if(C[e.to] == C[i]){
ok = false;
}
}
}
cout<<(ok?"Yes":"No")<<endl;
}
N頂点N辺グラフ分析
概要
連結な$N$頂点$N$辺無向グラフは閉路が必ず$1$つできる. その一つの閉路を$O(N)$で求める.このようなグラフは俗になもりグラフと呼ばれる.
ソースコード
自己ループには対応していない.
struct Edge{
int to;
};
using Graph = vector<vector<Edge>>;
void dfs(Graph&G,int n,vector<int> &pre,int &x){
for(Edge&e:G[n]){
if(e.to==pre[n])continue;
if(pre[e.to]==-1){
pre[e.to]=n;
dfs(G,e.to,pre,x);
}else{
x = n;
}
}
}
vector<int> namori(Graph graph){
//なもりグラフの閉路に含まれる頂点を返す
int n=(int)graph.size();
vector<int> res;
vector<int> pre(n,-1);//前回の頂点
int x=-1;
dfs(graph,0,pre,x);
cout<<x<<endl;
if(x==-1)return res;
int y=pre[x];
while(x!=y){
res.push_back(y);
y=pre[y];
}
res.push_back(x);
return res;
}
木の直径
概要
最も離れた$2$点の距離を木の直径と呼ぶ.頂点数を$V$とすると$O(V)$で求める.
ソースコード
中身はDFSを2回するだけ.
struct Edge{
int to;
};
using Graph = vector<vector<Edge>>;
void dfs(Graph &G,int v,int p,int d,vector<int>&dist){
dist[v]=d;
for(auto&e:G[v]){
if(e.to==p)continue;
dfs(G,e.to,v,d+1,dist);
}
}
int treeDiameter(Graph &Tree){
int N = Tree.size();
vector<int> dist(N,-1);
dfs(Tree,0,-1,0,dist);
int farthest = max_element(all(dist))-dist.begin();
vector<int> dist2(N,-1);
dfs(Tree,farthest,-1,0,dist2);
return *max_element(all(dist2));
}
Level Ancestor
概要
根つき木で,前計算を行うことで,ある頂点の$x$個先の祖先,ある頂点で深さが$d$である祖先を高速に求める.
計算量
頂点数を$N$とすると前計算$O(N\log N)$,各クエリ$O(\log N)$で動作.
ソースコード
struct Edge{
int to;
};
using Graph = vector<vector<Edge>>;
struct LA{
Graph G;
vector<vector<int>> ancestor;
//ancestor[i][j]:=頂点iの2^j個親
vector<int> depth;//深さ
int N;
int root = 0;
const int maxDepth = 25;
LA(int _N){
this-> N = _N;
init();
}
void init(){
G.resize(N);
depth.resize(N);
ancestor.resize(N);
for(int i=0;i<N;i++){
ancestor[i].resize(maxDepth);
}
}
void build(){
ancestor[root][0] = root;
bfs(root,root,0);
for(int i=1;i<maxDepth;i++){
for(int j=0;j<N;j++){
ancestor[j][i] = ancestor[ancestor[j][i-1]][i-1];
}
}
}
void bfs(int n,int pre,int d){
depth[n] = d;
ancestor[n][0] = pre;
for(auto&E:G[n]){
if(E.to==pre)continue;
bfs(E.to,n,d+1);
}
}
//頂点nのs個先の祖先
int anc(int n,int s){
for(int i=0;i<maxDepth;i++){
if(s&(1<<i)){
n = ancestor[n][i];
}
}
return n;
}
//頂点nの深さs(root=0)の祖先
int levelAnc(int n,int s){
return anc(n,depth[n]-s);
}
};
Lowest Common Ancestor
概要
2つの頂点について,それぞれの共通祖先のうち最も深いものを求める.
計算量
前計算$O(N\log N)$,クエリ$O(\log N)$
ソースコード
struct Edge{
int to;
};
using Graph = vector<vector<Edge>>;
struct LA{
Graph G;
vector<vector<int>> ancestor;
//ancestor[i][j]:=頂点iの2^j個親
vector<int> depth;//深さ
int N;
int root = 0;
const int maxDepth = 25;
LA(int _N){
this-> N = _N;
init();
}
void init(){
G.resize(N);
depth.resize(N);
ancestor.resize(N);
for(int i=0;i<N;i++){
ancestor[i].resize(maxDepth);
}
}
void build(){
ancestor[root][0] = root;
bfs(root,root,0);
for(int i=1;i<maxDepth;i++){
for(int j=0;j<N;j++){
ancestor[j][i] = ancestor[ancestor[j][i-1]][i-1];
}
}
}
void bfs(int n,int pre,int d){
depth[n] = d;
ancestor[n][0] = pre;
for(auto&E:G[n]){
if(E.to==pre)continue;
bfs(E.to,n,d+1);
}
}
//頂点nのs個先の祖先
int anc(int n,int s){
for(int i=0;i<maxDepth;i++){
if(s&(1<<i)){
n = ancestor[n][i];
}
}
return n;
}
//頂点nの深さs(root=0)の祖先
int levelAnc(int n,int s){
return anc(n,depth[n]-s);
}
//頂点a,bの共通最近祖先
int lca(int a,int b){
if(depth[a]<depth[b])swap(a,b);
a = levelAnc(a,depth[b]);//同じ深さにする
if(a==b)return a;
for(int k=maxDepth-1;k>=0;k--){
if(ancestor[a][k]!=ancestor[b][k]){
a = ancestor[a][k];
b = ancestor[b][k];
}
}
return ancestor[a][0];
}
};
木の巡回
概要
木の頂点に番号を振る.
DFSの順
ソースコード
//dfsの行きがけ順
void preorder(const Graph &G,int n,int p,vector<bool>&visited,vector<int>&order){
visited[n]=true;
order.push_back(n);
for(auto&e:G[n]){
if(e.to==p)continue;
if(visited[e.to])continue;
preorder(G,e.to,n,visited,order);
}
}
//dfsの帰りがけ順
void postorder(const Graph &G,int n,int p,vector<bool>&visited,vector<int>&order){
visited[n]=true;
for(auto&e:G[n]){
if(e.to==p)continue;
if(visited[e.to])continue;
postorder(G,e.to,n,visited,order);
}
order.push_back(n);
}
BFSの順
ソースコード
オイラーツアー
❌サイクル検出
❌橋検出
❌関節点検出
01BFS
辺の重みが$0,1$のどちらかであるようなグラフで,頂点$s$からの最短距離を効率よく求めるアルゴリズム.
計算量
$O(V+E)$
ソースコード
struct Edge{
int from;
int to;
long cost;
};
using Graph = vector<vector<Edge>>;
/*01BFS*/
void bfs01(Graph &G,int s,vector<long>&dist){
int n = G.size();
dist.assign(n,INFl);
deque<int> que;
dist[s] = 0;
que.push_back(s);
while(!que.empty()){
int v = que.front();
que.pop_front();
for(auto &e:G[v]){
if(dist[e.to] > dist[v] + e.cost){
dist[e.to] = dist[v] + e.cost;
if(e.cost == 0){
que.push_front(e.to);
}else{
que.push_back(e.to);
}
}
}
}
}
Strongly Connected Component
プリム法
クラスカル法
最小全域木を求める.AOJ
依存
計算量
$O(|E|\log|E|)$(本当?)
ソースコード
struct UnionFind{
public:
vector<int> par;//親
vector<long> weight;//重み
vector<long> weightAlone;//単体の重み
UnionFind(int n):par(n),weight(n),weightAlone(n){
for(int i=0;i<n;i++){
par[i]=i; //親は自分自身にしておく
weight[i] = weightAlone[i] = 1;
}
}
//途中で実行すると壊れます
void setWeight(int i,long w){
weight[i] = weightAlone[i] = w;
}
//途中で実行しても大丈夫
void changeWeight(int i,long w){
long pre = weightAlone[i];
weightAlone[i] = w;
weight[root(i)] += w-pre;
}
int root(int x){
if(par[x]==x){
return x;
}else{
int r = root(par[x]);
par[x]=r;
return r;
}
}
void unite(int x,int y){
int rx=root(x);
int ry=root(y);
if(rx==ry){
return;
}
par[rx]=ry;
weight[ry] += weight[rx];
}
bool same(int x,int y){
int rx=root(x);
int ry=root(y);
return rx==ry;
}
long getWeight(int x){
return weight[root(x)];
}
vector<vector<int>> getGroups(){
vector<vector<int>> res;
map<int,vector<int>> mp;
for(int i=0;i<(int)par.size();i++){
mp[root(i)].push_back(i);
}
for(auto&[k,v]:mp){
(void)k; //使いません
res.push_back(v);
}
return res;
}
};
/*クラスカル法*/
struct Edge{
int from;
int to;
long cost;
};
using Graph = vector<vector<Edge>>;
struct Kruskal{
vector<Edge> edges;
Graph G;
int V;
Kruskal(int V):V(V){
G.resize(V);
}
//無向グラフ!
void addEdge(int from,int to,long cost){
edges.push_back({from,to,cost});
}
long long build(){
sort(edges.begin(),edges.end(),[](const Edge& e1,const Edge& e2){
return e1.cost<e2.cost;
});
UnionFind uf(V);
long long res = 0;
for(auto& e:edges){
if(!uf.same(e.from,e.to)){
uf.unite(e.from,e.to);
res += e.cost;
G[e.from].push_back({e.from,e.to,e.cost});
G[e.to].push_back({e.to,e.from,e.cost});
}
}
return res;
}
};
❌木dp
❌全方位木dp
❌重心分解
最大流
最小コスト
未分類
imos法
概要
要素数$N$の数列について,区間への加算を$O(1)$で行い,$O(N)$の処理を行うことで各値が$O(1)$で得られる.
ソースコード
template<class T> struct Imos{
vector<T> A;
int N;
constexpr Imos(int N){
this -> N = N;
init();
}
constexpr void init(){
A.resize(N+1);
fill(A.begin(),A.end(),0);
}
/*[l,r]にxを加算*/
constexpr void add(int l,int r,T x){
A[l] += x;
A[r+1] += -x;
}
constexpr void calc(){
for(int i=0;i<N;i++){
A[i+1] += A[i];
}
}
constexpr T get(int i){
return A[i];
}
};
2次元imos法
概要
ソースコード
template<class T> struct Imos2d{
vector<vector<T>> A;
int H,W;
constexpr Imos2d(int H,int W){
this->H = H;
this->W = W;
init();
}
constexpr void init(){
A.resize(H+1);
for(int i=0;i<=H;i++){
A[i].resize(W+1);
fill(A[i].begin(),A[i].end(),0);
}
}
/*四角形にxを加算*/
constexpr void add(int ly,int lx,int ry,int rx,T x){
A[ly][lx] += x;
A[ry+1][lx] += -x;
A[ly][rx+1] += -x;
A[ry+1][rx+1] += x;
}
constexpr void calc(){
for(int h=0;h<=H;h++){
for(int w=0;w<W;w++){
A[h][w+1] += A[h][w];
}
}
for(int h=0;h<H;h++){
for(int w=0;w<=W;w++){
A[h+1][w] += A[h][w];
}
}
}
constexpr T get(int y,int x){
return A[y][x];
}
};
座標圧縮imos法
ソースコード
template<class T> bool underElement(vector<T> &A,T x,T &find,int &index){
index = upper_bound(A.begin(),A.end(),x) - A.begin();
index--;
if(index >= (int)A.size())return false;
find = A[index];
return true;
}
template<class T> struct shrinkImos{
vector<pair<long,T>> query;
vector<pair<long,T>> result;
shrinkImos(){
init();
}
void init(){
query.clear();
result.clear();
}
/*[l,r]にxを加算*/
void add(int l,int r,T x){
query.push_back({l,x});
query.push_back({r,0});
query.push_back({r+1,-x});
}
void calc(){
result.clear();
for(const auto&[p,v]:query){
result.push_back({p,v});
}
sort(result.begin(),result.end());
for(int i=0;i<(int)result.size()-1;i++){
result[i+1].second += result[i].second;
}
}
constexpr T get(long i){
pair<long,T> f;
int _;
if(!underElement(result,i,f,_)){
return 0;
}
return f.second;
}
};
座標圧縮2次元imos法
ソースコード
template<class T> bool underElement(vector<T> &A,T x,T &find,int &index){
index = upper_bound(A.begin(),A.end(),x) - A.begin();
index--;
if(index >= (int)A.size())return false;
find = A[index];
return true;
}
template<class T1,class T2> struct shrinkImos2d{
vector<T1> Ay,Ax;
vector<tuple<T1,T1,T2>> query;
vector<vector<T2>> result;
shrinkImos2d(){
init();
}
void init(){
Ax.push_back(2001002003);
Ay.push_back(2001002003);
/*
Ax.push_back(1001002003004005006);
Ay.push_back(1001002003004005006);
*/
result.clear();
}
/*[l,r]にxを加算*/
void add(T1 ly,T1 lx,T1 ry,T1 rx,T2 x){
Ay.push_back(ly);
Ay.push_back(ry);
Ay.push_back(ry+1);
Ax.push_back(lx);
Ax.push_back(rx);
Ax.push_back(rx+1);
query.push_back({ly,lx,x});
query.push_back({ry+1,lx,-x});
query.push_back({ly,rx+1,-x});
query.push_back({ry+1,rx+1,x});
}
void calc(){
result.clear();
sort(Ax.begin(),Ax.end());
sort(Ay.begin(),Ay.end());
Ax.erase(unique(Ax.begin(), Ax.end()), Ax.end());
Ay.erase(unique(Ay.begin(), Ay.end()), Ay.end());
result.resize(Ay.size());
for(int i=0;i<(int)Ay.size();i++){
result[i].resize(Ax.size());
fill(result[i].begin(),result[i].end(),0);
}
for(const auto&[y,x,a]:query){
int yi = lower_bound(Ay.begin(),Ay.end(),y)-Ay.begin();
int xi = lower_bound(Ax.begin(),Ax.end(),x)-Ax.begin();
result[yi][xi] += a;
}
for(int y=0;y<(int)result.size();y++){
for(int x=0;x<(int)result[y].size()-1;x++){
result[y][x+1] += result[y][x];
}
}
for(int y=0;y<(int)result.size()-1;y++){
for(int x=0;x<(int)result[y].size();x++){
result[y+1][x] += result[y][x];
}
}
}
constexpr T2 get(T1 y,T1 x){
T1 yf,xf;
int yi,xi;
bool a = underElement(Ay,y,yf,yi);
bool b = underElement(Ax,x,xf,xi);
if(a||b)return 0;
return result[yi][xi];
}
constexpr long area(T1 iy,T1 ix){
return (long)(Ax[ix+1]-Ax[ix])*(long)(Ay[iy+1]-Ay[iy]);
}
void print(){
for(int i=0;i<(int)Ax.size();i++){
cout<<Ax[i]<<" ";
}
cout<<endl;
for(int i=0;i<(int)Ay.size();i++){
cout<<Ay[i]<<" ";
}
cout<<endl;
for(int y=0;y<(int)result.size();y++){
for(int x=0;x<(int)result[y].size();x++){
cout << result[y][x] <<" ";
}
cout<<endl;
}
}
};
累積和
概要
静的な数列の区間和を高速で求める.
計算量
- 構築: $O(N)$
- クエリ:$O(1)$
ソースコード
template<class T> struct CumulativeSum{
size_t n;
vector<T> A;
CumulativeSum(size_t n){
this->n=n;
init();
};
void init(){
A.resize(n+1);
}
void add(int i,T x){
A[i+1]=x;
}
void build(){
for(int i=0;i<n;i++){
A[i+1]+=A[i];
}
}
/*[l,r)の総和を求める*/
T query(int l,int r){
return A[r]-A[l];
}
};
2次元累積和
概要
$H$行$W$列の行列があり,各要素を$A_{ij}$とする.このとき,前処理を行うことで,次の値を$O(1)$で求めることができる. $$ \sum_{h_1\leq i< h_2}\sum_{w_1\leq j<w_2} A_{ij} $$
計算量
- 構築: $O(HW)$
- クエリ: $O(1)$
ソースコード
template<class T> struct CumulativeSum2D{
size_t H,W;
vector<vector<T>>data,A;
CumulativeSum2D(size_t H,size_t W){
this->H=H;
this->W=W;
data.resize(H+1,vector<T>(W+1,0));
A.resize(H+1,vector<T>(W+1,0));
}
void add(size_t h,size_t w,T x){
A[h][w]+=x;
}
void build(){
int H=A.size();
int W=A[0].size();
data=vector<vector<T>>(H+1,vector<T>(W+1,0));
rep(i,H){
rep(j,W){
data[i+1][j+1]=A[i][j];
}
}
rep(i,H+1){
rep(j,W){
data[i][j+1]+=data[i][j];
}
}
rep(i,H){
rep(j,W+1){
data[i+1][j]+=data[i][j];
}
}
}
/*w1<=x<w2, h1<=y<h2*/
T sum(int h1,int w1,int h2,int w2){
return data[h2][w2]-data[h1][w2]-data[h2][w1]+data[h1][w1];
}
};
要素を2分探索
概要
C++にはlower_bound
やupper_bound
,binary_search
が用意されているが,例外処理が面倒なので関数したもの.
次の関数を作成した.
- その数が存在するかの
isExist
- $x$以上の最小の数を探す
lowerElement
- $x$より大きい最小の数を探す
upperlement
- $x$以下の最大の数を探す
underElement
- $x$未満の最大の数を探す
lessElement
- $x$の個数を数える
countElement
- $l$以上$r$以下の個数を数える
countElementInRange
注意
vector
はソート済みである必要がある.
ソースコード
/*
Aにxが存在するか
O(log N)
*/
template<class T> bool isExist(vector<T> &A,T x){
return binary_search(A.begin(),A.end(),x);
}
template<class T> bool isExist(set<T> &A,T x){
auto it = A.lower_bound(x);
if(it==A.end()){
return false;
}
return *it == x;
}
/*
Aに存在するx以上の数
最も小さいindexとその値find
O(log N)
*/
template<class T> bool lowerElement(vector<T> &A,T x,T &find,int &index){
index = lower_bound(A.begin(),A.end(),x) - A.begin();
if(index == (int)A.size())return false;
find = A[index];
return true;
}
template<class T> bool lowerElement(set<T> &A,T x,T &find){
auto it = A.lower_bound(x);
if(it==A.end())return false;
find = *it;
return true;
}
/*
Aに存在するxより大きい数
最も小さいindexとその値find
O(log N)
*/
template<class T> bool upperElement(vector<T> &A,T x,T &find,int &index){
index = upper_bound(A.begin(),A.end(),x) - A.begin();
if(index == (int)A.size())return false;
find = A[index];
return true;
}
template<class T> bool upperElement(set<T> &A,T x,T &find){
auto it = A.upper_bound(x);
if(it == A.end())return false;
find = *it;
return true;
}
/*
Aに存在するx以下
最も大きいindexとその値find
O(log N)
*/
template<class T> bool underElement(vector<T> &A,T x,T &find,int &index){
index = upper_bound(A.begin(),A.end(),x) - A.begin();
index--;
if(index >= (int)A.size())return false;
find = A[index];
return true;
}
template<class T> bool underElement(set<T> &A,T x,T &find){
auto it = A.upper_bound(x);
if(it == A.begin())return false;
it--;
find = *it;
return true;
}
/*
Aに存在するxより小さい数
最も大きいindexとその値find
O(log N)
*/
template<class T> bool lessElement(vector<T> &A,T x,T &find,int &index){
index = lower_bound(A.begin(),A.end(),x) - A.begin();
index--;
if(index < 0)return false;
find = A[index];
return true;
}
template<class T> bool lessElement(set<T> &A,T x,T &find){
auto it = A.lower_bound(x);
if(it == A.begin())return false;
it--;
find = *it;
return true;
}
/*
Aに含まれるxの個数をO(log N)で求める
*/
template<class T> int countElement(vector<T> &A,T x){
T f1,f2;
int i1,i2;
lowerElement(A,x,f1,i1);
underElement(A,x,f2,i2);
return isExist(A,x)?i2-i1+1:0;
}
/*
Aに含まれるl以上r以下の個数をO(log N)で求める
*/
template<class T> int countElementInRange(vector<T> &A,T l,T r){
T f1,f2;
int i1,i2;
lowerElement(A,l,f1,i1);
underElement(A,r,f2,i2);
int c = i2-i1+1;
return c>0?c:0;
}
日付計算
曜日計算
/*曜日計算*/
int wday(int y,int m,int d){
if(m==1||m==2){
m+=12;
y--;
}
return (y+y/4-y/100+y/400+(13*m+8)/5+d)%7;
//0,1,2,...日,月,火
}
経過日数
1年1月1日からの経過日数
/*1年1月1日からの経過日数(1年1月1日が0日目)*/
int days(int y,int m,int d){
if(m==1||m==2){
m+=12;
y--;
}
return 365*y+y/4-y/100+y/400+(306*(m+1))/10+d-429;
}
Mex関数
❌Mo's algorithm
有名問題
ナップザック問題
多くの亜種,応用パターンがある.
⚠️ todo 厳密には漸化式が間違っているので直す
01ナップザック
-
$1\leq N\leq 100$
-
$1\leq v_i\leq 1000$
-
$1\leq w_i\leq 1000$
-
$1\leq W\leq 10000$
$$ \begin{align} dp[i][j] &:= i番目まで選んで,重さの総和がjとなるように選んだときの価値の総和の最大値\\ dp[i][j] &= \begin{cases} 0&(i=0)\\ \max(dp[i-1][j],dp[i-1][j-w_i]+v_i) & (j\geq w_i)\\ dp[i-1][j] & (otherwise) \end{cases} \end{align} $$
復元は,その時の品物を選んだかどうかを記録しておく方法もある.
int main(void){
//01ナップザック
int N,W;
cin>>N>>W;
vector<int> w(N+1),v(N+1);
for(int i=0;i<N;i++){
cin>>v[i+1]>>w[i+1];
}
vector<vector<long>> dp(N+1,vector<long>(W+1,0));
// dp[i][j] = i番目まで選んで重さがjになるような最大の価値
long ans = 0;
for(int i=1;i<=N;i++){
for(int j=0;j<=W;j++){
if(j-w[i]>=0){
//選ぶ
chmax(dp[i][j],dp[i-1][j-w[i]]+v[i]);
}
//選ばない
chmax(dp[i][j],dp[i-1][j]);
chmax(ans,dp[i][j]);
}
}
cout<<ans<<endl;
//復元
vector<int> res;
int j = W;
for(int i=N;i>=1;i--){
if(dp[i][j] == dp[i-1][j-w[i]]+v[i]){
res.push_back(i);
j -= w[i];
}
}
cout<<res<<endl;
}
01ナップザック2(価値が小さい)
-
$1\leq N\leq 100$
-
$1\leq v_i\leq 100$
-
$1\leq w_i\leq 10^7$
-
$1\leq W\leq 10^9$ $$ \begin{align} dp[i][j] &:= i番目まで選んで,価値の総和がjとなるように選んだときの重さの総和の最小値\\ dp[i][j] &= \begin{cases} 0&(i=0,j=0)\\ \min(dp[i-1][j],dp[i-1][j-v_i]+w_i) & (j\geq v_i)\\ dp[i-1][j] & (otherwise) \end{cases} \end{align} $$
int main(void){
//01ナップザック
int N,W;
cin>>N>>W;
vector<int> w(N+1),v(N+1);
for(int i=0;i<N;i++){
cin>>v[i+1]>>w[i+1];
}
vector<vector<long>> dp(N+1,vector<long>(100001,INFl));
//dp[i][j]:=i番目までの品物から価値の総和がjになるように選んだときの重さの総和の最小値
dp[0][0] = 0;
for(int i=1;i<=N;i++){
for(int j=0;j<=100000;j++){
if(j-v[i]>=0){
dp[i][j] = min(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
long ans = 0;
for(int i=0;i<=100000;i++){
if(dp[N][i]<=W){
ans = i;
}
}
cout<<ans<<endl;
//復元
vector<int> res;
int j = ans;
for(int i=N;i>=1;i--){
if(dp[i][j]==dp[i-1][j])continue;
res.push_back(i);
j-=v[i];
}
cout<<res<<endl;
}
個数制限なしナップザック
-
$1\leq N\leq 100$
-
$1\leq v_i\leq 1000$
-
$1\leq w_i\leq 1000$
-
$1\leq W\leq 10000$ $$ \begin{align} dp[i][j] &:= i番目まで選んで,重さの総和がjとなるように選んだときの価値の総和の最大値\\ dp[i][j] &= \begin{cases} 0&(i=0,j=0)\\ \max(dp[i-1][j],dp[i][j-w_i]+v_i) & (otherwise) \end{cases} \end{align} $$
int main(void){
//01ナップザック
int N,W;
cin>>N>>W;
vector<int> w(N+1),v(N+1);
for(int i=0;i<N;i++){
cin>>v[i+1]>>w[i+1];
}
vector<vector<long>> dp(N+1,vector<long>(W+1,0));
// dp[i][j] = i番目まで選んで重さがjになるような最大の価値
long ans = 0;
for(int i=1;i<=N;i++){
for(int j=0;j<=W;j++){
dp[i][j] = dp[i-1][j];
}
for(int j=0;j<=W;j++){
if(j-w[i]>=0){
//選ぶ
chmax(dp[i][j],dp[i][j-w[i]]+v[i]);
}
chmax(ans,dp[i][j]);
}
}
cout<<ans<<endl;
}
復元するために,そのdpテーブルを更新するときに品物を選んだかを記録しておく(他に方法がある?).
int main(void){
//01ナップザック
int N,W;
cin>>N>>W;
vector<int> w(N+1),v(N+1);
for(int i=0;i<N;i++){
cin>>v[i+1]>>w[i+1];
}
vector<vector<long>> dp(N+1,vector<long>(W+1,0));
// dp[i][j] = i番目まで選んで重さがjになるような最大の価値
vector<vector<int>> pre(N+1,vector<int>(W+1,0));
long ans = 0;
for(int i=1;i<=N;i++){
for(int j=0;j<=W;j++){
dp[i][j] = dp[i-1][j];
}
for(int j=0;j<=W;j++){
if(j-w[i]>=0){
//選ぶ
if(chmax(dp[i][j],dp[i][j-w[i]]+v[i])){
pre[i][j] = 1;
}
}
chmax(ans,dp[i][j]);
}
}
cout<<ans<<endl;
vector<int> res;
int j = W;
int i=N;
while(i>0){
if(pre[i][j]){
res.push_back(i);
j -= w[i];
}else{
i--;
}
}
cout<<res<<endl;
}
個数制限つきナップザック
-
$1\leq N\leq 100$
-
$1\leq v_i\leq 1000$
-
$1\leq w_i\leq 1000$
-
$1\leq m_i\leq 10000$
-
$1\leq W\leq 10000$
-
$m_i$個の品物$i$を,$2^0$個,$2$個,$2^2$個,$2^3$,$\cdots$,$2^k$個と余りの塊に分けると,これらの組み合わせで$1$から$m_i$個の組み合わせが実現でき,せいぜい$O(\log m_i)$個で表現できる.
- $O(\sum\log{m_i})$個の品物がある01ナップザックに帰着する.
int main(void){
long N,W;cin>>N>>W;
vector<long> w,v;
w.push_back(0);//インデックス埋め
v.push_back(0);
rep(i,N){
long a,b,c;cin>>a>>b>>c;
long k=1;
while(k<=c){
v.push_back(a*k);
w.push_back(b*k);
c-=k;
k*=2;
}
if(c>0){
v.push_back(a*c);
w.push_back(b*c);
}
}
N=(int)w.size();
vector<vector<long>> dp(N+1,vector<long>(W+1,0));
// dp[i][j] = i番目まで選んで重さがjになるような最大の価値
long ans = 0;
for(int i=1;i<=N;i++){
for(int j=0;j<=W;j++){
if(j-w[i]>=0){
//選ぶ
chmax(dp[i][j],dp[i-1][j-w[i]]+v[i]);
}
//選ばない
chmax(dp[i][j],dp[i-1][j]);
chmax(ans,dp[i][j]);
}
}
cout<<ans<<endl;
}
個数制限付きナップザック問題(価値が小さい)
- 個数制限付きナップザック問題 II
- $1\leq N\leq 50$
- $1\leq v_i\leq 50$
- $1\leq w_i\leq 10^9$
- $1\leq m_i\leq 10^9$
- $1\leq W\leq 10^9$
巨大ナップザック(品物が少ない)
- 巨大ナップザック問題
- $1\leq N\leq 40$
- $1\leq v_i\leq 10^{15}$
- $1\leq w_i\leq 10^{15}$
- $1\leq W\leq 10^{15}$
- 半分全列挙
転倒数計算
問題
- 問題リンク:転倒数
- 転倒数とは,配列の中で,ある要素の前にある要素の値が大きいものの個数のこと
- 厳密には,長さ$N$の数列$A_1,A_2,A_3,\cdots,A_N$で,$i<j$で$A_i>A_j$となる$(i,j)$の個数
- あるいは,バブルソートを行ったときに,要素の交換回数のこと
解法
実際にバブルソートをすると要素数$N$で$O(N^2)$かかってしまうが,転倒数を計算するだけなら$1$点加算と区間和の取得ができるデータ構造BITやセグ木を用いて$O(N\log N)$で計算できる.
template<typename T>
struct RSQ{
int n;
vector<T>dat;
const T ZERO;
RSQ(int n_,T ZERO_):ZERO(ZERO_){
n=1;
while(n<n_)n*=2;
//完全二分木にする
dat.assign(2*n-1,ZERO);
}
void update(int k,T a){
k+=n-1;// i番目は、配列上では n-1+i 番目に格納されている
dat[k]+=a;// 葉の更新
while(k>0){
k=(k-1)/2; //親のインデックス
// 子の和を計算
dat[k]=dat[k*2+1]+dat[k*2+2];
}
}
T query(int a,int b,int k,int l,int r){
if(r<=a||b<=l)return ZERO;//範囲外
if(a<=l&&r<=b)return dat[k]; //範囲内である
else{
T vl=query(a,b,k*2+1,l,(l+r)/2);
T vr=query(a,b,k*2+2,(l+r)/2,r);
return vl+vr;
}
}
//[a,b)の総和を求める
T query(int a,int b){
return query(a,b,0,0,n);
}
};
int main(void){
/*転倒数計算*/
int n;cin>>n;
vector<int>A(n);
for(int i=0;i<n;i++){
cin>>A[i];
A[i]--;
}
RSQ<int>rsq(n,0);
long ans=0;
for(int i=0;i<n;i++){
ans+=rsq.query(A[i]+1,n);
rsq.update(A[i],1);
}
cout<<ans<<endl;
}
包除原理
簡単な例
$f(X)$を集合$X$の大きさとすると, $$ f(A\cup B)= f(A) + f(B) -f(A\cap B) $$ が成り立つ.これを一般化したものが包除原理.
例題
- 正の整数$N$と$A_1,A_2,A_3,\cdots,A_K$が与えられる
- $1\leq K \leq 10$
- $1\leq N \leq 10^{12}$
- $1$以上$N$以下の整数で$A_1,A_2,A_3,\cdots,A_K$のいずれかの倍数であるものの個数を求める.
言い換えると,$A_1$の倍数,$A_2$の倍数,$A_3$の倍数,$\cdots$,$A_K$の倍数,の$K$個の集合の和集合の大きさを求める.この$K$個の集合から$1$つ以上を選ぶ全ての組み合わせについて,
- 奇数個の集合であれば,積集合の大きさを足す
- 偶数個の集合であれば,積集合の大きさを引く
を行っていくと,最終的には辻褄が合って,条件を1つ以上満たすものの個数が求まる.
long GCD(long a,long b){
if(a<b)return GCD(b,a);
if(b==0)return a;
return GCD(b,a%b);
}
long LCM(long a,long b){
return (a/GCD(a,b))*b;
}
int main(void){
long N,K;cin>>N>>K;
vector<long> V(K);
rep(i,K)cin>>V[i];
long ans = 0;
for(int i=1;i<(1<<K);i++){
int c=0;
long lcm=1;
for(int k=0;k<K;k++){
if(i&(1<<k)){
c++;
lcm = LCM(V[k],lcm);
}
}
ans += (N/lcm) * (c%2==1?1L:-1L);
}
cout<<ans<<endl;
}
線形漸化式
線形漸化式の第$n$項を求める様々な方法を紹介する. ここでは例として,代表的な線形漸化式であるフィボナッチ数列の第$n$項を$\mod 10^9$で求めることにする.計算量は,$k$項間の線形漸化式の場合のものを示す.
愚直
第$3$項,第$4$項,第$5$項と定義通り順に求めていく方法.計算量は$O(kn)$. 第$n$項までの全ての項の値がほしいときに有効である.
long N;cin>>N;
vector<mint> fibo={1,1};
for(int i=3;i<=N;i++){
int a = (int)fibo.size();
fibo.push_back(fibo[a-1]+fibo[a-2]);
}
cout<<fibo.back()<<endl;
行列累乗
ダブリングの考え方によって,$O(k^3\log n)$で求まる. 行列の掛け算自体がコストが高く,$k$が大きいときに時間がかかる.
long N;cin>>N;
Matrix<mint> mat(2,2);
mat[0][0] = 1;
mat[0][1] = 1;
mat[1][0] = 1;
mat[1][1] = 0;
auto ans = pow(mat,N-1);
cout<<ans[0][0]<<endl;
きたまさ法
これもダブリングである. 計算量は,$O(k^2\log n)$である. 参考(誤植がある気がする)
$k$項漸化式版はこちら.
vector<mint> kitamasa(long n,const vector<mint>&d,const int k){
//f(n)の係数ベクトルを求める.
if(n==k){
return d;
}
if(n&1||n<k*2){
vector<mint> res(k);
auto x1 = kitamasa(n-1,d,k);
for(int i=0;i<k;i++){
res[i] = d[i]*x1[k-1] + (i>0?x1[i-1]:0);
}
return res;
}else{
vector<mint> res(k,0);
auto x0 = kitamasa(n/2,d,k);
auto x1 = x0;
for(int l=0;l<k;l++){//f(n/2+l)
for(int j=0;j<k;j++){
res[j] += x0[l]*x1[j];
}
vector<mint> next(k);
for(int i=0;i<k;i++){
next[i] = d[i]*x1[k-1] + (i>0?x1[i-1]:0);
}
swap(x1,next);
}
return res;
}
}
int main(){
vector<mint> d = {1,1};
auto a = kitamasa(N-1,d,2);//N-1が2以下だと壊れる
cout<<a[0]*d[0]+a[1]*d[1]<<endl;
}
スライド最小値
概要
- 問題
- 数列Aに対して,長さがLの区間の最小値を求める.
ソースコード
セグ木でも$O(N\log N)$で殴れるが,この手法では$O(N)$で解ける.
template<class T>
vector<T> SlidingMinimumElement(vector<T>&A,int k){
int n=A.size();
vector<T>res(n-k+1);
deque<int>deq;
for(int i=0;i<n;i++){
while(!deq.empty()&&A[deq.back()]>A[i])deq.pop_back();
deq.push_back(i);
if(i-k+1>=0){
res[i-k+1]=A[deq.front()];
if(deq.front()==i-k+1)deq.pop_front();
}
}
return res;
}
❌ヒストグラムの最大長方形
❌写像12相
❌ポリアの数え上げ定理
Longest Increase Subsequence
Longest Common Subsequence
❌編集距離
巡回セールスマン問題
❌燃やす埋める問題
❌牛ゲー
❌最長回文
PAST
第1回アルゴリズム検定
A - 2倍チェック
各文字数字であるかをチェックする.数字以外が一つでもあったらエラー.
int main() {
string S;cin>>S;
bool error=false;
for(int i=0;i<3;i++){
if(S[i]<'0'||'9'<S[i])error=true;
}
if(error){
cout<<"error"<<endl;
}else{
cout<<stoi(S)*2<<endl;
}
}
B - 増減管理
問題文の通りに実装するだけ.
int main(void){
int N;cin>>N;
vector<int>A(N);
rep(i,N)cin>>A[i];
rep(i,N-1){
int d=abs(A[i]-A[i+1]);
if(A[i]==A[i+1])cout<<"stay"<<endl;
if(A[i]<A[i+1])cout<<"up "<<d<<endl;
if(A[i]>A[i+1])cout<<"down "<<d<<endl;
}
}
C - 3番目
ソートすれば楽.
int main(void){
vector<int> A(6);
rep(i,6)cin>>A[i];
sort(all(A));
cout<<A[3]<<endl;
}
D - 重複検査
添字に注意して実装する.
int main(void){
int N;cin>>N;
vector<int> A(N+1,0);
bool c=true;
int d=0;
rep(i,N){
int a;cin>>a;
A[a]++;
if(A[a]==2){
d=a;
c=false;
}
}
int n=0;
rep(i,N)if(A[i+1]==0)n=i+1;
if(c){
cout<<"Correct"<<endl;
}else{
cout<<d<<" "<<n<<endl;
}
}
E - SNSのログ
問題文の通りに丁寧に実装する. コードが汚い.
int main(void){
int N;cin>>N;
int Q;cin>>Q;
vector<vector<bool>> F(N,vector<bool>(N,false));
rep(i,Q){
vector<vector<bool>> F2(N,vector<bool>(N,false));
int t;cin>>t;
if(t==1){
int a,b;cin>>a>>b;
a--;b--;
F2[a][b]=true;
}else if(t==2){
int a;cin>>a;a--;
for(int k=0;k<N;k++){
if(F[k][a]){
F2[a][k]=true;
}
}
}else if(t==3){
int a;cin>>a;a--;
for(int k=0;k<N;k++){
if(F[a][k]){
for(int l=0;l<N;l++){
if(F[k][l]&&l!=a){
F2[a][l]=true;
}
}
}
}
}
for(int i=0;i<N;i++){
for(int k=0;k<N;k++){
F[i][k]=F[i][k]||F2[i][k];
}
}
}
for(int i=0;i<N;i++){
for(int k=0;k<N;k++){
cout<<(F[i][k]?"Y":"N");
}
cout<<endl;
}
}
F - DoubleCamelCase Sort
問題文の通りに実装する.
int main(void){
string S;cin>>S;
vector<pair<string,string>> T(0);
string tmp="";
tmp.push_back(S[0]);
for(int i=1;i<S.size();i++){
tmp.push_back(S[i]);
if('A'<=S[i]&&S[i]<='Z'){
string tmp2="";
for(char&c:tmp){
if('A'<=c&&c<='Z')tmp2.push_back(c-'A'+'a');
else tmp2.push_back(c);
}
T.push_back({tmp2,tmp});
i++;
tmp="";
tmp.push_back(S[i]);
}
}
sort(all(T));
for(auto&t:T)cout<<t.second;
cout<<endl;
}
G - 組分け
$3^10$があまり大きくないので,$3^10$通り全探索する.答えがマイナスになるパターンがあるので注意.
vector<vector<int>> permutation(int N,int U,bool h=false){
//0,1,2,3,...,N-1のN個からU個選ぶ順列
vector<vector<int>> A(0);
auto fun = [&h,&A,&N,&U](auto &fun,vector<int> &B)->void{
if((int)B.size()==U){
auto C=B;
do{
A.push_back(C);
}while(next_permutation(C.begin(),C.end()));
return;
}
int s = (h?0:-1);
if(!B.empty())s = B.back();
for(int x=s+(h?0:1);x<N;x++){
B.push_back(x);
fun(fun,B);
B.pop_back();
}
};
vector<int> C={};
fun(fun,C);
return A;
}
int main(void){
int N;cin>>N;
vector<vector<long>> A(N,vector<long>(N));
for(int b=0;b<N;b++){
for(int a=b+1;a<N;a++){
cin>>A[a][b];
}
}
auto B = permutation(3,N,true);
long ans = -INFl;
for(auto &C:B){
long s = 0;
for(int b=0;b<N;b++){
for(int a=b+1;a<N;a++){
if(C[a]==C[b]){
s += A[a][b];
}
}
}
chmax(ans,s);
}
cout<<ans<<endl;
}
H - まとめ売り
全種類販売をするとき,1番カードの残りが少ないものよりも$a$が大きければ販売することができる. そうでなければ販売できない.よって,常に1番カードの残りが少ないものを記録しておけば良い.
int main(void){
long N;cin>>N;
vector<long>C(N);cin>>C;
int Q;cin>>Q;
long cnt = 0;
long odd = 0,sum = 0;
long minOdd = INFl,minEven = INFl;
rep(i,N){
if(i%2==1){//偶数
chmin(minEven,C[i]);
}else{
chmin(minOdd,C[i]);
}
}
rep(i,Q){
int t;cin>>t;
if(t==1){
long x,a;cin>>x>>a;
x--;
if(x%2==0){//奇数
if(C[x]-odd-sum-a>=0){
C[x]-=a;
cnt+=a;
chmin(minOdd,C[x]);
}
}else{
if(C[x]-sum-a>=0){
C[x]-=a;
cnt+=a;
chmin(minEven,C[x]);
}
}
}else if(t==2){
long a;cin>>a;
if(minOdd-odd-sum-a>=0){
odd+=a;
cnt+=a*((N+1)/2);
}
}else if(t==3){
long a;cin>>a;
if(minOdd-odd-sum-a>=0 && minEven-sum-a>=0){
sum+=a;
cnt+=a*N;
}
}
}
cout<<cnt<<endl;
}
I - 部品調達
bitDPの問題.
int main(void){
int N,M;cin>>N>>M;
vector<int> S(M,0);
vector<long> C(M);
rep(i,M){
string s;cin>>s;
for(auto&c:s){
S[i]<<=1;
S[i]|=(c=='Y');
}
cin>>C[i];
}
vector<vector<long>> dp(M+1,vector<long>(1<<N,INFl));
//dp[a][b] := a番目まで集合bを得るコストの最小値
dp[0][0] = 0;
for(int a=0;a<M;a++){
for(int b=0;b<(1<<N);b++){
chmin(dp[a+1][b],dp[a][b]);
chmin(dp[a+1][b|S[a]],dp[a][b]+C[a]);
}
}
auto ans = dp[M][(1<<N)-1];
if(ans==INFl)ans=-1;
cout<<ans<<endl;
}
J - 地ならし
一見面倒な探索問題だが,$O((HW)^3)$でも間に合うことに甘えて愚直実装をする.
int main(void){
int H,W;cin>>H>>W;
vector<vector<long>>A(H+2,vector<long>(W+2,INFl));
rep(i,H){
rep(j,W){
cin>>A[i+1][j+1];
}
}
vector<vector<long>> BL(H+2,vector<long>(W+2,INFl));
vector<vector<long>> BR(H+2,vector<long>(W+2,INFl));
vector<vector<long>> UR(H+2,vector<long>(W+2,INFl));
vector<int> dy={1,-1,0,0};
vector<int> dx={0,0,1,-1};
BL[H][1]=BR[H][W]=UR[1][W]=0;
rep(_,H*W){
rep1(i,H){
rep1(j,W){
rep(k,4){
chmin(BL[i][j],BL[i+dy[k]][j+dx[k]]+A[i][j]);
chmin(BR[i][j],BR[i+dy[k]][j+dx[k]]+A[i][j]);
chmin(UR[i][j],UR[i+dy[k]][j+dx[k]]+A[i][j]);
}
}
}
}
long ans = INFl;
rep1(i,H){
rep1(j,W){
chmin(ans,BL[i][j]+BR[i][j]+UR[i][j]-2*A[i][j]);
}
}
cout<<ans<<endl;
}