概要

4/27(日)に実施のABC403のD問題には、C問題までを40分以内で解き終えた後、残り1時間をフルに使って挑戦しました。コンテスト終了1分前に何とかコードを提出したものの、実行結果は、RE(ランタイムエラー)でした。コンテスト終了後に自分でデバッグを試み、2つのバグを修正したものの結局WA(不正解)は解消せず、特定の入力パターンでエラーが発生する原因を突き止められませんでした。ランダムテストを繰り返してようやく例外ケースを発見するも、そこで修正を断念。代わりに公式の解説と解説動画を視聴し、正しい解法を理解したうえで再度コーディングし、無事AC(正解)を獲得しました。


問題

 

解法の流れ

ACに至ったアルゴリズムは、以下のステップです。

  1. 度数分布を作成
    入力配列 a の各値の出現頻度をカウントする。

  2. 幅 d=0 の場合
    幅が0の場合は、各値の度数から1を引いたものをすべて合計して返す。

  3. 幅 d≥1 の場合

    • 度数分布を「幅 d」ごとに飛ばし取り出し、新しいリスト array を作成する。

    • array の要素を選ぶ際、隣り合うインデックスを同時に選ばないようにしつつ、
      残した度数の和を最大化するために動的計画法(DP)を用いる。

  4. 開始位置をずらして繰り返し
    リスト array のスタート位置を 0 から d−1 まで変えながら、上記手順を計 d 回実施。

  5. 最終結果の計算
    元の度数分布の合計度数 ー 各 array で選ばれた要素の度数の最大和 を答えとして出力。

特にステップ3の“隣接項目を同時に残さないDP”の組み方がポイントでした。


 

ソースコード
#include <vector>
#include <algorithm>
using ll = long long;
static const int MAX_B = /* 適宜定義 */;

int solve(ll n, ll d, const std::vector& a) {
    // 度数分布を作成
    std::vector b(MAX_B);
    for (int i = 0; i < n; ++i) {
        b[a[i]]++;
    }

    // 幅 d=0 の特別処理
    if (d == 0) {
        ll ans = 0;
        for (int i = 0; i < MAX_B; ++i) {
            if (b[i] >= 2) {
                ans += (b[i] - 1);
            }
        }
        return ans;
    }

    // d > 0 の場合
    ll ans = 0;
    for (int start = 0; start < d; ++start) {
        // d 間隔で飛ばし取り出すリストと総和
        std::vector arr;
        ll total = 0;
        for (int i = start; i < MAX_B; i += d) {
            arr.push_back(b[i]);
            total += b[i];
        }

        // 隣接しない要素の和を DP で最大化
        ll prev = 0, curr = arr.empty() ? 0 : arr[0];
        for (size_t i = 1; i < arr.size(); ++i) {
            ll tmp = curr;
            curr = std::max(curr, prev + arr[i]);
            prev = tmp;
        }

        ans += (total - curr);
    }
    return ans;
}

振り返り・気づき

  • DPの練習不足:d飛ばしで、隣接する度数を消していく際の考え方の整理ができず、DPを使うまでに考えが至らなかった。DPについての典型問題を解くべき。(解説動画で、問題 - Educational DP Contest / DP まとめコンテスト や、アルゴ式を紹介)
  • 問題文の条件見落とし:度数分布の配列サイズをnとしていたが、100000としなければならなかった。また、aの値も、1以上しか考慮しておらず、0も考慮に入れるべきだった。

  • ランダムテスト:ランダムテストは想定外の境界ケースでのバグ発見に有効だった。ランダムテストのコーディングも10分ほどで組めるようになる必要あり。

  • 解説動画:実装パターンとDP設計の解説が非常にわかりやすく、理解が飛躍的に深まった。


コメントやアドバイス、大歓迎です!