文字列と配列操作に関するアルゴリズム解析

コードのマクロ定義とフレームワークの約束

#include <bits/stdc++.h>
using namespace std;
#define FASTIO ios::sync_with_stdio(false); cin.tie(nullptr);
#define ENDL '\n'
#define RANGE(_x, _y) (_x).begin(), (_x).end()
#define LOOP(_i, _s, _e) for (int _i = _s; _i < _e; ++_i)
typedef long long ll;
const int MAXN = 200010;

signed main() {
    FASTIO;
    int test_cases = 1;
    // cin >> test_cases;
    while (test_cases--) solve();
    return 0;
}

アナグラム検索

問題の概要

文字列 \(S\) と \(T\) が似ているとは、\(S\) を並べ替えることで \(T\) にできる場合を指す。与えられた文字列 \(A\) と \(B\) について、\(A\) のどの部分文字列(連続した部分)が \(B\) に似ているかを求めよ。

また、\(A\) には任意の文字に置き換えられる ? 文字が含まれる。

解法の考え

この問題では、スライディングウィンドウを使用する。文字列 \(A\) の中で、長さが \(len(B)\) のウィンドウを維持し、その中の各文字の出現数をカウントして \(B\) と比較する。

void solve() {
    string S, T;
    cin >> S >> T;
    vector<int> countB(26), windowCount(26);
    int result = 0;
    for (char c : T) countB[c - 'a']++;
    for (int i = 0; i < S.size(); ++i) {
        if (S[i] != '?') windowCount[S[i] - 'a']++;
        if (i >= T.size() && S[i - T.size()] != '?') windowCount[S[i - T.size()] - 'a']--;
        if (i >= T.size() - 1) {
            bool match = true;
            for (int j = 0; j < 26; ++j) {
                if (windowCount[j] > countB[j]) {
                    match = false;
                    break;
                }
            }
            result += match;
        }
    }
    cout << result << ENDL;
}

区間内の最大差

問題の概要

長さ \(n\) のシーケンスから、要素間の差が最大でも \(k\) である最長の部分シーケンスを見つける。

解法の考察

この問題は二分探索とセグメントツリーを用いる。まず、元のシーケンスに対してセグメントツリーを構築し、区間内の最大値と最小値を管理する。次に、各起点から開始し、二分探索を用いて最適な終点を見つけ、条件を満たすかどうかを確認する。

struct SegmentTreeNode {
    int left, right, maxVal, minVal;
} segTree[MAXN * 4];

void update(int node) {
    segTree[node].maxVal = max(segTree[node * 2].maxVal, segTree[node * 2 + 1].maxVal);
    segTree[node].minVal = min(segTree[node * 2].minVal, segTree[node * 2 + 1].minVal);
}

void buildSegTree(int node, int l, int r, const vector<int>& arr) {
    segTree[node] = {l, r, arr[r], arr[r]};
    if (l == r) return;
    int mid = (l + r) / 2;
    buildSegTree(node * 2, l, mid, arr);
    buildSegTree(node * 2 + 1, mid + 1, r, arr);
    update(node);
}

SegmentTreeNode querySegTree(int node, int l, int r) {
    if (segTree[node].left >= l && segTree[node].right <= r) return segTree[node];
    int mid = (segTree[node].left + segTree[node].right) / 2;
    if (r <= mid) return querySegTree(node * 2, l, r);
    else if (l > mid) return querySegTree(node * 2 + 1, l, r);
    else {
        auto L = querySegTree(node * 2, l, r);
        auto R = querySegTree(node * 2 + 1, l, r);
        SegmentTreeNode res = {l, r, max(L.maxVal, R.maxVal), min(L.minVal, R.minVal)};
        return res;
    }
}

車内での音楽再生

問題の概要

2つの長さ \(n\) のシーケンス \(A_i\) と \(B_i\) が与えられ、それぞれ要素の貢献度とコストを表す。\(w\) 回の操作を行い、総コスト \(K\) 以下で最大の貢献度を得る。

解法の考察

この問題では、貪欲法とデータ構造を組み合わせて解決する。双方向ポインタと平衡木(multiset)を用いて、操作後のコストを効率的に管理する。

void solve() {
    int n, w, k;
    cin >> n >> w >> k;
    vector<int> a(n + 1), b(n + 1);
    LOOP(i, 1, n + 1) cin >> a[i];
    LOOP(i, 1, n + 1) cin >> b[i];
    int l = 1, totalCost = 0, totalContribution = 0, bestAns = 0;
    multiset<int> originalSet, discountedSet;
    LOOP(i, 1, n + 1) {
        discountedSet.insert(b[i]);
        totalCost += (b[i] + 1) / 2;
        totalContribution += a[i];
        if (discountedSet.size() > w) {
            originalSet.insert(*discountedSet.begin());
            totalCost += *discountedSet.begin();
            totalCost -= (*discountedSet.begin() + 1) / 2;
            discountedSet.erase(discountedSet.begin());
        }
        while (totalCost > k) {
            if (b[l] >= *discountedSet.begin()) {
                totalCost -= (b[l] + 1) / 2;
                discountedSet.erase(discountedSet.find(b[l]));
                if (!originalSet.empty()) {
                    discountedSet.insert(*originalSet.rbegin());
                    totalCost -= *originalSet.rbegin();
                    totalCost += (*originalSet.rbegin() + 1) / 2;
                    originalSet.erase(originalSet.find(*originalSet.rbegin()));
                }
            } else {
                totalCost -= b[l];
                originalSet.erase(originalSet.find(b[l]));
            }
            totalContribution -= a[l];
            l++;
        }
        bestAns = max(bestAns, totalContribution);
    }
    cout << bestAns << ENDL;
}

旅行カードの選択

問題の概要

バスに乗るために以下のチケットが提供されている:

  • 単程券:20円
  • 90分以内自由乗車券:50円
  • 1440分以内自由乗車券:120円

\(n\) 回のバス利用計画において、最小の金額を求める。

解法の考察

これは典型的な動的計画法問題であり、各利用回数における最小コストを計算する。

void solve() {
    int n;
    cin >> n;
    vector<int> times(n + 1), dp(n + 1);
    LOOP(i, 1, n + 1) cin >> times[i];
    LOOP(i, 1, n + 1) {
        dp[i] = dp[i - 1] + 20;
        int idx = lower_bound(RANGE(times, 1, i + 1), times[i] - 89) - times.begin() - 1;
        if (idx >= 1) dp[i] = min(dp[i], dp[idx] + 50);
        idx = lower_bound(RANGE(times, 1, i + 1), times[i] - 1439) - times.begin() - 1;
        if (idx >= 1) dp[i] = min(dp[i], dp[idx] + 120);
    }
    LOOP(i, 1, n + 1) cout << dp[i] - dp[i - 1] << ENDL;
}

タグ: KMP算法 セグメントツリー 滑动窗口 贪心算法 动态规划

8月2日 05:41 投稿