競技プログラミング演習:基礎アルゴリズムから動的計画法への実践

L1-1 自動コード生成

入力された整数をそのまま特定の出力形式に変換する基礎問題です。標準入力から数値を読み込み、指定の文字列フォーマットに埋め込んで標準出力に送ります。

void process() {
    int input_val;
    std::cin >> input_val;
    std::cout << "print(" << input_val << ")";
}

L1-2 線形結合の計算

二つの整数パラメータを受け取り、その合計から定数1を引いた結果を返す単純な演算タスクです。

void process() {
    int a, b;
    std::cin >> a >> b;
    std::cout << (a + b - 1) << "\n";
}

L1-3 軌道限界判定

質量、天体タイプ、安全距離を入力とし、タイプに応じた係数で計算された限界値が安全範囲内に収まるかを判定します。結果に応じて異なるステータス文字列を付与して出力します。

void process() {
    double mass, type, limit;
    std::cin >> mass >> type >> limit;
    double factor = (type == 0.0) ? 2.455 : 1.26;
    double result = mass * factor;
    std::printf("%.2f %s\n", result, (result <= limit) ? "^_^" : "T_T");
}

L1-4 栄養摂取アドバイス

性別、身長、年齢の三要素に基づき、条件分岐を適用して適切な推奨メッセージを生成します。閾値をまとめて評価することで、ネストされた条件式をフラットな構造に最適化しています。

void process() {
    int gender, height, age;
    std::cin >> gender >> height >> age;
    int h_thresh = (gender == 1) ? 130 : 129;
    int a_thresh = (gender == 1) ? 27 : 25;
    
    std::string height_msg = (height == h_thresh) ? "wan mei!" : ((height > h_thresh) ? "ni li hai!" : "duo chi yu!");
    std::string age_msg = (age == a_thresh) ? "wan mei!" : ((age > a_thresh) ? "shao chi rou!" : "duo chi rou!");
    
    std::cout << height_msg << ' ' << age_msg << "\n";
}

L1-5 桁和の不変性検証

与えられた数を2倍にした場合の各桁の和を基準とし、3倍から9倍までの範囲で同じ桁和が維持されるかを検証します。条件を満たせば基準和を、満たさなければ否定応答を出力します。

int calculate_digit_sum(int val) {
    int sum = 0;
    while (val > 0) {
        sum += val % 10;
        val /= 10;
    }
    return sum;
}

void process() {
    int num;
    std::cin >> num;
    int base_sum = calculate_digit_sum(num * 2);
    bool is_consistent = true;
    for (int i = 3; i <= 9; ++i) {
        if (calculate_digit_sum(num * i) != base_sum) {
            is_consistent = false;
            break;
        }
    }
    std::cout << (is_consistent ? std::to_string(base_sum) : "NO") << "\n";
}

L1-6 文字列パターン適合チェック

入力文字列が特定の大小文字交互ルールと文字コードの連続性ルールに従っているかを逐次検証します。上位の条件が崩れた時点で検証を中断し、最終的な適合判定を出力します。

void process() {
    std::string text;
    std::cin >> text;
    bool valid = true;
    for (size_t i = 1; i < text.length(); ++i) {
        char prev = text[i-1];
        char curr = text[i];
        bool prev_upper = (prev >= 'A' && prev <= 'Z');
        
        if (prev_upper) {
            if (std::abs(prev - curr) != 32 && prev + 1 != curr) {
                valid = false; break;
            }
        } else {
            if (std::abs(prev - curr) != 32 && prev - 1 != curr) {
                valid = false; break;
            }
        }
    }
    std::cout << (valid ? "Y" : "N") << "\n";
}

L1-7 行列の列方向シフト処理

奇数列に対して下部への循環シフトを適用し、上位に空いた領域を指定の埋め込み値で補完します。シフト量は列の進行に伴い累積され、上限値に達するとリセットされます。処理完了後に各行の要素総和を計算・出力します。

