六省联考2017 問題の解法

P3745 [六省聯考 2017] 期末試験

m 種の試験があり、各試験 i は第 b_i 日に成績が発表される予定である。n 人の学生が成績を待っている。学生 i は第 t_i 日までにすべての成績を知りたいと思っている。最終的に最も遅く成績が出る試験の日を T とする。以下の3種類の不快度が生じる。

  • 各学生は C × max(T - t_i, 0) の不快度を負担する。
  • 試験 x を1日遅らせて試験 y を1日前倒しにする操作には不快度 A がかかる。
  • 試験 x を1日前倒しにする操作には不快度 B がかかる。

最小の不快度を求めよ(1 ≤ n, m, t_i, b_i ≤ 10⁵, 0 ≤ A, B, C ≤ 10⁵)。

調整後の T は元の T 以下であり、T ≤ 10⁵ であることから、可能なすべての T についてコストを計算し、最小値を求める戦略が有効である。特定の T へ調整する際には、「前倒し」が必要な科目と「遅延可能」な科目がある。このとき、貪欲法で最適な操作を決定できる。

もし B < A なら、前倒し操作のみを用いればよい。必要前倒し日数が x ならば、コストは Bx となる。一方、A < B ならば、遅延可能な科目を最大限活用すべきである。前倒しが必要日数が x、遅延可能日数が y ならば、コストは A × min(x, y) + B × max(0, x - y) となる。

これらの値を効率的に更新するために、b_i と t_i をソートし、指標を用いて範囲内の科目数を管理する。各 T に対して、前倒し・遅延可能な科目数を均摊 O(1) で更新できる。

また、学生による不快度も同様に動的更新可能。学生の t_i が昇順に並んでおり、そのうち成績待ちが終了する人数を指標で管理することで、不快度の変化量を即時計算できる。

全体の時間計算量は O(n + m + max(t_i)) となり、実装も簡潔にできる。

#include <cstdio>
#include <algorithm>
const int N = 1e5 + 10;
typedef long long ll;

int a[N], b[N], A, B, C, n, m;
int main() {
    scanf("%d%d%d%d%d", &A, &B, &C, &n, &m);
    ll ans = 1e18;

    for (int i = 1; i <= n; ++i) scanf("%d", a + i);
    for (int i = 1; i <= m; ++i) scanf("%d", b + i);

    std::sort(a + 1, a + n + 1);
    std::sort(b + 1, b + m + 1);

    ll res = 0, need = 0, now = 0, add;
    int pos_a = n, pos_b = m;

    while (pos_b >= 1 && b[pos_b] == b[m]) --pos_b;
    for (int i = 1; i < m; ++i) res += (b[m] - b[i]);
    for (int i = 1; i <= n; ++i) {
        if (a[i] >= b[m]) { pos_a = i - 1; break; }
        now += (ll)C * (b[m] - a[i]);
    }

    ans = std::min(ans, now);

    for (int i = b[m] - 1, cnt; i >= 1; --i) {
        cnt = 0;
        while (pos_b >= 1 && b[pos_b] == i) --pos_b, ++cnt;
        res -= pos_b + cnt;
        need += m - pos_b - cnt;

        if (A < B && res > 0)
            add = std::min(res, need) * A + std::max(0LL, need - res) * B;
        else
            add = need * B;

        while (pos_a >= 1 && a[pos_a] == i) now -= C, --pos_a;
        now -= (ll)pos_a * C;

        ans = std::min(ans, now + add);
    }

    printf("%lld\n", ans);
    return 0;
}

P3747 [六省聯考 2017] 相逢是問候

長さ n の配列を扱い、定数 c, p が与えられる。次の2種類の操作を行う:

  • 区間 [l, r] 内の各要素 a_i に置き換える:a_i ← c^a_i
  • 区間 [l, r] の和を mod p で出力

(1 ≤ n, m ≤ 5×10⁴, 1 ≤ c, a_i < p ≤ 10⁸)

指数の冪乗はオイラー関数 φ によって再帰的に簡約できる。特に、φ(φ(…φ(p))) は急速に 1 に収束するため、深さは O(log p) に抑えられる。これにより、操作回数が非常に少ないことがわかる。

線分木を用い、各節点に「最大の操作深さ」(minx)と「区間和」(sum)を保持する。深さが既に上限に達していれば更新しない。そうでなければ、葉まで再帰的に処理する。

