(数年単位で)長期間取り組むテーマとの向き合い方

最近競プロが停滞気味なので、プログラミング・統計への向き合い方について頭の整理を兼ねてまとめました。

テーマが自分にとってメインのことなため、スランプのような時期も含めて少なくとも数年かけて勉強をしたい時に気をつけることを先人の知恵をかりながら絞りました。

毎日、少なくとも関連する基礎本にふれる時間をとる

時間がある日でもない日でも最低何はやるかという下限の話。基本動作をいつでも迷うことなく、正確に、速くできる状態をつくるのが目的。100マス計算や詰将棋、レイアップを準備運動として行うみたいなイメージ。「調べればわかる」みたいな状態だとどうしても処理できる要領が小さくなるので、身体が覚えている状態を増やしていく。

何度もとき直している高校数学の参考書の中にまさに勉強に向かう姿勢についてこれだ!ということを書かれている。

基礎は初歩と混同されがちです。初歩は所詮初歩ですが、基礎をきちんと築き上げることは意外に難しいのです。初歩を終えただけで基礎を理解したと誤解してしまうと、真に深い基礎を築いた人のみが解決できる難問に出会った時に、歯が立ちません。そうなると自分には数学的才能がないのだと諦めたり、才能の不足を、問題の解き方の暗記でカバーするという絶望的な「努力の道」を自らに科すことになるのではないでしょうか。

~

では数学の基礎力をつけるにはどうしたら良いのでしょうか、答えは簡単です。「正しく書かれた本を、理解できるまで何度も何度も繰り返し読んで考えることである。」

1つの分野に取り組むのは時間のある日でも3時間まで

逆に1日の上限の話。上限も設けておくのがポイントかなと時間管理を振り返って思った。時間は個人差あるはずだが、ここでの3時間はtogglを振り返った時に1つの分野でここを越えると頭がボーッとしてくるというライン。
あまりに長時間同じことに取り組むと、疲れて変な癖がついたり大して記憶に定着しない割に一時的にはやった感、覚えた感を覚えてしまうので厄介。

デスク作業に一区切りがついたら必ず休憩と軽い運動をはさむ

上限を設けたら余った時間何をするのかという話。必ず無理をせずに休憩を取る。理由は2つあって1つはあくまで長期戦なのでコンディションを安定させて毎日継続できる状態を作っておくこと、1つは作業直後頭が燃えているのでボーッとしたり散歩したりする時間を取った方が頭が落ち着いて整理してくれる。

勉強方法各論

勉強方法各論はこの辺にあります。

aoki-shiraki21.hatenablog.com

参考

勉強方法を見直す時にいつも立ち返る本。コンパクトに入門>中級>スランプ>上級と方法がまとまっていて、見比べながら今の自分を客観視しやすい。

受験の叡智【受験戦略・勉強法の体系書】共通テスト完全対応版

受験の叡智【受験戦略・勉強法の体系書】共通テスト完全対応版

  • 作者:合格の天使
  • 発売日: 2020/09/26
  • メディア: 単行本(ソフトカバー)

受験の叡智【受験戦略・勉強法の体系書】完全版 (YELL books)

受験の叡智【受験戦略・勉強法の体系書】完全版 (YELL books)

  • 作者:合格の天使
  • 発売日: 2018/06/02
  • メディア: 単行本(ソフトカバー)

受験というハードな市場で選ばれ続けているテキストは、なんというか熱量が籠っている。序文や後書きを読むと勉強への向き合い方の参考になることが多い。

総合的研究 数学I+A (高校総合的研究)

総合的研究 数学I+A (高校総合的研究)

AtCoder初歩的な書き方、考え方(〜水色になるまで)

感想とか過去問記録とかは特になく、覚えたことと勉強方法の記録です。

茶、緑になるまではこちらから。

aoki-shiraki21.hatenablog.com

aoki-shiraki21.hatenablog.com

入力

  • 複数行入力は下にコピペしていくのではなく、リストとしてもらってアンパックが楽。
 a,b,c,x = [i_input() for i in range(4)]
  • 使わない情報がはっきりしている場合は、入力の段階で消しておけると実装上シンプルになる。
# 文字列を斜めに抽出する
c = [input()[i] for i in range(3)]

配列

  • チェックしたい値の管理は、値自体を配列に入れるのではなく、値がインデクスとなる位置にフラグをつけて管理した方が見通しがよくなる場合がある。

配列に値が入っていると、a in list やbisect(list, a)を使って検索をかけることになる。これを毎回やるのは遅いし、特に二分探索は左右どちらを入れる入れないみたいなミスの元(それも正確に書けるようにならないといけないのも事実だが)。
例えばT/Fを活用してlist[a]のような形で値が入っているかをチェックする方が楽だったりする。

条件分岐

  • すぐに全探索+ifの条件分岐に逃げない。遅い(実装的にも計算時間的にも)し、階層が深くなってミスの元。与えられた条件で最初から限定できないかを考える。(ABC107/C)

    • k個選ぶなら、k個の範囲内でループを回せないか
    • 行列の周囲にダミーの値を入れておくことでコーナーケースを防げないか

匂わせワードとアプローチ

  • 「操作」:操作をした後の結果を日本語で表現してみると、それが最適解であることが多い
  • 「最大何回」:貪欲なパターンを作っておいてそれを回数制限まで行うと最適化されることが多い

  • まずは可能性の全探索を試みる、計算量を落とす必要がある場合に時点の方針を考える

  • 3,400点の問題でコードがゴチャつきそうになったらその時点でアプローチが間違っているんじゃないかと考え直す

計算処理

  • 各桁の値の和は文字列として受け取ったものをmapで展開して総和がかんたん。
