競技プログラミング問題の解法と実装

A - 二つのオーブンを使用した最小調理時間

N個の料理を連続して調理するのに必要な時間がT_i分与えられます。二つのオーブンを使用する場合の全料理の最短調理時間を求めます。

解法

動的計画法を用いて、一方のオーブンで実現可能な調理時間の組み合わせを求め、最小の最大調理時間を探索します。

#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> times(n);
    int total = 0;
    for (int i = 0; i < n; i++) {
        cin >> times[i];
        total += times[i];
    }
    
    vector<bool> dp(total + 1, false);
    dp[0] = true;
    for (int t : times) {
        for (int j = total; j >= t; j--) {
            if (dp[j - t]) dp[j] = true;
        }
    }
    
    int result = total;
    for (int i = 0; i <= total; i++) {
        if (dp[i]) {
            result = min(result, max(i, total - i));
        }
    }
    cout << result << endl;
    return 0;
}

B - 二部グラフにおける最大マッチング

n個の点が与えられ、x_i < x_jかつy_i < y_jの条件を満たすペアを形成できます。一点が複数のペアに使用されない場合の最大ペア数を求めます。

解法

ハンガリアンアルゴリズムを用いた二部グラフの最大マッチングを実装します。

#include <vector>
#include <iostream>
#include <cassert>
using namespace std;

class BipartiteMatcher {
    vector<vector<int>> graph;
    vector<int> matchA, matchB;
    vector<int> visited;
    int timestamp;
    
public:
    BipartiteMatcher(int n, int m) : graph(n), matchA(n, -1), matchB(m, -1), visited(n, 0), timestamp(0) {}
    
    void addEdge(int a, int b) {
        graph[a].push_back(b);
    }
    
    bool dfs(int a) {
        if (visited[a] == timestamp) return false;
        visited[a] = timestamp;
        for (int b : graph[a]) {
            if (matchB[b] == -1 || dfs(matchB[b])) {
                matchA[a] = b;
                matchB[b] = a;
                return true;
            }
        }
        return false;
    }
    
    int solve() {
        int count = 0;
        for (int i = 0; i < graph.size(); i++) {
            timestamp++;
            if (dfs(i)) count++;
        }
        return count;
    }
};

int main() {
    int n;
    cin >> n;
    vector<pair<int, int>> reds(n), blues(n);
    for (auto &p : reds) cin >> p.first >> p.second;
    for (auto &p : blues) cin >> p.first >> p.second;
    
    BipartiteMatcher matcher(n, n);
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (reds[i].first < blues[j].first && reds[i].second < blues[j].second) {
                matcher.addEdge(i, j);
            }
        }
    }
    cout << matcher.solve() << endl;
    return 0;
}

C - ドミノ配置の可能性判定

n×mグリッドにk個の水平ドミノと残りを垂直ドミノで敷き詰める可能性を判定します。

解法

nとmの偶奇に基づいて場合分けし、数学的に可能性を判定します。

#include <iostream>
using namespace std;

void solve() {
    int n, m, k;
    cin >> n >> m >> k;
    if (n % 2 == 0 && m % 2 == 0) {
        cout << (k % 2 ? "NO" : "YES") << endl;
    } else if (n % 2 == 1) {
        int required = m / 2;
        if (k < required || (k - required) % 2 != 0) {
            cout << "NO" << endl;
        } else {
            cout << "YES" << endl;
        }
    } else {
        int vertical_needed = n / 2;
        int vertical_available = n * m / 2 - k;
        if (vertical_available < vertical_needed || (vertical_available - vertical_needed) % 2 != 0) {
            cout << "NO" << endl;
        } else {
            cout << "YES" << endl;
        }
    }
}

int main() {
    int t;
    cin >> t;
    while (t--) solve();
    return 0;
}

D - 三配列からの最大k個の合計値

三つの配列から要素を一つずつ選び、その和が大きい順にk個出力します。

解法

まず二つの配列の和の上位k個を計算し、それらと三つ目の配列の和の上位k個を求めます。

#include <vector>
#include <queue>
#include <algorithm>
#include <iostream>
using namespace std;
using ll = long long;

int main() {
    int x, y, z, k;
    cin >> x >> y >> z >> k;
    vector<ll> A(x), B(y), C(z);
    for (auto &a : A) cin >> a;
    for (auto &b : B) cin >> b;
    for (auto &c : C) cin >> c;
    
    priority_queue<ll, vector<ll>, greater<ll>> pq;
    for (ll a : A) {
        for (ll b : B) {
            ll sum = a + b;
            if (pq.size() < k) {
                pq.push(sum);
            } else if (pq.top() < sum) {
                pq.pop();
                pq.push(sum);
            }
        }
    }
    
    vector<ll> ab_sums;
    while (!pq.empty()) {
        ab_sums.push_back(pq.top());
        pq.pop();
    }
    
    vector<ll> results;
    for (ll ab : ab_sums) {
        for (ll c : C) {
            results.push_back(ab + c);
        }
    }
    sort(results.rbegin(), results.rend());
    for (int i = 0; i < k; i++) {
        cout << results[i] << endl;
    }
    return 0;
}