各要素の更新は、事前に φ 序列を計算し、光速累乗(Exponential Preprocessing)を用いて O(1) で行う。各層でのべき乗は O(log p) だが、全体で O(m log²p) 以内に抑えられる。

事前計算として φ 関数の値を O(√p log p) で算出し、累乗テーブルを構築。これにより、全体の時間計算量は O((√p + √m) log p + m log²p + n log n) となり、制限内に収まる。

#include <cstdio>
#include <algorithm>
const int N = 5e4 + 10, D = 100, B = 1e4;
typedef long long ll;

int pw[2][D][B + 10], ov[2][D][B + 10], phi[D], dep, n, m, p, c, over, a[N];

inline int Phi(int n) {
    int res = n;
    for (int i = 2; i * i <= n; ++i) {
        if (n % i == 0) res = res / i * (i - 1);
        while (n % i == 0) n /= i;
    }
    if (n > 1) res = res / n * (n - 1);
    return res;
}

inline void pre() {
    phi[0] = p;
    while (phi[dep] > 1) ++dep, phi[dep] = Phi(phi[dep - 1]);
    phi[++dep] = 1;

    for (int i = 0; i <= dep; ++i) {
        pw[0][i][0] = 1;
        for (int j = 1; j <= B; ++j) {
            ll t = (ll)pw[0][i][j - 1] * c;
            if (t >= phi[i]) ov[0][i][j] = 1;
            pw[0][i][j] = t % phi[i];
            ov[0][i][j] |= ov[0][i][j - 1];
        }

        pw[1][i][0] = 1;
        for (int j = 1; j <= B; ++j) {
            ll t = (ll)pw[1][i][j - 1] * pw[0][i][B];
            if (t >= phi[i]) ov[1][i][j] = 1;
            pw[1][i][j] = t % phi[i];
            ov[1][i][j] |= ov[1][i][j - 1];
        }
    }
}

inline int gsm(int d, int b) {
    int v1 = b / B, v2 = b % B;
    over |= ov[0][d][v2] | ov[1][d][v1];
    ll t = (ll)pw[0][d][v2] * pw[1][d][v1];
    if (t >= phi[d]) over = 1;
    return t % phi[d];
}

int calc(int p, int cnt, int d) {
    over = 0;
    if (d == cnt) {
        if (a[p] >= phi[d]) over = 1;
        return a[p] % phi[d];
    }
    int x = calc(p, cnt, d + 1);
    if (over) x += phi[d + 1], over = 0;
    return gsm(d, x);
}

struct SegTree {
    #define ls(k) (k << 1)
    #define rs(k) (k << 1 | 1)
    struct node { int l, r, cnt, sum; } h[N << 2];

    void pushup(int k) {
        h[k].cnt = std::min(h[ls(k)].cnt, h[rs(k)].cnt);
        h[k].sum = (h[ls(k)].sum + h[rs(k)].sum) % p;
    }

    void build(int k, int l, int r) {
        h[k].l = l; h[k].r = r;
        if (l == r) return h[k].sum = a[l], void();
        int mid = (l + r) >> 1;
        build(ls(k), l, mid); build(rs(k), mid + 1, r);
        pushup(k);
    }

    void change(int k, int x, int y) {
        if (h[k].cnt >= dep) return;
        if (h[k].l == h[k].r) {
            ++h[k].cnt;
            h[k].sum = calc(h[k].l, h[k].cnt, 0);
            return;
        }
        int mid = (h[k].l + h[k].r) >> 1;
        if (x <= mid) change(ls(k), x, y);
        if (mid < y) change(rs(k), x, y);
        pushup(k);
    }

    int query(int k, int x, int y) {
        if (x <= h[k].l && h[k].r <= y) return h[k].sum;
        int mid = (h[k].l + h[k].r) >> 1, ret = 0;
        if (x <= mid) (ret += query(ls(k), x, y)) %= p;
        if (mid < y) (ret += query(rs(k), x, y)) %= p;
        return ret;
    }
} sgt;

int main() {
    scanf("%d%d%d%d", &n, &m, &p, &c);
    pre();
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    sgt.build(1, 1, n);

    for (int i = 1, op, l, r; i <= m; ++i) {
        scanf("%d%d%d", &op, &l, &r);
        if (op) printf("%d\n", sgt.query(1, l, r));
        else sgt.change(1, l, r);
    }
    return 0;
}

P3746 [六省聯考 2017] 組合數問題

整数 n, p, k, r が与えられ、次の値を求める:

