C. Adjacent Sums (easy)

\(M=2\) 时,每次在某个位置加 \(1\) 就是把该位置的二元值取反,所以我们只关心每个位置的奇偶(模 2)。约束

\[A_i+A_{i+1}≡B_i \pmod 2 \]

等价于

\[A_{i+1}≡A_i \oplus B_i \pmod 2 \]

因此一旦确定了 \(A_1\) 的模 \(2\) 值,整个满足约束的目标序列就唯一确定了:用递推 \(x_{i+1}=x_i \oplus B_i\) 得到期望的每个位置的值。目标是把原序列变成这个目标序列,代价就是不同位的个数(每处只需翻转一次即可),即海明距离。

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

using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<int> a(n), b(n-1);
    rep(i, n) cin >> a[i];
    rep(i, n-1) cin >> b[i];
    
    int ans = n;
    rep(s, 2) {
        int now = 0;
        int x = s;
        rep(i, n) {
            if (a[i] != x) now++;
            if (i != n-1) x ^= b[i];
        }
        ans = min(ans, now);
    }
    
    cout << ans << '\n';
    
    return 0;
}

D. Concentric Circles

对于一对点 \(P,Q\),所有满足 \(∣OP∣=∣OQ∣\) 的点 \(O\) 构成 \(PQ\) 的垂直平分线。垂直平分线可以用“中点 + 垂直方向向量”表示:中点是 \(\frac{P+Q}{2}\),垂直方向是把向量 \(P−Q\) 旋转 \(90^{∘ }\) 得到的向量。

同理,对 \(R,S\) 得到另一条垂直平分线:中点 \(\frac{R+S}{2}\),方向是 \(R−S\) 旋转 \(90^{∘}\)

问题等价于判断这两条直线相交:

  • 若方向向量不共线(即向量叉积非 \(0\)),两直线必相交 \(\to\) 存在公共圆心。
  • 若方向向量共线(平行),则检查两条直线是否重合:等价于中点连线与方向向量共线(即方向向量与 \(b-a\) 的叉积为 \(0\)),若重合则有无限个交点;否则平行但不同直线。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

// Vector
struct V {
  ll x, y;
  V(ll x=0, ll y=0): x(x), y(y) {}
  V& operator+=(const V& v) { x += v.x; y += v.y; return *this;}
  V operator+(const V& v) const { return V(*this) += v;}
  V& operator-=(const V& v) { x -= v.x; y -= v.y; return *this;}
  V operator-(const V& v) const { return V(*this) -= v;}
  V& operator*=(ll s) { x *= s; y *= s; return *this;}
  V operator*(ll s) const { return V(*this) *= s;}
  ll dot(const V& v) const { return x*v.x + y*v.y;}
  ll cross(const V& v) const { return x*v.y - v.x*y;}
  ll norm2() const { return x*x + y*y;}
  V rotate90() const { return V(y, -x);}
};
istream& operator>>(istream& is, V& v) {
  is >> v.x >> v.y; return is;
}
ostream& operator<<(ostream& os, const V& v) {
  os<<"("<<v.x<<","<<v.y<<")"; return os;
}

bool solve() {
    V p, q, r, s;
    cin >> p >> q >> r >> s;
    
    V a = p+q, b = r+s;
    V v = (p-q).rotate90(), w = (r-s).rotate90();
    if (v.cross(w) != 0) return true;
    if (v.cross(b-a) == 0) return true;
    return false;
}

int main() {
    int t;
    cin >> t;
    
    while (t--) {
        if (solve()) puts("Yes");
        else puts("No");
    }
    
    return 0;
}

E. Adjacent Sums (hard)

假设我们最终决定让 \(A_0\) 增加 \(x\) 次(由于是在模 \(M\) 意义下,我们只需要考虑 \(0 \le x < M\))。

