本記事では,UnionFindクラスを用いる典型的な問題を参考に,RE(実行時エラー)や,その元であるIndex ouf of rangeエラーが発生する原因を明らかにしていきます.UnionFindクラスを用いた,中上級者向けの問題を参考にしていますが,実行時エラー全般にも言える発見がありました.
1. AtCoder ABC270 D
こちらが,今回エラーの解決に時間のかかった問題です.
詳細は上のリンクをご覧いただきたいと思いますが,UnionFindクラスを用いて,与えられるデータのグループ分けを行うことで解ける問題です.
2. UnionFindクラスの実装
UnionFindクラスの実装については,以下の記事を参考にしました.
PythonでのUnion-Find(素集合データ構造)の実装と使い方 | note.nkmk.me
こちらでは,ここで紹介するUnionFindクラスの応用について書いています.
実装例
from collections import defaultdict
class UnionFind():
"""
Union Find木クラス
Attributes
--------------------
n : int
要素数
patents : list
指定した要素の親(1つ上の)要素を格納
指定した要素が根の場合は,
-(グループの要素数) を格納
=> sizeメソッドに反映
"""
def __init__(self, n):
self.n = n
self.parents = [-1] * n
def find(self, x):
"""
ノードxの根を見つける
"""
if self.parents[x] < 0:
return x
else:
self.parents[x] = self.find(self.parents[x])
return self.parents[x]
def union(self, x, y):
"""
木に新たな要素を併合(マージ)
Parameters
---------------------
x, y : int
併合するノード
"""
x = self.find(x)
y = self.find(y)
if x == y:
return
if self.parents[x] > self.parents[y]:
x, y = y, x
self.parents[x] += self.parents[y]
self.parents[y] = x
def size(self, x):
"""
xの属する木のサイズ
"""
return -self.parents[self.find(x)]
def same(self, x, y):
"""
x, yが同じ木に属するか判定
"""
return self.find(x) == self.find(y)
def members(self, x):
"""
xの属する木に属する要素をリストで返す
"""
root = self.find(x)
return [i for i in range(self.n) if self.find(i) == root]
def roots(self):
"""
全ての根をリストで返す
"""
return [i for i, x in enumerate(self.parents) if x < 0]
def group_count(self):
"""
グループの数を返す
"""
return len(self.roots())
def all_group_members(self):
"""
全てのグループの要素情報を辞書で返す
"""
group_members = defaultdict(list)
for member in range(self.n):
group_members[self.find(member)].append(member)
return group_members
def __str__(self):
"""
print(uf)で全てのグループの要素情報を簡単に出力する
"""
return '\n'.join(f'{r}: {m}' for r, m in self.all_group_members().items())
3. REになった解答コード
n = int(input())
a = list(map(int, input().split()))
es = set()
uf = UnionFind(n) ###※(n)だと9ケースでエラーが発生
for i in range(n // 2):
if a[i] != a[n - 1 - i]:
uf.union(a[i] - 1, a[n - 1 - i] - 1)
ans = 0
for r in uf.roots():
if uf.size(r) >= 2:
ans += uf.size(r) - 1
print(ans)
入力例1~3の全てでACとなったため,この実装で間違いないと思っていました.しかし,提出してみると,31ケース中9のテストデータに対してREが発生しました.この対処として,次の順で対処を考えました.
- 上で示したUnionFindクラスは,何度も色々な問題で使う機会があるため,UnionFindクラスの実装には問題はないはず.
- 一応最終出力の型があっているが,答えの数値だけが間違っているWAエラー(Wrong Answer)ではない.
- RE(実行時エラー)の多くは,ライブラリを読み込めていない(or不要なライブラリを読み込んでいる)ことや,入力例では扱っていない大小関係を持った数値の集合に対応しきれていないことが挙げられる.
- 特に後者については,配列(リスト)を扱う時に,Index out of range(リストに対して,存在しないインデックスを指定してしまっている)がかなりの割合で発生する.
そこで気になったのが,次のようにUnionFindクラスからオブジェクト(インスタンス)を生成する際,
uf = UnionFind(n)
この引数nは,UnionFindクラス内ではそもそも何だっけということです.
そして,UnionFindの実装の中身をよく見ると,引数nは,
UnionFindクラスでは,0, 1, 2, … n – 1までのインデックスを持ったn個のノード(要素)からなるグラフ(木構造)を生成する.
ことに相当します.そして,
始めはn個のノードは各々独立しており,エッジ(辺)の情報(x, y)が与えられたら,ノードx, yを同じグループとする.
という機能が,UnionFindクラスの持つfindメソッドの役割です.
つまり,n = 3を与えられた場合,辺の情報として与えていいのは,
(x, y) = (0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 0)
だけであり,
(x, y) = (1, 5)
のように,辺(の両端のどちらか)として,ノードの最大値n-1を超える値を持ったものを追加することは許されないです.
したがって,
3
5 1 9
という入力に対しては,
IndexError: list index out of range
というエラーを返されます.
4. コードの修正
では,対処方法はというと,
# uf = UnionFind(n)
# => 次のように書き直す
uf = UnionFind(2 * 10 ** 5)
のように,引数として,問題で与えられうる最大の要素数2 * 10 ** 5を与えればよいです.これならば,nよりも大きな値を持った要素が配列aに含まれていても,問題ありません.
最後に
本記事では,UnionFindクラスを用いた問題で,一部テストケースでREエラーが発生してしまう原因を特定しました.結論としては,インスタンス生成の際に与える引数に,必要となりうる最大の値をとるという,非常に簡単な対処をすればよいだけのことでした.
しかし,あえて記事にしたのは,こういったエラーは日々の競技プログラミングやデータ分析でよく起こるため,今後の参考として書き残しておきたかったからです.AtCoderでのREのような,データ構造に関するエラーが発生した場合,使用するデータ構造(オブジェクト)が,そもそもどう定義されているかを再確認する必要があります.
コメント