ベスパリブ

プログラミングを主とした日記・備忘録です。ベスパ持ってないです。

PythonでUnion-Find木

Union-Find木とは

Union-FindまたはDisjoint Setとは、蟻本の言葉を借りると「グループ管理のためのデータ構造」。

・要素aと要素bが同じグループかどうか

・aが属するグループと、bが属するグループを併合(unite)する

ということをしたいときに使います。

Union-Find木(深さバージョン)の実装

蟻本に書いてあるとおりの実装。

グループの併合のとき、木の深さが深いものに浅いものを併合する。

あまり使わない。競プロでは後述のサイズバージョンばかり使用している。

class UnionFindVerDepth():
    def __init__(self, N):
        """ N個のノードのUnion-Find木を作成する """
        # 親の番号を格納する。自分が親だった場合は、自分の番号になり、それがそのグループの番号になる
        self._parent = [n for n in range(0, N)]
        # グループの深さ
        self._depth = [1] * N

    def find_root(self, x):
        """ xの木の根(xがどのグループか)を求める """
        if self._parent[x] == x: return x
        self._parent[x] = self.find_root(self._parent[x]) # 縮約
        return self._parent[x]

    def unite(self, x, y):
        """ xとyの属するグループを併合する """
        gx = self.find_root(x)
        gy = self.find_root(y)
        if gx == gy: return

        # 小さい方を大きい方に併合させる(木の偏りが減るので)
        if self._depth[gx] < self._depth[gy]:
            self._parent[gx] = gy
        else:
            self._parent[gy] = gx
            if self._depth[gx] == self._depth[gy]: self._depth[gx] += 1

    def get_depth(self, x):
        """ xが属するグループの深さを返す """
        return self._depth[self.find_root(x)]

    def is_same_group(self, x, y):
        """ xとyが同じ集合に属するか否か """
        return self.find_root(x) == self.find_root(y)

    def calc_group_num(self):
        """ グループ数を計算して返す """
        N = len(self._parent)
        ans = 0
        for i in range(N):
            if self.find_root(i) == i:
                ans += 1
        return ans

Union-Find木(サイズバージョン)の実装

Union-Findのサイズバージョン。

グループの併合のとき、グループのサイズ(ノードの個数)が大きいものに、小さいものを併合させる。

競プロでは大抵、ノードの個数やグループ数の最小値とかを求めるので、こちらを使うことが多い。

class UnionFindVerSize():
    def __init__(self, N):
        """ N個のノードのUnion-Find木を作成する """
        # 親の番号を格納する。自分が親だった場合は、自分の番号になり、それがそのグループの番号になる
        self._parent = [n for n in range(0, N)]
        # グループのサイズ(個数)
        self._size = [1] * N

    def find_root(self, x):
        """ xの木の根(xがどのグループか)を求める """
        if self._parent[x] == x: return x
        self._parent[x] = self.find_root(self._parent[x]) # 縮約
        return self._parent[x]

    def unite(self, x, y):
        """ xとyの属するグループを併合する """
        gx = self.find_root(x)
        gy = self.find_root(y)
        if gx == gy: return

        # 小さい方を大きい方に併合させる(木の偏りが減るので)
        if self._size[gx] < self._size[gy]:
            self._parent[gx] = gy
            self._size[gy] += self._size[gx]
        else:
            self._parent[gy] = gx
            self._size[gx] += self._size[gy]

    def get_size(self, x):
        """ xが属するグループのサイズ(個数)を返す """
        return self._size[self.find_root(x)]

    def is_same_group(self, x, y):
        """ xとyが同じ集合に属するか否か """
        return self.find_root(x) == self.find_root(y)

    def calc_group_num(self):
        """ グループ数を計算して返す """
        N = len(self._parent)
        ans = 0
        for i in range(N):
            if self.find_root(i) == i:
                ans += 1
        return ans

使い方

N = 10
uf = UnionFindVerSize(N)

uf.unite(0, 1)
uf.unite(1, 2)
uf.unite(2, 3)
uf.unite(4, 5)
uf.unite(4, 6)

# {0, 1, 2, 3}, {4, 5, 6}, {7}, {8}, {9} の5グループ

a = uf.find_root(2)
print(a) # 0

a = uf.get_size(2)
print(a) # 4

a = uf.is_same_group(3, 4)
print(a) # False

a = uf.calc_group_num()
print(a) # 5

Union-Find木を使う問題

(あともう一問くらいあった気がするけど忘れた)

