C. Self-Stopping Shower

将按键时间升序并去掉 \(>T\),加入哨兵 \(T\),维护当前覆盖右端 \(r\),遍历每个时间点 \(x\):若 \(r<x\) 则补按 \(m=⌈\frac{x−r}{K}⌉\) 次,随后令 \(r=x+K\),累加所有 \(m\) 即为最少按键次数。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    int n, k, t;
    cin >> n >> k >> t;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    
    ranges::sort(a);
    
    while (a.size() and a.back() > t) a.pop_back();
    a.push_back(t);
    n = a.size();
    
    int ans = 0;
    int r = 0;
    for (int x : a) {
        if (r < x) {
            ans += (x-r + k-1)/k;
        }
        r = x+k;
    }
    
    cout << ans << '\n';
    
    return 0;
}

D. Concatenation of Prefixes

令 \(S(i) = \sum\limits_{k=1}^i A_k\),\(T(i) = \sum\limits_{j=1}^i S(j)\)
则 \(T(i)\) 就是把前 \(i\) 个完整块 \((A_1), (A_1, A_2), \cdots, (A_1, \cdots, A_i)\) 全部展开后的元素和;再用 \(len(i)\) 表示前 \(i\) 个块的总长度

定义 \(f(r)\) 为 \(B\) 的前 \(r\) 个元素的和:先用二分找出最大的 \(i\) 使得
\(len(i)<r\),那么前 \(i\) 个完整块的和是 \(T(i)\),剩下的 \(r−len(i)\) 个元素正好是前缀 \((A_1,…,A_{r−len(i)})\),其和为 \(S(r−len(i))\),因此
\(f(r)=T(i)+S(r−len(i))\) 。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

int main() {
    int n, q;
    cin >> n >> q;
    
    vector<ll> a(n);
    rep(i, n) cin >> a[i];
    
    vector<ll> s(n+1);
    rep(i, n) s[i+1] = s[i]+a[i];
    vector<ll> t(n+1);
    rep(i, n) t[i+1] = t[i]+s[i+1];
    
    vector<ll> len(n+1);
    rep(i, n) len[i+1] = len[i] + i+1;
    
    auto f = [&](ll r) -> ll {
        if (r == 0) return 0;
        int si = lower_bound(len.begin(), len.end(), r)-len.begin()-1;
        ll res = t[si];
        res += s[r-len[si]];
        return res;
    };
    
    rep(qi, q) {
        ll l, r;
        cin >> l >> r;
        ll ans = f(r) - f(l-1);
        cout << ans << '\n';
    }
    
    return 0;
}

E. Product-Bounded Arithmetic Sequences

枚举等差数列长度 \(l\),只需试到 \(l≤19\),因为 \(20!>10^{18}\),长度 \(≥20\) 时最小乘积已超上界。

对于固定 \(l\) 和首项 \(a\),要统计满足

\[\prod_{i=0}^{l-1}(a+di) \leqslant N \]

的正整数公差 \(d \geqslant 1\) 的个数;若 \(d=1\) 都不行(即 \(\prod\limits_{i=0}^{l-1} (a+i) > N\)),则更大的 \(a\) 也不可能,故终止枚举该 \(l\) 下的 \(a\)。

对每个合法的 \(a\) 用二分查找最大的 \(d\) 使得乘积不超 \(N\),乘积判断通过对
\(m=N\) 依次做 \(m \mathbin{\text{/=}} (a+di)\) 的整除检测以防溢出(只要最后
\(m≥1\) 即表明乘积 \(≤N\))。

将每个 \((l,a)\) 对应的最大 \(d\) 累加即为答案。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

int main() {
    ll n;
    cin >> n;
    
    ll ans = 0;
    for (int l = 3; l <= 19; ++l) {
        for (ll a = 1;; ++a) {
            {
                ll m = n;
                rep(i, l) m /= a+i;
                if (m == 0) break;
            }
            auto judge = [&](ll d) {
                ll m = n;
                rep(i, l) m /= a+d*i;
                return m >= 1;
            };
            ll ac = 1, wa = 1e9;
            while (abs(ac-wa) > 1) {
                ll wj = (ac+wa)/2;
                if (judge(wj)) ac = wj; else wa = wj;
            }
            ans += ac;
        }
    }
    
    cout << ans << '\n';
    
    return 0;
}

F. Cake Cutting

令 \(W = \frac{S}{M}\)

考虑把蛋糕划分成若干组,使得每组蛋糕的大小之和都是 \(W\) 的倍数。
假设最多可以划分成 \(K\) 组,那么答案就是

\[\boxed{M-K} \]

