以下の内容はhttps://drken1215.hatenablog.com/より取得しました。


JOI 春合宿 2024 day1-1 魚 3 (Fish 3) (3D, 難易度 10)

久しぶりに JOI の重たい問題が解けた!

問題概要(意訳)

正の整数  D が与えられる。

一般に、 0 以上の整数からなる長さ  M の数列  A_{1}, A_{2}, \dots, A_{M} に対して、次の操作を繰り返すことによって広義単調増加であるようにすることが可能であるとき、 f_{D}(A) をその操作回数の最小値とし、不可能であるとき  f_{D}(A) = -1 とする。

  • 操作:数列 A の要素を 1 つ選び、 D を引く (ただし、負になる場合にはこの操作はできない)

さて、長さ  N の数列  C_{1}, C_{2}, \dots, C_{N} が入力として与えられるので、以下の  Q 個のクエリに答えよ:

「各クエリでは整数  L, R ( 1 \le L \le R \le N) が与えられるので、 f_{D}(C_{L}, C_{L+1}, \dots, C_{R}) を求めよ」

制約

  •  1 \le N, Q \le 300000
  •  1 \le D \le 10^{12}
  •  0 \le C_{i} \le 10^{12}

考えたこと:まずはクエリでないバージョンを解く

まずは、クエリではないバージョンの問題を解くことを考える。つまり、一般に数列  A に対して  f_{D}(A) を求める方法を考える。

たとえば、 D = 3 A = (8, 11, 7, 18, 29, 19) のとき、右から順に見ていき、「右隣の値より小さい範囲で最大の数にする」という Greedy に基づく解法でよい。

なお、小課題 1 はこの解法で正解できる。

右端を右へずらしていく

それでは、クエリに対処していく。ここでは、区間の右端を徐々に右にずらしていくことを考える。

数列  C の区間  \lbrack 0, r) を最適にした状態( i 番目の要素  C_{i} は操作後に  A_{i} になるとする)から、区間 [0, r+1) を最適にする際の差分に着目しよう。

  •  A_{r-1} \le C_{r} のとき:既存の数列  A には特に修正を加えず、値  C_{r} が末尾に加わる
  •  A_{r-1} \gt C_{r} のとき:既存の数列  A の末尾の値が  C_{r} 以下になるように操作を繰り返して、 A の残りの要素についても同様に操作を繰り返していく

後者について、もし既存の数列  A のどの隣接する要素の差も  D 未満であったならば、下の図のように、一様に  A の各要素から同じ回数分だけ操作を実行することになる。(0 未満になる部分があっても一旦気にしないこととする)

次に、もし既存の数列  A において、隣接する要素が  D 以上の箇所があれば、「隣接する要素の右側から何回まで  D を引いても単調増加性が壊れないか」を示す値を管理しよう。この値を用いることで、下の図のように、既存の数列  A の各要素に対する操作回数が、右から順に確定していくことが分かる。

したがって、次のデータ構造を毎回更新していけばよいことがわかった。


  • 操作後の数列  A (区間加算のできるセグ木・BIT)
  • 操作後の数列  A について、差が  D 以上であるような隣接 2 要素についての、「隣接する要素の右側に対して何回まで  D を引いても単調増加性が壊れないか」を管理する値

前者については、一律に同じ値を減算する部分を遅延評価セグメント木または BIT で実現する。

ここで後者について考えると、右端が広がっていく際に、一度差が  D 未満になった隣接要素については、二度と差が  D 以上になることはない。したがって、前者の「一律に同じ値を減算する」の発生回数は高々  O(N+Q) 回に抑えられる。

以上から、全体として  O((N+Q)\log N) の計算量で問題を解くことができた。

コード

僕の実装だと、long long 型を使うとオーバーフローしてしまったので、int128__ 型を使った。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using i128 = __int128_t;
using pll = pair<ll, ll>;
using tll = array<ll, 3>;
using vll = vector<ll>;
#define COUT(x) cout << #x << " = " << (x) << " (L" << __LINE__ << ")" << endl

// output
template<class T> ostream& operator << (ostream &s, const vector<T> &P)
{ for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; }
template<class T> ostream& operator << (ostream &s, const deque<T> &P)
{ for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; }

// int 128
i128 to_integer(const string &s) {
    i128 res = 0;
    for (auto c : s) {
         if (isdigit(c)) res = res * 10 + (c - '0');
    }
    if (s[0] == '-') res *= -1;
    return res;
}
istream& operator >> (istream &is, i128 &x) {
    string s;
    is >> s;
    x = to_integer(s);
    return is;
}
ostream& operator << (ostream &os, const i128 &x) {
    i128 ax = (x >= 0 ? x : -x);
    char buffer[128];
    char *d = end(buffer);
    do {
         --d;
        *d = "0123456789"[ax % 10];
        ax /= 10;
    } while (ax != 0);
    if (x < 0) {
        --d;
        *d = '-';
    }
    int len = end(buffer) - d;
    if (os.rdbuf()->sputn(d, len) != len) {
        os.setstate(ios_base::badbit);
    }
    return os;
}

