AtCoder

ABC323 E問題(Playlist)を解く

AtCoder_ABC323_E

AtCoder が提供しているABC(AtCoder Beginner Contest)323 のE問題をC++とPythonで解いてみました。ABC323は、2023年10月7日21:00に実施されました。

AtCoder の紹介はこちらに、プログラミングの方針はこちらに記事があります。

E問題 Playlist(Difficulty : 1279)

問題はリンク先をご覧ください。

ABC323 E問題 Playlist

確率DPで解くことができました。AtCoder Problems による Difficulty は、1279 でした。※2023年10月15日 Difficulty を追記しました。

解答案

確率を求める問題です。背反な事象のいずれかが発生する確率は、それぞれの事象の確率の和となります(和の法則)。

dp[i] を時刻 $i$ で曲が切り替わる確率とします。dp[i] は以下の式で求めることができます。以下の式で $i – t_j \;(0 \leqq j < n)$ が $0$ 以上の場合のみ加えます。確率の和になっているのは、それぞれが背反事象であるからです。

dp[i] = dp[i – t0] / N + … + dp[i – tN] / N

初期値は、

dp[0] = 1、それ以外は dp[i] = 0

とします。曲が切り替わる確率を求めることができれば、時刻 $X+0.5$ 秒後で、曲0 が流れている確率は、$0 \leqq i \leqq X$ における以下の和となります。

dp[i] / N (i + t0 > X の場合のみ加える)

確率の和になっているのは、それぞれの事象が背反であるからです。

C++ プログラム例(ABC323E)

MOD付の演算は、足し算、引き算、掛け算は自然にできますが、割り算は工夫が必要です。フェルマーの小定理を使えば、$N \pmod M$ で割ることは、$N^{M-2} \pmod M$ を掛けることと同じになります。

この計算をするために累乗を計算する関数 power を使います(7ー17行目)。この関数は自前ライブラリとして整備したものです。$N$ で割る場合に掛ける値を変数 invN として計算しておきます(28行目)。

先に考察した漸化式を計算します(35、44行目)。

以下が、C++プログラムとなります。

#include <bits/stdc++.h>
using namespace std;

#define MOD 998244353
typedef unsigned long long int ull;

ull power(ull n, ull p, ull mod)
{
	if (p == 0) {
		return 1;
	}
	if (p % 2 == 0) {
		ull t = power(n, p / 2, mod);
		return t * t % mod;
	}
	return (n * power(n, p - 1, mod))% mod;
}

int main()
{
	int n, x;
	cin >> n >> x;
	vector<int> t(n);
	for (int i = 0; i < n; ++i) {
		cin >> t[i];
	}

	ull invN = power(n, MOD - 2, MOD);

	vector<ull> dp(x + 1, 0);
	dp[0] = 1;
	for (int i = 1; i <= x; ++i) {
		for (int j = 0; j < n; ++j) {
			if (i - t[j] >= 0) {
				dp[i] += (dp[i - t[j]] * invN) % MOD;
				dp[i] %= MOD;
			}
		}
	}

	ull result = 0;
	for (int i = 0; i <= x; ++i) {
		if (i + t[0] > x) {
			result += (dp[i] * invN) % MOD;
			result %= MOD;
		}
	}

	cout << result << endl;

	return 0;
}

AtCoder では、AtCoder Library(ACL)と呼ばれるライブラリが提供されています。

ACL で、MOD付きの計算ができる型 modint も提供されています。この問題は、modint を使うと簡潔にプログラムを書くことができます。

  • 必要な宣言は、2行目、4行目、6行目です。
  • modint を使うことにより漸化式がすっきり書けています(22、30行目)。

以下は、modint を使ったプログラムです。

#include <bits/stdc++.h>
#include <atcoder/modint>
using namespace std;
using namespace atcoder;

typedef modint998244353 mint;

int main()
{
	int n, x;
	cin >> n >> x;
	vector<int> t(n);
	for (int i = 0; i < n; ++i) {
		cin >> t[i];
	}

	vector<mint> dp(x + 1, 0);
	dp[0] = 1;
	for (int i = 1; i <= x; ++i) {
		for (int j = 0; j < n; ++j) {
			if (i - t[j] >= 0) {
				dp[i] += dp[i - t[j]] / n;
			}
		}
	}

	mint result = 0;
	for (int i = 0; i <= x; ++i) {
		if (i + t[0] > x) {
			result += dp[i] / n;
		}
	
	}

	cout << result.val() << endl;

	return 0;
}

どちらも AC(Accepted=正しいプログラム)と判定されました。

Python プログラム例(ABC323E)

基本的な考え方は同じです。累乗は演算子 ** で計算できます。MOD計算付きの累乗は、組込み関数 pow で計算することができます(6行目)。

以下のプログラムは、「Python (CPython 3.11.4)」では、TLE判定となり、「Python (PyPy 3.10-v7.3.12)」で、AC判定となりました。

"""AtCoder Beginner Contest 323 E"""
MOD = 998244353
n, x = map(int, input().split())
t = list(map(int, input().split()))

invN = pow(n, MOD - 2, MOD)

dp = [0] * (x + 1)
dp[0] = 1
for i in range(1, x + 1):
    for j in range(n):
        if i - t[j] >= 0:
            dp[i] += (dp[i - t[j]] * invN) % MOD
            dp[i] %= MOD

result = 0
for i in range(x + 1):
    if i + t[0] > x:
        result += (dp[i] * invN) % MOD
        result %= MOD

print(result)

最後に

確率を用いるDPは苦手意識があって、解く前にあきらめていました。公式解説を読んで、漸化式を理解することができました。

確率DPは、一定の頻度で出題されるため、過去問を解こうと思います。

引き続き ABC の問題を紹介していきます。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA