つちのこの日記

競プロと音ゲーがメインです。

ねこ鍋改造計画(仮) (AOJ 2337)

この記事は帰ってきた AOJ-ICPC Advent Calendar 2022 12 日目の記事です。

問題リンク

https://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2337 (1000 点)

問題概要

$N_a$ 匹のオスのネコ、 $N_b$ 匹のメスのネコがいて、各猫には固有の重さとかわいさが定まっている。

重さの和が $W$ を超えないようにオスのネコを $1$ 匹以上選ぶ。メスのネコについても同様に選ぶ。 このとき、$\mathrm{max}( |$選んだオスのネコの重さの和 - 選んだメスのネコの重さの和$|,$ 選んだネコの可愛さの最大値 - 選んだネコの可愛さの最小値$)$ を最小化せよ。

制約

  • $1 \leq N_A,N_B \leq 500$
  • $1 \leq W \leq 10^ 4 $
  • $1 \leq$ 各ネコの重さ $ \leq 10^ 4 $
  • $1 \leq$ 各ネコのかわいさ $ \leq 10^ 9 $

解法

まず、重さが $W$ より大きいネコが入力に含まれうる(非本質。。)のでそのようなネコを捨てます。

次に、目的関数の第二項、すなわち (選んだネコの可愛さの最大値 - 選んだネコの可愛さの最小値) の取り得る最小値を求めます。これは適当に全探索しても十分間に合いますし、ソートすればより高速です。

第二項の最小値が $W-1$ 以上のとき、第一項は $W-1$ 以下であることからそれを答えとして出力すればよいです。

以下そうでない場合を解きます。最大値の最小化なので二分探索をします。答えを $x$ 以下に出来るかという判定問題について考えます。

愚直な解法として、ネコの可愛さの最小値を決め打つと、使えるネコの集合が決まるので、部分和問題を解くことで判定問題に答えられます。しかしこの解法は $\mathrm{O}(N ^ 2 W) $ かかります。(昔の問題なので bitset 高速化でこの解法も通ってしまうようです)

ここではより計算量のよい解法で解きます。ネコをかわいさでソートしておき、尺取り法のように部分和 DP を更新することを考えます。この際、使えなくなったネコによる遷移を打ち消す(戻す DP)必要性が生じます。

「部分集合であって重さ w を達成できるか?」という bool を管理する DP は戻すことが難しいですが、 boolを持つのでなく、かわりに「部分集合であって重さ w を達成できるものは何通りか $\text{mod}$ 適当な数」という個数を管理する DP にすることで、戻す DP が可能となり尺取り法のような DP 更新が可能となります。

以上でこの問題を $\mathrm{O}(NW \log W) $ で解くことが出来ました。

実装例 (c++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
#define rep(i, x)    for(int i = 0; i < (x); i++)
#define rng(i, l, r) for(int i = (l); i < (r); i++)
#define sz(v)        (int)(v).size()
#define all(v)       (v).begin(), (v).end()

constexpr int mod = 1000000007;
struct mint {
    ll v;
    mint(ll x = 0) {
        v = x % mod;
        if(v < 0) v += mod;
    }
    mint operator+(mint a) { return v + a.v; }
    mint operator-(mint a) { return v - a.v; }
    mint operator*(mint a) { return v * a.v; }
};

using fps = vector<mint>;

// f *= (1 + x^d)
void multiply(fps &f, int d) {
    for(int i = sz(f) - 1 - d; i >= 0; i--)
        f[i + d] = f[i + d] + f[i];
}

// f /= (1 + x^d)
void divide(fps &f, int d) {
    rep(i, sz(f) - d)
        f[i + d] = f[i + d] - f[i];
}

int main() {
    int na, nb, w;
    cin >> na >> nb >> w;

    using T = tuple<int, int, int>;
    vector<T> ctm;

    rep(i, na) {
        int m, c;
        cin >> m >> c;
        if(m > w) continue;
        ctm.push_back({c, 0, m});
    }
    rep(i, nb) {
        int m, c;
        cin >> m >> c;
        if(m > w) continue;
        ctm.push_back({c, 1, m});
    }

    sort(all(ctm));

    int term2_min = 1 << 30;
    rep(i, sz(ctm) - 1) {
        if(get<1>(ctm[i]) != get<1>(ctm[i + 1])) {
            term2_min = min(term2_min, get<0>(ctm[i + 1]) - get<0>(ctm[i]));
        }
    }
    if(term2_min >= w - 1) {
        cout << term2_min << endl;
        return 0;
    }

    auto f = [&](int x) -> bool {
        vector<fps> fs(2, fps(w + 1));
        fs[0][0] = fs[1][0] = 1;
        queue<T> que;
        for(auto [c, t, m] : ctm) {
            while(!que.empty() and c - get<0>(que.front()) > x) {
                auto [oc, ot, om] = que.front();
                que.pop();
                divide(fs[ot], om);
            }
            que.push(T{c, t, m});
            multiply(fs[t], m);
            pii bef = {-(1 << 30), 0};
            rng(i, 1, w + 1) {
                rep(j, 2) {
                    if(fs[j][i].v != 0) {
                        if(i - bef.first <= x and bef.second != j) return true;
                        bef = {i, j};
                    }
                }
            }
        }
        return false;
    };

    int ok = w, ng = -1, mid;
    while(mid = (ok + ng) >> 1, ok - ng > 1) (f(mid) ? ok : ng) = mid;

    cout << ok << endl;
}

感想

AOJ-ICPC では 1000 点が付いていますが、特に難しい考察もなく、実装も平易なので 1000 点初 AC を狙いたい人にはお勧めかもしれません。