ABC 456

writer: Murakami1704

A - Dice

配点 : 100 点

問題文

6 つの面を持つサイコロが 3 個あります。どのサイコロも、面には 1, 2, 3, 4, 5, 6 が書かれています。
これらのサイコロを同時に振ったとき、出た目の合計が X になることはありますか?

制約

  • X は 1 以上 20 以下の整数

入力

入力は以下の形式で標準入力から与えられる。

X

出力

出た目の合計が X になることがあれば Yes、なければ No を出力せよ。


入力例 / 出力例

入力例 1
15
出力例 1
Yes

※出目が 4, 5, 6 のとき、合計は 15 となります。

入力例 2
2
出力例 2
No

解説

3個のサイコロの目をそれぞれ $a, b, c$ とすると、各サイコロについて $1 \le a, b, c \le 6$ となります。

出た目の合計 $a + b + c = X$ において:

  • 最小値: $a=1, b=1, c=1$ のとき、3
  • 最大値: $a=6, b=6, c=6$ のとき、18

したがって、3つのダイスを振ったとき、3以上18以下の値のすべてを作ることができます。

判定基準:
・入力 $X$ が 2以下 または 19以上のとき → No
・入力 $X$ が 3以上 18以下のとき → Yes

組み合わせの例(メモより抜粋)

合計 3: (1,1,1) 合計 11: (1,4,6) / (2,4,5) / (3,4,4) など
合計 4: (1,1,2) 合計 12: (1,5,6) / (2,4,6) / (4,4,4) など
合計 5: (1,1,3) / (1,2,2) 合計 13: (1,6,6) / (2,5,6) / (4,4,5) など
合計 6: (1,1,4) / (1,2,3) / (2,2,2) 合計 14: (2,6,6) / (3,5,6) / (4,5,5) など
合計 7: (1,1,5) / (1,2,4) / (2,2,3) 合計 15: (3,6,6) / (4,5,6) / (5,5,5)
合計 8: (1,1,6) / (1,3,4) / (2,3,3) 合計 16: (4,6,6) / (5,5,6)
合計 9: (1,2,6) / (1,4,4) / (3,3,3) 合計 17: (5,6,6)
合計 10: (1,3,6) / (2,4,4) / (2,2,6) など 合計 18: (6,6,6)

ソースコード (C++)

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

int main(void){
    int x;
    cin >> x;

    // 3以上18以下の範囲内か判定
    if(x >= 3 && x <= 18){
        cout << "Yes" << endl;
    }
    else {
        cout << "No" << endl;
    }

    return 0;
}

配点 : 200 点

問題文

6 つの面を持つサイコロが 3 個あります。$i$ 個目のサイコロの $j$ 個目の面には $A_{i,j}$ が書かれています。
これらのサイコロを同時に振ったとき、4, 5, 6 の書かれた目が 1 つずつ出る確率を求めてください。

制約

  • $A_{i,j}$ は 1 以上 6 以下の整数

入力例 / 出力例

入力例 1
1 2 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
出力例 1
0.0277777778
入力例 2
4 5 6 4 5 6
4 4 5 5 6 6
6 5 4 4 5 6
出力例 2
0.2222222222

解説

まず、サイコロの面が必ずしも 1, 2, 3, 4, 5, 6 とは限らない点に注意してください。
入力例2のように、同じ数字が複数ある場合や、特定の数字が含まれない場合があります。

解法:全探索

各サイコロは 6 面なので、3 つのサイコロの目の出方は全部で $6 \times 6 \times 6 = 216$ 通りです。
この 216 通りをすべて調べ、条件(4, 5, 6 が 1 つずつ出る)を満たす数を count とすると、答えは count / 216 となります。

1
2
3
4
5
6
1
2
3
4
5
6
1
2
3
4
5
6
→ カウントはふえない
1
2
3
4
5
6
1
2
3
4
5
6
1
2
3
4
5
6
→ カウントを増やす
1
2
3
4
5
6
1
2
3
4
5
6
1
2
3
4
5
6
→ カウントを増やす
実装のポイント:
count / 216 と書くと、count が int型の場合、結果が 0 になってしまうことがあります。
count / 216.0 と書くことで、double型として計算され、正しい小数を得ることができます。

条件の判定方法(詳細図解)

画像メモに基づいた、具体的な判定処理のイメージです。

1. ソート法

適当な配列に push_back してソートする方法

例: temp = [5 | 4 | 6]

↓ ソート

temp = [4 | 5 | 6]
このようになっていたらカウントを増やす

2. バケット法

データを添字とする配列でつくる

temp: [ 5 | 4 | 6 ]
0
1
0
2
0
3
1
4
1
5
1
6

ここが 1 かどうかで判断

3. 積と和
  • $4+5+6 = 15$
  • $4 \times 5 \times 6 = 120$

足して 15、かけて 120 は {4, 5, 6} しかない(1〜6の範囲内)

4. if文で
4|5|6 4|6|5 5|4|6 ... 6|5|4

