Codeforces Round 1051 (Div. 2) A~D2問題の解説

A. 全ての長さの減算

思考問題。

長さが \(k(k \in [1,n])\) の区間を選び1を引く操作を繰り返す場合、まず\(a_i = n\) の位置を特定します。次に、\(n\) が存在する区間を維持し、\(n-k+1\) がその両側に存在するか確認し、存在すれば区間を拡張します。存在しない場合は操作は不可能です。

コードを表示``` #include <bits/stdc++.h>

using namespace std;

using i64 = long long;

void solve() { int size; cin >> size;

int left = 0, right = size;
vector<int> positions(size + 1);
for (int i = 1; i <= size; i++) {
    cin >> positions[i];
    if (positions[i] == size) {
        left = right = i;
    }
}

for (int i = size - 1; i >= 1; i--) {
    if (right + 1 <= size && positions[right + 1] == i) {
        right += 1;
    } else if (left - 1 >= 1 && positions[left - 1] == i) {
        left -= 1;
    } else {
        cout << "NO\n";
        return;
    }
}

cout << "YES\n";

}

int main() { ios::sync_with_stdio(false); cin.tie(nullptr);

int test_cases;
cin >> test_cases;
while (test_cases--) {
    solve();
}

return 0;

}


B. 割引
============

貪欲法。

価値\\(x\\)のクーポンを使用するには、\\(x-1\\)個の最も高価な商品を購入する必要があります。支払いを最小限に抑え、無料で入手できる商品の価値を最大化するため、価値\\(x\\)が小さいクーポンを使用して、ソートされた商品の上位\\(x\\)個を購入する戦略が最適です。

コードを表示```
#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

void solve() {
    int item_count, coupon_count;
    cin >> item_count >> coupon_count;

    vector<int> items(item_count + 1), coupons(coupon_count + 1);
    for (int i = 1; i <= item_count; i++) {
        cin >> items[i];
    }
    for (int i = 1; i <= coupon_count; i++) {
        cin >> coupons[i];
    }

    sort(items.begin() + 1, items.end());
    sort(coupons.begin() + 1, coupons.end());

    i64 total_cost = 0;
    int coupon_index = 1;
    for (int i = item_count; i >= 1; i--) {
        if (coupon_index <= coupon_count && i - coupons[coupon_index] + 1 >= 1) {
            for (int j = i; j > i - coupons[coupon_index] + 1; j--) {
                total_cost += items[j];
            }
            i = i - coupons[coupon_index] + 1;
            coupon_index++;
        } else {
            total_cost += items[i];
        }
    }

    cout << total_cost << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int test_cases;
    cin >> test_cases;
    while (test_cases--) {
        solve();
    }

    return 0;
}

C. 最大値木

トポロジーソート。

貪欲に考えると、各辺の貢献度を最大化するために、親の値が子より大きくなるように辺の向きを決定します。これにより、キューに入れられた頂点に\(n, n-1, n-2...\)の順に値を割り当てることができます。

ここでは、辺の向きを貢献度に基づいて決定しましたが、辺の数が\(n-1\)しかないため、各辺の貢献度を比較して向きを決定するだけで十分です。

コードを表示``` #include <bits/stdc++.h>

using namespace std;

using i64 = long long;

void solve() { int node_count; cin >> node_count;

vector<array<int,3>> edge_list;
for (int i = 1; i < node_count; i++) {
    int u, v, x, y;
    cin >> u >> v >> x >> y;
    edge_list.push_back({x, u, v});
    edge_list.push_back({y, v, u});
}

vector<int> in_degree(node_count + 1);
vector<vector<int>> graph(node_count + 1);

set<array<int,2>> edge_set;

sort(edge_list.begin(), edge_list.end(), greater<>());
for (auto &[weight, from, to] : edge_list) {
    if (edge_set.count({from, to}) || edge_set.count({to, from})) {
        continue;
    }
    edge_set.insert({from, to});
    in_degree[to]++;
    graph[from].push_back(to);
}

int current_value = node_count;
vector<int> node_values(node_count + 1);

queue<int> q;
for (int i = 1; i <= node_count; i++) {
    if (in_degree[i] == 0) {
        q.push(i);
    }
}

while (!q.empty()) {
    auto node = q.front();
    q.pop();

    node_values[node] = current_value--;
    for (auto &neighbor : graph[node]) {
        if (!--in_degree[neighbor]) {
            q.push(neighbor);
        }
    }
}

for (int i = 1; i <= node_count; i++) {
    cout << node_values[i] << " \n"[i == node_count];
}

}

int main() { ios::sync_with_stdio(false); cin.tie(nullptr);

int test_cases;
cin >> test_cases;
while (test_cases--) {
    solve();
}

return 0;

}


D1. 転置グラフ彩色(簡単版)
================================

動的計画法。

手動で計算またはテーブルを作成すると、\\(i a\_k\\)となるような存在は不合法であることがわかります。これは3つの頂点がすべて異なる色で塗られる必要があるためですが、2色しか使用できないためです。つまり、最長単調減少部分列の長さが2以下でなければなりません。

\\(dp\_{i,j,k}\\)を前\\(i\\)個の数値の中で、最長単調減少部分列の最初の数が\\(j\\)、2番目の数が\\(k\\)である場合の数と定義します。

\\(i\\)番目の数を選ばない場合、遷移は以下のようになります:

\[dp_{i,j,k}=dp_{i-1,j,k} \]\\(i\\)番目の数を選ぶ場合、遷移は以下のようになります:

\[\begin{aligned} dp_{i,a_i,k} = dp_{i,a_i,k} + dp_{i-1,j,k} &amp; &amp; a_i \ge j \\ dp_{i,j,a_i} = dp_{i,j,a_i} + dp_{i-1,j,k} &amp; &amp; j &gt; a_i \ge k \end{aligned} \]\\(a\_i < k\\)の場合、長さが3の単調減少部分列が形成されるため、不合法な遷移となります。

コード中のZ型は modulo クラスです。

コードを表示```
using Z = ModInt<MOD[0]>;

