問題
$N$ 個の行列 $M_1, M_2, \ldots, M_N$(各 $d \times d$, 整数要素)が与えられる。$Q$ 個のクエリ $(l_i, r_i)$ それぞれについて、積 $M_{l_i} \cdot M_{l_i+1} \cdots M_{r_i}$ が零行列かどうかを高速に判定せよ。
各行列の要素は $[0, p)$($p = 998244353$)に属する整数で与えられ、積の計算も $\bmod p$ で行う。
制約
$1 \le N \le 10^4$
$1 \le d \le 4$
$1 \le Q \le 10^5$
$1 \le l_i \le r_i \le N$
$p = 998244353$
時間制限: 3秒
入出力例
入力例 1
3 2 2
1 0
0 0
0 1
0 0
1 0
0 1
1 2
1 3
出力例 1
Yes
Yes
$M_1=[[1,0],[0,0]]$, $M_2=[[0,1],[0,0]]$, $M_3=[[1,0],[0,1]]$。$M_1 M_2=[[0,1],[0,0]]\cdot...$は実際に計算して確認。
概念図: Schwartz–Zippel 補題による零行列判定
ヒント(段階的開示)
ヒント1: 方向性
行列積が零行列かどうかを全クエリで毎回計算すると $O(Q \cdot N \cdot d^3)$ となり TLE。Schwartz-Zippel 補題を使い、ランダムベクトルとの積で零行列を高確率で判定できます。
ヒント2: スパーステーブルで行列積を高速化
$\text{sparse}[k][i] = M_i \cdots M_{i+2^k-1}$($2^k$ 個の行列の積)を前計算($O(N \log N \cdot d^3)$)し、クエリ $(l, r)$ を $O(\log N \cdot d^3)$ で分解します。
ヒント3: Schwartz–Zippel の適用骨格
import random
p = 998244353
K = 3 # ランダムベクトルの本数
# K 本のランダムベクトル(0 を避ける)
rands = [[random.randrange(1, p) for _ in range(d)] for _ in range(K)]
def mat_vec(M, v):
# M×v の計算
return [(sum(M[i][j] * v[j] for j in range(d))) % p for i in range(d)]
# クエリ (l, r) の処理
M_prod = query_mat(l, r) # スパーステーブルから積を取得
is_zero = all(
all(x == 0 for x in mat_vec(M_prod, v))
for v in rands
)
模範解答 (Python)
import sys
import random
from sys import stdin
def solve():
data = stdin.buffer.read().split()
idx = 0
N, d, Q = int(data[idx]), int(data[idx+1]), int(data[idx+2]); idx += 3
p = 998244353
mats = []
for i in range(N):
M = []
for r in range(d):
row = [int(data[idx+c]) for c in range(d)]
idx += d
M.append(row)
mats.append(M)
def mat_vec(M, v):
res = []
for row in M:
s = 0
for j in range(d):
s += row[j] * v[j]
res.append(s % p)
return res
def mat_mul(A, B):
res = [[0]*d for _ in range(d)]
for i in range(d):
for k in range(d):
if A[i][k] == 0: continue
for j in range(d):
res[i][j] = (res[i][j] + A[i][k] * B[k][j]) % p
return res
LOG = max(1, N.bit_length())
# sparse[k][i] = M_i * ... * M_{i+2^k-1} (1-indexed)
sparse = [[None] * (N + 2) for _ in range(LOG + 1)]
for i in range(1, N + 1):
sparse[0][i] = mats[i - 1]
for k in range(1, LOG + 1):
for i in range(1, N + 1):
half = 1 << (k - 1)
j = i + half
if j <= N:
sparse[k][i] = mat_mul(sparse[k-1][i], sparse[k-1][j])
def query_mat(l, r):
result = None
i = l
for k in range(LOG, -1, -1):
if i + (1 << k) - 1 <= r:
part = sparse[k][i]
if result is None:
result = part
else:
result = mat_mul(result, part)
i += 1 << k
return result
# Randomized zero test with K vectors
K = 3
rands = [[random.randrange(1, p) for _ in range(d)] for _ in range(K)]
out = []
for _ in range(Q):
l, r = int(data[idx]), int(data[idx+1]); idx += 2
M_prod = query_mat(l, r)
is_zero = True
for v in rands:
result = mat_vec(M_prod, v)
if any(x != 0 for x in result):
is_zero = False
break
out.append('Yes' if is_zero else 'No')
sys.stdout.write('\n'.join(out) + '\n')
solve()
Step-by-Step 解説
1Schwartz–Zippel 補題
多変数多項式 $F \not\equiv 0$(次数 $d$)をランダム点で評価したとき $F = 0$ になる確率は $\le d/|\mathbb{F}|$。$p = 998244353$ では誤判定確率 $\le d/p \approx 4/10^9$。
多変数多項式 $F \not\equiv 0$(次数 $d$)をランダム点で評価したとき $F = 0$ になる確率は $\le d/|\mathbb{F}|$。$p = 998244353$ では誤判定確率 $\le d/p \approx 4/10^9$。
2行列の零行列判定への適用
ランダム $\mathbf{v}$ で $M\mathbf{v} \ne \mathbf{0}$ なら確実に非零行列。$M\mathbf{v} = \mathbf{0}$ でも $M = 0$ でないケースは低確率です。
ランダム $\mathbf{v}$ で $M\mathbf{v} \ne \mathbf{0}$ なら確実に非零行列。$M\mathbf{v} = \mathbf{0}$ でも $M = 0$ でないケースは低確率です。
3スパーステーブル構築
$\text{sparse}[k][i] = M_i \cdots M_{i+2^k-1}$ を $O(N \log N \cdot d^3)$ で前計算。クエリ $(l, r)$ の積を $O(\log N \cdot d^3)$ で計算します。
$\text{sparse}[k][i] = M_i \cdots M_{i+2^k-1}$ を $O(N \log N \cdot d^3)$ で前計算。クエリ $(l, r)$ の積を $O(\log N \cdot d^3)$ で計算します。
4ベクトル評価
行列積 $M$ を明示的に持ち、$K$ 本のランダムベクトル $\mathbf{v}$ で $M\mathbf{v}$ を評価します。全て 0 なら零行列と判定。
行列積 $M$ を明示的に持ち、$K$ 本のランダムベクトル $\mathbf{v}$ で $M\mathbf{v}$ を評価します。全て 0 なら零行列と判定。
5誤判定確率の管理
$K$ 本の独立ベクトルで誤判定確率 $\le (d/p)^K$。$K=3, d=4$ では約 $10^{-26}$。実用上は 1 本でも十分です。
$K$ 本の独立ベクトルで誤判定確率 $\le (d/p)^K$。$K=3, d=4$ では約 $10^{-26}$。実用上は 1 本でも十分です。
計算量
スパーステーブル構築: $O(N \log N \cdot d^3)$
各クエリ: $O(\log N \cdot d^3 + K \cdot d^2)$
全体: $O(N \log N \cdot d^3 + Q \log N \cdot d^2)$
空間: $O(N \log N \cdot d^2)$
各クエリ: $O(\log N \cdot d^3 + K \cdot d^2)$
全体: $O(N \log N \cdot d^3 + Q \log N \cdot d^2)$
空間: $O(N \log N \cdot d^2)$
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| スパーステーブルの順序を逆にする | $M_l \cdots M_r$ の順序 | sparse[k][i] = sparse[k-1][i] × sparse[k-1][i+2^{k-1}] |
| ベクトルがすべて 0 のとき | $\mathbf{v}=\mathbf{0}$ では常に $M\mathbf{v}=\mathbf{0}$ | random.randrange(1, p) で 0 を避ける |
| 誤判定方向を誤解 | $M\mathbf{v}=0$ は確定でない | 「$M\mathbf{v}\ne 0$ なら確実に非零行列」のみ確定 |
| LOG の計算漏れ | $N=1$ のとき LOG=0 | LOG = max(1, N.bit_length()) |
次のステップ
- 発展問題: ランダム行列フィンガープリンティングで2つの多項式が等しいか判定(Day026 Q5 の復習)
- 関連: Schwartz-Zippel を使った完全マッチング判定(Edmonds 行列)
- 応用: 区間行列積を永続データ構造で管理するオンライン版