のいずれかに当てはまったらカウント

「4, 5, 6 が 1 つずつ出る」という条件を判定するための主な実装例です:

1. ソート法(汎用的・推奨)
sort(t.begin(), t.end());
if(t[0] == 4 && t[1] == 5 && t[2] == 6){ count++; }
2. バケット法(カウント配列)
vector<int> temp(7, 0);
for(int i = 0; i < 3; i++){ 
    temp[t[i]]++;
}
if(temp[4] == 1 && temp[5] == 1 && temp[6] == 1){
    count++;
}
3. 積と和の利用
int sum = t[0] + t[1] + t[2];
int temp = t[0] * t[1] * t[2];
if(sum == 15 && temp == 120){
    count++;
}
4. すべて if 文で判別
int v1 = t[0], v2 = t[1], v3 = t[2];
if((v1 == 4 && v2 == 5 && v3 == 6) || (v1 == 4 && v2 == 6 && v3 == 5) ||
   (v1 == 5 && v2 == 4 && v3 == 6) || (v1 == 5 && v2 == 6 && v3 == 4) ||
   (v1 == 6 && v2 == 4 && v3 == 5) || (v1 == 6 && v2 == 5 && v3 == 4)){
    count++;
}

使用アルゴリズム:全探索


ソースコード (C++)

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

int main(void){
    vector<int> a(6), b(6), c(6);
    for(int i = 0; i < 6; i++) cin >> a[i];
    for(int i = 0; i < 6; i++) cin >> b[i];
    for(int i = 0; i < 6; i++) cin >> c[i];

    int count = 0;

    // 3重ループで全 216 通りを全探索
    for(int i = 0; i < 6; i++){
        for(int j = 0; j < 6; j++){
            for(int k = 0; k < 6; k++){
                vector<int> t;
                t.push_back(a[i]);
                t.push_back(b[j]);
                t.push_back(c[k]);

                // ソートして 4, 5, 6 になるか判定
                sort(t.begin(), t.end());
                if(t[0] == 4 && t[1] == 5 && t[2] == 6){
                    count++;
                }
            }
        }
    }

    // 誤差 10^-6 以下にするため double型で出力
    cout << fixed << setprecision(10);
    cout << count / 216.0 << endl;

    return 0;
}

配点 : 300 点

問題文

a, b, c からなる文字列 $S$ が与えられます。 $S$ の空でない部分文字列であって、同じ文字が隣り合わないものの個数を 998244353 で割った余りを求めてください。

※ 2 つの部分文字列が文字列として一致しても、取り出す位置が異なるならば区別します。

制約

  • $S$ は a, b, c からなる長さ 1 以上 $3 \times 10^5$ 以下の文字列

入力

S

出力

条件を満たす部分文字列の個数を 998244353 で割った余りを出力せよ。


入力例 / 出力例

入力例 1
abbc
出力例 1
6

※「ab」「bc」など、隣り合う文字が異なるものだけを数えます。

入力例 2
cabcabcbc...(略)
出力例 2
760

解説

1. 区間分割の考え方

「部分文字列」は元の文字列から連続した範囲を抜き出したものです。 もし元の文字列に aabb のように同じ文字が隣り合っている箇所があれば、その境界をまたいで抜き出した部分文字列は、必ず「同じ文字が隣り合う」ことになり、条件を満たしません

そのため、同じ文字が連続する地点を「区切り」として文字列を分割し、それぞれの独立した各区間内で条件を満たす部分文字列の数を計算します。

例:abbc  → [ ab ] | [ bc ]
(bとbの間で分割。ここをまたぐ「abb」などは数えません)

2. 三角数による計数(一般化と具体例)

隣り合う重複がない長さ $n$ の区間において、そこから取り出せる部分文字列の総数は、以下の図のように三角数の形で一般化できます。

【図解:長さ $n$ の区間から取れる部分文字列】
[○] [○] [○] [○] … [○]
(長さ1 の部分文字列 $n$ 個)
[○ ○] [○ ○] … [○ ○]
(長さ2 の部分文字列 $n-1$ 個)
[○ ○ ○] … [○ ○ ○]
(長さ3 の部分文字列 $n-2$ 個)
[○ ○ ○ ○ … ○]
(長さ$n$ の部分文字列 1 個)
合計: $n + (n-1) + (n-2) + \dots + 1 = \mathbf{\frac{n(n+1)}{2}}$
【図解:長さ $n$ の区間から取れる部分文字列の一例】
[a] [b] [c] [a] [b] [c]
(長さ 1 の部分文字列 6 個)
[ab] [bc] [ca] [ab] [bc]
(長さ 2 の部分文字列 5 個)
[abc] [bca] [cab] [abc]
(長さ 3 の部分文字列 4 個)
[abc] [bca] [cab] [abc]
(長さ 3 の部分文字列 4 個)
[abcab] [bcabc]
(長さ 5 の部分文字列 2 個)
[abcabc]
(長さ 6 の部分文字列 1 個)
合計: $\mathbf{\frac{6(6+1)}{2}} = 21$

