C. Striped Horse

观察选定的正整数 \(x\) 后,满足条件的下标集合只与 \((i+x) \bmod 2W\) 是否落在区间 \([0,W−1]\) 有关。也就是说,把下标按模 \(2W\) 分组,所选的格子就是这 \(2W\) 个同余类中某个长度为 \(W\) 的连续区间(环上的连续区间)。

因此先把每个残类的代价累加成数组 \(d\):令

\[d[r] = \sum_{i \equiv r \pmod {2W}} c_i \ , \ r = 0 , 1, \cdots, 2W-1 \]

对于某个 \(x\),总费用就是在环长 \(2W\) 的数组 \(d\) 上取一个长度为 \(W\) 的连续窗口的和。

问题就变成在环上找长度为 \(W\) 的连续子数组的最小和。常见做法是破环成链,然后用滑动窗口求长度为 \(W\) 的最小窗口和。

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

using namespace std;
using ll = long long;

void solve() {
    int n, w;
    cin >> n >> w;
    int w2 = w*2;
    
    vector<int> c(n);
    rep(i, n) cin >> c[i];
    
    vector<ll> d(w2);
    rep(i, n) d[i%w2] += c[i];
    
    rep(i, w2) d.push_back(d[i]);
    
    ll sum = 0;
    rep(i, w) sum += d[i];
    ll ans = sum;
    rep(i, w2) {
        sum -= d[i];
        sum += d[i+w];
        ans = min(ans, sum);
    }
    
    cout << ans << '\n';
}

int main() {
    int t;
    cin >> t;
    
    while (t--) solve();
    
    return 0;
}

D. Forbidden List 2

二分套二分