∑_{i≥0} C(nk, ik + r) mod p

(1 ≤ n ≤ 10⁹, 0 ≤ r < k ≤ 50, 2 ≤ p ≤ 2³⁰ - 1)

k が小さいため、剰余クラス k に注目する。状態を f[i][j]:i 個のアイテムから選んだ個数が ≡ j (mod k) となる組み合わせ数とする。初期状態は f[0][0] = 1。

遷移式は:

f[i][j] = f[i-1][j] + f[i-1][(j-1) mod k]

これは定係数線形漸化式であり、行列累乗で高速化可能。転置行列を用いた畳み込み形式で、次のような行列を構築:

M[i][j] = 1 if j == i or j == (i-1)%k else 0

この行列を (nk) 乗して、初期ベクトルとの積を取ることで答えを得る。時間計算量は O(k³ log(nk))。

#include <cstdio>
const int N = 60;
typedef long long ll;

int n, p, k, r;
struct Matrix {
    int a[N][N], n, m;
    int* operator[](int n) { return a[n]; }
    void init(int N, int M, int on = 0) {
        n = N; m = M;
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j)
                a[i][j] = on ? (i == j) : 0;
    }
    Matrix operator*(Matrix A) {
        Matrix ret; ret.init(n, A.m);
        for (int i = 1; i <= ret.n; ++i)
            for (int j = 1; j <= ret.m; ++j)
                for (int k = 1; k <= m; ++k)
                    (ret[i][j] += (ll)a[i][k] * A[k][j] % p) %= p;
        return ret;
    }
} A, B, I;

Matrix ksm(Matrix A, ll b) {
    Matrix ret = I;
    while (b) {
        if (b & 1) ret = ret * A;
        A = A * A; b >>= 1;
    }
    return ret;
}

int main() {
    scanf("%d%d%d%d", &n, &p, &k, &r);
    A.init(1, k); B.init(k, k, 1); I.init(k, k, 1);
    A[1][1] = 1;
    for (int i = 2; i <= k; ++i) ++B[i][i - 1];
    ++B[1][k];
    B = ksm(B, (ll)n * k);
    A = A * B;
    printf("%d\n", A[1][r + 1]);
    return 0;
}

P3750 [六省聯考 2017] 分手は願い

n 個のランプがあり、初期状態は明暗混合。スイッチ i は、d|i となるすべての位置を反転させる。ある人がランダムにスイッチを押すが、最適操作が ≤ k 手で終了できる場合、最適手順を採用する。期待操作回数 × n! を 10⁵+3 で出力せよ。

最適手順は、番号が大きい順に、明になっているランプがあればスイッチを押す。これにより最小手数 m が得られる。この手数は O(n√n) で計算可能。

期待値を求めるため、f[i]:最適手順が i 手必要な状態の期待操作回数とする。再帰的に:

f_i = (i/n) f_{i-1} + ((n-i)/n) f_{i+1} + 1

差分 d_i = f_i - f_{i-1} を導入すると、

d_i = ((n-i)/i) d_{i+1} + (n/i)

この式は逆順に計算可能。f_k = k とし、d_n = 1 から逆算すれば、f_m が得られる。特例として、k ≥ m の場合は直接出力。

#include <cstdio>
const int N = 1e6 + 10, mod = 100003;

int a[N], f[N], d[N];

inline int ksm(int a, int b) {
    int ret = 1;
    while (b) {
        if (b & 1) ret = 1ll * ret * a % mod;
        a = 1ll * a * a % mod; b >>= 1;
    }
    return ret;
}

int main() {
    int n, m = 0, k, fac = 1;
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        fac = 1ll * fac * i % mod;
    }

    for (int i = n; i >= 1; --i) {
        if (!a[i]) continue;
        for (int j = 1; j * j <= i; ++j) {
            if (i % j) continue;
            a[j] ^= 1;
            if (j * j != i) a[i / j] ^= 1;
        }
        ++m;
    }

    if (m <= k) {
        printf("%lld\n", 1ll * m * fac % mod);
        return 0;
    }

    d[n] = 1;
    for (int i = n - 1; i >= k; --i) {
        int inv = ksm(i, mod - 2);
        d[i] = (1ll * inv * (n - i) % mod * d[i + 1] % mod + 1ll * inv * n % mod) % mod;
    }

    f[k] = k;
    for (int i = k + 1; i <= m; ++i)
        f[i] = (f[i - 1] + d[i]) % mod;

    f[m] = 1ll * f[m] * fac % mod;
    printf("%d\n", f[m]);
    return 0;
}