因为一组蛋糕总大小若为 \(qW\),就可以分给 \(q\) 个人,最多需要 \(q-1\) 次切割。反过来,不同组之间无需混合,而每个相互独立的组至少需要 \(q-1\) 次切割,因此最大化组数即可使切割次数最少。

预处理所有子集合 \(\text{sum}[s]\)
考虑状压 \(\text{dp}\),记 \(\text{dp}[s]\) 表示选中集合 \(s\) 内能凑出的和为 \(W\) 的组数的最大值
最后的答案为 \(M-\text{dp}[2^n-1]\)

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

inline void chmax(int& a, int b) { if (a < b) a = b; }

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    
    int n2 = 1<<n;
    vector<ll> sum(n2);
    rep(i, n) {
        rep(j, 1<<i) sum[j|1<<i] = sum[j]+a[i];
    }
    
    ll w = sum[n2-1]/m;
    
    vector<int> dp(n2);
    rep(s, n2) {
        rep(i, n) if (~s>>i&1) {
            int ns = s|1<<i;
            chmax(dp[ns], dp[s]+!(sum[ns]%w));
        }
    }
    
    cout << m-dp[n2-1] << '\n';
    
    return 0;
}

G. Zonal Score Minimization

设 \(dp[i]\) 表示前 \(i\) 个元素 \(A_0,\ldots,A_{i-1}\) 的最小总分,初始化 \(dp[0]=0\),\(dp[1]=\infty\)。
考虑最后一段为 \(A_j,\ldots,A_i\),由于段长至少为 \(2\),必须满足 \(j<i\)。转移为

\[dp[i+1]=\min_{0\le j<i} \left\{ dp[j]+\max_{j\le k\le i}A_k-\min_{j\le k\le i}A_k \right\}. \]

直接枚举 \(j\) 的复杂度为 \(\mathcal{O}(N^2)\),需要进一步优化。

关键在于维护每个起点的转移候选值。
处理右端点 \(i\) 时,对每个起点 \(j\) 维护

\[v_j=dp[j]+\max(A_j,\ldots,A_i)-\min(A_j,\ldots,A_i). \]

那么 \(dp[i+1]\) 就是所有 \(j<i\) 的 \(v_j\) 的最小值。可以用支持区间加、区间最小值查询和单点赋值的线段树维护这些候选值。

当右端点向右移动、加入新元素 \(A_i\) 时,只需考虑区间最大值和最小值的变化:

  • 维护区间最小值: 使用单调递增栈。当新元素 \(A_i\) 小于栈顶值 \(x\) 时,部分区间的最小值由 \(x\) 变成 \(A_i\),对应候选值统一增加 \(x-A_i\)。将这些起点对应的连续区间在另一端的数据结构中整体加上该值。
  • 维护区间最大值: 使用单调递减栈。同理,当新元素大于栈顶值 \(x\) 时,对应候选值统一增加 \(A_i-x\),进行区间加。

每次弹栈后,新的栈顶可以确定受到影响的起点范围,因此能够通过一次区间加完成更新。由于每个元素在每个单调栈中最多入栈、出栈各一次,所有弹栈操作总计为 \(\mathcal{O}(N)\)。

处理完当前元素后,通过线段树查询起点范围 \([0,i)\) 的最小值,得到 \(dp[i+1]\),再将该状态作为新的起点加入线段树。

代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;
using P = pair<int, int>;

ll op(ll a, ll b) { return min(a, b); }
ll e() { return 1e18; }
ll mapping(ll f, ll x) { return x+f; }
ll composition(ll f, ll g) { return f+g; }
ll id() { return 0; }

int main() {
    int n;
    cin >> n;
    
    lazy_segtree<ll, op, e, ll, mapping, composition, id> dp(n+1);
    dp.set(0, 0);
    
    const int INF = 1001001001;
    vector<P> sl = {{-INF, -1}}, sr = {{INF, -1}};
    rep(i, n) {
        int a;
        cin >> a;
        while (sl.size() and sl.back().first > a) {
            auto [b, r] = sl.back();
            sl.pop_back();
            int l = sl.back().second;
            l++; r++;
            dp.apply(l, r, b-a);
        }
        sl.emplace_back(a, i);
        while (sr.size() and sr.back().first < a) {
            auto [b, r] = sr.back();
            sr.pop_back();
            int l = sr.back().second;
            l++; r++;
            dp.apply(l, r, a-b);
        }
        sr.emplace_back(a, i);
        
        dp.set(i+1, dp.prod(0, i));
    }
    
    cout << dp.get(n) << '\n';
    
    return 0;
}