先对数组 \(A\) 排序。记 \(i(r)=\#\{a_j \leqslant r\}\),用 \(\text{upper_bound}\) 得到。

定义函数

\[f(r) = r-i(r) \]

这表示在区间 \([1,r]\) 中不在数组里的整数个数。\(f(r)\)\(r\) 单调不降。

对一次查询 \((X,Y)\),要找最小的 \(r\) 使得区间 \([X,r]\) 内缺失的整数数目 \(\geqslant Y\)。区间内的缺失数为

\[f(r) - f(X-1) \]

因此用二分查找最小 \(r\) 满足 \(f(r)−f(X−1) \geqslant Y\)

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

using namespace std;

int main() {
    int n, q;
    cin >> n >> q;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    sort(a.begin(), a.end());
    
    auto f = [&](int r) {
        int i = upper_bound(a.begin(), a.end(), r) - a.begin();
        return r - i;
    };
    
    rep(qi, q) {
        int x, y;
        cin >> x >> y;
        
        int wa = x-1, ac = x+y+n+1;
        while (wa+1 < ac) {
            int wj = wa + (ac-wa)/2;
            if (f(wj)-f(x-1) >= y) ac = wj; else wa = wj; 
        }
        cout << ac << '\n';
    }
    
    return 0;
}

E. Cookies

把题目转成“把 \(K\) 个相同球放入 \(N\) 个桶,每种放 \(\sum d_i=K\)”。每个状态由数组 \(d=(d_0,…,d_{n−1})\) 表示,代价为 \(\sum a_i \times d_i\) 。若把 \(a\) 按降序排序,初始最大状态是 \(d=(K,0,…,0)\)

目标是按代价从大到小枚举前 \(X\) 个不同的 \(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() {
    int n, k, x;
    cin >> n >> k >> x;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    sort(a.begin(), a.end(), greater<>());
    
    vector<int> d(n);
    d[0] = k;
    
    priority_queue<pair<ll, vector<int>>> q;
    auto push = [&](const vector<int>& d) {
        ll s = 0;
        rep(i, n) s += (ll)a[i]*d[i];
        q.emplace(s, d);
    };
    push(d);
    
    while (x--) {
        auto [s, d] = q.top(); q.pop();
        cout << s << '\n';
        int i = n-1;
        while (d[i] == 0) i--;
        if (d.back() == 0) {
            d[i]--; d[i+1]++;
            push(d);
            d[i]++; d[i+1]--;
        }
        if (i >= 1 and d[i-1] > 0) {
            d[i-1]--; d[i]++;
            push(d);
        }
    }
    
    return 0;
}

F. Egoism

将总心情记做 \(S = \sum A_i\) 。对于任意排列,总满足度可写成

\[S + \sum_{j=2}^N A_{p_j} \cdot (B_{p_{j-1}} - 1) \]

由于 \(B∈\{1,2\}\),第二项就是“所有处于某个有 \(B=2\) 的马后面的马的心情之和”。如果当前有 \(B=2\) 的马,则我们最多能让 \(t\) 个不同的马作为“后继”从而各自多得到 \(+A\)(注意最多只能有 \(N−1\) 个后继),因此理想情况下额外能加上的最大值就是对所有 \(A_i\) 取前 \(k=\min(t,N−1)\) 大的和。

需要实现以下需求的数据结构:

  • 数的添加/删除
  • 最大的 \(k\) 个数的和
  • \(k\) \(+1 / -1\)

可以用对顶堆来实现

如果有 \(k\)\(2\),基本上把按降序的前 \(k\) 个设为 \(2\);但若按降序的前 \(k\) 个都是 \(2\) 会形成闭环,所以要舍弃一个(让其中一个不为 \(2\))。
因此,还需要额外开两个 \(\text{multiset}\) 来分别维护 \(B=1\)\(A_i\) 以及 \(B=2\)\(A_i\)

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

using namespace std;
using ll = long long;
using S = multiset<int>;

struct Divset {
    S ls, rs;
    int k; ll rsum;
    Divset(): k(0), rsum(0) {}
    void fix() {
        while (rs.size() < k) {
            int x = *ls.rbegin();
            rsum += x;
            rs.insert(x);
            ls.erase(ls.find(x));
        }
        while (rs.size() > k) {
            rsum -= *rs.begin();
            ls.insert(*rs.begin());
            rs.erase(rs.begin());
        }
    }
    
    void add(int x) {
        rs.insert(x); rsum += x;
        fix();
    }
    void del(int x) {
        if (ls.find(x) != ls.end()) ls.erase(ls.find(x));
        else rs.erase(rs.find(x)), rsum -= x;
        fix();
    }
    void inc() {
        k++; fix();
    }
    void dec() {
        k--; fix();
    }
};

int main() {
    int n, q;
    cin >> n >> q;
    
    ll sum = 0, sum2 = 0;
    S s1, s2;
    Divset ds;
    auto add = [&](int a, int b) {
        ds.add(a);
        sum += a;
        if (b == 2) ds.inc(), sum2 += a;
        if (b == 1) s1.insert(a); else s2.insert(a);
    };
    auto del = [&](int a, int b) {
        if (b == 1) s1.erase(s1.find(a)); else s2.erase(s2.find(a));
        if (b == 2) ds.dec(), sum2 -= a;
        ds.del(a);
        sum -= a;
    };
    
    vector<int> a(n), b(n);
    rep(i, n) {
        cin >> a[i] >> b[i];
        add(a[i], b[i]);
    }
    
    rep(qi, q) {
        int i, x, y;
        cin >> i >> x >> y;
        --i;
        del(a[i], b[i]);
        a[i] = x; b[i] = y;
        add(a[i], b[i]);
        
        ll ans = sum + ds.rsum;
        if (sum2 != 0 and ds.rsum == sum2) {
            ans -= *s2.begin();
            if (sum2 < sum) ans += *s1.rbegin();
        }
        cout << ans << '\n';
    }
    
    return 0;
}

G. Haunted House

先沿着上升方向建 \(\text{DAG}\),记录每个点的 \(\text{top2}\) 并做 \(\text{dp}\),随后考虑允许“返回一次”的情况,对每个点维护 \(\text{top2}\) 继续 \(\text{dp}\)