// Range Add Range sum
template<class Abel> struct RangeAddRangeSum {
    Abel UNITY_SUM = 0;
    vector<Abel> dat[2];

    // [0, n)
    RangeAddRangeSum(int n, Abel unity = 0) : UNITY_SUM(unity) {
        init(n);
    }
    void init(int n) {
        for (int iter = 0; iter < 2; ++iter)
            dat[iter].assign(n + 1, UNITY_SUM);
    }
    int size() const {
        return (int)dat[0].size();
    }
    
    // [a, b), a and b are 0-indexed
    inline void sub_add(int p, int a, Abel x) {
        for (int i = a; i < (int)dat[p].size(); i |= i + 1)
            dat[p][i] = dat[p][i] + x;
    }
    inline void add(int a, int b, Abel x) {
        sub_add(0, a, x * (-a));
        sub_add(1, a, x);
        sub_add(0, b, x * b);
        sub_add(1, b, x * (-1));
    }
    
    // [a, b), a and b are 0-indexed
    inline Abel sub_sum(int p, int a) {
        Abel res = UNITY_SUM;
        for (int i = a - 1; i >= 0; i = (i & (i + 1)) - 1)
            res = res + dat[p][i];
        return res;
    }
    inline Abel sum(int a, int b) {
        return sub_sum(0, b)
            + sub_sum(1, b) * b
            - sub_sum(0, a)
            - sub_sum(1, a) * a;
    }
    inline Abel operator [] (int i) {
        return sum(i, i + 1);
    }
    
    // debug
    friend ostream& operator << (ostream &s, RangeAddRangeSum bit) {
        for (int i = 0; i < (int)bit.size(); ++i) s << bit[i] << " ";
        return s;
    }
};


int main() {
    i128 N, D, Q; cin >> N >> D;
    vector<i128> C(N);
    for (int i = 0; i < N; i++) cin >> C[i];
    cin >> Q;
    vector<i128> L(Q), R(Q), res(Q, -1);
    vector<vector<pair<i128,i128>>> qs(N+1);
    for (int q = 0; q < Q; q++) {
        cin >> L[q] >> R[q], L[q]--, qs[R[q]].emplace_back(L[q], q);
    }
    
    RangeAddRangeSum<i128> cnt(N, 0), A(N, 0);
    deque<pair<i128,i128>> walls;
    for (int r = 0; r < N; r++) {
        A.add(r, r+1, C[r]);
        if (r >= 1 && C[r] >= A[r-1] + D) walls.emplace_back(r, (C[r] - A[r-1]) / D);
        if (r >= 1 && C[r] < A[r-1]) {
            i128 need = (A[r-1] - C[r] + D - 1) / D, right = r;
            while (!walls.empty() && need > 0) {
                auto [left, rem] = walls.back(); 
                walls.pop_back();
                A.add(left, right, -D * need), cnt.add(left, right, need);
                i128 sub = min(rem, need);
                rem -= sub, need -= sub;
                if (rem > 0) walls.emplace_back(left, rem);
                right = left;
            }
            if (need > 0) A.add(0, right, -D * need), cnt.add(0, right, need);
        }
        for (auto [l, id] : qs[r+1]) if (A[l] >= 0) res[id] = cnt.sum(l, r+1);
    }
    for (auto val : res) cout << val << endl;
}

AtCoder AGC 009 C - Division into Two (3D, 橙色, 1100 点)

2017 年の AGC の問題。現代なら ABC F で出そうだ。

問題概要

相異なる  N 個の整数  S_{0}, S_{1} \dots, S_{N-1} を 2 つのグループ X, Y に分けたい。

  • グループ X では、どの 2 個の要素も差が  A 以下
  • グループ Y では、どの 2 個の要素も差が  B 以下

となるようにする分け方は何通りあるか? 1000000007 で割った余りを求めよ。

制約

  •  1 \le N \le 10^{5}
  •  1 \le S_{i}, A, B \le 10^{18}
  •  S は小さい順に並んでいる

考えたこと

この手の「どちらに振り分けるか」という選択を繰り返すような問題は、DP で解けることが多い。

今回は、各グループにすでに入っている数と新たに入れる数との差が  A, B 以下であるかどうかを判定したいので、次のように DP を定義してみることにした。


  • dpX[i+1][j] S の最初の  i + 1 個のみについて考える。それらを X, Y に振り分ける方法のうち、最後の要素  S_{i} を X に入れる方法であって、Y に入っているものの最大値が  S_{j} であるような方法の個数
  • dpY[i+1][j] S の最初の  i + 1 個のみについて考える。それらを X, Y に振り分ける方法のうち、最後の要素  S_{i} を Y に入れる方法であって、X に入っているものの最大値が  S_{j} であるような方法の個数

DP の遷移を詰める