P3749 [六省聯考 2017] 寿司レストラン

寿司 n 種類、代号 a_i。購入は連続区間。区間 [l, r] の美味度は d_{l,r} で、同じ代号の寿司は1度だけ課金される。課金額は m×x² + c×x。最大の利益(美味度 - 費用)を求める。

制約 n ≤ 100 なので、最大權閉路問題としてネットワークフローで解ける。構成方法:

  • 区間 [l, r] のノード → 美味度 = d_{l,r}
  • 代号ごとに虚ノード → 次のノードに接続、価値 = -m×x²
  • 各寿司 i → 代号ノードに接続、価値 = -a_i
  • 区間 [l, r] → [l, r-1] と [l+1, r] に接続(境界は自己参照)

最大權閉路は、源点から正の重みノードへ、負の重みノードからターゲットへ、容量無限の辺を張り、最大流を引くことで得られる。実装では代号を座標圧縮。

#include <queue>
#include <cstdio>
#include <cstring>
#include <algorithm>
const int N = 5e6 + 10, M = 110, inf = 2e9;

int a[M], b[M], rev[M], tmp[M], id[M][M], ID[M], len;
struct edge { int v, c, next; } E[N << 1];
int cur[N], p[N], cnt, S, T = N - 1, tn, d[N];

inline void init() {
    memset(p, -1, sizeof(p));
    memset(cur, -1, sizeof(cur));
    cnt = 0;
}

inline void insert(int u, int v, int c) {
    E[cnt].v = v; E[cnt].c = c; E[cnt].next = p[u]; p[u] = cnt++;
}

inline void addedge(int u, int v, int c) {
    insert(u, v, c); insert(v, u, 0);
}

inline bool bfs() {
    memset(d, -1, sizeof(int) * (tn + 10));
    d[T] = -1;
    std::queue<int> q; q.push(S); d[S] = 0; cur[S] = p[S];
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = p[u], v; i + 1; i = E[i].next) {
            v = E[i].v; cur[v] = p[v];
            if (d[v] == -1 && E[i].c) d[v] = d[u] + 1, q.push(v);
        }
    }
    return d[T] != -1;
}

int dfs(int u, int flow) {
    if (u == T) return flow;
    int ans = 0, ret;
    for (int i = cur[u], v; i + 1; i = E[i].next) {
        v = E[i].v; cur[u] = i;
        if (d[v] == d[u] + 1 && E[i].c) {
            ret = dfs(v, std::min(E[i].c, flow));
            E[i].c -= ret; E[i ^ 1].c += ret;
            flow -= ret; ans += ret; if (!flow) break;
        }
    }
    if (!ans) d[u] = -1;
    return ans;
}

int dinic() { int ans = 0; while (bfs()) ans += dfs(S, inf); return ans; }

int main() {
    init();
    int n, m, ans = 0;
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]), tmp[i] = a[i];
    std::sort(tmp + 1, tmp + 1 + n);
    len = std::unique(tmp + 1, tmp + 1 + n) - tmp - 1;

    for (int i = 1; i <= n; ++i) {
        b[i] = std::lower_bound(tmp + 1, tmp + 1 + len, a[i]) - tmp;
        rev[b[i]] = a[i];
    }

    for (int i = 1; i <= len; ++i)
        addedge(++tn, T, m * rev[i] * rev[i]);

    for (int i = 1; i <= n; ++i) {
        addedge(ID[i] = ++tn, T, a[i]);
        addedge(ID[i], b[i], inf);
    }

    for (int i = 1, d; i <= n; ++i)
        for (int j = 1; j <= n - i + 1; ++j) {
            scanf("%d", &d);
            if (d < 0) addedge(!id[i][j] ? id[i][j] = ++tn : id[i][j], T, -d);
            else addedge(S, !id[i][j] ? id[i][j] = ++tn : id[i][j], d), ans += d;

            if (j == 1) addedge(id[i][j], ID[i], inf);
            else {
                addedge(id[i][j], id[i][j - 1], inf);
                addedge(id[i][j], (!id[i + 1][j - 1] ? (id[i + 1][j - 1] = ++tn) : id[i + 1][j - 1]), inf);
            }
        }

    printf("%d\n", ans - dinic());
    return 0;
}

タグ: 線分木 ネットワークフロー 行列累乗 オイラー関数 貪欲法

10月6日 06:50 投稿