Pythonのリスト操作のinsert(0, v)やpop(0)の計算コストはO(N)かかる

散々語り尽くされていますが、リストの先頭に対する要素の挿入(insert)や要素の削除(pop)の計算コストはO(N)かかります。このことは公式ドキュメントのcolections.dequeの説明に記載されていているのでその該当箇所を引用すると、

docs.python.org

list オブジェクトでも同様の操作を実現できますが、これは高速な固定長の操作に特化されており、内部のデータ表現形式のサイズと位置を両方変えるような pop(0) や insert(0, v) などの操作ではメモリ移動のために O(n) のコストを必要とします。

と書いてあります。

つまり、N回のforループ内で先頭に要素を追加する処理をしたら計算コストはO(N2)になります。

具体的に言うと、 C - pushpush のような問題でリストを使って解こうとすると、TLEします。

よって、先頭に対する挿入や削除操作を何度もするときはリストの代わりにcolections.dequeを使うと良いという話になります。deque(デックと読む)の先頭に対する挿入や削除操作の計算コストはO(1)です。ただしソートは使えないので注意。

検証

リストとdequeの検証を少ししてみます。

listとdequeの先頭の要素に対する操作の比較 · GitHub

from collections import deque
import time

N = 2*10**5

def list_append():
    a = []
    start = time.time()

    for i in range(N):
        a.append(100)  # 末尾に要素を追加

    end = time.time()

    print(f"list_pop: {end-start}sec.")


def list_insert():
    a = []
    start = time.time()

    for i in range(N):
        a.insert(0, 100)  # 先頭に要素を挿入

    end = time.time()

    print(f"list_insert: {end-start}sec.")


def list_pop():
    a = [1] * N
    start = time.time()

    for i in range(N):
        a.pop(0)  # 先頭の要素の削除

    end = time.time()

    print(f"list_pop: {end-start}sec.")


def deque_append():
    a = deque()
    start = time.time()

    for i in range(N):
        a.append(100)  # 末尾に要素を追加

    end = time.time()

    print(f"deque_append: {end-start}sec.")

def deque_appendleft():
    a = deque()
    start = time.time()

    for i in range(N):
        a.appendleft(100)  # 先頭に要素を挿入
    
    end = time.time()

    print(f"deque_appendleft: {end-start}sec.")


def deque_popleft():
    a = [1] * N
    a = deque(a)
    start = time.time()

    for i in range(N):
        a.popleft()  # 先頭の要素の削除
    
    end = time.time()

    print(f"deque_popleft: {end-start}sec.")


if __name__ == "__main__":
    list_append()
    list_insert()
    list_pop()

    print()

    deque_append()
    deque_appendleft()
    deque_popleft()

結果は以下の通り。

list_append: 0.009970903396606445sec.
list_insert: 5.814519643783569sec.
list_pop: 3.187533140182495sec.

deque_append: 0.008010149002075195sec.
deque_appendleft: 0.00896906852722168sec.
deque_popleft: 0.008002996444702148sec.

やはりリストでは、先頭に対する挿入と削除操作(insertとpop)は時間がかかっています。appendは両方共高速です。

RecursionError: maximum recursion depth exceeded in comparison の対策(Python3)

再帰関数を書いたら以下のエラーが発生した。

RecursionError: maximum recursion depth exceeded in comparison

エラーで検索したら以下の記事にあたった。

Python3で再帰上限数の変更 - Qiita

書いてあるとおりに、ソースに以下のコードを追加したらなおった。

import sys
sys.setrecursionlimit(100000)  # RecursionError対策

おわり。

おまけ

以下の問題を解いていた。

atcoder.jp

DPで解くと以下になる。

# -*- coding:utf-8 -*-

def solve():
    """ DP """
    N = int(input())
    H = list(map(int, input().split()))

    INF = 10**4+10
    dp = [INF]*(N+10)
    dp[0] = 0
    dp[1] = abs(H[1] - H[0])
    for i in range(2, N):
        dp[i] = min(dp[i-1]+abs(H[i]-H[i-1]), dp[i-2]+abs(H[i]-H[i-2]))
    
    print(dp[N-1])

if __name__ == "__main__":
    solve()

再帰関数で解くと以下になる。

