競技プログラミングにおける数論と DP による最適化戦略

問題 1:数値変換プロセスと剰余定理

この問題では、巨大な数値 $n$ に対して特定の減算操作を繰り返す際の最終状態を求めます。操作の規則は、$n$ を $s$ で割った商と余りの和を次の $n$ とするものです。入力値が非常に大きくなる可能性があるため、単純なシミュレーションでは時間制限を超えてしまいます。

部分点対策:桁ごとのシミュレーション

まず、数値を文字列として受け取り、桁ごとに整数へ変換しながら処理を行うアプローチが考えられます。各桁の変換後に、値が閾値 $s$ を超えるたびに規定の減算処理を行います。この方法では、中間的な得点を得ることが可能です。

#include <iostream>
#include <string>
#include <vector>

using namespace std;

// 70 点相当のシミュレーション実装
void solve_simulation() {
    int testCases;
    cin >> testCases;
    while (testCases--) {
        int divisor;
        string numStr;
        cin >> divisor >> numStr;

        long long currentVal = 0;
        for (char c : numStr) {
            currentVal = currentVal * 10 + (c - '0');
            while (currentVal >= divisor) {
                long long quotient = currentVal / divisor;
                currentVal -= quotient * (divisor - 1);
            }
        }
        cout << currentVal << endl;
    }
}

満点対策:数論的アプローチ

数学的な分析を行うと、この操作は最終的に $n \bmod (s-1)$ の値に収束することがわかります。$n=0$ の場合は例外として 0 を返します。それ以外の場合、答えは $1 + ((n - 1) \bmod (s - 1))$ で求められます。入力値が巨大であるため、読み込み段階で $s-1$ に対する剰余を計算する最適化が必要です。

#include <cstdio>
#include <cctype>

using namespace std;

// 入力受け取りながら剰余を計算する関数
int readWithModulo(int mod, bool& isZero) {
    int result = 0;
    char ch = getchar();
    int digitCount = 0;
    
    while (!isdigit(ch)) ch = getchar();
    
    while (isdigit(ch)) {
        if (ch == '0' && digitCount == 0) {
            isZero = true;
        }
        result = (result * 10 + (ch - '0')) % mod;
        ch = getchar();
        digitCount++;
    }
    return result;
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        int s;
        scanf("%d", &s);
        bool zeroFlag = false;
        
        // s-1 で剰余を取りながら読み込み
        int nMod = readWithModulo(s - 1, zeroFlag);
        
        if (zeroFlag && nMod == 0) {
            puts("0");
        } else {
            // 数論的公式による計算
            int ans = (nMod + s - 2) % (s - 1) + 1;
            printf("%d\n", ans);
        }
    }
    return 0;
}

問題 2:アイテム取得を伴う最短経路問題

二次元平面上に複数の地点が配置されており、スタート地点から特定のアイテムを取得しながらゴールを目指します。アイテムを使用するたびに移動速度が向上するため、どの順序でアイテムを取得するかが時間コストに影響します。

動的計画法による状態遷移

この問題は動的計画法(DP)を用いて解決できます。状態を $dp[i][j]$ と定義し、これは $i$ 番目の地点に到達し、スタート以外で $j$ 個のアイテムを使用した際の最小時間を表します。地点間の距離はマンハッタン距離で計算され、移動時間は距離を使用アイテム数(速度)で割った値となります。

遷移式は以下の通りです:

$dp[i][j] = \min(dp[i][j], dp[k][j-1] + \frac{dist(i, k)}{j})$

ここで、$k$ は直前に訪れた地点を示します。

#include <iostream>
#include <vector>
#include <cmath>
#include <iomanip>
#include <algorithm>

using namespace std;

struct Point {
    int x, y;
};

// マンハッタン距離の計算
int calcDistance(const Point& a, const Point& b) {
    return abs(a.x - b.x) + abs(a.y - b.y);
}

int main() {
    int n, m, startX, startY, endX, endY;
    cin >> n >> m >> startX >> startY >> endX >> endY;

    // スタート地点のアイテムは必須使用ため、利用可能回数を減算
    m--;

    vector<Point> locations(n + 2);
    // 0: ゴール地点,1: スタート地点,2..n+1: アイテム地点
    locations[0] = {endX, endY};
    locations[1] = {startX, startY};
    
    for (int i = 2; i <= n + 1; ++i) {
        cin >> locations[i].x >> locations[i].y;
    }

    int pointCount = n + 1;
    // DP テーブルの初期化
    vector<vector<double>> minTime(pointCount + 1, vector<double>(m + 1, 1e9));

    minTime[1][0] = 0;
    double globalMin = calcDistance(locations[0], locations[1]);

    // アイテム不使用で各地点へ移動する初期コスト
    for (int i = 2; i <= pointCount; ++i) {
        minTime[i][0] = calcDistance(locations[1], i);
    }

    // DP 遷移処理
    for (int used = 1; used <= m; ++used) {
        for (int curr = 1; curr <= pointCount; ++curr) {
            for (int prev = 1; prev <= pointCount; ++prev) {
                if (curr == prev) continue;
                double cost = minTime[prev][used - 1] + (double)calcDistance(locations[prev], locations[curr]) / used;
                if (cost < minTime[curr][used]) {
                    minTime[curr][used] = cost;
                }
            }
            // ゴールへの最終移動コストを更新
            double toGoal = minTime[curr][used] + (double)calcDistance(locations[0], locations[curr]) / (used + 1);
            if (toGoal < globalMin) {
                globalMin = toGoal;
            }
        }
    }

    cout << fixed << setprecision(12) << globalMin << endl;
    return 0;
}

タグ: competitive-programming dynamic-programming modular-arithmetic algorithm-design

10月13日 15:03 投稿