E - 木上の塗り分けゲーム

二人のプレイヤーが交互に木の頂点を塗り、先手がより多くの頂点を塗れるか判定します。

解法

各頂点への距離を計算し、先手が到達可能な頂点数を比較します。

#include <vector>
#include <iostream>
using namespace std;

void dfs(const vector<vector<int>> &tree, vector<int> &dist, int u, int parent) {
    for (int v : tree[u]) {
        if (v == parent) continue;
        dist[v] = dist[u] + 1;
        dfs(tree, dist, v, u);
    }
}

int main() {
    int n;
    cin >> n;
    vector<vector<int>> tree(n + 1);
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        tree[u].push_back(v);
        tree[v].push_back(u);
    }
    
    vector<int> dist1(n + 1), dist2(n + 1);
    dfs(tree, dist1, 1, 0);
    dfs(tree, dist2, n, 0);
    
    int count1 = 0, count2 = 0;
    for (int i = 1; i <= n; i++) {
        if (dist1[i] <= dist2[i]) count1++;
        else count2++;
    }
    cout << (count1 > count2 ? "Fennec" : "Snuke") << endl;
    return 0;
}

F - 乗算表におけるk番目の要素

n×m乗算表でk番目に大きい要素を求めます。

解法

二分探索を用いて、各数値以下の要素数を効率的に数えます。

#include <iostream>
using namespace std;
using ll = long long;

int main() {
    ll n, m, k;
    cin >> n >> m >> k;
    ll left = 1, right = n * m;
    while (left <= right) {
        ll mid = (left + right) / 2;
        ll count = 0;
        for (ll i = 1; i <= n; i++) {
            count += min(mid / i, m);
        }
        if (count >= k) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    cout << left << endl;
    return 0;
}

G - 二進数操作による数値変換

二進数操作を繰り返して数値xをyに変換できるか判定します。

解法

深さ優先探索を用いて可能な操作を試行し、変換可能性をチェックします。

#include <unordered_set>
#include <iostream>
using namespace std;
using ll = long long;

ll reverse_bits(ll x) {
    ll result = 0;
    while (x) {
        result = (result << 1) | (x & 1);
        x >>= 1;
    }
    return result;
}

bool dfs(ll current, ll target, unordered_set<ll> &visited) {
    if (current == target) return true;
    if (current > target * 2 || visited.count(current)) return false;
    visited.insert(current);
    return dfs(reverse_bits(current), target, visited) || 
           dfs(reverse_bits(current * 2 + 1), target, visited);
}

int main() {
    ll x, y;
    cin >> x >> y;
    unordered_set<ll> visited;
    cout << (dfs(x, y, visited) ? "YES" : "NO") << endl;
    return 0;
}

H - りんごの最安購入法

単品購入と3個セット購入を組み合わせて、n個のりんごを最小コストで購入します。

解法

セット購入が単品購入より安い場合、可能な限りセットを利用します。

#include <iostream>
using namespace std;

int main() {
    int single, set, n;
    cin >> single >> set >> n;
    int set_count = n / 3;
    int single_count = n % 3;
    int cost1 = set_count * set + single_count * single;
    int cost2 = n * single;
    cout << min(cost1, cost2) << endl;
    return 0;
}

I - 倍数条件を満たす数列の分割

数列を分割し、各セグメントの和がそのセグメント番号の倍数となる分割方案の数を数えます。

解法

動的計画法とモジュラ演算を組み合わせて効率的に計算します。

#include <vector>
#include <iostream>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;

int main() {
    int n;
    cin >> n;
    vector<ll> arr(n + 1), prefix(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
        prefix[i] = prefix[i - 1] + arr[i];
    }
    
    vector<vector<ll>> dp(n + 1, vector<ll>(n + 1));
    vector<vector<ll>> mod_table(n + 1, vector<ll>(n + 1));
    mod_table[0][0] = 1;
    
    for (int seg = 1; seg <= n; seg++) {
        for (int idx = 1; idx <= n; idx++) {
            ll rem = prefix[idx] % seg;
            dp[idx][seg] = mod_table[rem][seg - 1];
            mod_table[prefix[idx] % (seg + 1)][seg] = (mod_table[prefix[idx] % (seg + 1)][seg] + dp[idx][seg]) % MOD;
        }
    }
    
    ll total = 0;
    for (int i = 1; i <= n; i++) {
        total = (total + dp[n][i]) % MOD;
    }
    cout << total << endl;
    return 0;
}

タグ: 動的計画法 二部グラフ 組合せ最適化 二分探索 深さ優先探索

8月4日 20:00 投稿