競技プログラミング模試解説:数論・文字列・区間クエリ処理

本記事では、CSP(中国情報オリンピック予選)直前に実施された模擬コンテストの4問について、解法と実装を解説する。

A. 素因数分解による数字列変換

各桁の数字を素因数に分解し、大きい素因数から順に出力する問題。例えば「6」は2×3なので、最終的な出力では「3」より「2」が先に来る必要がある。

各数字の素因数の個数を前計算し、7, 5, 3, 2の順に貪欲に出力する。

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

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int tc;
    cin >> tc;
    while (tc--) {
        int len;
        string str;
        cin >> len >> str;
        
        bool only01 = true;
        for (char c : str) {
            if (c != '0' && c != '1') {
                only01 = false;
                break;
            }
        }
        if (only01) {
            cout << "-1\n";
            continue;
        }
        
        int cnt[10] = {};
        for (char c : str) {
            int d = c - '0';
            if (d <= 1) continue;
            // 各数字の素因数分解: 2,3,5,7
            if (d == 2) cnt[2]++;
            else if (d == 3) cnt[2]++, cnt[3]++;
            else if (d == 4) cnt[2] += 3, cnt[3]++;
            else if (d == 5) cnt[2] += 3, cnt[3]++, cnt[5]++;
            else if (d == 6) cnt[2] += 4, cnt[3] += 2, cnt[5]++;
            else if (d == 7) cnt[2] += 4, cnt[3] += 2, cnt[5]++, cnt[7]++;
            else if (d == 8) cnt[2] += 7, cnt[3] += 2, cnt[5]++, cnt[7]++;
            else if (d == 9) cnt[2] += 7, cnt[3] += 4, cnt[5]++, cnt[7]++;
        }
        
        // 大きい素因数から貪欲に出力
        while (cnt[7]--) {
            cnt[5]--, cnt[3] -= 2, cnt[2] -= 4;
            cout << '7';
        }
        while (cnt[5]--) {
            cnt[3]--, cnt[2] -= 3;
            cout << '5';
        }
        while (cnt[3]--) {
            cnt[2]--;
            cout << '3';
        }
        while (cnt[2]--) {
            cout << '2';
        }
        cout << '\n';
    }
    return 0;
}

B. 辞書順制約付き列削除

n個の文字列をm列比較し、辞書順を崩す列を最小化する問題。ある列で下の文字列が上より小さい場合、その列を削除するか、既に確定している行(前の列で大小関係が確定した行)を使って回避する。

確定済みの行を管理しつつ、削除が必要な列をカウントする。

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

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n, m;
    cin >> n >> m;
    
    vector<string> grid(n);
    for (int i = 0; i < n; i++) {
        cin >> grid[i];
    }
    
    vector<bool> fixed(n, false); // fixed[i]: iとi-1の大小関係が確定
    int removed = 0;
    
    for (int col = 0; col < m; col++) {
        bool needRemove = false;
        for (int row = 1; row < n; row++) {
            if (grid[row][col] < grid[row-1][col] && !fixed[row]) {
                needRemove = true;
                break;
            }
        }
        if (needRemove) {
            removed++;
            continue;
        }
        // この列で確定する関係を記録
        for (int row = 1; row < n; row++) {
            if (grid[row][col] > grid[row-1][col]) {
                fixed[row] = true;
            }
        }
    }
    
    cout << removed << '\n';
    return 0;
}

C. 区間マージDP(メモリ最適化版)

隣接する同値区間をマージできる操作を繰り返し、得られる最大値を求める問題。dp[i][j] = 「値iを左端jからマージして得られる右端+1」の形で定義すると、dp[i][j] = dp[i-1][dp[i-1][j]] と遷移する。

メモリ制約に注意し、2行のスクロール配列で実装する。

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

const int MAXN = 3e6 + 10;
const int LOGV = 60;

int nxt[MAXN];
int val[MAXN];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> val[i];
    }
    
    long long answer = 0;
    
    for (int v = 1; v <= LOGV; v++) {
        for (int pos = 1; pos <= n; pos++) {
            if (val[pos] == v) {
                nxt[pos] = pos + 1;
            } else if (nxt[pos]) {
                nxt[pos] = nxt[nxt[pos]];
            }
            if (nxt[pos]) answer = v;
        }
    }
    
    cout << answer << '\n';
    return 0;
}

D. 区間異或と出現回数の組み合わせ

静的区間クエリで「区間内に偶数回出現する値の異或和」を求める問題。これは「区間内の全要素の異或和」と「区間内に奇数回出現する値の異或和」の差として表せる。

後者はMo's algorithmやBITで処理できるが、ここでは「最終出現位置」を管理するBIT解法を用いる。各値について、出現位置をトグルしながら前処理する。

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

const int MAXN = 1e6 + 10;

int arr[MAXN], prefixXor[MAXN];
int lastPos[MAXN], coord[MAXN];
int bit[MAXN], answer[MAXN];

struct Query {
    int L, R, idx;
    bool operator<(const Query& o) const {
        return R < o.R;
    }
} queries[MAXN];

void bitUpdate(int pos, int val, int n) {
    for (; pos <= n; pos += pos & -pos) bit[pos] ^= val;
}

int bitQuery(int pos) {
    int res = 0;
    for (; pos > 0; pos -= pos & -pos) res ^= bit[pos];
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n;
    cin >> n;
    
    // 座標圧縮
    vector<int> allVals;
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
        prefixXor[i] = prefixXor[i-1] ^ arr[i];
        allVals.push_back(arr[i]);
    }
    sort(allVals.begin(), allVals.end());
    allVals.erase(unique(allVals.begin(), allVals.end()), allVals.end());
    
    int m = allVals.size();
    for (int i = 1; i <= n; i++) {
        coord[i] = lower_bound(allVals.begin(), allVals.end(), arr[i]) - allVals.begin() + 1;
    }
    
    int q;
    cin >> q;
    for (int i = 0; i < q; i++) {
        cin >> queries[i].L >> queries[i].R;
        queries[i].idx = i;
    }
    sort(queries, queries + q);
    
    // 右端でソートして処理
    int ptr = 0;
    for (int r = 1; r <= n; r++) {
        int c = coord[r];
        if (lastPos[c]) {
            bitUpdate(lastPos[c], arr[r], n); // 前回の出現をキャンセル
        }
        bitUpdate(r, arr[r], n);
        lastPos[c] = r;
        
        while (ptr < q && queries[ptr].R == r) {
            int L = queries[ptr].L, R = queries[ptr].R;
            int oddXor = bitQuery(R) ^ bitQuery(L-1);
            answer[queries[ptr].idx] = prefixXor[R] ^ prefixXor[L-1] ^ oddXor;
            ptr++;
        }
    }
    
    for (int i = 0; i < q; i++) {
        cout << answer[i] << '\n';
    }
    return 0;
}

この問題はCodeforces 703D「Mishka and Interesting sum」と同一の構造を持つ。区間クエリの前処理テクニックとして重要なパターンである。

タグ: 競技プログラミング C++ 動的計画法 Binary Indexed Tree 区間クエリ

9月4日 02:09 投稿