前回までの最後の要素が X 側で、新要素を X 側に入れるとき

  •  S_{i} - S_{i-1} \le A のとき:dpX[i+1][j] += dpX[i][j]
  • そうでないとき:dpX[i+1][j] = 0 ([tex: j = 0, 1, \dots, i-2)

前回までの最後の要素が Y 側で、新要素を Y 側に入れるとき

  •  S_{i} - S_{i-1} \le B のとき:dpY[i+1][j] += dpY[i][j]
  • そうでないとき:dpY[i+1][j] = 0 ([tex: j = 0, 1, \dots, i-2)

前回までの最後の要素が X 側で、新要素を Y 側に入れるとき

 S_{j} \le S_{i} - B となる  j について

dpY[i+1][i-1] += dpX[i][j]

前回までの最後の要素が Y 側で、新要素を X 側に入れるとき

 S_{j} \le S_{i} - A となる  j について

dpX[i+1][i-1] += dpY[i][j]

DP 高速化

上の DP は、in-place 化した上で、

  • 区間 Change:区間 [l, r) の値をすべて x に更新する
  • 区間和取得:区間 [l, r) の総和を求める

ができる遅延評価セグメント木を用いると、 O(N \log N) の計算量で解ける。

......というのが、僕の AC した解法であった。

が、実は、区間 Change は必要なかった。僕は、in-place 化した配列 dp 上で dp[j] = 0 といった更新をする部分で、区間 Change をした。しかし、「最後に 0 にした箇所」を記録すれば、尺取法の要領で愚直に 0 にしていけばよいのだ。

コード

とはいえ、遅延評価セグメント木で殴るのは楽。ここでは遅延評価セグメント木で通したコードを。

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")

#include <bits/stdc++.h>
using namespace std;

template<class S, class T> inline bool chmax(S &a, T b) { return (a < b ? a = b, 1 : 0); }
template<class S, class T> inline bool chmin(S &a, T b) { return (a > b ? a = b, 1 : 0); }

using pint = pair<int, int>;
using pll = pair<long long, long long>;
using tint = array<int, 3>;
using tll = array<long long, 3>;
using fint = array<int, 4>;
using fll = array<long long, 4>;
using qint = array<int, 5>;
using qll = array<long long, 5>;
using sint = array<int, 6>;
using sll = array<long long, 6>;
using vint = vector<int>;
using vll = vector<long long>;
using ll = long long;
using u32 = unsigned int;
using u64 = unsigned long long;
using i128 = __int128_t;
using u128 = __uint128_t;
template <class T>
using min_priority_queue = priority_queue<T, vector<T>, greater<T>>;

#define REP(i, a) for (long long i = 0; i < (long long)(a); i++)
#define REP2(i, a, b) for (long long i = a; i < (long long)(b); i++)
#define RREP(i, a) for (long long i = (a)-1; i >= (long long)(0); --i)
#define RREP2(i, a, b) for (long long i = (b)-1; i >= (long long)(a); --i)
#define EB emplace_back
#define PB push_back
#define MP make_pair
#define MT make_tuple
#define FI first
#define SE second
#define ALL(x) x.begin(), x.end()
#define COUT(x) cout << #x << " = " << (x) << " (L" << __LINE__ << ")" << endl


//------------------------------//
// Library
//------------------------------//

// mod inv
template<class T_VAL, class T_MOD>
constexpr T_VAL mod_inv(T_VAL a, T_MOD m) {
    T_VAL b = m, u = 1, v = 0;
    while (b > 0) {
        T_VAL t = a / b;
        a -= t * b, swap(a, b);
        u -= t * v, swap(u, v);
    }
    u %= m;
    if (u < 0) u += m;
    return u;
}

// modint
template<int MOD = 998244353, bool PRIME = true> struct Fp {
    // inner value
    unsigned int val;
    
    // constructor
    constexpr Fp() : val(0) { }
    template<std::signed_integral T> constexpr Fp(T v) {
        long long tmp = (long long)(v % (long long)(get_umod()));
        if (tmp < 0) tmp += get_umod();
        val = (unsigned int)(tmp);
    }
    template<std::unsigned_integral T> constexpr Fp(T v) {
        val = (unsigned int)(v % get_umod());
    }
    constexpr long long get() const { return val; }
    constexpr static int get_mod() { return MOD; }
    constexpr static unsigned int get_umod() { return MOD; }
    
    // arithmetic operators
    constexpr Fp operator + () const { return Fp(*this); }
    constexpr Fp operator - () const { return Fp() - Fp(*this); }
    constexpr Fp operator + (const Fp &r) const { return Fp(*this) += r; }
    constexpr Fp operator - (const Fp &r) const { return Fp(*this) -= r; }
    constexpr Fp operator * (const Fp &r) const { return Fp(*this) *= r; }
    constexpr Fp operator / (const Fp &r) const { return Fp(*this) /= r; }
    constexpr Fp& operator += (const Fp &r) {
        val += r.val;
        if (val >= get_umod()) val -= get_umod();
        return *this;
    }
    constexpr Fp& operator -= (const Fp &r) {
        val -= r.val;
        if (val >= get_umod()) val += get_umod();
        return *this;
    }
    constexpr Fp& operator *= (const Fp &r) {
        unsigned long long tmp = val;
        tmp *= r.val;
        val = (unsigned int)(tmp % get_umod());
        return *this;
    }
    constexpr Fp& operator /= (const Fp &r) {
        return *this = *this * r.inv(); 
    }
    constexpr Fp pow(long long n) const {
        assert(n >= 0);
        Fp res(1), mul(*this);
        while (n) {
            if (n & 1) res *= mul;
            mul *= mul;
            n >>= 1;
        }
        return res;
    }
    constexpr Fp inv() const {
        if (PRIME) {
            assert(val);
            return pow(get_umod() - 2);
        } else {
            assert(val);
            return mod_inv((long long)(val), get_umod());
        }
    }

    // other operators
    constexpr bool operator == (const Fp &r) const {
        return this->val == r.val;
    }
    constexpr bool operator != (const Fp &r) const {
        return this->val != r.val;
    }
    constexpr bool operator < (const Fp &r) const {
        return this->val < r.val;
    }
    constexpr bool operator > (const Fp &r) const {
        return this->val > r.val;
    }
    constexpr bool operator <= (const Fp &r) const {
        return this->val <= r.val;
    }
    constexpr bool operator >= (const Fp &r) const {
        return this->val >= r.val;
    }
    constexpr Fp& operator ++ () {
        ++val;
        if (val == get_umod()) val = 0;
        return *this;
    }
    constexpr Fp& operator -- () {
        if (val == 0) val = get_umod();
        --val;
        return *this;
    }
    constexpr Fp operator ++ (int) {
        Fp res = *this;
        ++*this;
        return res;
    }
    constexpr Fp operator -- (int) {
        Fp res = *this;
        --*this;
        return res;
    }
    friend constexpr istream& operator >> (istream &is, Fp<MOD> &x) {
        long long tmp = 1;
        is >> tmp;
        tmp = tmp % (long long)(get_umod());
        if (tmp < 0) tmp += get_umod();
        x.val = (unsigned int)(tmp);
        return is;
    }
    friend constexpr ostream& operator << (ostream &os, const Fp<MOD> &x) {
        return os << x.val;
    }
    friend constexpr Fp<MOD> pow(const Fp<MOD> &r, long long n) {
        return r.pow(n);
    }
    friend constexpr Fp<MOD> inv(const Fp<MOD> &r) {
        return r.inv();
    }
};

// Lazy Segment Tree
template<class Monoid, class Action> struct LazySegmentTree {
    // various function types
    using FuncMonoid = function<Monoid(Monoid, Monoid)>;
    using FuncAction = function<Monoid(Action, Monoid)>;
    using FuncComposition = function<Action(Action, Action)>;

    // core member
    int N;
    FuncMonoid OP;
    FuncAction ACT;
    FuncComposition COMP;
    Monoid IDENTITY_MONOID;
    Action IDENTITY_ACTION;
    
    // inner data
    int log, offset;
    vector<Monoid> dat;
    vector<Action> lazy;
    
    // constructor
    LazySegmentTree() {}
    LazySegmentTree(const FuncMonoid op, const FuncAction act, const FuncComposition comp,
                    const Monoid &identity_monoid, const Action &identity_action) 
                    : OP(op), ACT(act), COMP(comp), 
                    IDENTITY_MONOID(identity_monoid), IDENTITY_ACTION(identity_action) {}
    LazySegmentTree(int n, const FuncMonoid op, const FuncAction act, const FuncComposition comp,
                    const Monoid &identity_monoid, const Action &identity_action) {
        init(n, op, act, comp, identity_monoid, identity_action);
    }
    LazySegmentTree(const vector<Monoid> &v,
                    const FuncMonoid op, const FuncAction act, const FuncComposition comp,
                    const Monoid &identity_monoid, const Action &identity_action) {
        init(v, op, act, comp, identity_monoid, identity_action);
    }
    void init(const FuncMonoid op, const FuncAction act, const FuncComposition comp,
              const Monoid &identity_monoid, const Action &identity_action) {
        OP = op, ACT = act, COMP = comp;
        IDENTITY_MONOID = identity_monoid, IDENTITY_ACTION = identity_action;      
    }
    void init(int n) {
        N = n, 
        log = 0, offset = 1;
        while (offset < N) ++log, offset <<= 1;
        dat.assign(offset * 2, IDENTITY_MONOID);
        lazy.assign(offset * 2, IDENTITY_ACTION);
    }
    void init(const vector<Monoid> &v) {
        init((int)v.size());
        build(v);
    }
    void init(int n, const FuncMonoid op, const FuncAction act, const FuncComposition comp,
              const Monoid &identity_monoid, const Action &identity_action) {
        init(op, act, comp, identity_monoid, identity_action);
        init(n);
    }
    void init(const vector<Monoid> &v,
              const FuncMonoid op, const FuncAction act, const FuncComposition comp,
              const Monoid &identity_monoid, const Action &identity_action) {
        init((int)v.size(), op, act, comp, identity_monoid, identity_action);
        build(v);
    }
    void build(const vector<Monoid> &v) {
        assert(N == (int)v.size());
        for (int i = 0; i < N; ++i) dat[i + offset] = v[i];
        for (int k = offset - 1; k > 0; --k) pull_dat(k);
    }
    int size() const {
        return N;
    }
    
    // basic functions for lazy segment tree
    void pull_dat(int k) {
        dat[k] = OP(dat[k * 2], dat[k * 2 + 1]);
    }
    void apply_lazy(int k, const Action &f) {
        dat[k] = ACT(f, dat[k]);
        if (k < offset) lazy[k] = COMP(f, lazy[k]);
    }
    void push_lazy(int k) {
        apply_lazy(k * 2, lazy[k]);
        apply_lazy(k * 2 + 1, lazy[k]);
        lazy[k] = IDENTITY_ACTION;
    }
    void pull_dat_deep(int k) {
        for (int h = 1; h <= log; ++h) pull_dat(k >> h);
    }
    void push_lazy_deep(int k) {
        for (int h = log; h >= 1; --h) push_lazy(k >> h);
    }
    
    // setter and getter, update A[i], i is 0-indexed, O(log N)
    void set(int i, const Monoid &v) {
        assert(0 <= i && i < N);
        int k = i + offset;
        push_lazy_deep(k);
        dat[k] = v;
        pull_dat_deep(k);
    }
    Monoid get(int i) {
        assert(0 <= i && i < N);
        int k = i + offset;
        push_lazy_deep(k);
        return dat[k];
    }
    Monoid operator [] (int i) {
        return get(i);
    }
    
    // apply f for index i
    void apply(int i, const Action &f) {
        assert(0 <= i && i < N);
        int k = i + offset;
        push_lazy_deep(k);
        dat[k] = ACT(f, dat[k]);
        pull_dat_deep(k);
    }
    // apply f for interval [l, r)
    void apply(int l, int r, const Action &f) {
        assert(0 <= l && l <= r && r <= N);
        if (l == r) return;
        l += offset, r += offset;
        for (int h = log; h >= 1; --h) {
            if (((l >> h) << h) != l) push_lazy(l >> h);
            if (((r >> h) << h) != r) push_lazy((r - 1) >> h);
        }
        int original_l = l, original_r = r;
        for (; l < r; l >>= 1, r >>= 1) {
            if (l & 1) apply_lazy(l++, f);
            if (r & 1) apply_lazy(--r, f);
        }
        l = original_l, r = original_r;
        for (int h = 1; h <= log; ++h) {
            if (((l >> h) << h) != l) pull_dat(l >> h);
            if (((r >> h) << h) != r) pull_dat((r - 1) >> h);
        }
    }
    
    // get prod of interval [l, r)
    Monoid prod(int l, int r) {
        assert(0 <= l && l <= r && r <= N);
        if (l == r) return IDENTITY_MONOID;
        l += offset, r += offset;
        for (int h = log; h >= 1; --h) {
            if (((l >> h) << h) != l) push_lazy(l >> h);
            if (((r >> h) << h) != r) push_lazy(r >> h);
        }
        Monoid val_left = IDENTITY_MONOID, val_right = IDENTITY_MONOID;
        for (; l < r; l >>= 1, r >>= 1) {
            if (l & 1) val_left = OP(val_left, dat[l++]);
            if (r & 1) val_right = OP(dat[--r], val_right);
        }
        return OP(val_left, val_right);
    }
    Monoid all_prod() {
        return dat[1];
    }
    
    // get max r that f(get(l, r)) = True (0-indexed), O(log N)
    // f(IDENTITY) need to be True
    int max_right(const function<bool(Monoid)> f, int l = 0) {
        if (l == N) return N;
        l += offset;
        push_lazy_deep(l);
        Monoid sum = IDENTITY_MONOID;
        do {
            while (l % 2 == 0) l >>= 1;
            if (!f(OP(sum, dat[l]))) {
                while (l < offset) {
                    push_lazy(l);
                    l = l * 2;
                    if (f(OP(sum, dat[l]))) {
                        sum = OP(sum, dat[l]);
                        ++l;
                    }
                }
                return l - offset;
            }
            sum = OP(sum, dat[l]);
            ++l;
        } while ((l & -l) != l);  // stop if l = 2^e
        return N;
    }

    // get min l that f(get(l, r)) = True (0-indexed), O(log N)
    // f(IDENTITY) need to be True
    int min_left(const function<bool(Monoid)> f, int r = -1) {
        if (r == 0) return 0;
        if (r == -1) r = N;
        r += offset;
        push_lazy_deep(r - 1);
        Monoid sum = IDENTITY_MONOID;
        do {
            --r;
            while (r > 1 && (r % 2)) r >>= 1;
            if (!f(OP(dat[r], sum))) {
                while (r < offset) {
                    push_lazy(r);
                    r = r * 2 + 1;
                    if (f(OP(dat[r], sum))) {
                        sum = OP(dat[r], sum);
                        --r;
                    }
                }
                return r + 1 - offset;
            }
            sum = OP(dat[r], sum);
        } while ((r & -r) != r);
        return 0;
    }
    
    // debug stream
    friend ostream& operator << (ostream &s, LazySegmentTree seg) {
        for (int i = 0; i < (int)seg.size(); ++i) {
            s << seg[i];
            if (i != (int)seg.size() - 1) s << " ";
        }
        return s;
    }
    
    // dump
    void dump() {
        for (int i = 0; i <= log; ++i) {
            for (int j = (1 << i); j < (1 << (i + 1)); ++j) {
                cout << "{" << dat[j] << "," << lazy[j] << "} ";
            }
            cout << endl;
        }
    }
};

// range change range sum
template<class Monoid> auto seg_op_add_with_sum = [](pair<Monoid, long long> p, pair<Monoid, long long> q) { 
    return make_pair(p.first + q.first, p.second + q.second);
};
template<class Monoid, class Action> auto seg_act_change_with_sum = [](Action f, pair<Monoid, long long> x) {
    return (f != Action(-1) ? make_pair(f * x.second, x.second) : x);
};
template<class Action> auto seg_comp_change = [](Action g, Action f) {
    return (g != Action(-1) ? g : f);
};
template<class Monoid, class Action> LazySegmentTree<pair<Monoid, long long>, Action> RangeChangeRangeSum(int N = 0) {
    return LazySegmentTree<pair<Monoid, long long>, Action>(
        N, seg_op_add_with_sum<Monoid>, seg_act_change_with_sum<Monoid, Action>, seg_comp_change<Action>,
        make_pair(Monoid(0), 1), Action(-1));
};


//------------------------------//
// Solver
//------------------------------//

int main() {
    const ll MOD = 1000000007, INF = 1LL << 61;
    using mint = Fp<MOD>;

    ll N, A, B;
    cin >> N >> A >> B;
    deque<ll> S(N);
    REP(i, N) cin >> S[i];
    S.push_front(-INF);
    N = S.size();

    //vector<mint> dpA(N+1, 0), dpB(N+1, 0); // A 側ラストのものと、B 側ラストのもの
    auto dpA = RangeChangeRangeSum<mint, mint>(N+1), dpB = RangeChangeRangeSum<mint, mint>(N+1);
    dpA.apply(0, mint(1)), dpB.apply(0, mint(1));
    
    REP2(i, 2, N) {
        // A ラストのものに B を入れる
        int it = upper_bound(ALL(S), S[i] - B) - S.begin();
        chmin(it, i-1);
        dpB.apply(i-1, dpA.prod(0, it).first);

        // B ラストのものに A を入れる
        it = upper_bound(ALL(S), S[i] - A) - S.begin();
        chmin(it, i-1);
        dpA.apply(i-1, dpB.prod(0, it).first);

        // A ラストのものにまた A を入れる
        if (S[i] - S[i-1] < A) dpA.apply(0, i-1, mint(0));

        // B ラストのものにまた B を入れる
        if (S[i] - S[i-1] < B) dpB.apply(0, i-1, mint(0));
    }

    mint res = dpA.all_prod().first + dpB.all_prod().first;
    cout << res << endl;
}

AtCoder ABC 432 C - Candy Tribulation (1Q, 茶色, 350 点)

C 問題としてはかなり難しいですね。

問題概要(一部意訳)

小さい飴は 1 個  X 円、大きい飴は 1 個  Y 円である( X \lt Y である)。

 N 人がいて、人  i は、飴を合わせて  A_{i} 個買おうとしている。ただし、 N 人全員の購入金額がちょうどぴったり一致するようにしなければならない。

 N 人による「大きい飴」の購入個数の総和の最小値を求めよ。

制約

  •  2 \le N \le 2 \times 10^{5}
  •  1 \le A_{i}, X, Y \le 10^{9}

考えたこと

この手の問題では、ある変数を決め打って考えてみるということがとても大切です。しかし、この問題では、どの変数を決め打つとよいかを見定めることが難しいですね。

たとえば、次のような方向性で考えてしまうと、ドツボにハマってしまいます。

 N 人それぞれの、小さい飴の個数と、大きい飴の個数を色々変えてみて、全員の購入金額が一致するように調整するにはどうしたら......」

そうではなく、今回は、次のように考えてみましょう。


  •  N 人全員の購入金額がちょうどぴったり一致すると言われているのだから、その金額を  P などと置いてみよう
  • そして、 Pを決め打って考えてみよう

なぜ、購入金額  P を決め打つとよいかというと、 P を決め打つと、それぞれの人が小さい飴と大きい飴を何個変えばよいのかが、一意に決まるのです!!!

まずは、このことを解明してみましょう!

購入金額を決め打ったときの、各人の購入金額

 i の、小さい飴の購入個数を  x_{i} 個、大きい飴の購入個数を  y_{i} 個としよう。このとき、次の方程式が立てられます。

  •  x_{i} + y_{i} = A_{i} (合計  A_{i} 個買うことから)
  •  X x_{i} + Y y_{i} = P (合計金額が  P であることから)

これは、中2で学ぶ連立方程式です。これを解くと、次のようになります。

  •  \displaystyle x_{i} = \frac{Y A_{i} - P}{Y - X}
  •  \displaystyle y_{i} = \frac{P - X A_{i}}{Y - X}

なお、この方程式の解き方が分からないという方は、中2の教科書に解き方が載っているので、復習すると良いと思います(教科書に載っているのは  X, Y, P, A_{i} の部分が具体的な数字になっているようなものですが、それらが文字になっても解き方は一緒です)。

補足:この方程式が解けない人のための解き方


 x_{i} + y_{i} = A_{i} ...... ①
 X x_{i} + Y y_{i} = P ...... ②

とする。

① ×  Y より

 Y X_{i} + Y y_{i} = Y A_{i} ...... ③

③ - ② より

 (Y - X) X_{i} = Y A_{i} - P ...... ③

よって

 \displaystyle x_{i} = \frac{Y A_{i} - P}{Y - X} ...... ④

④ を ① に代入して

 \displaystyle \frac{Y A_{i} - P}{Y - X} + y_{i} = A_{i}

よって

 y_{i} = A_{i} - \displaystyle \frac{Y A_{i} - P}{Y - X}  = \displaystyle \frac{(Y - X) A_{i}}{Y - X} - \frac{Y A_{i} - P}{Y - X}  = \displaystyle \frac{Y A_{i} - X A_{i} - Y A_{i} + P}{Y - X}  = \displaystyle \frac{P - X A_{i}}{Y - X}


ここで当然気になるのは、   \displaystyle \frac{Y A_{i} - P}{Y - X}  \displaystyle \frac{P - X A_{i}}{Y - X} って分数だけど大丈夫なの......?? というところでしょう。

他にも、 P が大きすぎると   x_{i} = \displaystyle \frac{Y A_{i} - P}{Y - X} が負の値になってしまい、それもよくありません。私たちは購入価格  P を決め打って考えてきましたが、 P がなんでもいいというわけではないのです。

そこで、次に、 P が満たすべき条件について考えてみましょう。

購入価格  P が満たすべき条件

 P がなんでもいいというわけではありません。

まず、購入個数  A_{i} の最小値を  m、最大値を  M とするとき、 P はどうあがいても  Ym より大きくすることはできません。購入個数が最も少ない人の立場で考えると、その人の購入金額を最大にするためには、すべて大きい飴を買うとよいですが、そのときの購入金額が  Ym 円だからです。

同様に、 P はどうあがいても  XM より小さくすることはできません。購入個数が最も多い人の立場で考えると、その人の購入金額を最小にするためには、すべて小さい飴を買うとよいですが、そのときの購入金額が  XM 円だからです。

これらをまとめると、次の条件が必要であることがわかります(逆に、この条件を満たせば、 x_{i}, y_{i} はともに 0 以上  A_{i} 以下の値になります)。


 P が満たすべき条件 その 1】
 XM \le P \le Ym


次に、大きい飴の個数   y_{i} = \displaystyle \frac{P - X A_{i}}{Y - X} が整数となる条件を考えます。なお、大きい飴の個数が整数であれば、小さい飴と大きい飴の個数の総和が整数( A_{i} です)であることから、小さい飴の個数も整数になります。したがって、大きい飴の個数が整数になる条件を考えれば十分です。

  y_{i} = \displaystyle \frac{P - X A_{i}}{Y - X} が整数であるということは、 P - X A_{i} Y - X で割り切れるということです。

つまり、 D = Y - X とおくと、 P - X A_{1}, P - X A_{2}, \dots, P - X A_{N} がすべて  D の倍数であるということです。このことを合同式でかくと、次のようになります(なお、合同式に馴染みのない方は、『チャート式 数学A』などで復習してみてください)。

  •  P \equiv X A_{1} \pmod D
  •  P \equiv X A_{2} \pmod D
  • ......
  •  P \equiv X A_{N} \pmod D

よって、

 X A_{1} \equiv X A_{2} \equiv \dots \equiv X A_{N} \pmod D

が成り立ちます。つまり、 X A_{1}, X A_{2}, \dots, X A_{N} D で割った余りがすべて等しくなければならないということです。さらに、 P で割った余りも、それと一致する必要があります。


 P が満たすべき条件 その 2】

 X A_{1}, X A_{2}, \dots, X A_{N} D で割った余りを  r とすると、

 P D で割った余りも  r である


以上の「条件その 1・条件その 2」を満たせば、人  i が小さい飴を買った個数  x_{i}、大きい飴を買った個数  y_{i} は、ともに  0 以上  A_{i} 以下の整数になります。

 

トドメ

最後に、「大きい飴」の購入個数の総和が最大になるような  P の決め方を考えましょう。これは、実は簡単です。

大きい飴の方が、小さい飴よりも単価が高いのですから、合計購入個数が同じならば、購入金額が高ければ高いほど、大きい飴の購入個数は大きくなります。実際、大きい飴の購入個数  y_{i} についての式

 \displaystyle y_{i} = \frac{P - X A_{i}}{Y - X}

を見ると、 P が大きくなればなるほど、 y_{i} も大きくなることが分かります。

以上をまとめると、この問題の解法は次のように整理できます。

 

最終的な解法

まず、  X A_{1}, X A_{2}, \dots, X A_{N} D で割った余りがすべて等しいという条件を満たさない場合は、-1 を出力する。

このとき、  D で割った余りが  r である  Ym 以下の最大の整数を  P とすればよい。

ただし、この  P XM 未満となるならば、条件を満たす  P は存在しないので、-1 を出力する。

補足:D で割った余りが r である Ym 以下の最大の整数の求め方

一般に「 q で割った余りが  r である  L 以下の最大の整数」は次のコードのように求められます。

// a を b で割ったときの商(a が負の場合にも対応できるもの)
long long floor(long long a, long long b) {
    if (a % b == 0 || a >= 0) return a / b;
    else return -((-a) / b) - 1;
}

// q で割った余りが r である、L 以下の最大の整数を求める
long long max_qr(long long q, long long r, long long L) {
    // L - r 以下の最大の q の倍数を求めて、それに r を足したものを求めればよい
    // L - r 以下の最大の q の倍数は、q * floor(L - r, q) である
    return q * floor(L - r, q) + r;
}

 

AC コード

#include <bits/stdc++.h>
using namespace std;


// a を b で割ったときの商(a が負の場合にも対応できるもの)
long long floor(long long a, long long b) {
    if (a % b == 0 || a >= 0) return a / b;
    else return -((-a) / b) - 1;
}

int main() {
    long long N, X, Y;
    cin >> N >> X >> Y;
    vector<long long> A(N);
    for (int i = 0; i < N; i++) cin >> A[i];

    // M: A の最大値、m: A の最小値
    long long M = A[0], m = A[0], D = Y - X;
    for (int i = 0; i < N; i++) {
        M = max(M, A[i]);
        m = min(m, A[i]);
    }

    // X A[0], X A[1], ... を D で割った余りはすべて等しくなければならない
    long long r = X * A[0] % D;
    for (int i = 0; i < N; i++) {
        if (X * A[i] % D != r) {
            cout << -1 << endl;
            return 0;
        }
    }

    // Ym 以下の、D で割った余りが r である整数のうち、最大のものを P とする
    long long P = D * floor(Y * m - r, D) + r;

    // P が XM 未満はダメ
    if (P < X * M) {
        cout << -1 << endl;
        return 0;
    }

    // 最終結果を求める
    long long res = 0;
    for (int i = 0; i < N; i++) {
        long long yi = (P - X * A[i]) / D;
        res += yi;
    }
    cout << res << endl;
}

OUPC 2024 day1-C Handai Color (3D)

面白かった!

問題概要

3 つの文字 B, Y, K からなる長さ  N の文字列  S が与えられる。

この文字列をいくつかの区間に分割する。各区間について、

  • ランレングス圧縮すると、B -> Y -> K の順になるもの:区間の長さだけスコアを獲得
  • ランレングス圧縮すると、K -> Y -> B の順になるもの:区間の長さだけスコアを獲得
  • それ以外:スコア獲得なし

区間分割の仕方を最適化したときに、得られるスコアの総和の最大値を求めよ。

制約

  •  1 \le N \le 10^{6}

考えたこと

仮に  O(N^{2}) の計算量を費やしてもよいならば、いつもの「区間の分割の仕方を走査していく DP」で解ける。それがうまくいかない場合には、耳DPで解けることがよくある。今回は、次のような DP で上手くいった。


  • dp[i][0] S の先頭から  i 文字目までを考えて、その終端で一旦区間を区切る場合の、スコアの最大値
  • dp[i][j = "B", "BY", "BYK", "K", "KY", "KYB"] S の先頭から  i 文字目までを考えて、最後に区切った区間から終端までをランレングス圧縮すると  j になる場合についての、スコアの最大値

なお、わかりやすい図が公式解説にあった。

遷移をちゃんと詰めると AC となった。計算量は  O(N) と評価できる。

コード

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")

#include <bits/stdc++.h>
using namespace std;

template<class S, class T> inline bool chmax(S &a, T b) { return (a < b ? a = b, 1 : 0); }
template<class S, class T> inline bool chmin(S &a, T b) { return (a > b ? a = b, 1 : 0); }

using pint = pair<int, int>;
using pll = pair<long long, long long>;
using tint = array<int, 3>;
using tll = array<long long, 3>;
using vll = vector<long long>;
using ll = long long;
using u32 = unsigned int;
using u64 = unsigned long long;
using int128 = __int128;
using u128 = unsigned __int128;
template <class T>
using min_priority_queue = priority_queue<T, vector<T>, greater<T>>;

#define REP(i, a) for (long long i = 0; i < (long long)(a); i++)
#define REP2(i, a, b) for (long long i = a; i < (long long)(b); i++)
#define RREP(i, a) for (long long i = (a)-1; i >= (long long)(0); --i)
#define RREP2(i, a, b) for (long long i = (b)-1; i >= (long long)(a); --i)
#define EB emplace_back
#define PB push_back
#define MP make_pair
#define MT make_tuple
#define FI first
#define SE second
#define ALL(x) x.begin(), x.end()
#define COUT(x) cout << #x << " = " << (x) << " (L" << __LINE__ << ")" << endl

// debug stream
template<class T1, class T2> ostream& operator << (ostream &s, pair<T1,T2> P)
{ return s << '<' << P.first << ", " << P.second << '>'; }
template<class T> ostream& operator << (ostream &s, vector<T> P)
{ for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; }
template<class T> ostream& operator << (ostream &s, deque<T> P)
{ for (int i = 0; i < P.size(); ++i) { if (i > 0) { s << " "; } s << P[i]; } return s; }
template<class T> ostream& operator << (ostream &s, vector<vector<T> > P)
{ for (int i = 0; i < P.size(); ++i) { s << endl << P[i]; } return s << endl; }
template<class T> ostream& operator << (ostream &s, set<T> P)
{ for (auto it : P) { s << "<" << it << "> "; } return s; }
template<class T> ostream& operator << (ostream &s, multiset<T> P)
{ for (auto it : P) { s << "<" << it << "> "; } return s; }
template<class T1, class T2> ostream& operator << (ostream &s, map<T1,T2> P)
{ for (auto it : P) { s << "<" << it.first << "->" << it.second << "> "; } return s; }


int main() {
    cin.tie(nullptr);
    ios_base::sync_with_stdio(false);

    const int NON = 0;
    const int B = 1;
    const int BY = 2;
    const int BYK = 3;
    const int K = 4;
    const int KY = 5;
    const int KYB = 6;

    int N;
    string S;
    cin >> N >> S;
    N = (int)S.size();

    const int INF = 1<<29;
    vector dp(N+1, vector(7, -INF));
    dp[0][0] = 0;
    REP(i, N) {
        chmax(dp[i+1][NON], dp[i][NON]);
        if (S[i] == 'B') {
            chmax(dp[i+1][B], dp[i][NON] + 1);
            chmax(dp[i+1][B], dp[i][B] + 1);
            chmax(dp[i+1][KYB], dp[i][KY] + 1);
            chmax(dp[i+1][KYB], dp[i][KYB] + 1);
        } else if (S[i] == 'K') {
            chmax(dp[i+1][K], dp[i][NON] + 1);
            chmax(dp[i+1][K], dp[i][K] + 1);
            chmax(dp[i+1][BYK], dp[i][BY] + 1);
            chmax(dp[i+1][BYK], dp[i][BYK] + 1);
        } else {
            chmax(dp[i+1][KY], dp[i][K] + 1);
            chmax(dp[i+1][KY], dp[i][KY] + 1);
            chmax(dp[i+1][BY], dp[i][B] + 1);
            chmax(dp[i+1][BY], dp[i][BY] + 1);
        }
        chmax(dp[i+1][NON], dp[i+1][KYB]);
        chmax(dp[i+1][NON], dp[i+1][BYK]);
    }

    cout << dp[N][NON] << endl;
}

AtCoder ABC 329 F - Colored Ball (1D, 水色, 500 点)

人生で初めて「マージテク」を学ぶなら、この問題!!!

問題概要

 1, 2, \dots, N がある。箱  i には最初色  C_{i} のボールが入っている。以下の  Q 回のクエリに答えよ。

  • 整数  a, b が与えられる
  •  a のボールをすべて箱  b に移動させる
  • その後、箱  b に入っているボールの色の種類数を答える

制約

  •  N, Q \le 2 \times 10^{5}
  •  1 \le C_{i} \le N

考えたこと

まさに「マージテク」の問題。

この問題のクエリを愚直に実行すると、一見  O(Q N) の計算量を要するように見える。つまり、次のような実装が考えられる。

  • 各箱に入っているボールの色の種類を vector<unordered_set<int>> 型の変数(box とする)で管理する
  • box[a] の要素を 1 つ 1 つ、box[b] に挿入していく
    while (Q--) {
        cin >> a >> b;
        a--, b--;
        for (auto v : box[a]) box[b].insert(v);
        box[a].clear();
        cout << box[b].size() << '\n';
    }

確かに、このままだと  O(QN) の計算量を要する。

しかし、実は次の工夫をするだけで、 O(Q + N \log N) の計算量に改善できるのだ。


box[a] の要素を 1 つ 1 つ box[b] に挿入していく前に、もし

box[a].size() > box[b].size()

であるならば、swap(box[a], box[b]) をしておく


こうすることで、より少ないボールの移動でクエリを実行できる。まさかこのような小さな工夫が、計算量のオーダーレベルの改善をもたらすのは驚きだ!!

計算量が改善される理由

工夫後の実装において、各ボール  j について、何回箱を移動することになるかを考えてみよう。

ボール  j が箱  a から箱  b に移動するとき、box[a].size() <= box[b].size() であるとする(上述の工夫を施した場合の仮定)。

この仮定のもとでは、ボール  j の属する箱に入っているボールの色の種類数は、移動前後で 2 倍以上になっている。そのため、ボール  j の属する箱が入れ替わる回数は高々

 O(\log N) (回)

で抑えられるのだ。どのボールについても、このことが言えるので、クエリ全てに答えるのに要する計算量は  O(Q + N \log N) と評価できる。

コード

#include <bits/stdc++.h>
using namespace std;

int main() {
    int N, Q, a, b, C;
    cin >> N >> Q;
    vector<unordered_set<int>> box(N);
    for (int i = 0; i < N; i++) {
        cin >> C;
        box[i].insert(C);
    }
    while (Q--) {
        cin >> a >> b;
        a--, b--;
        if (box[a].size() > box[b].size()) swap(box[a], box[b]);
        for (auto v : box[a]) box[b].insert(v);
        box[a].clear();
        cout << box[b].size() << '\n';
    }
}



以上の内容はhttps://drken1215.hatenablog.com/より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14