以下の内容はhttps://torus711.hatenablog.com/entry/2024/06/09/181036より取得しました。


AtCoder Beginner Contest 357, D : 88888888

問題概要

 正整数をそれを十進数で表現する文字列に変換する関数を $f_s$ とし,正整数を十進で表す文字列を正整数に変換する関数を $f_i$ とする.また,文字列 $s$ を $k$ 回繰り返した文字列を $s^k$ で表す.
 正整数 $n$ に対し,$V_n$ を
\begin{equation*}
V_n := f_i( f_s( n )^n )
\end{equation*}
で定義する.
 $n$ が与えられるので,$V_n \bmod { 998{,}244{,}353 }$ を求めよ.

制約

  • $1 \leq n \leq 10^{18}$

解法

 $\bmod$ をとることについては,演算の度に適切に処理するなり AtCoder Library (ACL) を使うなりすればよく,本質的な話でもないので $V_n$ 自体を求める体で書きます.
 $f_s( n )$ を一つずつ連結して,それらに対応する $f_i$ の値を求めることを考えます.右から連結していくと思うと連結の度に桁数分「シフト」するので,$f_i( f_s( n )^k )$ を $R_k$ と書くことにすれば,$n$ の桁数を $d$ として*1
\begin{equation*}
R_k = \begin{cases}
0 & \text{($k = 0$)} \\
R_{ k - 1 } \times 10^d + n & \text{(otherwise)}
\end{cases}
\end{equation*}
という漸化式が得られます.これを元に DP*2 をすれば $R_n$ ($= V_n$) を求めることはできますが,$\Omega( n )$ 時間かかって TLE します.
 上記の DP を高速化することを考えます.やや天下り的ですが,上記の漸化式の再帰ケースを
\begin{equation*}
R_k = n \times 1 + 10^d \times R_{ k - 1 }
\end{equation*}
と書いてみると,ふたつのベクトル $\begin{pmatrix} n \\ 10^d \end{pmatrix}$ と $\begin{pmatrix} 1 \\ R_{ k - 1 } \end{pmatrix}$ の内積になっているように見えます.またまた天下り的ですが,これを元に行列とベクトルの積
\begin{equation*}
\begin{pmatrix}
1 & 0 \\
n & 10^d
\end{pmatrix}
\begin{pmatrix}
1 \\
R_k
\end{pmatrix}
=
\begin{pmatrix}
1 \\
R_{ k + 1 }
\end{pmatrix}
\end{equation*}
という式が作れます.行列をかけることで $R$ の添字が $1$ 進むので,$R_n$ を求めるには
\begin{equation*}
\begin{pmatrix}
1 & 0 \\
n & 10^d
\end{pmatrix}^n
\begin{pmatrix}
1 \\
R_0
\end{pmatrix}
=
\begin{pmatrix}
1 \\
R_n
\end{pmatrix}
\end{equation*}
を計算すればよいです.ここで,行列積は結合的な演算なので,行列の $n$ 乗の方を先に計算することができて,$n$ 乗は反復二乗法を用いることで乗算の回数を $\Theta( \log n )$ 回にすることができます.こうして行列の $n$ 乗を計算すれば,(かけられるベクトルが $\begin{pmatrix} 1 \\ 0 \end{pmatrix}$ なことから)結果の行列の $2$ 行 $1$ 列の要素が答えになっています.こういう方法を「行列累乗」と呼んだりします.
 計算量についてですが,行列のサイズが定数なので一回の乗算は $O( 1 )$ 時間で,全体では $\Theta( \log n )$ 時間となります.

コード

#include <atcoder/modint>
using namespace atcoder;
using mint = modint998244353;

using V = vector< mint >;
using M = vector< V >;

M mat_mul( const M &A, const M &B )
{
	M res = {
		{ 0, 0 },
		{ 0, 0 }
	};

	REP( i, 2 )
	{
		REP( j, 2 )
		{
			REP( k, 2 )
			{
				res[i][j] += A[i][k] * B[k][j];
			}
		}
	}

	return res;
}

M mat_pow( M &&A, LL N )
{
	M res = {
		{ 1, 0 },
		{ 0, 1 }
	};

	for ( ; N; N /= 2 )
	{
		if ( N & 1 )
		{
			res = mat_mul( res, A );
		}
		A = mat_mul( A, A );
	}

	return res;
}

int main()
{
	IN( LL, N );
	const int D = SZ( toString( N ) );

	const M A = mat_pow( {
			{ 1, 0 },
			{ N, mint( 10 ).pow( D ) }
			}, 
			N );

	cout << A[1][0].val() << endl;

	return 0;
}

*1:桁数は常用対数を使って求められますが,誤差がこわい気もするので,(標準ライブラリで)文字列化して長さを取った方が楽かつ安全だと思います.

*2:いわゆる「部分問題重複性」が無いので,言うほど DP じゃないかも?




以上の内容はhttps://torus711.hatenablog.com/entry/2024/06/09/181036より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14