TopCoder SRM527 Div1Hard

問題

ある国のコインは、次のような単純な規則に従っている。

  • それぞれのコインは、正の整数の価値を持つ。
  • 価値1のコインが存在する。
  • 任意のコインの価値は、それより価値の低いコインの価値の整数倍である。

ある金額を払う時、これらのコインを使って何通りの払い方があるか。

解法

金額が大きすぎてDPができない。しかし基本精神はDP。
簡単のため、コインの金額が1, 3, 6の場合を考える。
すると、DPで解く場合、

  • Ai = Bi = Ci = 0 (i < 0)
  • A0 = B0 = C0 = 1
  • Ai = Ai-1 (i >= 1)
  • Bi = Ai + Bi-3 (i >= 1)
  • Ci = Bi + Ci-6 (i >= 1)

という線形漸化式を考え、Ciが答えになることがわかる。
この漸化式は変形して、後の2式を

  • Bi = Ai-1 + Bi-3 (i >= 1)
  • Ci = Ai-1 + Bi-3 + Ci-6 (i >= 1)

としておく。(そのほうが後々都合が良い)
ここで、(Ai-1, Bi-1, Ci-1) -> (Ai, Bi, Ci) という線形変換があるととってもうれしい。しかし誰がどう考えてもそんな線形変換は存在しない。Biの決定にはBi-3が必要だからだ。
だが、iが「十分小さければ」Bi-3の項は0になるので無視できる。具体的には、i<=2ならばよい。
i=3の場合は、まずこの変換を3回行う(変換を複数回行うのは、行列の累乗なので簡単である)。すると、(Ai-3, Bi-3, Ci-3) -> (Ai, Bi, Ci)の変換「もどき」がわかる。「もどき」というのは、Bi-3が抜けているという意味である。ここにBi-3の項を補完してやればよい。
すると、まだCi-6の項を考えていないが、これも同様にすると補完できる。最終的に、(Ai-6, Bi-6, Ci-6) -> (Ai, Bi, Ci) の線形変換を表す行列までわかる。
結果、わかっている線形変換は、

  • (Ai-1, Bi-1, Ci-1) -> (Ai, Bi, Ci) (i<=2のとき用)
  • (Ai-3, Bi-3, Ci-3) -> (Ai, Bi, Ci) (i<=5のとき用)
  • (Ai-6, Bi-6, Ci-6) -> (Ai, Bi, Ci) (iはいつでもよい)

である。この3つを適切に組み合わせると、どんな場合でもCiが求められる。

コード

#include <vector>
using namespace std;

typedef long long i64;
const int MOD = 1000003;

int N;

struct matrix
{
	i64 val[40][40];
};

matrix mul(const matrix& A, const matrix& B)
{
	matrix ret;
	for(int i=0;i<N;i++){
		for(int j=0;j<N;j++){
			ret.val[i][j] = 0;
			for(int k=0;k<N;k++){
				ret.val[i][j] += A.val[i][k] * B.val[k][j];
			}
			ret.val[i][j] %= MOD;
		}
	}
	return ret;
}

matrix pow(const matrix& A, i64 p)
{
	if(p==1) return A;
	matrix tmp = pow(A, p/2);
	tmp = mul(tmp, tmp);
	if(p%2==1) tmp = mul(tmp, A);
	return tmp;
}

class P8XCoinChange
{
	i64 ret[40];
	
	void linear(const matrix& A)
	{
		i64 r2[40];
		for(int i=0;i<N;i++){
			r2[i] = 0;
			for(int j=0;j<N;j++) r2[i] += A.val[i][j] * ret[j];
			r2[i] %= MOD;
		}
		for(int i=0;i<N;i++) ret[i] = r2[i];
	}
	
public:
	int solve(i64 coins, vector<i64> val)
	{
		N = val.size();
		
		matrix op;
		for(int i=0;i<N;i++){
			ret[i] = 1;
			for(int j=0;j<N;j++) op.val[i][j] = 0;
		}
		
		for(int i=0;i<N;i++){
			for(int j=i;j<N;j++) op.val[j][i]++;
			
			i64 times = (i==N-1)?(coins / val[i]):((coins % val[i+1]) / val[i]);
			if(times) linear(pow(op, times));
			
			if(i!=N-1) op = pow(op, val[i+1] / val[i]);
			
		}
		return (int)ret[N-1];
		
	}
};