E - 粒子の拡散シミュレーション / Particle Diffusion Simulation 解説 by admin
claude4.8opus-high概要
\(N\) 個の容器の上を粒子が拡散し、各容器に「正電荷」「負電荷」の粒子量が管理されます。\(T\) ステップ後の容器 \(G\) における正電荷粒子の量を、線形変換の繰り返しとして捉え、行列累乗で高速に求める問題です。
考察
状態は線形変換で表せる
この問題の最大のポイントは、1 ステップの更新が線形変換(行列とベクトルの積)で表せることです。
各容器 \(i\) には「正電荷量 \(x_i^+\)」と「負電荷量 \(x_i^-\)」の 2 種類の値があります。これらをすべて並べた長さ \(2N\) のベクトルを状態とみなします。
更新式
\[y_j^{+} = \sum_{i} \Bigl( (1 - F_{i,j}) x_i^{+} P_{i,j} + F_{i,j} x_i^{-} P_{i,j} \Bigr)\]
\[y_j^{-} = \sum_{i} \Bigl( (1 - F_{i,j}) x_i^{-} P_{i,j} + F_{i,j} x_i^{+} P_{i,j} \Bigr)\]
を見ると、\(y\) の各成分は \(x\) の各成分の1 次式(係数倍の和)になっています。つまり、ある \(2N \times 2N\) 行列 \(A\) を用いて
\[\mathbf{y} = A \mathbf{x}\]
と書けます。これを \(T\) 回繰り返すと
\[\mathbf{x}_T = A^T \mathbf{x}_0\]
となります。
素朴なアプローチの問題点
1 ステップずつ愚直に更新すると \(O(T \cdot N^2)\) かかりますが、\(T \leq 10^{18}\) なので絶対に間に合いません。そこで 行列累乗(繰り返し二乗法) を使い、\(A^T\) を \(O(\log T)\) 回の行列積で求めます。
有理数の扱い
\(P_{i,j} = R_{i,j} / (\sum_{j'} R_{i,j'})\) には割り算が含まれます。答えは有理数ですが、出力は \(\bmod\ 10^9+7\) での値です。\(10^9+7\) は素数なので、割り算はモジュラ逆元(フェルマーの小定理による \(a^{-1} \equiv a^{p-2}\))に置き換えて計算します。行和の逆元をあらかじめ求めておけば、すべての計算を \(\bmod\ 10^9+7\) の整数演算で行えます。
アルゴリズム
状態のインデックス付け
長さ \(2N\) のベクトルにおいて、 - インデックス \(i\)(\(0 \leq i < N\)):容器 \(i\) の正電荷量 - インデックス \(N+i\):容器 \(i\) の負電荷量
とします。
遷移行列 \(A\) の構築
各 \((i, j)\) について \(P_{i,j} = R_{i,j} \cdot (\text{行和})^{-1}\) を計算し、 - 反転なし(\(F_{i,j}=0\)): - \(A[j][i] \mathrel{+}= P_{i,j}\)(正→正) - \(A[N+j][N+i] \mathrel{+}= P_{i,j}\)(負→負) - 反転あり(\(F_{i,j}=1\)): - \(A[j][N+i] \mathrel{+}= P_{i,j}\)(負→正) - \(A[N+j][i] \mathrel{+}= P_{i,j}\)(正→負)
ここで \(A[行][列]\) は「\(\mathbf{y} = A\mathbf{x}\)」の行列を表します。
答えの取り出し
初期ベクトル \(\mathbf{x}_0\) は「容器 \(S\) の正電荷」が \(1\)、他はすべて \(0\) です。つまり \(\mathbf{x}_0\) はインデックス \(S\) だけが \(1\) の単位ベクトルです。
\(\mathbf{x}_T = A^T \mathbf{x}_0\) の容器 \(G\) の正電荷量はインデックス \(G\) の成分なので、これは
\[\mathbf{x}_T[G] = (A^T)[G][S]\]
つまり \(A^T\) の \((G, S)\) 成分そのものです。行列累乗の結果から 1 要素を取り出すだけで答えが得られます。
計算量
行列のサイズは \(D = 2N \leq 60\) です。
- 時間計算量: \(O((2N)^3 \log T)\)
- 空間計算量: \(O((2N)^2)\)
\(N \leq 30\)、\(T \leq 10^{18}\) なので、\(60^3 \times 60 \approx 1.3 \times 10^7\) 程度で十分高速です。
実装のポイント
割り算は逆元で:行和 \(\sum_{j} R_{i,j}\) の逆元を
invで事前計算し、\(P_{i,j} = R_{i,j} \cdot \text{invrow}[i]\) とします。すべての値を \(\bmod\ 10^9+7\) で保持します。\(P_{i,j}=0\) のスキップ:\(R_{i,j}=0\) の遷移は寄与しないので飛ばします(反転ルールが設定されていても影響しません)。
行列の向きに注意:状態を「行列 \(\times\) 列ベクトル」として扱う場合、更新式の係数をそのまま \(A[\text{出力先}][\text{入力元}]\) に格納します。最終的に答えは \((G, S)\) 成分です。
同時更新:1 ステップの更新は「更新前の値のみ」から計算されますが、行列 \(\times\) ベクトルの形式は自動的にこの同時更新を表現しているため、特別な配慮は不要です。
\(T\) が大きい:\(T \leq 10^{18}\) なので
long longで受け取り、繰り返し二乗法を用います。ソースコード
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;
long long pw(long long a, long long b){
a%=MOD; if(a<0)a+=MOD;
long long r=1;
while(b>0){
if(b&1) r=r*a%MOD;
a=a*a%MOD;
b>>=1;
}
return r;
}
long long inv(long long a){ return pw(a, MOD-2); }
int main(){
int N, M; long long T;
scanf("%d %d %lld", &N, &M, &T);
int S, G;
scanf("%d %d", &S, &G);
S--; G--;
vector<vector<long long>> R(N, vector<long long>(N));
for(int i=0;i<N;i++)for(int j=0;j<N;j++) scanf("%lld",&R[i][j]);
vector<vector<int>> F(N, vector<int>(N,0));
for(int k=0;k<M;k++){
int u,v; scanf("%d %d",&u,&v); u--;v--;
F[u][v]=1;
}
// P[i][j] = R[i][j] * inv(rowsum_i)
vector<long long> invrow(N);
for(int i=0;i<N;i++){
long long s=0;
for(int j=0;j<N;j++) s+=R[i][j];
invrow[i]=inv(s%MOD);
}
int D=2*N;
// state: positive i -> i, negative i -> N+i
vector<vector<long long>> A(D, vector<long long>(D,0));
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
long long P = (R[i][j]%MOD)*invrow[i]%MOD;
if(P==0) continue;
if(F[i][j]==0){
// no flip
A[j][i] = (A[j][i] + P)%MOD; // y_j+ from x_i+
A[N+j][N+i] = (A[N+j][N+i] + P)%MOD; // y_j- from x_i-
} else {
A[j][N+i] = (A[j][N+i] + P)%MOD; // y_j+ from x_i-
A[N+j][i] = (A[N+j][i] + P)%MOD; // y_j- from x_i+
}
}
}
// matrix power A^T
auto mul=[&](vector<vector<long long>>&X, vector<vector<long long>>&Y){
vector<vector<long long>> Z(D, vector<long long>(D,0));
for(int i=0;i<D;i++){
for(int k=0;k<D;k++){
if(X[i][k]==0) continue;
long long v=X[i][k];
for(int j=0;j<D;j++){
Z[i][j]=(Z[i][j]+v*Y[k][j])%MOD;
}
}
}
return Z;
};
vector<vector<long long>> result(D, vector<long long>(D,0));
for(int i=0;i<D;i++) result[i][i]=1;
long long e=T;
vector<vector<long long>> base=A;
while(e>0){
if(e&1) result=mul(result,base);
base=mul(base,base);
e>>=1;
}
// initial vector: e[S]=1 (positive at S)
// after T steps: y = result * e, y[G] = result[G][S]
printf("%lld\n", result[G][S]%MOD);
return 0;
}
この解説は claude4.8opus-high によって生成されました。
投稿日時:
最終更新: