樹状配列と座標圧縮:アルゴリズム問題の実装解説

アルゴリズム問題におけるデータ構造の応用

本稿では、効率的なデータ処理を目的とした 5 つのアルゴリズム問題について解説します。特に、樹状配列(Binary Indexed Tree)や座標圧縮、篩を用いた前処理などの技術に焦点を当てます。

問題 1:自然数の出現頻度计数

入力された自然数の出現回数を统计し、数値の昇順で出力する問題です。値の範囲が非常に大きいため(1e10 超)、直接配列でインデックスとして扱うことはできません。ここでは座標圧縮(Discretization)を用いて値を小さい範囲に映射します。

実装時の注意点として、入力サイズに応じた適切な配列確保が必要です。メモリ制限を超えないよう、動的配列または十分な大きさの静的配列を用意します。

実装コード
#include <bits/stdc++.h>
using namespace std;

struct Data {
    long long value;
    int original_index;
    long long compressed_value;
};

bool compareData(const Data& a, const Data& b) {
    return a.value < b.value;
}

bool compareIndex(const Data& a, const Data& b) {
    return a.original_index < b.original_index;
}

int main() {
    int n;
    scanf("%d", &n);
    vector<Data> items(n);
    vector<long long> sorted_vals(n);

    for (int i = 0; i < n; ++i) {
        scanf("%lld", &items[i].value);
        items[i].original_index = i;
        sorted_vals[i] = items[i].value;
    }

    sort(sorted_vals.begin(), sorted_vals.end());
    
    for (int i = 0; i < n; ++i) {
        items[i].compressed_value = lower_bound(sorted_vals.begin(), sorted_vals.end(), items[i].value) - sorted_vals.begin();
    }

    map<long long, int> count_map;
    for (int i = 0; i < n; ++i) {
        count_map[items[i].value]++;
    }

    vector<pair<long long, int>> result;
    for (auto const& [val, count] : count_map) {
        result.push_back({val, count});
    }

    sort(result.begin(), result.end());

    for (const auto& p : result) {
        printf("%lld %d\n", p.first, p.second);
    }

    return 0;
}

問題 2:非素数の计数

1 から n までの整数のうち、素数でないものの総数を求める問題です。エラトステネスの篩(Sieve of Eratosthenes)を用いて効率的に素数判定を行います。各素数の倍数をマークしていくことで、合成数を特定できます。

実装コード
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    scanf("%d", &n);
    if (n < 2) {
        printf("0\n");
        return 0;
    }

    vector<bool> is_prime(n + 1, true);
    is_prime[0] = is_prime[1] = false;
    int non_prime_count = 0;

    for (int i = 2; i * i <= n; ++i) {
        if (is_prime[i]) {
            for (int j = i * i; j <= n; j += i) {
                is_prime[j] = false;
            }
        }
    }

    for (int i = 1; i <= n; ++i) {
        if (!is_prime[i]) {
            non_prime_count++;
        }
    }

    printf("%d\n", non_prime_count);
    return 0;
}

問題 3:Self-Number の探索

ある数 n に対して、d(n) = n + (n の各桁の和) と定義します。d(n) によって生成されない数を Self-Number と呼びます。n 以下の Self-Number の個数と、指定された順序の Self-Number を出力します。

すべての数について生成元となり得るかを布尔配列で管理し、生成された数をマークしていきます。マークされていない数が Self-Number となります。

実装コード
#include <iostream>
#include <vector>
using namespace std;

int generate_next(int n) {
    int sum = n;
    int temp = n;
    while (temp > 0) {
        sum += temp % 10;
        temp /= 10;
    }
    return sum;
}

int main() {
    int n, k;
    cin >> n >> k;
    vector<int> queries(k);
    for (int i = 0; i < k; ++i) {
        cin >> queries[i];
    }

    vector<bool> generated(n + 1, false);
    vector<int> self_numbers;

    for (int i = 1; i <= n; ++i) {
        if (!generated[i]) {
            self_numbers.push_back(i);
        }
        int next_val = generate_next(i);
        if (next_val <= n) {
            generated[next_val] = true;
        }
    }

    cout << self_numbers.size() << endl;
    for (int i = 0; i < k; ++i) {
        if (queries[i] - 1 < self_numbers.size()) {
            cout << self_numbers[queries[i] - 1] << " ";
        }
    }
    cout << endl;

    return 0;
}

問題 4:区間更新と単点查询

初期値 0 の配列に対し、区間 [l, r] の値を反転させる操作と、特定のインデックスの状態を查询する操作を行います。樹状配列を用いた差分管理により、区間更新を単点查询に変換して処理します。更新時には l に 1 を加え、r+1 に -1 を加えることで区間内の値に影響を与えます。

実装コード
#include <bits/stdc++.h>
using namespace std;

vector<int> fenwick;
int size_n;

void update(int idx, int val) {
    for (; idx <= size_n; idx += idx & -idx)
        fenwick[idx] += val;
}

int query(int idx) {
    int sum = 0;
    for (; idx > 0; idx -= idx & -idx)
        sum += fenwick[idx];
    return sum;
}

int main() {
    int m;
    scanf("%d%d", &size_n, &m);
    fenwick.assign(size_n + 1, 0);

    while (m--) {
        int type;
        scanf("%d", &type);
        if (type == 1) {
            int l, r;
            scanf("%d%d", &l, &r);
            update(l, 1);
            update(r + 1, -1);
        } else {
            int idx;
            scanf("%d", &idx);
            printf("%d\n", query(idx) % 2);
        }
    }
    return 0;
}

問題 5:区間内の出現回数 2 の要素计数

配列の区間 [L, R] において、ちょうど 2 回出現する値の個数を求める問題です。データ規模が大きいため、オフラインクエリと樹状配列を組み合わせます。右端点を固定しながら走査し、各値の出現履歴に基づいて樹状配列を更新します。

具体的には、ある値が 2 回目に出た時点で 1 を加算し、3 回目に出た時点で之前的贡献を調整します。これにより、任意の左端点に対する条件満たす要素数を前缀和で取得できます。クエリは右端点の昇順に処理する必要があります。

実装コード
#include <bits/stdc++.h>
using namespace std;

struct Query {
    int l, r, id;
};

vector<int> bit_array;
int max_n;

void add(int idx, int val) {
    for (; idx <= max_n; idx += idx & -idx)
        bit_array[idx] += val;
}

int sum(int idx) {
    int res = 0;
    for (; idx > 0; idx -= idx & -idx)
        res += bit_array[idx];
    return res;
}

int main() {
    int n, q;
    scanf("%d%d", &n, &q);
    max_n = n;
    bit_array.assign(n + 1, 0);
    
    vector<int> arr(n + 1);
    map<int, int> last_pos;
    vector<int> prev1(n + 1, -1);
    vector<int> prev2(n + 1, -1);

    for (int i = 1; i <= n; ++i) {
        scanf("%d", &arr[i]);
        if (last_pos.count(arr[i])) {
            prev1[i] = last_pos[arr[i]];
            if (prev1[i] != -1 && prev1[prev1[i]] != -1) {
                prev2[i] = prev1[prev1[i]];
            }
        }
        last_pos[arr[i]] = i;
    }

    vector<Query> queries(q);
    for (int i = 0; i < q; ++i) {
        scanf("%d%d", &queries[i].l, &queries[i].r);
        queries[i].id = i;
    }

    sort(queries.begin(), queries.end(), [](const Query& a, const Query& b) {
        return a.r < b.r;
    });

    vector<int> answers(q);
    int current_r = 0;

    for (const auto& qry : queries) {
        while (current_r < qry.r) {
            current_r++;
            if (prev1[current_r] != -1) {
                add(prev1[current_r], 1);
            }
            if (prev2[current_r] != -1) {
                add(prev2[current_r], -2);
            }
            if (prev1[current_r] != -1 && prev1[prev1[current_r]] != -1 && prev2[prev1[current_r]] != -1) {
                 // Logic adjustment for 4th occurrence based on original pattern
                 int p3 = prev2[prev1[current_r]]; 
                 if(p3 != -1) add(p3, 1);
            }
        }
        answers[qry.id] = sum(qry.r) - sum(qry.l - 1);
    }

    for (int i = 0; i < q; ++i) {
        printf("%d\n", answers[i]);
    }

    return 0;
}

実装上の技術的検討事項

問題 1 においては、座標圧縮を用いる際に配列のサイズ設定に注意が必要です。入力値の範囲ではなく、入力个数に基づいてメモリを確保する必要があります。問題 5 のような複雑な区間クエリでは、オフライン処理による順序制御が不可欠です。右端点の昇順で処理を行うことで、樹状配列の状態を効率的に維持し、重複计数の調整を O(log N) で実現できます。

タグ: Fenwick Tree Coordinate Compression Sieve of Eratosthenes Offline Query C++

9月19日 00:23 投稿