ABC220 D - FG operation C++解答例
AtCoder Beginner Contest 220 D - FG operationをC++で解きます。
問題
問題文をAtCoderのページより引用します。
問題文
0以上9以下の整数からなる長さ
の数列 N があり、この順に左から右に並んでいます。 A = (A_1, \ldots, A_N)
数列の長さが1になるまで、操作または操作 F を繰り返し行います。 G
- 操作
:左端の2つの値( F とする)を削除した後、一番左に x, y を挿入する (x + y) \% 10 - 操作
:左端の2つの値( G とする)を削除した後、一番左に x, y を挿入する (x \times y) \% 10 なお、
は a \% b を a で割った余りを意味します。 b
について、以下の問題に答えてください。 K = 0, 1, \dots, 9 操作手順としてあり得るものは
通りありますが、このうち最終的に残る値が 2^{N - 1} となる操作手順は何通りありますか? K
ただし答えは非常に大きくなる可能性があるので、998244353で割った余りを求めてください。
制約
2 \leq N \leq 10^5 0 \leq A_i \leq 9 - 入力はすべて整数
解答例
動的計画法を用いる
問題文にある通り、操作手順の全ての場合の数は
別の解き方を考えてみましょう。
ここで、与えられた2つの操作の内容を観察すると、どちらの操作も「数列の左端の2つの要素に対して演算をした後、その2つの要素と演算結果を置換する」操作であることがわかります。
また、1回の操作を行うために必要な情報は、現在の数列の左端にある2つの要素のみであることもわかります。
すなわち、それまでに行った操作の結果に関係なく、今ある数列の左端の要素だけが現在の操作の結果に影響するということです。
このような問題は動的計画法を用いて解くことができそうです。
DPテーブルを設計する
DPテーブルの構造
動的計画法で解けそうなことがわかったので、次にDPテーブルを設計します。
DPテーブルを以下のように定義します。
ただし、
DPの遷移
次に遷移を考えます。今回の動的計画法は、
そのような操作手順の場合の数は
このとき、
2つの操作のうちのどちらかを行う際、演算の対象となるのは数列の左端の要素です。
今、一番左端にある要素の値は
以上を踏まえると、
- 操作
:F (j + A_{i}) \% 10 - 操作
:G (j \times A_{i}) \% 10
- 操作
:F dp[i][(j + A_{i}) \% 10] = dp[i][(j + A_{i}) \% 10] + dp[i - 1][j] - 操作
:G dp[i][(j \times A_{i}) \% 10] = dp[i][(j \times A_{i}) \% 10] + dp[i - 1][j]
尚、DPテーブルを更新した際に998244353の余りを取るのを忘れないように注意してください。
DPテーブルの初期条件
今回の動的計画法ではDPテーブルの値として場合の数をとっているので、DPテーブルの各要素の初期値は0にしておきます。
また、まだ操作を行っていない状態(「0回目の操作を行った後の状態」と表現します)のとき、数列の左端の要素の値は
「何も操作を行わない」ことも1通りと数えられるので、DPテーブルの
プログラムの実装例
以上を踏まえてプログラムを実装します。設計した通りにDPテーブルを実装すればよいでしょう。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
int main() {
int64_t N;
std::cin >> N;
std::vector<int64_t> A(N);
for (int64_t i = 0; i < N; i++) {
std::cin >> A.at(i);
}
// DPテーブル。各要素の初期値は0にする。
std::vector<std::vector<int64_t>> dp(N, std::vector<int64_t>(10, 0));
// 初期条件
dp.at(0).at(A.at(0)) = 1;
// DPループ
for (int64_t i = 1; i < N; i++) {
for (int64_t j = 0; j < 10; j++) {
if (dp.at(i - 1).at(j) > 0) {
dp.at(i).at((j + A.at(i)) % 10) += dp.at(i - 1).at(j);
dp.at(i).at((j * A.at(i)) % 10) += dp.at(i - 1).at(j);
dp.at(i).at((j + A.at(i)) % 10) = dp.at(i).at((j + A.at(i)) % 10) % 998244353;
dp.at(i).at((j * A.at(i)) % 10) = dp.at(i).at((j * A.at(i)) % 10) % 998244353;
}
}
}
for (int64_t i = 0; i < 10; i++) {
std::cout << dp.at(N - 1).at(i) << std::endl;
}
return 0;
}
実際に提出したコードはこちら。
-
要素数が
の数列に対して操作N と操作F は合計でG 回まで行われるためです。また、初期条件としてまだ操作をしていない(「0回目の操作」と表現します)ときの数列の左端の要素が必要となるので、N - 1 のときの情報もDPテーブルに含めるようにしています。 ↩︎i = 0 -
数列
を0-indexedで考えると、A となります。1回目の操作の演算対象はA = (A_0, A_1, \ldots, A_{N - 1}) とA_0 で、2回めの操作の演算対象は、1回目の操作の演算結果およびA_1 です。従って、A_2 回目の操作の演算対象は、i 回目の操作の演算結果と、i - 1 ということになります。A_{i} はi までと定義されているので、配列の領域外参照を起こす心配はありません。 ↩︎N - 1
Discussion