🪷

競プロ典型90問 005 Restricted Digits(★7)

2024/10/21に公開

★7の最高難度の問題がやっとこさ出来たので解説を書く。

問題概要

与えられた数字のみを使ってN桁の正の整数を作る。そのうちBの倍数は何個か。
答えを10^9+7で割った余ってから出力する。

問題の把握

問題が 「1, 4, 9を使って2桁を作る時、7の倍数はいくつか」なら
11, 14, 19, 41, 44, 49, 91, 94, 99
の中で7の倍数を数えればよい。14, 49, 91 の3つが7の倍数なので答えは3。

基本的な解法

x桁使った時の数を余りごとに

  • 余り0がa個
  • 余り1がb個
  • 余り2がc個

のようにメモしておくやり方が基本。次のようにする。

  • 1桁使った時の余りを求める
  • それにもう1桁追加して2桁使った時の余りを求める
  • それにもう1桁追加して3桁使った時の余りを求める

ところがこの問題で与えられる桁数は10^18桁なので上のやり方では時間がオーバーする。

効率的解法の概要

解法の名前がわからんので「効率的解法」と呼ぶ。
大きく2に分けたものを組み合わせるやり方をする。たとえば、

  • 7の余り
  • 使える数字は 1, 4, 9
  • 7桁

のとき、7桁を上の3桁と下の4桁に分ける。
すでに3桁使ったときの余りデータと4桁使ったときの余りデータがあるとする。これを使って7桁の余りデータを作る。
余りデータとは、余りaがx個、余りbがy個…というもの。
余りの組み合わせからひとつ取り上げる。3桁の 449(余り1)と4桁の1949(余り3)から4491949の余りを求める。4491949は

4491949 = 449 * 10000 + 1949

なので10000の余りがわかればいい。これは4。4491949の余りを求めるのに必要なデータは揃った。

  • 449(余り1)
  • 1949(余り3)
  • 10000(余り4)

この3つのデータから(1 * 4 + 3) % 7 = 0となるので4491949は余り0となる。
効率的解法では、このように大きく分けた2つを組み合わせるやり方をする。
例えば、全体を10個に分けてそのうち2つを組み合わせて9個にし、また2つを組み合わせて8個…とする。
その分解の仕方だが効率のよい分解は
1桁、2桁、4桁、8桁、16桁…
となる。例えば7桁なら
7 = 4 + 2 + 1
152桁は
152 = 128 + 16 + 8
の組み合わせになる。2のべき乗にするとどんな数にも対応できる(問題の範囲内で)。

効率的解法の必要なデータ

必要なデータは

  • シフトデータ(2つのデータを組み合わせるときに片方をシフトするために必要。上の例では「1000の余りが4」というデータ)
  • 2のべき乗桁の余りデータ

となる。2のべき乗桁の余りデータのうち必要なものをシフトさせながら組み合わせる。
ではデータの詳細。

シフトデータ

2つの余りデータを組み合わせる時に使う。シフトデータは

  • 1桁シフト用に10を7で割った余り
  • 2桁シフト用に100を7で割った余り
  • 4桁シフト用に10000を7で割った余り
  • 8桁シフト用に100000000を7で割った余り
  • つづく

と用意すればよい。100000000を7で割った余りは10000を7で割った余りを2乗する方法で求める。

2のべき乗桁の余りデータ

1, 4, 9の3つを使って

  • 1桁を作る時の余りデータ
  • 2桁を作る時の余りデータ
  • 4桁を作る時の余りデータ
  • 8桁を作る時の余りデータ
  • つづく

を作成する。8桁の余りデータは、4桁の余りデータを2つ用意して片方を4桁シフトする。

効率的解法の中心処理

準備が出来たので計算する。小さい方から処理する。

与えられた桁が7なら
7 = 4 + 2 + 1
なので

  • 1桁のデータを作成
  • 2桁を足して3桁のデータを作成
  • 4桁を足して7桁のデータを作成

となる。
152桁の
152 = 128 + 16 + 8
なら

  • 1桁から4桁は無視
  • 8桁のデータを作成
  • 16桁を足して24桁のデータを作成
  • 32桁と64桁は無視
  • 128桁を足して152桁のデータを作成

となる。

もうちょっと具体的に書く。
ポイントは「0桁なら余り0が1個」という状態から出発すること。
例として11桁を求める。

  • 「0桁なら余り0が1個」の状態を作って現在のデータとする
  • 1桁のデータを追加する。これは既存のデータを1つ左にシフトしてから1桁のデータを追加する。
  • 2桁のデータを追加する。これは既存のデータを2つ左にシフトしてから2桁のデータを追加する。
  • 4桁のデータは追加しない
  • 8桁のデータを追加する。これは既存のデータを8つ左にシフトしてから8桁のデータを追加する。

以上で説明は終わり。

シフトデータ何を使うか

この問題の難しさのひとつが各段階で「シフトデータ何を使うか」で、

  • x桁使ったときの余りデータの作成
  • 答に向かって情報を追加する過程で、x桁を追加する時

この2つは使うシフトデータがずれています。
x桁使った時の余りデータの作成ではx/2桁を2つ組み合わせてx桁を作るのでx/2桁のシフトデータを使う。
最後の、答を得る工程でx桁を追加する時は、それまでのものをx桁シフトしてからx桁の情報を入れるのでx桁のシフトデータを使う。


以上で小課題3の説明は終わりだが、小課題2について少し補足する。

小課題2について

小課題2のC++の模範解答の行列Aは後ろから適用させるものになっている。

Xの中のある余りの情報(赤丸)が、行列内で影響を与えるのは横方向。

for (int i = 0; i < B; i++) {
 for (int j = 1; j <= K; j++) {
  int nex = (i * 10 + C[j]) % B;
  A.x[i][nex] += 1;
 }
}

行列内の横方向の位置nexは、Xの中のある余りの情報(i)に10倍して各Cを足した値。

Discussion