競技プログラミングのメモ
基本的に蟻本(Amazonのリンク)に書かれているアルゴリズムの実装や関連問題(主にAtCoder)をメモしている。
ここでは主にPythonでメモをする
- AtCoder
- my account : https://atcoder.jp/users/Kevinrobot34
- AtCoder Problems
- 似た難易度の問題を探すのに役立つ。便利。
- AtCoder Scores
- AtCoder Tags
- 特定ジャンルの問題を探したいときに参考になる。
- AtCoder Problems
- 過去の自分の解答 : https://github.com/Kevinrobot34/atcoder
- YouTube - AtCoder Live
- コンペの後、問題解説生放送をやってる。
- 1.5倍速くらいで見ると、コスパよく勉強になる。
- my account : https://atcoder.jp/users/Kevinrobot34
- Codeforces
- my account : https://codeforces.com/profile/Kevinrobot34
- AOJ
Tampermonkeyを使うと、いろんな便利機能を追加できる
- ac-predictor
- 順位表にパフォーマンス・レート変化を追記してくれるやつ
- AtCoderPerformanceColorizer
- 成績表ページでパフォーマンスとレートに色をつけてくれるやつ
- AtCoder Submission Status
- SubmissionページでテストケースのAC/WAの統計情報を表示してくれるやつ
基本的に競技プログラミングは標準入力から特定のフォーマットの入力を受け取り、 標準出力 から特定のフォーマットに従って出力をする。
標準入力からの入力の受け取りには、組み込み関数のinput()を使う。
input([prompt]) (略)この関数は入力から 1 行を読み込み、文字列に変換して (末尾の改行を除いて) 返します
# 文字列
s = input()
# 整数1つ
n = int(input())
# 整数複数
a, b = map(int, input().split())
# 整数をリストで
x = list(map(int, input().split()))
# m行の入力
m = int(input())
table = [list(map(int, input().split())) for _ in range(m)]多くの行から読み込みが必要な場合、input()は遅いらしい。そのような時には以下のように代わりにsys.stdin.readlineを使うようにしておくと早くなる。
import sys
input = sys.stdin.readline標準出力は当然print()
- 1行に複数の数を空白区切で出力する時、
ans=[1, 2, 3, 4, 5]として、としても良いが、print(' '.join([str(ansi) for ansi in ans]))
の方がスマートな気がする。print(*ans)
毎回テストデータを手入力していると面倒。適宜リダイレクトすると良い。
test.txtというファイルにテストデータを保存しておいて、
$ python hoge.py < test.txt
とすると、test.txtの内容を標準入力として入力しファイルを実行することができる。
pythonのstringはimmutableだぞっ。 https://qiita.com/Amtkxa/items/a03dabe050d8c648f098
print(ord('a')) # 97
print(chr(98)) # bprint(int('123'))
print(str(123))https://docs.python.org/ja/3.7/howto/sorting.html
pythonのソートはsorted(a)とa.sort()がある。
共通点は、
- ともにデフォルトでは昇順ソート。
reverse=Trueとパラメーターすると降順になる。 keyパラメーターにlambda式などを入れると、比較の仕方を変えられる。
で、違いは
sorted(a)は新たにソートされたリストを返すが、a.sort()はaをインプレースにソート済みのものに変更しNoneを返す。a.sort()はリストにのみ定義されている。
a = [5, 2, 3, 1, 4]
print(sorted(a)) # [1, 2, 3, 4, 5]
print(a) # [5, 2, 3, 1, 4]
a.sort() # return None
print(a) # [1, 2, 3, 4, 5]
b = [5, 2, 3, 1, 4]
print(sorted(a, reverse=True)) # [5, 4, 3, 2, 1]double-ended queueのことで、stackとqueueを一般化したもの。
発音は「デック」らしい。内部的には双方向連結リストとして実装されているらしい(要出典)。
https://docs.python.org/ja/3/library/collections.html#collections.deque
append(i), appendleft(i), pop(), popleft(), が
-
list オブジェクトでも(dequeと)同様の操作を実現できますが、これは高速な固定長の操作に特化されており、内部のデータ表現形式のサイズと位置を両方変えるような pop(0) や insert(0, v) などの操作ではメモリ移動のために O(n) のコストを必要とします。
- listでもlast要素のpop(つまりただの
.pop())であれば$O(1)$ 。https://wiki.python.org/moin/TimeComplexity
from collections import deque
dq = deque([5, 6, 7, 8])
dq.pop() # 8
len(dq) # 3
dq # deque([5, 6, 7])
dq.appendleft(4)
dq # deque([4, 5, 6, 7])
# 一応添字のアクセスもできる
dq[1] # 5問題
LIFO(Last In First Out)なコンテナ。 DFSの実装などに使える。再帰関数でも同様な動作。
stackはない。dequeをよしなに使うとよい。
listのappend(hoge)とpop()を使うことでも実装可能(参考:リストをスタックとして使う)。
FIFO(First In First Out)なコンテナ。待ち行列とも。 BFSの実装などに使える。
queueはない(queue.Queueは並行実行のためのmoduleで、純粋なデータ型ではない)。dequeをよしなに使うのが良さそう(参考:リストをキューとして使う)。
優先度付きキュー。挿入された順番通りにpopするのではなく(FIFOではなく)、優先度の高い要素から先に取り出すようになっているキュー。
二分ヒープを用いて実現している。
https://docs.python.org/ja/3/library/heapq.html
(実装は https://github.com/python/cpython/blob/master/Lib/heapq.py )
pythonのheapは二分min-heapなことに注意。つまり、
heap[k] <= heap[2*k+1]かつheap[k] <= heap[2*k+2]heap[0]が最小値
from heapq import heappush, heappop
h = []
heappush(h, (5, 'write code'))
heappush(h, (7, 'release product'))
heappush(h, (1, 'write spec'))
heappush(h, (3, 'create tests'))
print(h)
# [(1, 'write spec'), (3, 'create tests'), (5, 'write code'), (7, 'release product')]
heappop(h)
# (1, 'write spec')
print(h)
# [(3, 'create tests'), (7, 'release product'), (5, 'write code')]問題
- ABC062 D - 3N Numbers (500点)
- ABC123 D - Cake 123 (400点)
- priority_queueの使い方・挙動を理解するのに良い問題。別解も多くて勉強になる。
- ABC137 D - Summer Vacation (400点)
- ARC028 B - 特別賞
C++のset/mapに相当するもの。
Pythonにそんなものはない。対処法はいくつかある。
-
Treapなどを自分で実装する
-
BIT(+ 座標圧縮)で代用する
-
priority_queueを2本使う(参考:【Python】平衡二分木が必要な時に代わりに何とかするテク【競プロ】)
class PseudoSet(): def __init__(self): self.s = [] # set self.e = [] # erase candidate def insert(self, x): heappush(self.s, x) def erase(self, x): heappush(self.e, x) def get_min(self): while self.e and self.e[0] == self.s[0]: _ = heappop(self.s) _ = heappop(self.e) return self.s[0] if len(self.s) > 0 else None
- set: https://docs.python.org/ja/3/library/stdtypes.html#set-types-set-frozenset
- dict: https://docs.python.org/ja/3/library/stdtypes.html#mapping-types-dict
-
key in dictと書くと早い。key in dict.keys()とかやってると$O(N)$ になるので注意。
-
競プロでは文字通りの全列挙で解ける問題もしばしば出題されるので、DFS・BFS・bit使って全列挙などパッと書けるようになるのが大事。
また完全な意味での全列挙でなくても、「ある変数
問題
- ABC029 C - Brute-force Attack
- ABC031 D - 語呂合わせ
- 3bitの全探索
- ABC045 C - たくさんの数式 / Many Formulas (300点)
- ABC062 C - Chocolate Bar (400点)
- ABC080 C - Shopping Street (300点)
- ABC099 C - Strange Bank (300点)
- ABC099 D - Good Grid (400点)
- ABC107 C - Candles (300点)
- ABC112 C - Pyramid (300点)
- ABC128 D - equeue (400点)
- ABC144 C - Walk on Multiplication Table (300点)
- ABC145 C - Average Length (300点)
- ABC165 C - Many Requirements (300点)
- ARC034 B - 方程式
- JSUTC202004 C - Numbering Blocks (300点)
- エイジング2020 C - XYZ Triplets (300点)
- bit全探索
実装方法
- 再帰関数
- stack
問題
- ABC054 C - One-stroke Path (300点)
- グラフをdfsで全探索するいい練習問題
- ABC119 C - Synthetic Kadomatsu (300点)
- ABC114 C - 755 (300点)
- ABC126 D - Even Relation (400点)
実装方法
- queue
問題
- ABC025 C - 双子と○×ゲーム
- ゲーム木の全探索
- ABC007 C - 幅優先探索
- 典型的な幅優先探索の問題
- ABC088 D - Grid Repainting (400点)
- AGC033 A - Darker and Darker (300点)
- ABC151 D - Maze Master (400点)
- ABC168 D - .. (Double Dots) (400点)
二分探索アルゴリズムを一般化 〜 めぐる式二分探索法のススメ 〜
lower_boundやbisect_leftを使う
C++
https://cpprefjp.github.io/reference/algorithm/lower_bound.html
https://cpprefjp.github.io/reference/algorithm/upper_bound.html
python
https://docs.python.org/ja/3/library/bisect.html
bisect.bisect_left(a, x)- 昇順ソート済みlist
aの中で、a[index] >= xという条件を満たす最小のindexを返す
- 昇順ソート済みlist
bisect.bisect_right(a, x)- 昇順ソート済みlist
aの中で、a[index] > xという条件を満たす最小のindexを返す bisect.bisectのalias
- 昇順ソート済みlist
from bisect import bisect_left, bisect_right
# 0 1 2 3 4 5 6 7 8 9
a = [1,1,1,3,3,3,3,4,4,4]
bisect_left(a, 0), bisect_right(a, 0) # 0, 0
bisect_left(a, 1), bisect_right(a, 1) # 0, 3
bisect_left(a, 2), bisect_right(a, 2) # 3, 3
bisect_left(a, 3), bisect_right(a, 3) # 3, 7
bisect_left(a, 4), bisect_right(a, 4) # 7, 10
bisect_left(a, 5), bisect_right(a, 5) # 10, 10問題
- lower_bound / bisect_leftなど使ってソート済み配列を二分探索するタイプの問題
https://betrue12.hateblo.jp/entry/2019/05/11/013403
「答えを決め打つ」タイプの二分探索で問題を解くために必要な条件は
- 「ある値
xに対して、ある条件を満たすことができるか」という判定問題が解きやすい - 上記の判定問題の答えに単調性があるか(Yes/Noの境界が一つか)
いくつか典型のパターンがある
- 「〜〜を満たす最大(最小値)値を求めよ」
- 「〜〜の最大値の最小化(最小値の最大化)」
- 「〜〜の最大値をX以下にできるか」「任意の〜〜をX以下にできるか」などと言い換えて、二分探索に持ち込む
- 「平均の最大化」
- 「数列
{x[i]}の平均値がa」は「数列{x[i]-a}の総和が0」
- 「数列
- 「K番目の要素の値」
- まず「K番目の要素の値がX以下である」という判定問題を考えると、
Xを二分探索可能。- この判定問題はほぼ自明に単調生がある。
- さらに以下のように言い換えが可能なので、解きやすい。
- 「K番目の要素の値がX以下である」と「X以下の要素がK個以上ある」は同値
- 「X以下の要素がK個以上ある」という判定問題を解く
- まず「K番目の要素の値がX以下である」という判定問題を考えると、
- 「方程式の解を一つだけ求めよ」
- 条件(True/False)の切り替わる境界(方程式の解)が複数あっても、 それを一つ見つけるだけ で良いなら二分探索が使える。
- こういう方程式の数値的な解析は「二分法」というっぽい?
def check(x):
# xが条件を満たすか判定する関数
pass
lb = -1 # False
ub = hoge # True
while ub - lb > 1:
mid = (ub + lb) // 2
if check(mid):
ub = mid
else:
lb = mid
# lbがFalseの最大、ubがTrueの最小問題
- bisect_leftとほぼ同じだけど自分で書かないといけない問題
- 「〜〜を満たす最大(最小)値を求めよ」的な問題
- 「〜〜の最大値の最小化(最小値の最大化)」
- 「平均の最大化」
- 「K番目の要素の値」
- ABC107 D - Median of Medians (700点)
- ARC037 C - 億マス計算
-
ABC155 D - Pairs (400点)
- ARC037 Cの難しいVer
- 地味に計算が重いので、尺取と組み合わせて高速化が必要
- 「方程式の解を一つだけ見つける問題」
- 幾何との組み合わせ
-
ABC151 F - Enclose All (600点)
- 最小包含円の半径を求める問題だが、「求める半径が$R$以下である」という判定問題に落とし込むのが結構難しい
-
ABC157 F - Yakiniku Optimization Problem (600点)
- 「時間
$t$ 経った後、肉が$K$ 枚以上焼けてることはありあえるか」という判定問題に落とし込めばOK - ABC151FもABC157Fも、「二円の交点を列挙しておけばそれが解の候補となる」ことがポイント
- 「時間
-
ABC151 F - Enclose All (600点)
- ABC018 D - バレンタインデー
- ABC032 D - ナップサック問題
- 重さと価値に制限のないナップザック問題の場合、DPはできないが、荷物が少なければ半分全列挙で解ける
- ARC017 C - 無駄なものが嫌いな人
- AGC026 C - String Coloring (600点)
- ハッシュテーブル(Pythonのset/dict)を上手に使う
- 東京海上日動コン2020 D - Knapsack Queries on a tree (700点)
- 木の上で、半分全列挙を使ったナップザック問題を解く問題
- CodeThanksFestival2017 G - Mixture Drug (600点)
AtCoder 版!蟻本 (初級編) : 2-2 猪突猛進! "貪欲法"
問題
- ABC011 C - 123引き算
- ABC083 C - Multiple Gift (300点)
- ABC091 C - 2D Plane 2N Points (400点)
- ABC141 D - Powerful Discount Tickets (400点)
- ABC149 D - Prediction and Restriction (400点)
- ABC155 E - Payment (500点)
- こういう貪欲法苦手。DPでもできるので、それも要確認。
- ABC167 F - Bracket Sequencing (600点)
- ARC053 C - 魔法使い高橋君
- 良い問題
- 解説がわかりやすい
- エイジング2020 E - Camel Train (500点)
- 難しい
- キーエンス2020 B - Robot Arms (200点)
- M-SOLUTIONS 2020 D - Road to Millionaire (400点)
数列$$a_0, a_1, ..., a_{N-1}$$がある時に、$$\sum_{i=l}^r a_i$$を計算する問題を考える。
ナイーブに足し算をすると$$O(N)$$かかりうる。
しかしあらかじめ$$b_0 = 0, ~~ b_{n+1} = \sum_{j=0}^{n} a_j = b_n + a_n$$という数列を用意しておくと、
pythonicに書くと、sum(a[l:r]) が b[r] - b[l] で計算できると言うこと。
そう考えると、b[r] = sum(a[:r]) と分かりやすくて良い。
#id 0 1 2 3 4 5 6 7 8 9
a = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
b = [0] * (len(a) + 1)
for i in range(len(a)):
b[i+1] = b[i] + a[i]
print(b[:7]) # [0, 1, 4, 9, 16, 25, 36]
# sum(a[3:6])
print(b[6] - b[3]) # 27二次元累積和
for i in range(n):
for j in range(n):
ds[i + 1][j + 1] = ds[i + 1][j] + ds[i][j + 1] - ds[i][j] + d[i][j]問題
- ABC084 D - 2017-like Number (400点)
- ABC098 C - Attention (300点)
- ABC106 D - AtCoder Express 2 (400点)
- ABC122 C - GeT AC (300点)
- ABC146 E - Rem of Sum is Num (500点)
- 累積和的な見方をして、条件の式を変形すると見えてくるものがある
- 二次元累積和
- しゃくとり法 (尺取り法) の解説と、それを用いる問題のまとめ
- えびちゃんさんのツイート
-
尺取り,できるだけ区間を長くしたいときは左端をループ変数にして,短くしたいときは右端をループ変数にしてる気がする
for (固定したい方) { while (条件がいい感じ) ++固定されない方; }
-
ある長さNの配列a[i]を考える。
任意のiに対し、条件を満たすindexの範囲([0, j)や[j, N))の境界jを求めたい。
もしjがiに対して単調な振る舞いをする場合、これを
right = 1
for left in range(n):
while right < n and (some conditions):
# some process
right += 1
# Segment [left, right) satisfies conditions
# someprocess典型的な例
- 正数からなる数列において、総和がK以上となっている連続する部分列の個数
- 任意の
iに対して初めてsum(a[i:j]) >= Kとなるjを知りたい
- 任意の
- 正数からなる配列において、2つの要素の積がK以上となるペアの数
- 昇順ソート済みとする
- 任意の
iに対して、a[j] >= K / a[i]となるjを知りたい
- ある数列において、狭義単調増加となっている連続する部分列の個数
- ある数列において重複する項がないような連続する部分列の列挙
- スライド最小値
問題
- ABC017 D - サプリメント
- しゃくとり法とDPの組み合わせ、難しい
- ABC032 C - 列
- 典型的なしゃくとり法
- ABC038 C - 単調増加
- 典型的なしゃくとり法
- ABC124 D - Handstand (400点)
- ABC130 D - Enough Array (500点)
- 典型的なしゃくとり法
- ABC098 D - Xor Sum 2 (500点)
- ABC102 D - Equal Cut (600点)
- ABC139 F - Engines (600点)
- ABC153 F - Silver Fox vs Monster (600点)
- ABC155 D - Pairs (400点)
- ARC022 B - 細長いお菓子
- 典型的なしゃくとり法
https://imoz.jp/algorithms/imos_method.html
問題
- ABC014 C - AtColor
- ABC035 C - オセロ
- ABC080 D - Recording (400点)
- ABC153 F - Silver Fox vs Monster (600点)
- ARC045 B - ドキドキデート大作戦高橋君
def compress_coordinate(x: list, key=None, reverse=False):
zipped = {}
unzipped = {}
for i, xi in enumerate(sorted(set(x), key=None, reverse=reverse)):
zipped[xi] = i
unzipped[i] = xi
return zipped, unzipped問題
- ARC075 E - Meaningful Mean (600点)
- 大事なのは式変形・累積和・BITあたりだが、BITを使うために座標圧縮が必要になる
- ARC008 D - タコヤキオイシクナール
- メインテーマはセグツリーだが、座標圧縮の必要性も感じられる問題
- ABC036 C - 座圧
- ABC113 C - ID (300点)
- ABC168 F - . (Single Dot) (600点)
- 二次元グリッドをそのままでは扱えないので座標圧縮する
繰り返し行われる処理に関して事前に計算をしていくことで高速化することも大事。 累積和・Combinationの事前計算などもこのようなニュアンスでの高速化。
問題
- ABC005 D - おいしいたこ焼きの焼き方
- ABC159 D - Banned K (400点)
- Panasonic2020 E - Three Substrings (500点)
- 愚直にやると
$O(N^2)$ の比較が必要そうだけど、適切に式変形して整理すると必要な前処理が分かるやつ-
$f(i, j) = g(i, j) \Leftrightarrow h(i)=h(j)$ 的な -
ABC146 E - Rem of Sum is Num (500点)
$(S_j - S_i) \% K = j - i ~(j>i) \Leftrightarrow (S_j - j)\%K = (S_i - i)\%K ~(0<j-i<K)$
-
ABC166 E - This Message Will Self-Destruct in 5s (500点)
$A_j + A_i = \left|j-i \right| (i<j) \Leftrightarrow A_i + i = j - A_j$
-
ARC075 E - Meaningful Mean (600点)
$\frac{S_j - S_i}{j-i} \geq K ~(j>i) ~\Leftrightarrow~ S_j - Kj \geq S_i -Ki ~(j>i)$
-
うまく言えん... 問題
問題
問題
python
- 組み込み関数 pow(base, exp[, mod])
- 多分繰り返し二乗法で実装されてる[要検証]
問題
- ABC129 F - Takahashi's Basics in Education and Learning (600点)
- 行列累乗を繰り返し二乗法で高速化
- ABC156 D - Bouquet (400点)
- ABC167 D - Teleporter (400点)
- ARC020 C - A mod B Problem
- 一般の整数でのMODが無理なので、ダブリングして計算する的なやつ
- 本質的にダブリングが必要と感じる問題
https://www.hamayanhamayan.com/entry/2017/08/21/102212 「条件を満たす(数列|グラフ|操作列|その他)を一つ構成せよ」というタイプの問題。 難しい。
- 条件によく注目する
- まず特定の条件で解を構築してみる
- 小さい具体例で構成してみる
といったことをして、解の構成方法の糸口を探すしかない(?)。
問題
- ABC068 D - Decrease (Contestant ver.) (600点)
- ABC069 D - Grid Coloring (400点)
- ABC081 D - Non-decreasing (600点)
- ABC092 D - Grid Components(500点)
- ABC108 D - All Your Paths are Different Lengths (700点)
- ABC135 E - Golf (500点)
- ABC165 E - Rotation Matching (500点)
- ABC166 F - Three Variables Game (600点)
- AGC038 A - 01 Matrix (300点)
- AGC041 C - Domino Quality (900点)
- 日立コン2020 C - ThREE (600点)
- 木が二部グラフと捉えられることに着目して考えると良い
- diverta 2019 procon2 C - Successive Subtraction (500点)
グループ分けを管理するためのデータ構造。以下の操作が「効率的」に行える。
- 同じグループかどうかの確認
- 2つのグループの併合
一番ナイーブな実装は以下。
struct UnionFind {
vector<int> par;
UnionFind(int N) : par(N) {
for (int i = 0; i < N; i++) par[i] = i;
}
int find(int x) {
if (par[x] != x) par[x] = find(par[x]); // contraction
return par[x];
}
void unite(int x, int y) {
x = find(x);
y = find(y);
par[x] = y;
}
bool same(int x, int y) { return find(x) == find(y); }
};
UnionFind uf(5); // uf.par [0, 1, 2, 3, 4]
uf.unite(0, 1) // uf.par [1, 1, 2, 3, 4]
uf.unite(1, 2) // uf.par [1, 2, 2, 3, 4]
uf.find(0) // uf.par [2, 2, 2, 3, 4]class UnionFind():
def __init__(self, n):
self.par = [i for i in range(n)]
def find(self, x):
if self.par[x] != x:
self.par[x] = self.find(self.par[x]) # contraction
return self.par[x]
def unite(self, x, y):
x = self.find(x)
y = self.find(y)
self.par[x] = y
def same(self, x, y):
return self.find(x) == self.find(y)
uf = UnionFind(5) # uf.par [0, 1, 2, 3, 4]
uf.unite(0, 1) # uf.par [1, 1, 2, 3, 4]
uf.unite(1, 2) # uf.par [1, 2, 2, 3, 4]
uf.find(0) # uf.par [2, 2, 2, 3, 4]更に上手に実装すると、
- rank of union (蟻本はこっち)
- size of union (今回はこっち)
を持つこともできる。(参考 : http://drken1215.hatenablog.com/entry/2019/03/03/224600)
ポイントとしては、parという配列を
- 全て-1で初期化。
- rootのnodeについては、
-(size of union)を保持する。(逆に負の数であればroot。) - root以外のnodeについては、parentのidを保持する。(逆に正の数であればleaf。)
- (size of union)に簡単にアクセスできるようになったので、merge techniqueとして、 サイズが大きなものに小さなものを結合する ようにする。
- データ構造をマージする一般的なテク
大きさに気をつけて小さい方を大きい方にくっつけるという考え方を応用することで,色々な普通のデータ構造にマージ機能を追加することができます.
- データ構造をマージする一般的なテク
という形で実装すること。
class UnionFind():
def __init__(self, n):
self.par = [-1] * n
def root(self, x):
if self.par[x] < 0:
return x
else:
self.par[x] = self.root(self.par[x]) # contraction
return self.par[x]
def unite(self, x, y):
x, y = self.root(x), self.root(y)
if x != y:
if self.par[x] > self.par[y]: # merge technique
x, y = y, x
self.par[x] += self.par[y]
self.par[y] = x
def same(self, x, y):
return self.root(x) == self.root(y)
def size(self, x):
return -self.par[self.root(x)]問題
-
- UnionFindのちょっとした応用問題。Pythonだと際どい。
-
ABC168 F - . (Single Dot) (600点)
- 二次元グリッドをそのままでは扱えないので座標圧縮する
- セル同士が連結かどうかの判定にUFを使う
-
- Editorialはdijkstra風なやつだが、頂点が大きい方から見てUnionFind使うことでも解ける
長さ
- ある区間全体に対する演算
- ある点の値の変更
を
具体的な実装の注意点
- 二分木を0-indexedで持つか、1-indexedで持つか。
- 0-indexedの場合、ノード
kについて- left-child :
2*k+1 - right-child :
2*k+2 - parent :
(k-1) // 2
- left-child :
- 1-indexedの場合、ノード
kについて- left-child :
2*k - right-child :
2*k+1 - parent :
k // 2
- left-child :
- 0-indexedの場合、ノード
- queryの書き方
- アリ本的な再帰を用いた実装(こっちの方がわかりやすい)
- 再帰を使わず、木を登りながら計算する実装(こっちの方がpythonでは早い)
queryを再帰で書いたアリ本的な0-indexed Segment Tree。
- 動作確認済み
class SegmentTree0():
"""
0-indexed Segment Tree
"""
def __init__(self, n_, ele_id, op_func):
self.n = 1 << (n_ - 1).bit_length() # size
self.data = [ele_id] * (2 * self.n - 1) # binary tree (0-indexed)
self.ele_id = ele_id # identity element
self.op_func = op_func # binary operation of monoid
def __getitem__(self, i):
return self.data[i + self.n - 1]
def build(self, data_init):
for i in range(len(data_init)):
self.data[i + self.n - 1] = data_init[i] # set data in leaf
for i in range(self.n - 2, -1, -1):
self.data[i] = self.op_func(self.data[2 * i + 1],
self.data[2 * i + 2])
def update(self, i, x):
# change i-th element to x (i : 0-indexed)
i += self.n - 1
self.data[i] = x
while i > 0:
i = (i - 1) // 2 # go to parenet-node
self.data[i] = self.op_func(self.data[2 * i + 1],
self.data[2 * i + 2])
def query(self, a, b):
# query for interval [a, b) (a, b : 0-indexed)
return self.query_(a, b, 0, 0, self.n)
def query_(self, a, b, k, l, r):
if r <= a or b <= l:
# [a, b) and [l, r) have no intersection
return self.ele_id
if a <= l and r <= b:
# [a, b) includes [l, r)
return self.data[k]
else:
# [a, b) and [l, r) have some overlap
child_l = self.query_(a, b, 2 * k + 1, l, (l + r) // 2)
child_r = self.query_(a, b, 2 * k + 2, (l + r) // 2, r)
return self.op_func(child_l, child_r)queryを再帰ではなくループで書いた1-indexed Segment Tree 。
- 動作確認済み
class SegmentTree1():
"""
1-indexed Segment Tree
"""
def __init__(self, n_, ele_id, op_func):
self.n = 1 << (n_ - 1).bit_length() # size
self.data = [ele_id] * (2 * self.n) # binary tree (1-indexed)
self.ele_id = ele_id # identity element
self.op_func = op_func # binary operation of monoid
def __getitem__(self, i):
return self.data[i + self.n]
def build(self, data_init):
for i in range(len(data_init)):
self.data[i + self.n] = data_init[i] # set data in leaf
for i in reversed(range(self.n)):
self.data[i] = self.op_func(self.data[2 * i], self.data[2 * i + 1])
def update(self, i, x):
# change i-th element to x (i : 0-indexed)
i += self.n
self.data[i] = x
while i > 1:
i = i >> 1 # go to parenet-node
self.data[i] = self.op_func(self.data[2 * i], self.data[2 * i + 1])
def query(self, l, r):
# query for interval [l, r) (l, r : 0-indexed)
l += self.n
r += self.n
ret = self.ele_id
while l < r:
if l & 1: # right child
ret = self.op_func(ret, self.data[l])
l += 1
if r & 1: # right child
r -= 1
ret = self.op_func(ret, self.data[r])
# go to parent-nodes
l = l >> 1
r = r >> 1
return retINF = (1 << 31) - 1
st_rmq = SegmentTree0(n, INF, lambda a, b: min(a, b))ある範囲の最小値の値とそのindexも必要なqueryに答えなければならない場合、
ただのRMQを改良し、セグ木に(値, index)を持たせるようにすれば良い。
operation_funcも最初の要素の大小を比較するように明記すればOK。
INF = (10**10, -1)
operation_func = lambda a, b: a if a[0] < b[0] else b
st_rmq = SegmentTree0(n, INF, operation_func)st_rsq = SegmentTree0(n, 0, lambda a, b: a + b)GCDをユークリッドの互除法で適宜書く時、整数に対するGCDはモノイドをなす。単位元は0。
from fractions import gcd
st_gcd = SegmentTree0(n, 0, gcd)Reference
- https://www.slideshare.net/iwiwi/ss-3578491
- 秋葉さんによるデータ構造の解説
- セグメント木について by beet
- https://www.creativ.xyz/segment-tree-entrance-999/
- https://www.creativ.xyz/segment-tree-abstraction-979/
- セグツリーの抽象化について
- https://ei1333.github.io/luzhiled/snippets/structure/segment-tree.html
- セグツリーの抽象化と遅延評価について
- http://koba-e964.hatenablog.com/entry/2016/12/14/214132
- 蟻本 python セグメント木 競技プログラミング Atcoder
- https://www.onakasuitacity.com/segment-tree-fenwick-tree/
問題
- ABC038 D - プレゼント
- DPの漸化式の更新にRangeMaximumQueryを使う
- ABC125 C - GCD on Blackboard (300点)
- Range GCD Queryを解く問題としてSegmentTreeを使うこともできる。
- 第2回日経コン D - Shortest Path on a Line (600点)
- DPの漸化式更新のためにRMQする
- ABC146 F - Sugoroku (600点)
- DPの漸化式更新のためにRMQする
- ABC157 E - Simple String Queries (500点)
- BitSet + SegmentTree
- 26個のSegTree( or BIT)でRSQしてもOK
- ARC008 D - タコヤキオイシクナール
- 座標圧縮 + 関数の合成についてのsegtree
- ARC026 C - 蛍光灯
- DPの更新にSegmentTreeが必要
Fenwick Tree とも呼ばれる。数列に対し, ある要素に値を加える操作と, 区間和を求める操作をそれぞれ対数時間で行うことが出来るデータ構造。セグメント木や平衡二分探索木の機能を制限したものであるが, 実装が非常に単純で定数倍も軽いなどの利点がある。
BITはセグ木を制限したものなので、応用範囲は狭いが、実装が簡単で定数倍が軽いのが特徴らしい。 なので当然BITの問題はセグ木でも出来る。
- 前から
$m$ 項分の和の計算:$~ {\rm sum}(m) = \sum_{i=1}^{m} v_i$ - 特に
${\rm sum}(0) = 0$
- 特に
- 第
$i$ 項に加算:$~ v_i += x$
を
- 二分探索:
$~ {\rm sum}({\rm index}) \geq x$ を満たす最小のindexを見つける
も
class BIT1():
"""
Binary Indexed Tree (1-indexed)
"""
def __init__(self, n):
self.n = n
self.bit = [0] * (self.n + 1)
self.data = [0] * (self.n + 1)
def build(self, data):
pass
def add(self, idx, x):
# add x to idx-th element
# idx: 1-indexed
self.data[idx] += x
while idx <= self.n:
self.bit[idx] += x
idx += (idx & (-idx))
def sum(self, idx):
# get sum of [1, idx]
# idx: 1-indexed
s = 0
while idx:
s += self.bit[idx]
idx -= (idx & (-idx))
return s
def bisect_left(self, w):
# condition : always all element is not minus
# return minimum idx where bit.sum(idx) >= w
if w <= 0:
return 0
idx = 0 # self.bit[idx] < w
k = 1 << ((self.n).bit_length() - 1)
while k > 0:
if idx + k <= self.n and self.bit[idx + k] < w:
w -= self.bit[idx + k]
idx += k
k = k >> 1
return idx + 1
def bisect_right(self, w):
# condition : always all element is not minus
# return minimum idx where bit.sum(idx) > w
if w < 0:
return 0
idx = 0 # self.bit[idx] <= w
k = 1 << ((self.n).bit_length() - 1)
while k > 0:
if idx + k <= self.n and self.bit[idx + k] <= w:
w -= self.bit[idx + k]
idx += k
k = k >> 1
return idx + 1値の範囲(std::setライクな使い方ができる。具体的には
-
$v_i=~$ (数$i$ が集合に入っていれば1、そうでなければ0)
として、この数列にBITを適用することで、
- 集合への要素の追加・削除 (
add) - 指定された要素は何番目に小さいか (
sum) -
$x$ 番目に小さい要素は何か (bisect_left)
を全てbisect_leftが遅くなる or 実装が面倒)
Reference
- https://ei1333.github.io/luzhiled/snippets/structure/binary-indexed-tree.html
- http://hos.ac/slides/20140319_bit.pdf
- https://www.slideshare.net/hcpc_hokudai/binary-indexed-tree
- https://ikatakos.com/pot/programming_algorithm/data_structure/binary_indexed_tree
頑張るとできる
問題
- AOJ DSL_2_B Range Sum Query (RSQ)
- 転倒数(参考)関連
- 定義
- 長さ
$n$ の数列$\{ a_n \}$ について、$0 \leq i < j < n$ かつ$a_i > a_j$ なる$(i, j)$ のペア数 - BITを使いながら前から見ていき(
$j$ を0から$n$ まで動かすイメージ)、その時点での$a_j$ より大きい数を数えればいいだけ
- 長さ
-
ABC107 D - Median of Medians (700点)
- 二分探索のcheck関数のところで転倒数的なものを求める
- ARC088 E - Papple Sort (800点)
- 定義
- ABC136 F - Enclosed Points (600点)
-
ABC153 F - Silver Fox vs Monster (600点)
- 区間加算に対応する必要あり
- https://drken1215.hatenablog.com/entry/2020/01/26/234000
- ARC075 E - Meaningful Mean (600点)
- std::setライクな使い方をする問題
うまく言語化できないけど、競プロっぽい問題はたくさんあるし、慣れないと解けない。 ちょっとずつこれらのまとめもしていきたい。
問題
- その他
- AGC034 A - Kenken Race (400点)
- AGC034 B - ABC (600点)
- ABC136 D - Gathering Children (400点)
- 問題を細かく分解する、状況をよく整理して答えを書くなどが大事
- JSC2019-qual C - Cell Inversion (500点)
python関連
- python3でTLEする場合はPyPy3に変えてみると通ることもある。
- ただし再帰使って書いたDFSなどはpython3の方が早かったりもする
- python3でTLEする場合、多数行の読み込みが遅いだけのことがある。遅い
input()の代わりにsys.stdin.readlineを使おう。import sys input = sys.stdin.readline input = lambda: sys.stdin.readline().rstrip() # sys.stdin.readlineは改行文字を含んでしまうので注意
sys.stdin.buffer.readlineも最近見かける。こいつは一体...???
- pythonでtupleのlistやlistのlistをソートするのはそもそも遅い。keyにitemgetterを指定すると速くなったりする。
from operator import itemgetter x = [(1,2), (3, 4), (2, 5), (1, 0), (5, 2)] x.sort(key=itemgetter(0))
- pythonの謎の
REについて。- 再帰関数を使っているならば可能性の一つとして、
RecursionError: maximum recursion depth exceeded in comparisonがある。 - 対策は以下。
import sys sys.setrecursionlimit(10**6)
- 再帰関数を使っているならば可能性の一つとして、
- PyPy3(2.4.0)では
math.log2が使えない。 - Pythonは割り算(
/)するとdoubleの演算になって精度が64bitじゃなくなる - DPの配列の初期値にINFを入れたいときに、
INF = float("inf")を使うと遅いっぽい- 問題に応じて適宜
INF = 10**9とかにした方が良い
- 問題に応じて適宜
- PythonのListのランダムアクセス(して代入するの)はかなり遅いっぽい。
- DPなどで
と書くのであれば、
dp[j | c] = min(dp[j] + a, dp[j | c])
と、if文を書いた方が早い。if dp[j] + a < dp[j | c]: dp[j | c] = dp[j] + a
- DPなどで
Python関連
- Pythonで競技プログラミング
- pythonの各種アルゴリズムなどがまとまってる
- Pythonistaなら知らないと恥ずかしい計算量のはなし
- PythonのList, Deque, Dictの計算量の話
- Python_競技プログラミング高速化tips
- 「Pythonで競プロ」という選択について
C++関連
- Macで#include<bits/stdc++.h>を導入
- Macだとそのままではcppで
#include<bits/stdc++.h>は使えないが、自分で/usr/local/include/bits/stdc++.hを作ってしまえば使えるようになる。 - https://gist.github.com/Kevinrobot34/cd95d23d6917c30a39df854481d7468e
- Macだとそのままではcppで
- AtCoder Programming Guide for beginners (APG4b)
- C++を使った競技プログラミング入門