void solve() {
    int n;
    cin >> n;
    
    vector<int> sequence(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> sequence[i];
    }
    
    Z result = 0;
    
    vector<vector<Z>> dp(n + 1, vector<Z>(n + 1));
    dp[0][0] = 1;
    
    for (int i = 1; i <= n; i++) {
        vector<vector<Z>> new_dp(n + 1, vector<Z>(n + 1));
        for (int j = 0; j <= n; j++) {
            for (int k = 0; k <= j; k++) {
                new_dp[j][k] += dp[j][k];
                if (sequence[i] >= j) {
                    new_dp[sequence[i]][k] += dp[j][k];
                } else if (sequence[i] >= k) {
                    new_dp[j][sequence[i]] += dp[j][k];
                }
            }
        }
        dp = move(new_dp);
    }
    
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= n; j++) {
            result += dp[i][j];
        }
    }
    
    cout << result << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int test_cases;
    cin >> test_cases;
    while (test_cases--) {
        solve();
    }
    
    return 0;
}

D2. 転置グラフ彩色(難しい版)

データ構造による動的計画法の最適化。

上記の遷移を観察すると、\(dp_{i,a_i,k} = \sum_{j=0}^{a_i}dp_{i-1,j,k}\)と\(dp_{i,j,a_i}=\sum_{k=0}^{a_i}dp_{i-1,j,k}\)であることがわかります。

これらの接頭和は、Binary Indexed Tree (BIT) を使用して管理できます。\(j\)と\(k\)を2次元に展開すると、\(k\)列の前\(j\)行の和を維持するものと、\(j\)行の前\(k\)列の和を維持するものの2つが必要になります。

\(n\le 2000\)であるため、必ずローリングが必要です。1つの方法は、遷移する状態と値を保存し、最後にまとめて遷移させることです。

BITが1から始まらない場合は、1つずつオフセットしてください。

コード中のZは modulo クラスです。

コードを表示``` using Z = ModInt<MOD[0]>;

template<typename T> struct BIT { int size; vector<T> tree;

BIT() {}
BIT(int n) {
    this->size = n;
    tree.resize(n + 1);
}

void update(int index, T value) {
    for (; index <= size; index += index & -index) {
        tree[index] += value;
    }
}

T query(int index) {
    T result = 0;
    for (; index; index -= index & -index) {
        result += tree[index];
    }
    return result;
}

T query(int left, int right) {
    return query(right) - query(left - 1);
}

};

void solve() { int n; cin >> n;

vector<int> sequence(n + 1);
for (int i = 1; i <= n; i++) {
    cin >> sequence[i];
}

Z result = 0;

vector f(2, vector(n + 1, BIT<Z>(n + 2)));
f[0][0].update(1, 1);
f[1][0].update(1, 1);

for (int i = 1; i <= n; i++) {
    vector<tuple<int,int,Z>> updates;
    for (int j = 0; j <= sequence[i]; j++) {
        Z value = f[0][j].query(sequence[i] + 1);
        updates.emplace_back(sequence[i], j, value);
    }
    for (int j = sequence[i] + 1; j <= n; j++) {
        Z value = f[1][j].query(sequence[i] + 1);
        updates.emplace_back(j, sequence[i], value);
    }
    
    for(auto &[first, second, value] : updates){
        f[0][second].update(first + 1, value);
        f[1][first].update(second + 1, value);
    }
}

for (int i = 0; i <= n; i++) {
    result += f[1][i].query(n + 1);
}

cout << result << "\n";

}

int main() { ios::sync_with_stdio(false); cin.tie(nullptr);

int test_cases;
cin >> test_cases;
while (test_cases--) {
    solve();
}

return 0;

}

タグ: codeforces アルゴリズム 競技プログラミング 貪欲法 動的計画法

8月1日 18:57 投稿