void process() {
    int size, shift_limit, filler;
    std::cin >> size >> shift_limit >> filler;
    std::vector matrix(size, std::vector<int>(size));
    for (int i = 0; i < size; ++i) {
        for (int j = 0; j < size; ++j) {
            std::cin >> matrix[i][j];
        }
    }
    
    int shift_step = 0;
    for (int col = 0; col < size; ++col) {
        if (col % 2 != 0) {
            shift_step = (shift_step % shift_limit) + 1;
            std::vector<int> temp(size);
            for (int row = 0; row < size; ++row) temp[row] = matrix[row][col];
            
            for (int row = size - 1; row >= shift_step; --row) {
                matrix[row][col] = temp[row - shift_step];
            }
            for (int row = 0; row < shift_step; ++row) {
                matrix[row][col] = filler;
            }
        }
    }
    
    for (int row = 0; row < size; ++row) {
        int row_sum = 0;
        for (int val : matrix[row]) row_sum += val;
        std::cout << row_sum << (row == size - 1 ? "" : " ");
    }
    std::cout << "\n";
}

L1-8 素数トリプレットの探索

指定された範囲内から3つの異なる素数を選択し、それらの線形結合(積と和の組み合わせ)がすべて素数となる組み合わせの総数を数えます。効率的な素数判定関数を用いて三重ループを構成します。

bool is_prime(int num) {
    if (num < 2) return false;
    if (num == 2) return true;
    if (num % 2 == 0) return false;
    for (int i = 3; i * i <= num; i += 2) {
        if (num % i == 0) return false;
    }
    return true;
}

void process() {
    int lower, upper, count = 0;
    std::cin >> lower >> upper;
    for (int i = lower; i < upper - 1; ++i) {
        if (!is_prime(i)) continue;
        for (int j = i + 1; j < upper; ++j) {
            if (!is_prime(j)) continue;
            for (int k = j + 1; k <= upper; ++k) {
                if (!is_prime(k)) continue;
                if (is_prime(i * j + k) && is_prime(i * k + j) && is_prime(j * k + i)) {
                    count++;
                }
            }
        }
    }
    std::cout << count << "\n";
}

L2-1 包装ラインのマッピング管理

バッチ単位で入力されるバッジ識別子とボックス番号の対応表をハッシュマップで構築します。構築後、クエリ識別子に対して登録されたボックス番号を検索し、未登録の場合はエラー文字列を返します。

void process() {
    int total_badges, batch_size;
    std::cin >> total_badges >> batch_size;
    std::vector badge_names(total_badges);
    for (int i = 0; i < total_badges; ++i) std::cin >> badge_names[i];
    
    std::unordered_map badge_to_box;
    for (int batch = 0; batch < total_badges / batch_size; ++batch) {
        for (int item = 0; item < batch_size; ++item) {
            int box_id;
            std::cin >> box_id;
            int badge_idx = batch * batch_size + item;
            badge_to_box[badge_names[badge_idx]] = box_id;
        }
    }
    
    int queries;
    std::cin >> queries;
    while (queries--) {
        std::string query;
        std::cin >> query;
        auto it = badge_to_box.find(query);
        if (it != badge_to_box.end()) std::cout << it->second << "\n";
        else std::cout << "Wrong Number\n";
    }
}

L2-2 ソーシャルアクティビティランキング

各ユーザーの「いいね」対象IDリストを受け取り、重複を除いたユニーク数を計算します。ユニーク数と総数の優先順位に基づき降順にソートし、上位3ユーザーの名称を出力します。ユーザー数が不足する場合はダミー文字列で埋めます。

struct UserStats {
    std::string name;
    int total_likes;
    int unique_likes;
};

void process() {
    int user_count;
    std::cin >> user_count;
    std::vector<UserStats> users(user_count);
    for (int i = 0; i < user_count; ++i) {
        std::cin >> users[i].name >> users[i].total_likes;
        std::vector<int> liked_ids(users[i].total_likes);
        for (int &id : liked_ids) std::cin >> id;
        std::sort(liked_ids.begin(), liked_ids.end());
        users[i].unique_likes = std::unique(liked_ids.begin(), liked_ids.end()) - liked_ids.begin();
    }
    
    std::sort(users.begin(), users.end(), [](const UserStats& a, const UserStats& b) {
        if (a.unique_likes != b.unique_likes) return a.unique_likes > b.unique_likes;
        return a.total_likes > b.total_likes;
    });
    
    for (int i = 0; i < 3; ++i) {
        std::cout << (i < user_count ? users[i].name : "-") << (i == 2 ? "" : " ");
    }
    std::cout << "\n";
}

