Day 127-Q5 — 隣接行列のトレースによる閉路数え上げ

2026-08-19 赤色 Master / Phase 8+ ★★★★★★★★★ 行列累乗でtrace(B^K)を計算し長さKの閉路的ウォーク総数を O(N^3 log K) で求める

問題

$N$頂点の有向グラフが隣接行列$B$($B_{i,j}\in\{0,1\}$、多重辺・自己ループなし)として与えられる。整数$K$が与えられる。頂点$s$から辺をちょうど$K$本たどって$s$自身に戻ってくるウォークの総数を、全頂点$s$について合計した値を$\bmod\ M$($M$は素数)で求めよ。

入力形式

N K M
B_11 B_12 ... B_1N
...
B_N1 B_N2 ... B_NN

制約

$1 \le N \le 70$
$1 \le K \le 10^{18}$
$10^8 \le M \le 10^9+9$(素数)
$B_{i,i}=0$

入出力例

入力例1

3 2 1000000007
0 1 0
0 0 1
1 0 0

出力例1

0

3頂点の有向サイクル。サイクル長3は2と互いに素なので長さ2で元頂点に戻る道はない。

入力例2

2 2 1000000007
0 1
1 0

出力例2

2

B²=単位行列。1→2→1、2→1→2の2通り。

概念図

B^Kのトレース = 長さKの閉路的ウォーク総数 (B^k)_{i,j} = i→jへちょうどk本の辺で到達するウォークの本数 対角成分(B^k)_{i,i} = iから出発しiに戻るウォークの本数 trace(B^k) = Σ_i (B^k)_{i,i} = 全頂点を始点とする閉路的ウォークの総数 繰り返し二乗法: B, B², B⁴, B⁸, ... を自乗しながらKの2進ビットに応じて掛け合わせる B B² B⁴ ... O(log K) 回の行列積、各O(N³) 行列積の内側ループで毎回 mod M を取ることで数値の爆発を防ぐ

ヒント(段階的開示)

ヒント1(方向性)

隣接行列$B$の$k$乗の$(i,j)$成分は「頂点$i$から$j$へちょうど$k$本の辺で到達するウォークの本数」に等しい(行列積の定義と経路の合成が対応する)。

ヒント2(アプローチ)

求める値は $\operatorname{tr}(B^K)=\sum_i (B^K)_{i,i}$(トレース)。$K$が最大$10^{18}$なので繰り返し二乗法で$B^K$を$O(\log K)$回の行列積で求める。1回の行列積は$O(N^3)$なので全体$O(N^3\log K)$。

ヒント3(誘導)
def mat_pow(B, k, mod):
    n = len(B)
    result = [[1 if i==j else 0 for j in range(n)] for i in range(n)]  # 単位行列
    base = B
    while k > 0:
        if k & 1:
            result = mat_mul(result, base, mod)
        base = mat_mul(base, base, mod)
        k >>= 1
    return result

模範解答 (Python)

import sys


def solve():
    data = sys.stdin.buffer.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    k = int(data[idx]); idx += 1
    mod = int(data[idx]); idx += 1

    b = []
    for i in range(n):
        row = [int(data[idx + j]) for j in range(n)]
        idx += n
        b.append(row)

    def mat_mul(x, y):
        z = [[0] * n for _ in range(n)]
        for i in range(n):
            xi = x[i]
            zi = z[i]
            for kk in range(n):
                xik = xi[kk]
                if xik == 0:
                    continue
                yk = y[kk]
                for j in range(n):
                    zi[j] = (zi[j] + xik * yk[j]) % mod
        return z

    def mat_pow(mat, p):
        result = [[1 if i == j else 0 for j in range(n)] for i in range(n)]
        base = mat
        while p > 0:
            if p & 1:
                result = mat_mul(result, base)
            base = mat_mul(base, base)
            p >>= 1
        return result

    bk = mat_pow(b, k)
    trace = 0
    for i in range(n):
        trace = (trace + bk[i][i]) % mod

    print(trace)


solve()

Step-by-Step 解説

1行列積と経路数え上げの対応
$(B^k)_{i,j}=\sum B_{i,c_1}B_{c_1,c_2}\cdots B_{c_{k-1},j}$の各項は長さ$k$のウォークの存在に対応し、総和はその本数になる。
2トレースの意味
対角成分の総和が「全頂点を始点とする長さKの閉路的ウォークの総数」に一致する。
3繰り返し二乗法
$K$を2進展開し、$B,B^2,B^4,\dots$を自乗しながらビットが立つ位置だけ結果に掛け合わせる。
4計算量
1回の行列積$O(N^3)$、$\log K$回程度なので全体$O(N^3\log K)$。
5mod演算の徹底
行列積の内側ループで毎回modを取ることで数値の爆発的増大を防ぐ。

よくあるミス

ミス原因正しい書き方
単位行列の初期化を忘れる$B^0=I$を用意していないresultを単位行列で初期化してから二乗法を回す
行列積でmodを取り忘れる大きい整数のまま演算し続ける各要素更新のたびに%modを適用する
トレースを対角成分の最大値と勘違い用語の理解不足トレースは対角成分の総和
二分累乗のループ条件を誤るp=0での終了確認不足while p > 0:で正しく終了することを確認

次のステップ

  • 発展: 長さK以下の閉路的ウォーク総数を求める(等比行列和のブロック行列トリック)
  • 次回予告(Master Levelローテーション継続): 次回セッションで新たな高度テーマを選定

自己評価

自分の回答

気づき・メモ