根据题目条件:\((A'_i + A'_{i+1}) \bmod M = B_i\),只要第一个元素 \(A'_0\) 的值确定了,后面所有元素的最终目标值(在模 \(M\) 意义下)就全部被唯一确定了。

如果我们不进行任何操作(即 \(x=0\)),我们可以递推计算出每个位置的“基准目标值” \(C_i\)

  • \(C_0 = A_0\)
  • \(C_{i} = (B_{i-1} - C_{i-1}) \bmod M \quad (\text{对于 } i \ge 1)\)

现在,如果我们让 \(A_0\) 额外增加 \(x\),根据正负号的交替传播规律,每个位置最终的目标值 \(A'_i \pmod M\) 将会变成:

  • \(i\) 为偶数时:\(A'_i \equiv C_i + x \pmod M\)
  • \(i\) 为奇数时:\(A'_i \equiv C_i - x \pmod M\)

因为每个元素只能增加,所以每个位置实际的操作次数 \(\Delta_i = A'_i - A_i\) 必须是非负整数,且是满足同余方程的最小值:

  • \(i\) 为偶数时:$$\Delta_i = (C_i + x - A_i) \bmod M = (x - (A_i - C_i)) \bmod M$$我们记 \(p_i = (A_i - C_i) \bmod M\),则该位置的代价为 \((x - p_i) \bmod M\)
  • \(i\) 为奇数时:$$\Delta_i = (C_i - x - A_i) \bmod M = ((C_i - A_i) - x) \bmod M$$我们记 \(q_i = (C_i - A_i) \bmod M\),则该位置的代价为 \((q_i - x) \bmod M\)

我们要最小化的总代价函数就是:

\[f(x) = \sum_{i \text{ is even}} (x - p_i) \bmod M + \sum_{i \text{ is odd}} (q_i - x) \bmod M \]

如果把 \(f(x)\) 想象成一条几何曲线,随着 \(x\)\(0\) 连续增长到 \(M-1\)

  • 对于偶数位置的项 \((x - p_i) \bmod M\):随着 \(x\) 增大,它以斜率 \(+1\) 线性递增;但当 \(x\) 刚刚恰好超过 \(p_i\) 时,该项会瞬间从 \(M-1\) 跌落到 \(0\)(相当于骤减 \(M\))。
  • 对于奇数位置的项 \((q_i - x) \bmod M\):随着 \(x\) 增大,它以斜率 \(-1\) 线性递减;但当 \(x\) 离开 \(q_i\) 的瞬间(即 \(x\) 大于 \(q_i\)),该项会瞬间从 \(0\) 跳跃到 \(M-1\)(相当于骤增 \(M\))。

既然函数在绝大多数地方都是平滑的线性变化,最值必然在那些发生骤变的事件点处取得。

代码实现
#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, m;
    cin >> n >> m;
    
    vector<int> a(n), b(n-1);
    rep(i, n) cin >> a[i];
    rep(i, n-1) cin >> b[i];
    
    auto f = [&](int x) {
        return (x%m+m)%m;
    };
    
    vector<int> p, q, xs;
    {
        int c = 0;
        rep(i, n) {
            if (i%2 == 0) p.push_back(f(a[i]-c));
            else q.push_back(f(c-a[i]));
            if (i != n-1) c = f(b[i]-c);
        }
        xs = p; xs.insert(xs.end(), q.begin(), q.end());
        sort(xs.begin(), xs.end());
        xs.erase(unique(xs.begin(), xs.end()), xs.end());
        if (xs[0] == 0) xs.erase(xs.begin());
    }
    
    ll now = 0;
    map<int, int> pc, qc;
    for (int c : p) now += f(-c), pc[c]++;
    for (int c : q) now += f(c), qc[c]++;
    ll ans = now;
    
    ll px = 0;
    for (int x : xs) {
        now += (ll)m*qc[px];
        now += ll(p.size()-q.size())*(x-px);
        px = x;
        now -= (ll)m*pc[x];
        ans = min(ans, now);
    }
    
    cout << ans << '\n';
    
    return 0;
}

F. Email Scheduling Optimization

假设当前所有公司的参数固定。我们需要决定发送邮件的顺序。
利用微扰法(交换论证):考虑紧邻的两个公司 \(x\)\(y\)。如果先发 \(x\) 再发 \(y\),它们两个带来的最大总时间消耗相关项为 \(\max(A_x + B_x, A_x + A_y + B_y)\)。同理,若调换顺序则为 \(\max(A_y + B_y, A_y + A_x + B_x)\)
可以证明,当 \(B_x > B_y\) 时,先发 \(x\) 总是优于或等于先发 \(y\)

结论:最优策略是将所有公司按照 \(B_i\) 从大到小排序并依次发送邮件。

若我们将公司按照 \(B_i\) 从小到大重新编号为 \(1, 2, \dots, N\),那么写邮件的顺序就是从 \(N\)\(1\)。此时,第 \(i\) 个公司收到回信的时间为:

\[T_i = B_i + \sum_{j=i}^{N} A_j \]

我们要求的目标就是 \(\max\limits_{1 \le i \le N} T_i\)

由于存在修改操作,每次重新排序计算的时间复杂度为 \(O(N \log N)\),总复杂度 \(O(QN \log N)\) 会超时。我们需要动态维护上述公式的最大值。

  • 步骤一:离散化值域

    把所有初始的 \(B_i\) 以及所有询问中改动的目标值 \(x\) 统一放入一个数组中进行排序去重。把每个可能遇到的 \(B\) 值映射为一个 \([1, N+Q]\) 之间的整数标号。

  • 步骤二:线段树区间操作

    构建一棵线段树,其每个位置 \(p\) 维护当前位于该 \(B\) 值档位的公司的 \(T_p\) 值。线段树需要支持区间加减和查询全局最大值:

    • 当增加/修改一个公司 \(i\) 时:它会对所有 \(B\) 值比它小或相等的项产生影响。假设它离散化后的位置是 \(pos\),我们在线段树的单点 \(pos\) 处激活并赋予初始值 \(B_i\),然后对区间 \([1, pos]\) 统一加上 \(A_i\)
    • 当删除一个公司 \(i\) 时:执行相反操作,对区间 \([1, pos]\) 统一减去 \(A_i\),并将单点 \(pos\) 移出(设为 \(-\infty\))。

对于每个询问,如果是修改 \(A_i\),就先在旧位置扣除旧的 \(A_i\),加上新的 \(A_i\);如果是修改 \(B_i\),就将该公司从旧的离散化位置彻底删除,并插入到新的离散化位置。修改完成后,线段树的根节点维护的即为当前的最小总耗时。

代码实现
#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>;

// Coodinate Compression
template<typename T=int>
struct CC {
  bool initialized;
  vector<T> xs;
  CC(): initialized(false) {}
  void add(T x) { xs.push_back(x);}
  void init() {
    sort(xs.begin(), xs.end());
    xs.erase(unique(xs.begin(),xs.end()),xs.end());
    initialized = true;
  }
  int operator()(T x) {
    if (!initialized) init();
    return upper_bound(xs.begin(), xs.end(), x) - xs.begin() - 1;
  }
  T operator[](int i) {
    if (!initialized) init();
    return xs[i];
  }
  int size() {
    if (!initialized) init();
    return xs.size();
  }
};

const ll INF = 1e18;
ll op(ll a, ll b) { return max(a, b); }
ll e() { return -INF; }
ll mapping(ll f, ll x) { return f+x; }
ll composition(ll f, ll g) { return f+g; }
ll id() { return 0; }

int main() {
    int n, q;
    cin >> n >> q;
    
    vector<int> a(n), b(n), bid(n);
    rep(i, n) cin >> a[i];
    rep(i, n) cin >> b[i];
    
    CC<P> cc;
    rep(i, n) cc.add(P(b[i], i)), bid[i] = i;
    
    vector<tuple<int, int, int>> qs;
    rep(qi, q) {
        int type, i, x;
        cin >> type >> i >>x;
        --i;
        qs.emplace_back(type, i, x);
        if (type == 2) cc.add(P(x, n+qi));
    }
    
    int m = cc.size();
    lazy_segtree<ll, op, e, ll, mapping, composition, id> t(m);
    auto add = [&](int i) {
        int j = cc(P(b[i], bid[i]));
        ll d = t.get(j)+INF;
        t.set(j, b[i]+d);
        t.apply(0, j+1, a[i]);
    };
    auto del = [&](int i) {
        int j = cc(P(b[i], bid[i]));
        t.apply(0, j+1, -a[i]);
        ll d = t.get(j) - b[i];
        t.set(j, d-INF);
    };
    rep(i, n) add(i);
    rep(qi, q) {
        auto [type, i, x] = qs[qi];
        del(i);
        if (type == 1) a[i] = x;
        else b[i] = x, bid[i] = n+qi;
        add(i);
        cout << t.all_prod() << '\n';
    }
    
    return 0;
}

G. Many Sweets Problem

树套树板子