問題
素数 $p$、メッセージ長 $k$、訂正可能誤り数 $t$ が与えられる。
送信側は次数$k-1$以下の多項式 $P(x)=c_0+c_1x+\dots+c_{k-1}x^{k-1}\pmod p$ を、評価点 $x_i=i$($i=0,\dots,n-1$、$n=k+2t$)における値として符号化し送信する(Reed-Solomon符号)。
受信側では最大$t$個までの値が改ざんされた符号語 $y_0,\dots,y_{n-1}$ が届く。Berlekamp-Welch アルゴリズムを用いて元の係数 $c_0,\dots,c_{k-1}$ を復元せよ。
手順:
- $Q(x)$(次数$k+t-1$以下)と、モニックな誤り位置多項式$E(x)$(次数$t$)を未知数として用意
- 各$i$について $Q(x_i)=y_i\cdot E(x_i)\pmod p$ の連立方程式(未知数$n$個、方程式$n$本)を解く
- $Q(x)$を$E(x)$で多項式除算し、割り切れれば商が$P(x)$。割り切れない/連立方程式が解けない場合は訂正不能
訂正不能な場合は-1を出力せよ。
入力形式
p k t
y_0 y_1 ... y_{n-1}
制約
$17 \le p \le 10^9+9$(素数)
$1 \le k \le 50$
$1 \le t \le 50$
$n=k+2t\le p$
入出力例
入力例1
17 3 2
2 6 16 3 13 15 5出力例1
2 3 1入力例2
17 3 2
2 0 1 3 13 2 5出力例2
-1概念図
ヒント(段階的開示)
ヒント1(方向性)
「どこが改ざんされたか」を先に特定してから訂正しようとすると誤り位置の組み合わせ探索が必要になり計算量が爆発する。Berlekamp-Welchの核心は、「誤り位置が分かっていなくても、誤り位置多項式$E(x)$と補正後の値$Q(x)=P(x)E(x)$を同時に未知数として立式し、線形代数だけで解いてしまう」という点にある。
ヒント2(アプローチ)
誤り位置でだけ0になる多項式$E(x)$(次数$t$、誤り位置をちょうど根に持つ)を考えると、$y_i\cdot E(x_i)$は正しい位置では$P(x_i)E(x_i)$、誤った位置でも$E(x_i)=0$なのでどちらも0となり、すべての$i$で$Q(x_i)=y_iE(x_i)$が成り立つ。誤り位置を知らなくてもこの等式は無条件に成立するので、線形方程式として解ける。
ヒント3(誘導)
# 未知数: q_0..q_{k+t-1} (Q), e_0..e_{t-1} (E, 最高次はモニックで1固定)
# 方程式: sum q_j x_i^j - y_i*sum e_j x_i^j = y_i * x_i^t (i=0..n-1)
A = []
b = []
for i in range(n):
row = [pow(x[i], j, p) for j in range(k+t)] + [(-y[i]*pow(x[i], j, p)) % p for j in range(t)]
A.append(row)
b.append((y[i] * pow(x[i], t, p)) % p)
sol = gauss_solve(A, b, p)
if sol is None:
print(-1)
else:
Q = sol[:k+t]
E = sol[k+t:] + [1]
quot, rem = poly_divmod(Q, E, p)
if 余りが全て0でない:
print(-1)
else:
print(quot[:k])
模範解答 (Python)
import sys
def main():
data = sys.stdin.read().split()
idx = 0
p = int(data[idx]); idx += 1
k = int(data[idx]); idx += 1
t = int(data[idx]); idx += 1
n = k + 2 * t
ys = [int(data[idx + i]) % p for i in range(n)]
idx += n
xs = list(range(n))
def modinv(a):
return pow(a, p - 2, p)
def gauss_solve(A, b):
sz = len(A)
M = [row[:] + [b[i]] for i, row in enumerate(A)]
for col in range(sz):
piv = None
for r in range(col, sz):
if M[r][col] % p != 0:
piv = r
break
if piv is None:
return None
M[col], M[piv] = M[piv], M[col]
inv = modinv(M[col][col])
M[col] = [(x * inv) % p for x in M[col]]
for r in range(sz):
if r != col and M[r][col] % p != 0:
factor = M[r][col]
M[r] = [(M[r][j] - factor * M[col][j]) % p for j in range(sz + 1)]
return [M[i][sz] % p for i in range(sz)]
def poly_divmod(num, den):
num = num[:]
dn = len(den) - 1
while dn > 0 and den[dn] == 0:
dn -= 1
quot = [0] * max(1, len(num) - dn)
while True:
deg_num = len(num) - 1
while deg_num >= 0 and num[deg_num] % p == 0:
deg_num -= 1
if deg_num < dn:
break
coef = (num[deg_num] * modinv(den[dn])) % p
shift = deg_num - dn
quot[shift] = coef
for i in range(dn + 1):
num[shift + i] = (num[shift + i] - coef * den[i]) % p
return quot, num
A = []
b = []
for i in range(n):
xi = xs[i] % p
row = []
for j in range(k + t):
row.append(pow(xi, j, p))
for j in range(t):
row.append((-ys[i] * pow(xi, j, p)) % p)
A.append(row)
b.append((ys[i] * pow(xi, t, p)) % p)
sol = gauss_solve(A, b)
if sol is None:
print(-1)
return
Qc = sol[:k + t]
Ec = sol[k + t:] + [1]
quot, rem = poly_divmod(Qc, Ec)
if any(x % p for x in rem):
print(-1)
return
while len(quot) < k:
quot.append(0)
print(' '.join(str(x % p) for x in quot[:k]))
main()
Step-by-Step 解説
1なぜQ(x_i)=y_iE(x_i)が誤り位置に関わらず全て成り立つか
誤りのない位置では$y_i=P(x_i)$なので等式は定義通り成立。誤りのある位置($E(x_i)=0$)では$y_i$が何であっても$0=0$となり成立する。
誤りのない位置では$y_i=P(x_i)$なので等式は定義通り成立。誤りのある位置($E(x_i)=0$)では$y_i$が何であっても$0=0$となり成立する。
2未知数の数と方程式の本数が一致する理由
$Q$は係数$k+t$個、$E$はモニック制約で自由な係数$t$個、合計$k+2t=n$個の未知数に対し評価点も$n$個あるので正方の連立1次方程式になる。
$Q$は係数$k+t$個、$E$はモニック制約で自由な係数$t$個、合計$k+2t=n$個の未知数に対し評価点も$n$個あるので正方の連立1次方程式になる。
3連立方程式が一意に解ける理由(概略)
真の$E^*$と$Q^*=PE^*$は具体的な解の一つ。2組の解の差を取ると、$n$個の相異なる評価点で0になる次数$<n$の多項式の恒等式に帰着し、$Q/E$の値(=商$P$)はどの解を選んでも一致する。
真の$E^*$と$Q^*=PE^*$は具体的な解の一つ。2組の解の差を取ると、$n$個の相異なる評価点で0になる次数$<n$の多項式の恒等式に帰着し、$Q/E$の値(=商$P$)はどの解を選んでも一致する。
4多項式除算が割り切れない場合=訂正不能
誤り数が$t$を超えると「誤り位置をすべて根に持つ次数$t$の$E$」が存在せず、連立方程式に解がないか、割り切れない。どちらも-1を出力する。
誤り数が$t$を超えると「誤り位置をすべて根に持つ次数$t$の$E$」が存在せず、連立方程式に解がないか、割り切れない。どちらも-1を出力する。
5サンプルの検算
入力例1は$P(x)=2+3x+x^2$の符号語の位置2,5を改ざんしたもの。誤り数2=tちょうどなので訂正でき、元の係数[2,3,1]が復元される。入力例2は3箇所改ざんしており訂正できず-1になる。
入力例1は$P(x)=2+3x+x^2$の符号語の位置2,5を改ざんしたもの。誤り数2=tちょうどなので訂正でき、元の係数[2,3,1]が復元される。入力例2は3箇所改ざんしており訂正できず-1になる。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| E(x)の最高次係数(モニック制約)を未知数に含めてしまう | 未知数の数を$k+2t+1$にしてしまう | 最高次係数は常に1に固定し未知数は$e_0,\dots,e_{t-1}$のt個のみ |
| 方程式の右辺の符号を逆にしてしまう | 移項の際に符号を取り違える | 右辺は$y_ix_i^t$(プラス)が正しい |
| 多項式除算の余りチェックを先頭係数だけ確認 | 余り多項式の一部だけ見て判定 | 余りの全ての係数が0 mod pであることを確認する |
| 商の次数チェックを行わない | 訂正可能なら次数k-1以下になるはず | 次数が合わない場合は実装バグを疑う |
| mod p上で実数除算してしまう | 有限体上の演算であることを忘れる | 必ずフェルマーの小定理でモジュラ逆元を計算する |
次のステップ
- 発展: 実際のReed-Solomon復号ではBerlekamp-Massey法($O(n^2)$)を使う「シンドローム復号」がより高速。両者の関係と高速化の理由を調べてまとめよ。
- 発展2: 評価点を任意の相異なる元にした場合でも動くことを確認し、$n>p$になるケースを弾く入力検証を追加せよ。