n = input()
S = sum(map(int , n))

学習方法

  • パッと思いついたアプローチで愚直に書こうとして簡単な問題の実装に時間がかかる傾向にあるので、過去問を解く時に強制的にアプローチを考える時間をとる練習方法を採用してみよう。

AtCoderの書き方、考え方(〜緑になるまで)

感想とか過去問記録とかは特になく、覚えたことと勉強方法の記録です。

茶色になるまではこちら。

aoki-shiraki21.hatenablog.com

出力

  • リストを1行ずつにして出力するには、展開して改行のセパレートをかける。空白文字なども同様。intかstrかの違いを気にせず使える(joinだと一回str()を噛ませる必要があった)ので便利。
print(*list, sep="\n")
print(*list, sep=" ")
  • 「何らかの処理をした結果で並べ替えたいけど、出力するのは元の配列」みたいなパターンは関数を作っておいてlamdaで処理を渡してあげると楽。
    def f(s):
        t = ""
        for i,c in enumerate(s):
            k = B.index(c)
            t = t+str(k)
        return int(t)
    
    for ans in sorted(A, key = lambda x:f(x)):
        print(ans)

配列

  • 配列の要素の大小比較を繰り返し行う場合は、一度ソートして二分探索、結果を累積しておくのが定石。(ABC077/C)

  • 繰り返し処理をする時に、先頭・末尾に0やINFを入れる、インデックスの数字を合わせるなど処理をしやすく調整しておく。条件分岐でかくのはミスの元。