3. シミュレーション:区間の抽出

文字列 abbcabbcabcc を左から走査し、同じ文字が隣り合った瞬間にそれまでの文字数を記録します。

a b|b c a b|b c a b c|c
  • 2文字目と3文字目の 'b' が同じなので分割 → 長さ 2 を記録
  • 6文字目と7文字目の 'b' が同じなので分割 → 長さ 4 を記録
  • 11文字目と12文字目の 'c' が同じなので分割 → 長さ 5 を記録
  • 最後に残った長さ 1 を記録

得られる長さのリスト: t = {2, 4, 5, 1}

使用アルゴリズム:しゃくとり法


ソースコード (C++)

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

int main(void){
    string s;
    cin >> s;

    int l = s.length();
    vector<long long> c;
    int count = 1;

    // 隣り合う文字を比較して区間の長さを確定させる
    for(int i = 1; i < l; i++){
        if(s[i] == s[i-1]){
            c.push_back(count); // 重複地点でそれまでの長さを保存
            count = 1;          // 新しい区間のカウントを開始
        }
        else {
            count++;
        }
    }
    c.push_back(count); // 最後の区間を忘れずに追加

    long long ans = 0;
    long long MOD = 998244353;

    // 各区間の長さ n に対して n*(n+1)/2 を加算
    for(int i = 0; i < c.size(); i++){
        ans += (c[i] * (c[i] + 1) / 2);
        ans %= MOD;
    }

    cout << ans << endl;
    return 0;
}

配点 : 400 点

問題文

a, b, c からなる文字列 $S$ が与えられます。 $S$ の空でない部分列であって、同じ文字が隣り合わないものの個数を 998244353 で割った余りを求めてください。

※部分列とは、元の文字列から0文字以上を取り除き、残りを順序を変えずに連結したものです(連続していなくても良い)。

制約

  • $S$ は a, b, c からなる長さ 1 以上 $3 \times 10^5$ 以下の文字列

入力

S

出力

条件を満たす部分列の個数を 998244353 で割った余りを出力せよ。


入力例 / 出力例

入力例 1
abbc
出力例 1
11

※連続しない「ac」なども部分列としてカウントされます。

入力例 2
cabcabcbcaccacbcbcaabacbacaabccacbccbcacbacbacabcacabcaccaaaaabababcbabacaccabbcacbcbcbcababcbcbabca
出力例 2
378217423

解説

1. 状態の定義とDPの考え方

C問題との最大の違いは、文字を飛ばして選べる「部分列」である点です。 「今見ている文字を末尾に追加できるか」は、直前に選んだ文字にのみ依存します。

次のように状態を保存していくことで、効率的に計算できます。

DPの状態:
dp[a] : 最後が 'a' で終わる、条件を満たす部分列の総数
dp[b] : 最後が 'b' で終わる、条件を満たす部分列の総数
dp[c] : 最後が 'c' で終わる、条件を満たす部分列の総数

2. 遷移のシミュレーション(abbc の場合)

各文字に到達したとき、それ以前の「自分と異なる文字で終わる部分列」の末尾に自分をくっつけることができます。

文字番目 文字 dp[a] dp[b] dp[c] 新たに作れる部分列
1 a 1 0 0 a
2 b 1 2 0 b, ab
3 b 1 4 0 b, ab (※位置が違うため区別)
4 c 1 4 6 c, ac, bc, bc, abc, abc

合計: $1 + 4 + 6 = \mathbf{11}$

3. 遷移式(一般化)

現在見ている文字 $S[i]$ が $x$ ($x \in \{a, b, c\}$) のとき:

dp[x] = (自分以外の DP の和) + 1
※ "+1" はその文字単体で作る部分列の分です。

例:$S[i] = a$ の場合

$$dp[a]_{\text{new}} = dp[b] + dp[c] + 1$$

使用アルゴリズム:動的計画法 (DP)


ソースコード (C++)

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

int main(void){
    string s;
    cin >> s;

    // dp[0]:'a'で終わる数, dp[1]:'b'で終わる数, dp[2]:'c'で終わる数
    vector<long long> dp(3, 0);
    long long MOD = 998244353;
    int l = s.length();

    for(int i = 0; i < l; i++){
        if(s[i] == 'a'){
            // 直前が 'b' か 'c'、もしくは 'a' 単体
            dp[0] = (dp[1] + dp[2] + 1) % MOD;
        }
        else if(s[i] == 'b'){
            // 直前が 'a' か 'c'、もしくは 'b' 単体
            dp[1] = (dp[0] + dp[2] + 1) % MOD;
        }
        else {
            // 直前が 'a' か 'b'、もしくは 'c' 単体
            dp[2] = (dp[0] + dp[1] + 1) % MOD;
        }
    }

    // すべての状態の和が答え
    long long ans = (dp[0] + dp[1] + dp[2]) % MOD;
    cout << ans << endl;

    return 0;
}