L2-3 二分木の側面シルエット復元

中間順走査と後順走査の配列から二分木を再構築します。再構築過程中に各深度のノードを記録し、各層の左端と右端の値を抽出することで、木の左右のシルエットを出力します。

struct TreeNode { int val; TreeNode* left; TreeNode* right; };

int build_idx;
TreeNode* reconstruct(const std::vector<int>& inorder, const std::vector<int>& postorder) {
    if (inorder.empty()) return nullptr;
    int root_val = postorder[build_idx--];
    TreeNode* node = new TreeNode{root_val, nullptr, nullptr};
    auto it = std::find(inorder.begin(), inorder.end(), root_val);
    int split = std::distance(inorder.begin(), it);
    node->right = reconstruct(std::vector<int>(inorder.begin() + split + 1, inorder.end()), postorder);
    node->left = reconstruct(std::vector<int>(inorder.begin(), inorder.begin() + split), postorder);
    return node;
}

void collect_levels(TreeNode* root, int depth, std::vector& levels) {
    if (!root) return;
    if (depth >= levels.size()) levels.push_back({});
    levels[depth].push_back(root->val);
    collect_levels(root->left, depth + 1, levels);
    collect_levels(root->right, depth + 1, levels);
}

void process() {
    int n; std::cin >> n;
    std::vector<int> inorder(n), postorder(n);
    for (int &x : inorder) std::cin >> x;
    for (int &x : postorder) std::cin >> x;
    
    build_idx = n - 1;
    TreeNode* root = reconstruct(inorder, postorder);
    std::vector levels;
    collect_levels(root, 0, levels);
    
    std::cout << "R:";
    for (auto &lvl : levels) std::cout << " " << lvl.back();
    std::cout << "\nL:";
    for (auto &lvl : levels) std::cout << " " << lvl.front();
    std::cout << "\n";
}

L2-4 インタラクティブゲームのシミュレーション

複数のシーンと選択肢からなるグラフを定義し、プレイヤーの操作(シーン遷移、セーブ、ロード)を逐次処理します。現在位置を追跡し、セーブスロットへの状態保存と復元を正確に模擬して最終的なシーンIDを出力します。

void process() {
    int scenes, commands;
    std::cin >> scenes >> commands;
    std::vector options(scenes);
    for (int i = 0; i < scenes; ++i) {
        int k; std::cin >> k;
        options[i].resize(k);
        for (int &dst : options[i]) std::cin >> dst;
    }
    
    int current_scene = 1;
    std::vector<int> save_slots(commands + 1);
    while (commands--) {
        int cmd, slot;
        std::cin >> cmd >> slot;
        if (cmd == 0) {
            current_scene = options[current_scene - 1][slot - 1];
        } else if (cmd == 1) {
            save_slots[slot] = current_scene;
            std::cout << current_scene << "\n";
        } else {
            current_scene = save_slots[slot];
        }
    }
    std::cout << current_scene << "\n";
}

L3-2 予算制約下での報酬最大化

通常の0-1ナップサック問題ではコスト容量が巨大でTLEするケースに対し、DPの軸を「報酬」に逆転させる手法を適用します。各報酬達成に必要な最小コストを計算し、予算範囲内に収まる最大の報酬を探索して出力します。

void process() {
    int items, budget;
    std::cin >> items >> budget;
    std::vector<int> costs(items), rewards(items);
    int max_reward = 0;
    for (int i = 0; i < items; ++i) std::cin >> costs[i];
    for (int i = 0; i < items; ++i) { std::cin >> rewards[i]; max_reward += rewards[i]; }
    
    std::vector<int> min_cost(max_reward + 1, budget + 1);
    min_cost[0] = 0;
    
    for (int i = 0; i < items; ++i) {
        for (int r = max_reward; r >= rewards[i]; --r) {
            if (min_cost[r - rewards[i]] + costs[i] < min_cost[r]) {
                min_cost[r] = min_cost[r - rewards[i]] + costs[i];
            }
        }
    }
    
    for (int r = max_reward; r >= 0; --r) {
        if (min_cost[r] <= budget) {
            std::cout << r << "\n";
            return;
        }
    }
}

6月15日 17:25 投稿