for i,k in enumerate(t[1:], start=1):
  • 小ワザ。文字の配列の入力がめんどくさい("とか,とか)時にざっとスペースで文字を作ってsplitしてあげる方法。
dore = list("Do Re Mi Fa So La Si".split())

整数

  • a,mが互いに素の時に「abがmで割り切れるなら、bがmで割り切れる」の性質があることを理解しておく(ABC108/C)

累積

  • ある要素を除いた場合を考える場合は、先に全体を計算しておいて除くものを1つ決める考え方が定石。

mod

  • modの世界で計算をする時は指数計算にpowを使用する。

  • あまり結果をさらに処理する時はカッコをつける場所に注意する。

(3*2)//2 # => 3
(3//2)*2 # => 2

匂わせワードと定石

  • 「上下左右」→4つに分解して処理し、最後に足し合わせる

  • 「要素を除いた場合」→先に全体を求める

  • 「最適化」→損しないなら採択、損するなら削除する、のように考えて調べる範囲を限定していく

  • 「制約が段階的」→制約が厳しい方から先に処理して、後から残った簡単なものを条件に合わせる。

  • 「最大最小」→値を仮置きすることで判定問題(yes/noで答えられる問い)に持ち込めないか考える

  • 「経由点」→真ん中の要素に注目する。始点から経由点、経由点から終点と2つに分けると見通しが良くなる事がままある。

探索

  • dfsにおいて、全訪問経路をチェックしたい時は、一度訪問した時にスタンプを押して、それをdfs後にキャンセルさせれば良い。
for i in range(N):
        if l[n][i] == 1 and v[i] == 0:
            v[i] = 1
            dfs(i)
            v[i] = 0
  • 探索において全部の地点を行ったかどうかのチェックは、元の配列自体をスタンプとして使用すると実装もメモリ的にも楽。同じ処理を何回も行う場合は、deepcopyをかけておく
def dfs(y, x): # B自体が探索すべき配列であり、スタンプがわりでもある
    B[y][x] = "x"
    for a,b in t:
      if 0 <= y+a < 10 and 0 <= x+b < 10 and B[y+a][x+b] == "o":
        dfs(y+a, x+b)

座標、数列

  • 順位だけが問題の時は座標圧縮を行うと何かと便利なことがある。実装は簡単で、ソートしたセットを使って、何番目かを記憶しておく。
  A = [i_input() for i in range(N)]
  d = {}
  for i,num in enumerate(sorted(list(set(A)))):
    d[num] = i

  for i in A:
    print(d[i])

判定

  • この処理は「判定をしたい」のか、「値の操作をしたい」のかを明確に分ける。判定をしたつもりが配列のデータが変わっていたり、操作していることを忘れて次の判定に入る、みたいなのがABCのA〜Cレベルまでのミス原因としては多い。

学習方法

  • 難しい問題はだいたいどなたかが解説ブログを書いてくれているので、それで考えるプロセスをトレースする。

  • 難度自体がヒントになったり、難度を見て尻込みしちゃうと学習が深まらないので、Difficulty表示機能をオフにする。

  • 基本的には過去問を進める(他の方見る限り過去問解く量とパフォーマンスは基本比例する)。が、もともとアルゴリズムや数学の強いバックグラウンドがあるわけではないので、1日1コマは使ったアルゴリズムの整理をする時間をとる。

  • A,B問題も順番にとく。パフォーマンスが回答スピードに左右されるからというのはもちろん、アルゴリズムを活用する問題でも基本的なライブラリの使い方などでつまると思考が遅くなるので。

  • わからなかった問題は、納得できるまで安易に次の問題に行かない方が個人的には合っている。曖昧な理解のまま問題数だけたくさんやってもあんまり咀嚼できていない。

Python高速化の方法覚えたことまとめ

解き方そのものではなく、速度の問題で覚えたものをあげていきます。
厳密な根拠は少しずつ理解していければなと。

コードの速度計測に使用めも

import time

start = time.time()

process_time = time.time() - start

print(process_time)

本編

  • forよりもリスト内包表記を使用する。

  • グローバル変数よりもローカル変数化して書いた方が高速。

def main():
    """"ここに今までのコード"""

if __name__ == '__main__':
    main()
  • PyPyを使った方が基本的に同じコードでも高速。

  • inputではなくsysのreadlineを使用した方が速い。ただし、文字列のときは改行文字を消す処理を行うこと

import sys
input=sys.stdin.readline
n = input().rstrip()

小ネタ

  • 操作の視認性を高くしておく。テーマと同系色の操作を減らす。
    • コメントを気持ち明るめに
    • 選択範囲も同じく少し明るめに。
# AtCoder用のコピペ集
"""
初期操作
"""

# 立ち上げ時のコピペ内容

import math
from math import gcd,pi,sqrt
INF = float("inf")
MOD = 10**9 + 7

import sys
sys.setrecursionlimit(10**6)
import itertools
import bisect
from collections import Counter,deque
def i_input(): return int(input())
def i_map(): return map(int, input().split())
def i_list(): return list(i_map())
def i_row(N): return [i_input() for _ in range(N)]
def i_row_list(N): return [i_list() for _ in range(N)]
def s_input(): return input()
def s_map(): return input().split()
def s_list(): return list(s_map())
def s_row(N): return [s_input for _ in range(N)]
def s_row_str(N): return [s_list() for _ in range(N)]
def s_row_list(N): return [list(s_input()) for _ in range(N)]


def main():


if __name__=="__main__":
    main()

# 入力操作

## 単純な数字
n = int(input())

## 文字入力
n = input().rstrip() # readlineが改行文字含むので注意!

## 値を分割して保持
a,b = map(int, input().split())

## リストとして保持
l = list(map(int, input().split()))

"""
配列操作
"""

# 組み合わせ系

## 順列
list(itertools.permutations(list, n))

## 組み合わせ
list(itertools.combinations(list, n))

## 出現回数

l = Counter(list)
l.most_common()

## nCrのmod計算

def fur(n,r):
    p,q = 1, 1
    for i in range(r):
        p = p*(n-i)%MOD
        q = q*(i+1)%MOD
    return (p * pow(q,MOD-2,MOD)) % MOD

## 普通のnCr

def comb(n, r):
    if n-r>=0:
        return math.factorial(n) // (math.factorial(n - r) * math.factorial(r))
    else:
        return 0

"""
数値計算
"""

## 単純な素数判定
def isPrime(n):
    if n == 2:
        return True
    if n%2 == 0 or n == 1:
        return False
    m = math.floor(math.sqrt(n))+1
    for p in range(3,m,2):
        if n%p == 0:
            return False
    return True

## エラトステネスの篩
def primes(n):
    is_prime = [True] * (n + 1)
    is_prime[0], is_prime[1] = False, False
    for i in range(2, int(n**0.5) + 1):
        if not is_prime[i]:
            continue
        for j in range(i * 2, n + 1, i):
            is_prime[j] = False
    return [i for i in range(n + 1) if is_prime[i]]

## 最大公約数
gcd(a,b)

## 最小公倍数
a*b//gcd(a,b)

## 素因数分解:O(n**1/2)
def prime_decomposition(n):
    i = 2
    table = []
    while i * i <= n: # sqrt(n)で計算が済む
        while n % i == 0:
            n //= i
            table.append(i)
        i += 1
    if n > 1:
        table.append(n)
    return table

"""
配列操作
"""

## 転置行列

l_2 = [list(x) for x in zip(*l)]

参考

pythonの速度で気にするところ(高速化メモ) - nobUnagaの日記

https://juppy.hatenablog.com/entry/2019/06/14/Python_%E7%AB%B6%E6%8A%80%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E9%AB%98%E9%80%9F%E5%8C%96tips_%28Python%E3%81%A7Atcoder%E3%82%92%E3%82%84%E3%82%8B%E9%9A%9B%E3%81%AB%E5%80%8B

Python で AtCoder を遊ぶときに知ってると便利かもしれないこと - 菜

使えるようになってきたアルゴリズム覚え書き

自分の言葉でメモっておいた方が記憶に残るので、記事としてはn番煎じですがまとめていきます。

アルゴリズムまでは いかない細かいテクニックはこちらです。

aoki-shiraki21.hatenablog.com

探索

ビット全探索

n個のものからある要素を取るか、取らないかを全部列挙して探索する方法。

for分だと何回繰り返せば良いか分からない時に応用が効かないのでビット全探索か再帰関数で対応する。 O(n2)探索するので範囲が小さい時にしか使えないのは注意。

# よくある形
for i in range(1<<n): # 左シフトで2**nを作る
    cnt = 0
    for j in range(n): # 全ての選択肢について取る/取らないをチェックする
        flg = True
        if i >> j & 1: # ビットに復元して、その要素が含まれるかをチェックする
            cnt += 1
            if ~~ # 要素が含まれた時に満たすべき条件を書く
                    flg = False
                    break
            if not flg:
                break # 条件に当てはまらないときはフラグを使って二重ループを抜ける
    if flg:
  # 満たすべき条件がokだった時に最終的な処理をする

参考

bit 全探索 - けんちょんの競プロ精進記録

深さ優先探索

とにかく下に下に探索していく。下位構造が上位を決定しているようなときに便利。

1. グラフを行列で表す
2. 最初の頂点をスタックに積む
3. スタックに要素がある間、繰り返し
  - スタックのトップを訪問する(訪問時の処理をする)
  - 次の未訪問要素があればスタックに積む
  - なければ訪問した要素をスタックから削除する(終了時の処理をする)
# デックにデータを格納しながら処理。popをどちらから行うかでDFSとBFSを切り替えられるのが楽
def search(G,s):
    """
    グラフGに対し、頂点sを始点とする探索(デック)
    """
    N = len(G)
    todo = deque() # BFSならキュー、DFSならスタックとして使用
    seen = [False]*N # 発見チェック(seen:True & not in todo => 訪問済み)
    
    # 初期状態
    seen[s] = True
    todo.append(s)
    
    # todoが空になるまで探索を行う
    while todo:
        v = todo.popleft() # DFSならpop()
        for x in G[v]:
            if seen[x]: # 発見済み
                continue
            seen[x] = True
            todo.append(x)
# 再帰を使った処理。記述が簡潔、行きがけ/帰りがけの処理を書きやすい。
def dfs(G,v):
    """
    頂点vから辿れる頂点を全て訪問する(再帰)
    """
    # 訪問時の処理
    seen[v] = True
    
    # 再帰的に探索
    for next_v in G[v]:
        if seen[next_v]:
            continue
        dfs(G, next_v) # 再帰的に探索
    
    # 帰りがけの処理
        
N,M = map(int,input().split())
G = [[] for i in range(N)] # 隣接リスト
for i in range(M):
    a,b = map(int, input().split())
    a -= 1
    b -= 1
    G[a].append(b)
print(G)

seen = [False]*N
for v in range(N): # 全頂点から探索
    if seen[v]:
        continue
    dfs(G,v)

幅優先探索

デックを使って順番を管理し、先頭から取り出して処理をする。処理途中で深さが発生する場合は、デックに追加しておくことで後から処理をできるようにする。

浅いところから順番に見て行くので、根からの最短経路を求めるのに向いている。

def BFS(G, s):
    """
    グラフGに関して,頂点sからの最短路長を求める
    input: グラフG, 探索の視点s
    data: sからの距離dist配列(!=-1なら発見済み), 探索予定をキューとして格納するque
    output: sから各頂点への最短路長を表すdist配列
    """
    # 初期設定
    N = len(G)
    dist = [-1]*N
    que = deque()
    dist[s] = 0
    que.append(s)
    
    # 探索
    while que:
        v = que.popleft()
        for x in G[v]:
            if dist[x] != -1:
                continue
            dist[x] = dist[v] + 1
            que.append(x)
    return dist

N,M = map(int, input().split())
G = [[] for i in range(N)]
for i in range(M):
    a,b = map(int, input().split())
    a -= 1
    b -= 1
    G[a].append(b)

dist = BFS(G,0)

二分探索

短調増加(減少)の指標に対して中間地点をとってそれより大きいか小さいかで範囲を絞る。1回ごとに半分になるためlogスケールになり高速。
最大、最小系の問題は解を仮置きして二分探索に帰着させると計算量を抑えられる。

def is_ok(arg):
    # 条件を満たすかどうか?問題ごとに定義
    pass


def meguru_bisect(ng, ok):
    '''
    初期値のng,okを受け取り,is_okを満たす最小(最大)のokを返す
    まずis_okを定義すべし
    ng ok は  とり得る最小の値-1 とり得る最大の値+1
    最大最小が逆の場合はよしなにひっくり返す
    '''
    while (abs(ok - ng) > 1):
        mid = (ok + ng) // 2
        if is_ok(mid):
            ok = mid
        else:
            ng = mid
    return ok

計算

最大公約数

ユークリッドの互除法
xをyで割ったあまりをrとすると、x,yの最大公約数とy,rの最大公約数は同じになる(証明はリンク先より)から割り切れるまで繰りかえり処理を行うことで求める。

def gcd(x, y): # ユークリッドの互除法
    if y > x:
        y,x = x,y
    while y > 0:
        r = x%y
        x = y
        y = r
    return x

【ユークリッドの互除法】やり方&証明を解説!センター試験にも役立つ! | Studyplus(スタディプラス)

(実運用上は、mathモジュールに入っているのでそれで良いが。)

import math
math.gcd(x,y)

最小公倍数

最大公約数*最小公倍数=a*bというシンプルで綺麗な定理を使うとすぐに求められる。

a*b//bcd(a,b)

最大公約数と最小公倍数の積の性質の2通りの証明 | 高校数学の美しい物語

いもす法

Nコのデータに対するM回の処理共通範囲を求める処理をO(MN)からO(N+M)に減らす。始点と終点だけメモしておいて、最後に累積和をとる。

imos = [0]*(n+2)
    for i in range(m):
        a,b = i_map()

        imos[a] += 1
        imos[b+1] -= 1

imos = list(itertools.accumulate(imos))

素数判定

単純な素数判定

nに対してsqrt(n)まで調べれば良い。

def isPrime(n):
    if n == 2:
        return True
    if n%2 == 0 or n == 1:
        return False
    m = math.floor(math.sqrt(n))+1
    for p in range(3,m,2):
        if n%p == 0:
            return False
    return True

エラトステネスの篩

ある数までに含まれる素数を列挙する。sqrt(n)までに合成数は篩落とされるので高速。

def primes(n):
    is_prime = [True] * (n + 1)
    is_prime[0], is_prime[1] = False, False
    for i in range(2, int(n**0.5) + 1):
        if not is_prime[i]:
            continue
        for j in range(i * 2, n + 1, i):
            is_prime[j] = False
    return [i for i in range(n + 1) if is_prime[i]], is_prime

素因数分解

素数でない限り、sqrt(n)より大きい素因数が発生することはないので、O(n1/2)で十分

def prime_decomposition(n):
    i = 2
    table = []
    while i * i <= n: # sqrt(n)で計算が済む
        while n % i == 0:
            n //= i
            table.append(i)
        i += 1
    if n > 1:
        table.append(n)
    return table

データ構造

キュー

FIFOで処理をする待ち行列。順番に処理をして、またその処理結果を順番に処理すると繰り返したいときに便利。

from collections import deque
q = deque([1,2,3,4,5,6,7,8,9])
    for _ in range(k):
        x = q.popleft()
        q.append(x*10 + x0 - 1)

優先度付きキュー

要素の挿入、最大値(最小値)の抽出を繰り返したい時に使用する。
最小値しか取り出せないので、最大値を管理したい時にはマイナスをかけておいて、取り出した後マイナスをかけて復元処理をする。

import heapq

h = []
heapq.heappush(h, -i)
heapq.heappop(h)

UnionFind木

素集合データ構造を作成するクラス。経路圧縮と深さのランクづけで高速化を行なっている。

Python:Union-Find木について - あっとのTECH LOG

  class UnionFind:
        def __init__(self, n):
            self.par = [i for i in range(n+1)]
            self.rank = [0] * (n+1)
            self.size = [1] * (n+1) # 親に集約される
    
        # 検索
        def find(self, x):
            if self.par[x] == x:
                return x
            else:
                self.par[x] = self.find(self.par[x])
                return self.par[x]
    
        # 併合
        def union(self, x, y):
            x = self.find(x)
            y = self.find(y)
    
            if x == y:
                return
    
            if self.rank[x] < self.rank[y]:
                self.par[x] = y
                self.size[y] += self.size[x]
                self.size[x] = 0
            else:
                self.par[y] = x
                self.size[x] += self.size[y]
                self.size[y] = 0
                if self.rank[x] == self.rank[y]:
                    self.rank[x] += 1
    
        # 同じ集合に属するか判定
        def same(self, x, y):
            return self.find(x) == self.find(y)
    
        # すべての頂点に対して親を検索する
        def all_find(self):
            for n in range(len(self.par)):
                self.find(n)

経路

ワーシャルフロイド法

全ての頂点間の最短距離を更新しながら求める方法。ぱっと見ただの三重ループの綺麗な形になっているのがすごい。計算量はそのままO(n3)。

素人によるワーシャルフロイド法 - Qiita

for k in range(10): # 経由
        for i in range(10): # 始点
            for j in range(10): # 終点
                c[i][j] = min(c[i][j], c[i][k] + c[k][j])

スニペット

# AtCoder用ライブラリ
"""
初期操作
"""

# 立ち上げ時のコピペ内容

import math
from math import gcd,pi,sqrt
INF = float("inf")
MOD = 10**9 + 7

import sys
input = sys.stdin.readline
sys.setrecursionlimit(10**6)
import itertools
import bisect
import re
import numpy as np
from collections import Counter,deque,defaultdict
def iinput(): return int(input())
def imap(): return map(int, input().split())
def ilist(): return list(imap())
def irow(N): return [iinput() for i in range(N)]
def sinput(): return input().rstrip()
def smap(): return sinput().split()
def slist(): return list(smap())
def srow(N): return [sinput() for i in range(N)]

def main():

if __name__=="__main__":
    main()


"""
配列操作
"""

# 組み合わせ系

## 順列
list(itertools.permutations(list, n))

## 組み合わせ
list(itertools.combinations(list, n))

## 出現回数

l = Counter(list)
l.most_common()

## nCrのmod計算

def fur(n,r):
    p,q = 1, 1
    for i in range(r): # p=(n-r+1)!,q=r!を作る
        p = p*(n-i)%MOD
        q = q*(i+1)%MOD
    return (p * pow(q,MOD-2,MOD)) % MOD # qの逆元を作ってp/qを処理する

## 普通のnCr

def comb(n, r):
    if n-r>=0:
        return math.factorial(n) // (math.factorial(n - r) * math.factorial(r))
    else:
        return 0

"""
数値計算
"""

## 単純な素数判定
def isPrime(n):
    if n == 2:
        return True
    if n%2 == 0 or n == 1:
        return False
    m = math.floor(math.sqrt(n))+1
    for p in range(3,m,2):
        if n%p == 0:
            return False
    return True

## エラトステネスの篩(素数列挙)
def primes(n):
    if n <= 0:
        return []
    is_prime = [True] * (n + 1)
    is_prime[0], is_prime[1] = False, False
    for i in range(2, int(n**0.5) + 1):
        if not is_prime[i]:
            continue
        for j in range(i * 2, n + 1, i):
            is_prime[j] = False
    return [i for i in range(n + 1) if is_prime[i]]

## 最大公約数
gcd(a,b)

## 最小公倍数
a*b//gcd(a,b)

## 素因数分解(一次元配列)
def prime_decomposition(n):
    i = 2
    table = []
    while i * i <= n: # sqrt(n)で計算が済む
        while n % i == 0:
            n //= i
            table.append(i)
        i += 1
    if n > 1:
        table.append(n)
    return table

## 素因数分解(素因数、指数の二次元配列)
def prime_decomposition_2dim(n):
    arr = []
    temp = n
    for i in range(2, int(-(-n**0.5//1))+1):
        if temp%i==0:
            cnt=0
            while temp%i==0:
                cnt+=1
                temp //= i
            arr.append([i, cnt])
    if temp!=1:
        arr.append([temp, 1])
    if arr==[]:
        arr.append([n, 1])
    return arr


## 約数列挙
def make_divisors(n):
    lower_divisors , upper_divisors = [], []
    i = 1
    while i*i <= n:
        if n % i == 0:
            lower_divisors.append(i)
            if i != n // i:
                upper_divisors.append(n//i)
        i += 1
    return lower_divisors + upper_divisors[::-1]

## 公約数列挙
def common_divisors(n, m):
    n = gcd(n,m)
    lower_divisors , upper_divisors = [], []
    i = 1
    while i*i <= n:
        if n % i == 0:
            lower_divisors.append(i)
            if i != n // i:
                upper_divisors.append(n//i)
        i += 1
    return lower_divisors + upper_divisors[::-1]

## 10進数からn進数への変換
def Base_10_to_n(X, n):
    if (int(X/n)):
        return Base_10_to_n(int(X/n), n)+str(X%n)
    return str(X%n)

"""
2次元配列操作
"""

## 転置行列

l_2 = [list(x) for x in zip(*l)]

## 8方向への移動
dx=[0,1,0,-1,1,1,-1,-1]
dy=[1,0,-1,0,1,-1,1,-1]

"""
幾何
"""

### 2点を通る直線
def line(x0,y0,x1,y1): # (x0,y0),(x1,y1)を通る直線ax+by+c=0を求める
        x = (d-b)
        y = -c+a
        z = b*(c-a) - a*(d-b)
        return x,y,z
    
### 点と直線の距離
def dist(x0,y0,a,b,c): # (x0,y0)から直線ax+by+c=0までの距離を求める
    up = abs(a*x0 + b*y0 + c)
    down = math.sqrt(a**2 + b**2)
    return up/down

AtCoder初歩的な書き方、考え方(〜茶色になるまで)

AtCoder入門者です。 AtCoder Problemsを解いていて便利だなと思った表現方法やアプローチをまとめています。(Pythonです)

色変する頃には気をつけるポイントも変わっていると思うので記事分けます。

基本全ての処理にどの問題でその操作を使用したか書いているので、同じく入門者の方は問題を解くときに参照していただければ。

アルゴリズムの勉強はこちらです。

aoki-shiraki21.hatenablog.com

Input/Output

  • 事前処理

使いたい情報は先に辞書を作っておくと楽。(ABC173/B)

  • 入力、出力処理の基本 複数の値を一気に受け取って下処理するときはmapで対応させると楽。(ABC173/C)
h, w, k = map(int, input().split())

出力するときにjoinなどの操作をしたいときはstringに変換しておく。

print(" ".join(map(str,num_list)))

基本演算

  • 四則演算系

累乗は^じゃなくて**だから注意。(ABC172/A)
また、**は優先順位が強いため、累乗根の計算をするときは**(1/2)のようにカッコをつけておく。

  • マイナスへの整数除算

あまりが正の数になるように考える。ので、-5//2は-2ではなく-3になる。(ABC173/D)

-5 // 2  #=> -3
  • ある桁までの数字の抽出

10で割ったあまりが1桁目、100で割ったあまりが1~2桁目みたいな操作ができる。str型にしてスライスとかでもいいけど。(ABC168/A)

  • 変数定義

あとで元の変数を使用する可能性がある場合は、変数名を変える。(ABC168/C)
基本的に初期値として与えられるものはそのまま保持しておいた方が良いことが多いので、加工するときに別の変数名を与える。

  • n列目までの総和

単純に毎回sumをやるとO(n2)になり時間がかかりすぎる。階差数列として捉えればO(n)でいける。(ABC172/C)

A = [1,2,3,4,5]

a = [0]
for i in range(len(A)):
    a.append(sum(A[:i+1]))
print(a)

b = [0]
for i in range(len(A)):
    b.append(b[i]+A[i])
print(b)
  • 誤差

少数の計算は誤差を生むので、計算したい桁に修正して、後から戻す。(ABC169/C)

x = int(x)
y = int(y.replace(".","")) # 少数第二位まで与えられる想定。 0.01 => 1
print(x*y//100) # 計算した後に100でわることで元の少数第二位の計算に戻す
  • stringで表現された式の評価

evalを使用すればstring型で書かれた式を計算してくれる。(ARC61/C)

https://qiita.com/kyoshidajp/items/57ae371b3f5d8a84fb13

基本関数、メソッド

  • リストへの要素の追加

メソッドを使用するときに、処理と返り値に注意。insertは返り値はNoneになる。trial_num.insert(1,i)

繰り返し処理

  • 適切な深さで処理をすることで無駄に繰り返さない

繰り返しを行うときは、それがforの何番目で行う必要があるのかを確認する。一番奥で処理をすると、forのたびに毎度処理を行うことになる。(ABC162/C)

  • 奇数/偶数判定など定数倍にはステップ幅を使う

forの後にif n%2==0などで書くと遅いので、ステップでrange(0,n,2)のようにそもそも繰り返しを減らす。ifもなくなってスッキリする。

平方根とったのに、その後を繰り返したら計算量があまり変わらない。どこまでを繰り返す必要があるのかを確認する。(ABC144/C)

配列の操作

  • 配列の簡単な操作

for,if文で書くと長くなるので、メソッドで処理する。maxmin、'sum'が使えないか検討する。(ABC172/C, ABC95/C)

# aがbより大きければ差を、それ以外は0をみたいなパターン
if a>b:
   n = a-b
else:
    n = 0

# 差と0との最大値をとれば良い
n = max(a-b,0)

list.count(要素)を活用してコンパクトにかく。==0で有無の判定も可能。(ABC170/C)

ある要素の場所を特定するにはlist.index(要素)を使う。(ABC170/A)

  • 要素の有無判定

a in listの形で探索すると毎回丁寧に全部を見にくので低速。要素の重複なく保持したい、有無判定を高速にしたいみたいなケースはset型が便利。(ABC164/C)

【Python】set型の基本と、死ぬほど遅い「hoge in list」からの脱却 | ゆとって生きたい。

Pythonで"in list"から"in set"に変えただけで爆速になった件とその理由 - Qiita

5. データ構造 — Python 3.8.5 ドキュメント

  • 列ごとに分割する表記

*を使うと、配列のその他の部分を表現できて便利(ABC167/C)

c, *a = book[j]
  • 転置行列の作成

*で展開したリストをzipでまとめて処理する。行列を扱う時に、行ごとの方が繰り返しの操作がしやすいので。

l_2 = [list(x) for x in zip(*l)]
  • 配列の要素の判定

要素全てにチェックをしたいとき(よくある)、allを使って内包表記のように表すと簡単(ABC167/C)

all(lj >= x for lj in l)
  • あるインデックス以下に共通の足し算

一番上のインデックスに処理をメモっておいて、最後に累積和をとると楽。結局全部足せば良いので(ABC138/D)

  • n乗のfor文の形。

①行列の中の処理する,しない(yes,no)をそれぞれブール値の0,1と対応させる。 (ABC173/C) n行であれば、range(1 << n)
そうしておけば、あとは調査したいだけ右シフトして、1かどうかによって処理を決める。

再帰関数を用いて、n-1コの状態とnコ目に分割する。

[row][three:two+three]

  • ある値からの等距離の判定

-1,+1を両方の要素に試してみる操作が絶対値を求めるときなどに相当する。for s in [-1, +1]:(ABC170/C)

  • 逆順配列の設計

これはもう定型表現のように覚える。

l = s.copy()
l.reverse()

s[::-1]

辞書型

  • バリューが最大(最小)のキーを得たい時は結構ある。その場合はキーを指定して、getでバリューを取得してあげる。
max(dic) # 最大のキー
max(dic.values()) # 最大のバリュー
max(dic, key = dic.get) # バリュー最大のキー

Pythonで辞書の値の最大値・最小値とそのキーを取得 | note.nkmk.me

整数の処理

漸化式

  • 漸化式を求めることで入力も処理も見通しが良くなる(ABC174/C)

倍数、あまり、割り算

  • ある数kで割った時のあまりは0~k-1の範囲。k回の処理で割り切れない場合は割り切れないことを疑う(ABC174/C)

  • 誤差を避けるために整数化して比較する場合、処理が逆に辿れるのかを確認する。つまりA2 < B2だから必ずA < Bとは言えない。Bが負の場合があるため。B>0の条件があれば逆に辿れる。

  • mod10は要は1桁目を取り出す操作。(ABC161/D)

  • 半分にした数字をあつかいたい、偶数の時はそのまま半分、奇数の時はあまり分も入れて、みたいな操作をしたい時がある。(あまりなしなら//で問題なし)。その場合一旦普通に/2をして少数を繰り上げておくと偶奇どちらでも思っている処理になる。

math.ceil(a / 2)

累積和

  • 便利な性質として、1+2+3+..t =x ならば1~tの部分和によって0~xのどの整数でも作れるというものがある。大小判定系で活用できる。(ABC056/C)

よく使うモジュール、ライブラリ

itertools

イテレータの操作に使う(そのまま)

  • itertools.combinations_with_replacement(p, r)

p配列からr個の重複組み合わせを全列挙する。

  • itertools.product(list, repeat=N)

1つのイテラブルから直積を生成する。要は重複順列を作る時に使える。

itertools --- 効率的なループ実行のためのイテレータ生成関数 — Python 3.8.5 ドキュメント

bisect

  • bisect.bisect(l,a)

ソートされたlに対してaが何番目に入るかを高速で検出する。リストの中である範囲の数を知りたい時に便利。

図形

  • 三角形の成立条件は以下が直感的にわかりやすく書きやすい。
a+b>c and b+c>a and a+c>b

その他考え方、アプローチ

  • ややこしくなりそうなら先に擬似コード、docstringで流れを書く。結果その方が早い。

  • 本当に毎回全可能性を探るのではなく、結果を格納する配列を作っておいて、そこに結果を更新していくみたいなアプローチが有効な場合がある。明らかに全探索な問題以外でO(n2)以上になったら疑う(ABC166/C, ABC166/B)

  • 点ではなく点によって区切られる線で考えると?最小値を求めたい時に補集合の最大値を考えると?みたいな視点を切り替えると求めるべきことがシンプルだったりする(ABC160/C)

  • 105(10万)通りくらいなら全列挙しても時間的に問題ない。(ABC95/C)

  • 解答パターンが結局そんなに大きなければ、解答を仮に設定しちゃって条件と矛盾がないかを確認する決め打ちの方が計算量が少なく済む。(三井住友信託銀行プロコン2019/D)

  • ちょっとややこしくなってきたら紙に書いて周期性を確認する。(ABC133/C)

  • sumなどのリストを全部確認するのは毎回O(n)かかるので、前後での変化感を知るだけで良い可能性はないかを検討する(ABC171/D)

  • 倍数や約数系の問題は、先に答えのリストを有効な制約分設けておいて、そこに条件に当てはまるものだけをTrueで残していくような操作がシンプル(ABC170/D)

  • 順番のパスを考える時は、あるn番目のパスに至る方法がどう表されるかを考えると漸化式として一般化できる。だいたいn=1とかの時は求められるので、組み合わせで一般解を出せると。ポイントは「ある地点からどこに行けるか」ではなく「ある地点に到着する方法はどう説明できるか」(ABC129/C)

  • lognはほぼ無視できる。O(n)とO(nlogn)は105程度であれば定数倍くらいで考えておけば問題ない(ABC126/C)

  • 与えられた条件で全探索が難しい時に、本当にその条件なのか、別の条件を組み合わせると範囲を絞り込むことができないかを検討する。複数の全探索が走る場合は、大概最後の探索が他の探索の結果によって規定されるので1回分ループを少なくできる。(ABC166/D, ABC165/D)

  • 累積の計算、かつ条件がある場合に、条件を無視して累積の計算設計をしておいて、その中に条件分岐を入れる方が楽な場合がある。(ABC163/D)

  • 答えの桁が大きくなる時は、途中で小さくできる処理があるなら噛ませた方が良い。あまりを提出する時は特に。(ABC006/B)

  • 「ランダムに選んだ操作を繰り返す時の対象物の最終的な地点」→水を入れたり戻したりする操作としてイメージする(AGC002/B)

  • 難しい状況から簡単な状況に分解していくよりも、最初から作っていく方が簡単(ABC120/D)

Kindleか紙の本にするか。

(*最初にこの記事を書いてから1年経ち、大幅加筆しました。結論「技術書などの自分の専門領域に関しては紙派」に回帰してます。)

2020年の考え

ここ1~2年でだいぶKindleへの移行が進みましたが、まだどうしても単語帳なんかを使う時には紙だよなーみたいなところがあります。

今日本屋でどっちにしようか迷ったので、今後発生しないように改めて言葉にしておこうと思います。

基本方針はKindle

基本的には買う場合はKindleにしてます。

以下の理由からです。

  • カバンが重くなるのを避けたい、テーブルを占居されたくない(要はでかい本は持ちたくない)
  • 過去に読んだものをパッと検索したい
  • 本棚を超える量を保有したくない(すぐ溢れる)
  • 1~2年のスパンで引っ越しなどの想定がある(判断材料として結構大きい)

スタンスとしては、「紙の方が記憶に残りやすいことはデータとしても経験則的にも理解しつつ、条件を考えると今はKindle優位にしておいた方が良い」判断をしています。

逆に言えば、将来的に定住が前提になり、大きめの書斎をもつことを前提にする(家をもつならそうしたい)なら紙メインに戻す予定です。

紙で買うもの

とはいえあくまで勉強をするときは紙の方が良い派です。
紙の手触りや全体のどの辺に載っていたか、ページの折り目から感じられる達成感など、五感に働きかける力を感じます。

そこで以下の場合に限り、紙での購入をOKにするということで運用していきます。

  • 相当な回数(数十周とか)読み込み、マルっと覚えきるほど覚悟がはっきりしたもの
  • 完璧にするに値するほど説明に定評のある(版を重ねている)もの
  • 問題を解くタイプで、気づきの書き込みを本に直接するもの
  • ページ数が500Pを超えないもの(技術書や専門書はさすがに大きすぎて、紙によるデメリットが大きい)

ちなみに、マルッと覚えきる覚悟というのはこのイメージです。受験勉強で1つの参考書をボロボロになるまで使った後の無双感は、一度やったことがあれば共感してもらえるのではないかと。

f:id:aoki-shiraki21:20200712175522p:plain

センター試験の「世界史」で"満点"だった女子高生 「工夫と努力の天才」と称賛されるワケ

条件を守ると、教科書のような薄く説明のカチッとしたものや、目的に沿った問題集がメインになる想定です。

本のラインナップが、ここへ来てどんどん学生の頃に戻っていく感覚があります。

参考

"タブレットで勉強" ってほんとうに効果はあるの? "紙で勉強" と科学的に比較してみた。 - STUDY HACKER|これからの学びを考える、勉強法のハッキングメディア

電子書籍vs紙の本⋯勉強するなら紙の本の圧勝!!|院長ブログ|五本木クリニック

2021年になって

(特に自分の専門領域について)紙を優先。

昨年は基本全てkindleにして行こうと考えていましたが、自分の専門領域に関しては紙で買うべきではないか?という結論になってきました。

昨年1年いろいろな勉強方法を試す中でも、「紙の本による勉強効率の良さ」は再確認させられたものの一つです。

再確認した紙のメリット

良かったこととしてはよく言われることの再確認という感じですが

  • 身体的な感覚(紙の手触り、大体何ページ目くらいにあったかなど)と共に記憶に刷り込まれる
  • 書き込みがしやすい(タブレットでもできるとは言え、紙とペンが最も直感的)
  • 物理的に重さのあるものを買う、目に止まるということによる学習モチベのアップ(スペースを取るからこそ、あとでやるかもではなく本当に勉強するものを買うようになる)
  • 本棚に並べた時に達成感が感じられる、どの辺の領域が足りていないかを考えられる

身体的な感覚というのは、勉強効率を語る上で馬鹿にできないなと思います。

デメリットをどう考えるか

デメリットに関しては、クリティカルなものは本棚問題くらいであとは気にならないかなと考えるようになりました。

  • カバンが重くなる、出先で勉強ができない
    →このご時世そもそも自宅勉強メインになることを想定しています。また電車など隙間時間はそれ用の本を選択すればよいかなと。

  • 過去に読んだ本、本の中の文字検索の性能
    →これはメリットで書いたように指でなんとなくこの辺、この本、と覚えている感覚の方が頼りになりました。何度も同じ本を開いているとその感覚になると思います。

  • 本棚がいっぱいになる、引越しなどで身が重くなる
    →確かにこれは否定できません。が、そもそも勉強は覚えないことには意味がないのでまずは「本棚効率」よりも「学習効率」が優先で、本棚問題が発生したら十分理解した本や今の重点テーマではない本をpdf化するなど検討するがよいかなと考えるようになりました。

kindleの扱いはどうするのか

kindleも使わないというわけではありません。というか「学習効率」観点では紙に回帰したいよね、というだけなのでそれ以外は今後もkindleを使用する予定です。

逆にこれまでよりもkindle化を進めていきたいものとして「所有欲」で集めていた本を移行していきたいというものがあります。具体的には漫画、小説、一部のビジネス書など。

上記の学習書で書いた本棚に並べたときの達成感というのは副次的なもので、基本的には使う時の利便性のあるものを手元に残すことで本棚を使いたいです。なので、「持っていることが嬉しい」が動機になっているものはできるだけ手放すようにしていきたいなと。