def solve2():
    """ メモ化再帰 
    N=1000以上で RecursionError: maximum recursion depth exceeded in comparison

1000
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    """
    import sys
    sys.setrecursionlimit(10**5+10)  # RecursionError対策

    N = int(input())
    H = list(map(int, input().split()))

    INF = 10**14+10
    dp = [INF] * (N+10)
    dp[0] = 0
    dp[1] = abs(H[0]-H[1])

    def dfs(i):
        """ 足場iの時点の最小値を返す """
        if dp[i] < INF: return dp[i]
                
        ans1, ans2 = INF, INF
        ans1 = dfs(i-1) + abs(H[i]-H[i-1])  # i-1の足場から来た場合
        if i-2 >= 0:
            ans2 = dfs(i-2) + abs(H[i]-H[i-2])  # i-2の足場から来た場合

        dp[i] = min(ans1, ans2)

        return dp[i]
    
    print(dfs(N-1))


if __name__ == "__main__":
    solve2()

ソースのコメントに書いてあるとおり、N=1000以上のときにRecursionErrorになることを確認したが、sys.setrecursionlimit(10**5+10)を追加したら無事ACした。

競技プログラミング再帰関数を書くときは注意しよう。

私のGitの定形フロー

  • 2020/12/17更新
    • 現在のブランチ状態をわかりやすくした

Gitに慣れ、だいたい以下の流れが定形になったので、備忘録として残します。

以下では、

(develop) $

上記を、developブランチにいることを表しています。

1. featureブランチの作成

機能Aを追加することになったので、developブランチから機能Aを実装する用のブランチ(feat/task-A)を作り、そこで機能Aを実装します。

# ローカルのdevelopブランチを最新の状態にする(リモートのdevelopブランチの状態にする)
(master) $ git checkout develop  # developブランチへ移動する
(develop) $ git fetch  # リモートの情報を、リモート追跡ブランチに取ってくる
(develop) $ git merge origin/develop  # リモート追跡ブランチをマージする

# developブランチからfeat/task-Aブランチを作成する
(develop) $ git checkout -b feat/task-A  # feat/task-Aブランチを作成し、移動する

2. 機能Aの実装中

機能Aを実装中、適当にきりのいい作業単位でコミットする

(feat/task-A) $ git add .             # ファイルをすべてインデックスに追加する
(feat/task-A) $ git status           # addしたファイルの確認
(feat/task-A) $ git reset piyo.txt    # インデックスから除外したいファイルがある場合はresetコマンドを使う(piyo.txtが除外される)
(feat/task-A) $ git commit        # インデックスをコミットする

# エディタが開くので、コミットメッセージを入力する
# 保存をするとコミット完了。やっぱりコミットしたくないときは保存しないで終了する

# コミットログを見たいときは以下のようにします。
(feat/task-A) $ git log     # commitのログが見れる(qで終了)

機能Aが実装完了するまで、これを繰り返します。

3. 機能Aの実装完了

feat/task-Aブランチで機能Aを実装しました。忘れずコミットします。

# 機能Aを実装完了したので、きちんとコミットする
(feat/task-A) $ git add .
(feat/task-A) $ git commit

ローカルリポジトリのコミットを、リモートリポジトリにpushします。

# ローカルのfeat/task-Aブランチのコミットを、リモートのfeat/task-Aにpushする。
(feat/task-A) $ git push origin feat/task-A

pushができたら、プルリクエストをします。プルリクエストをコマンドでやる方法はわからないので、GitHubだったらそのページに行ってぽちぽち適当にボタンをクリックします。

3-1. コンフリクトが発生したら

# 競合の解決方法例

# developブランチを最新の状態にする
(feat/task-A) $ git checkout develop
(develop) $ git fetch
(develop) $ git merge origin/develop

# developブランチをfeat/task-Aにマージする
(develop) $ git checkout feat/task-A
(feat/task-A) $ git merge develop

# 競合が発生した場合、競合を解決する必要がある
# 競合の解決は、基本的に手動でファイルを修正する
# 注意1. 自分の変更内容を必ずしも優先しない 
# 注意2. わからなければ誰かに相談する

# <<<<<<< HEAD
# 自分の環境の変更点
# =======
# マージを試みた他の環境での変更点
# >>>>>>> [commit id]

# ファイルを修正して競合を解決したら、通常通りにaddしてコミットします。
(feat/task-A) $ git add .
(feat/task-A) $ git commit -m "fix conflict"

# コミット出来たらfeat/task-Aをリモートへpushします。
(feat/task-A) $ git push origin feat/task-A

4. プルリクエストが承認されたら

ローカルのfeatureブランチはもう必要ないので削除します。

# ブランチ削除の前に、ブランチの作業分をdevelopにマージする
(feat/task-A) $ git checkout develop
(develop) $ git fetch
(develop) $ git merge origin/develop

# マージしたら削除できる
(develop) $ git branch --delete feat/task-A  # feat/task-Aブランチの削除

その他

git branch --delete feat/task-A できないとき

ローカルのdevelopブランチにfeat/task-Aの内容がマージされてない場合、エラーが出る。なので、developブランチを最新の状態にしてやれば良い。

# developブランチを最新の状態にする
$ git checkout develop
$ git fetch
$ git merge origin/develop

# 消せるようになる
$ git branch --delete feat/task-A

git checkout [ブランチ] できないとき

ブランチを移動できないとき、大抵はコミットしてないのが原因です。コミットしたくない場合、stashで一時保存すればよい。

Chrome拡張のContent Scriptのメモ

Content Scriptについて

  • Content ScriptはWebページに差し込まれる
  • Webページ内のidなどの要素にはアクセス可能
    • document.getElementById('title')などが可能
  • Webページ内のJavaScriptの変数にはアクセス不可能(スコープが違うので、Content Scriptの変数とバッティングすることはない。深く考える必要はない)
  • Chrome拡張のAPIの一部のみ使用可能
  • 上記の使用不可能なものを使いたいときは、Event Pageを介してデータのやり取りを行う
    • chrome.runtimeにあるSend Messageを使って、Content ScriptからEvent Pageに対してMessage Passingしてやり取りを行う

Anacondaの環境変数のパス(Windows)

$ conda update anaconda-navigator をしたらCondaHTTPErrorとかSSLErrorとかエラーだらけになった。

メインPCだとエラーにならず、ノートPCだとエラーになったので、メインPCと同じ設定にしようと思って環境変数のパスを確認したらパスが足りなかった。最終的なパスを残しておく。

C:\Users\XXXX\Anaconda3
C:\Users\XXXX\Anaconda3\Library\mingw-w64\bin
C:\Users\XXXX\Anaconda3\Library\bin
C:\Users\XXXX\Anaconda3\Library\usr\bin
C:\Users\XXXX\Anaconda3\Scripts

SSHでもBitbucketのpushやfetch時に毎回パスワード聞かれるときの対処法

ググったら似たような症状が出るわけです。

ただし上記の記事は、「HTTPSからSSHに変更したら直る」という内容です。

私の場合、SSHからHTTPSに変更したら直りました。以前まではSSH推奨であったが、今ではHTTPS推奨に変わったのでしょうか?(本当か?)推奨は時代によって変わるので、そういうことかもしれませんが確証はありません。

# リモートリポジトリのURLを表示する(git@で始まっているので、SSH接続)
$ git remote -v
origin  git@bitbucket.org:YYYY/MYPROJECT.git (fetch)
origin  git@bitbucket.org:YYYY/MYPROJECT.git (push)

# fetchすると、毎回SSHキーのパスワードを聞かれる
$ git fetch
Enter passphrase for key '/c/Users/XXXX/.ssh/bitbucket/id_ed25519':

# リモートリポジトリのURLをHTTPSに変更する
$ git remote set-url origin https://USERNAME@bitbucket.org/XXXX/MYPROJECT.git

# リモートリポジトリのURLを確認する(HTTPSになっていることを確認)
$ git remote -v
origin  https://USERNAME@bitbucket.org/YYYY/MYPROJECT.git (fetch)
origin  https://USERNAME@bitbucket.org/YYYY/MYPROJECT.git (push)

# 初回だけパスワード聞かれるが、二回目以降は聞かれなくなった
$ git fetch

ちなみにGitHubHTTPS推奨(2019年3月14日現在)というのがわかりますが、Bitbucketはそういう記事を探しても見つかりませんでした。

ただし、Change the remote URL to your repositoryの記事を読んでみると、

Update the URL for Git repositories

(中略)

If you update your URL from HTTPS to SSH, next time you push or pull from your repository, the terminal responds that it is adding the Bitbucket host to the list of known hosts. You also won't have to enter a password.

とあり、「(Gitリポジトリを)HTTPSからSSHに変更した場合は次回からパスワード聞かれない」とあります。

また、

Update the URL for Mercurial repositories

(中略)

If you update your URL from HTTPS to SSH, next time you push or pull from your repository, the terminal responds that it is adding the Bitbucket host to the list of known hosts. You also won't have to enter a password.

とあり、「(Mercurialリポジトリを)SSHからHTTPSに変更した場合は次回からパスワード聞かれない」とあります。

つまりぜんぜんわからないですが、URLを変更したらパスワードは聞かれなくなるということでしょうか。本当か?