C. Flush

本题的关键是代入庄家思维!
要让你输掉游戏,最优策略必然是每次选 \(b-1\) 个茶包,因此最终答案为 \(1 + \sum \min(A_i, b-1)\)

可以先将 \(A\) 做升序排序,找到 \(A\) 中第一个大于等于 \(b-1\) 的位置,从这个位置往后的贡献必然都是 \(b-1\),那么它前面的贡献就只能是 \(A_i\) 了。于是还需对 \(A\) 做一遍前缀和

8.10

代码实现
#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<int> a(n);
    rep(i, n) cin >> a[i];
    
    ranges::sort(a);
    
    vector<ll> s(n+1);
    rep(i, n) s[i+1] = s[i]+a[i];
    
    rep(qi, q) {
        int b;
        cin >> b;
        
        if (a.back() < b) {
            puts("-1");
            continue;
        }
        int i = lower_bound(a.begin(), a.end(), b-1) - a.begin();
        ll x = ll(n-i)*(b-1) ;
        x += s[i];
        cout << x+1 << '\n';
    }
    
    return 0;
}

D. XNOR Operation

如果题目条件不是 XNOR 而是 XOR 时,美丽字符串的答案条件将是“\(1\) 的个数为奇数”。但换成 XNOR 后,可以发现充要条件其实是 "\(0\) 的个数为偶数"。
(操作不变量是 \(0\) 的个数的奇偶性)

然后接下来的任务就是统计有多少个子串满足 \(0\)

一种做法是用前缀和来处理

代码实现
#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;
    string t;
    cin >> n >> t;
    
    vector<int> a(n);
    rep(i, n) a[i] = t[i] == '0';
    vector<int> s(n+1);
    rep(i, n) s[i+1] = s[i]+a[i];
    rep(i, n+1) s[i] %= 2;
    
    vector<int> cnt(2);
    rep(i, n+1) cnt[s[i]]++;
    
    ll ans = 0;
    rep(i, 2) ans += (ll)cnt[i]*(cnt[i]-1)/2;
    
    cout << ans << '\n';
    
    return 0;
}

另一种做法是dp

dp[i][f] 表示以 \(T_i\) 结尾的子串中,包含 奇数/偶数\(0\) 的数量

代码实现
#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;
    string t;
    cin >> n >> t;
    
    ll ans = 0;
    vector<int> cnt(2);
    rep(i, n) {
        if (t[i] == '0') swap(cnt[0], cnt[1]);
        cnt[t[i]-'0']++;
        ans += cnt[1];
    }
    
    cout << ans << '\n';
    
    return 0;
}

E. Trapezium

容斥
枚举所有两点连成的线段,统计平行线段的对数即可。
注意重复统计的情况,当四边形为平行四边形时会被重复计算,因此需要额外减去“平行且长度相同”的线段对数!

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

using namespace std;
using ll = long long;

struct Frac {
    ll a, b;
    Frac(ll _a=0, ll _b=1): a(_a), b(_b) {
        if (b < 0) a = -a, b = -b;
        if (b == 0) a = 1;
        ll g = gcd(abs(a), b);
        a /= g; b /= g;
    }
    bool operator<(const Frac& f) const {
        return a*f.b < f.a*b;
    }
    ll toll() const { return a*ll(1e9)+b; }
};

int main() {
    int n;
    cin >> n;
    
    vector<int> x(n), y(n);
    rep(i, n) cin >> x[i] >> y[i];
    
    auto calc = [&](bool len=false) {
        ll res = 0;
        boost::unordered_map<pair<ll, int>, int> mp;
        rep(i, n)rep(j, i) {
            int dx = x[i]-x[j];
            int dy = y[i]-y[j];
            Frac f(dy, dx);
            pair<ll, int> p(f.toll(), abs(dx)+abs(dy));
            if (!len) p.second = 0;
            res += mp[p];
            mp[p]++;
        }
        return res;
    };
    
    ll ans = calc() - calc(true)/2;
    cout << ans << '\n';
    
    return 0;
}

F. We're teapots

问题分析

首先,我们来理解问题的约束条件:

  1. 相邻茶壶约束:任意相邻的两个茶壶中,至少有一个装的是茶。

    • 这意味着,如果我们将装咖啡的茶壶用'C'表示,装茶的茶壶用'T'表示,则序列中不能出现'CC'。
    • 这个问题可以被简化为:对N个位置进行填充,要求没有连续两个'C'。
    • 满足该条件的填充方式数量是经典的斐波那契数列问题。对于长度为 \(k\) 的序列,填充方式的数量为 \(F_{k+1}\),其中 \(F_n\) 是斐波那契数列,\(F_0=0, F_1=1, F_2=1, \dots\)
  2. 咖啡数量约束:若 \(a_i \ne -1\),则前 \(i\) 个茶壶中恰好有 \(a_i\) 个装的是咖啡。

    • 这引入了额外的约束,将整个序列划分成若干段。
    • \(I = \{i \mid a_i \ne -1\}\), 且 \(0 \in I\) (通过 \(a_0 = 0\) 引入),我们将整个序列分成若干段 \([l, r]\),其中 \(l, r \in I\)\(l, r\)\(I\) 中是相邻的。
    • 对于每一段 \([l, r]\),我们知道前 \(l\) 个茶壶中有 \(a_l\) 个咖啡,前 \(r\) 个茶壶中有 \(a_r\) 个咖啡。
    • 因此,在 \([l+1, r]\) 这段长度为 \(w = r-l\) 的茶壶中,恰好有 \(s = a_r - a_l\) 个咖啡。

动态规划和矩阵乘法

解决这类问题的一种常见方法是使用动态规划,并利用矩阵乘法来加速计算。

考虑一个长度为 \(w\) 的子序列,我们想计算其填充方式的数量,同时考虑与子序列边界(即第 \(l\) 个和第 \(r\) 个茶壶)的连接情况。

我们可以定义一个 \(2 \times 2\) 的矩阵来表示一个子序列 \([l+1, r]\) 的状态转移:
\(M_{i,j}\) 表示:

  • \(i=0\):第 \(l\) 个茶壶装茶
  • \(i=1\):第 \(l\) 个茶壶装咖啡
  • \(j=0\):第 \(r+1\) 个茶壶装茶
  • \(j=1\):第 \(r+1\) 个茶壶装咖啡
    注意:在代码中,矩阵 \(M\) 的含义略有不同,它是从第 \(l\) 个茶壶的状态转移到第 \(r\) 个茶壶的状态。
  • M[i][j] 表示:第 \(l\) 个茶壶的状态为 \(i\) (0:茶, 1:咖啡),第 \(r\) 个茶壶的状态为 \(j\) 时,区间 \([l+1, r-1]\) 的合法填充方式数量。

对于一个长度为 \(w\) 的子序列 \([l, r-1]\),它的状态转移矩阵 \(M\) 包含了从 \(l-1\) 转移到 \(r\) 的信息。
\(M = M_{l-1 \to l} \times M_{l \to l+1} \times \dots \times M_{r-1 \to r}\)

整个问题的解就是将所有这些子序列的矩阵相乘。


子问题分解

根据 \(a_i\) 的值,我们将问题分解为以下两种情况:

  1. \(a_i=-1\) 的区间

    • 对于一个连续的、没有 \(a_i \ne -1\) 约束的区间,长度为 \(w\),从 \(l\)\(r\)
    • 我们需要计算从 \(l\)\(r\) 的状态转移。
    • \(dp(i, 0)\) 为前 \(i\) 个茶壶中第 \(i\) 个装茶的方案数,\(dp(i, 1)\) 为第 \(i\) 个装咖啡的方案数。
    • \(dp(i, 0) = dp(i-1, 0) + dp(i-1, 1)\)
    • \(dp(i, 1) = dp(i-1, 0)\)
    • 这是一个斐波那契数列的递推关系。
    • \(i-1\) 转移到 \(i\) 的矩阵是 \(\begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}\)
    • 对于长度为 \(w\) 的区间,其转移矩阵为 \((\begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix})^w\)
    • 矩阵幂的计算可以通过矩阵快速幂在 \(O(\log w)\) 内完成。在代码中,通过预计算斐波那契数列来直接得到结果。
    • 长度为 \(w\) 的区间的转移矩阵为 \(\begin{pmatrix} F_{w+1} & F_w \\ F_w & F_{w-1} \end{pmatrix}\)
    • 在代码中,fib 数组预计算了斐波那契数列,add 函数中的 if (a[r] == -1) 分支正是处理这种情况。
  2. \(a_i \ne -1\) 的区间

    • 对于一个区间 \([l, r]\),其中 \(a_l\)\(a_r\) 已知。
    • 区间 \([l+1, r-1]\) 长度为 \(w-1\)
    • 共有 \(s = a_r - a_l\) 个咖啡。
    • 如果第 \(l\) 个茶壶装咖啡,第 \(r\) 个茶壶装咖啡,那么区间 \([l+1, r-1]\) 中有 \(s-2\) 个咖啡。
    • 如果第 \(l\) 个茶壶装咖啡,第 \(r\) 个茶壶装茶,那么区间 \([l+1, r-1]\) 中有 \(s-1\) 个咖啡。
    • ...以此类推。
    • 这种问题可以转化为:在长度为 \(L\) 的序列中,放置 \(K\) 个咖啡,且没有相邻咖啡的方案数。
    • 这个问题等价于:将 \(L-K\) 个茶壶和 \(K\) 个咖啡进行排列,其中咖啡不能相邻。这等价于将 \(K\) 个咖啡插入到 \(L-K+1\) 个茶壶之间的空隙中。
    • 方案数为 \(C(L-K+1, K)\)
    • 转移矩阵的每个元素 d[i][j] 对应一个 f 函数的调用,参数根据边界状态 \((i, j)\) 和总咖啡数 \(s\) 调整。
    • 例如,d[0][0] 对应第 \(l\)\(r\) 都装茶,区间 \([l+1, r-1]\) 长度为 \(w-1\),咖啡数为 \(s\)。方案数为 \(f(w-1, s)\)
    • d[1][1] 对应第 \(l\)\(r\) 都装咖啡,区间 \([l+1, r-1]\) 长度为 \(w-1\),咖啡数为 \(s-2\)。方案数为 \(f(w-1, s-2)\)
    • d[0][1] 对应第 \(l\) 装茶,第 \(r\) 装咖啡,区间 \([l+1, r-1]\) 长度为 \(w-1\),咖啡数为 \(s-1\)。方案数为 \(f(w-1, s-1)\)
    • d[1][0] 对应第 \(l\) 装咖啡,第 \(r\) 装茶,区间 \([l+1, r-1]\) 长度为 \(w-1\),咖啡数为 \(s-1\)。方案数为 \(f(w-1, s-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 mint = modint998244353;

struct modinv {
  int n; vector<mint> d;
  modinv(): n(2), d({0,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(-d[mint::mod()%n]*(mint::mod()/n)), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} invs;
struct modfact {
  int n; vector<mint> d;
  modfact(): n(2), d({1,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(d.back()*n), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} facts;
struct modfactinv {
  int n; vector<mint> d;
  modfactinv(): n(2), d({1,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(d.back()*invs(n)), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} ifacts;
mint comb(int n, int k) {
  if (n < k || k < 0) return 0;
  return facts(n)*ifacts(k)*ifacts(n-k);
}

using S = array<array<mint, 2>, 2>;
using T = array<mint, 2>;
S gen(int a, int b, int c, int d) { return {(T){a, b}, (T){c, d}}; }
S op(S a, S b) {
    S c;
    rep(i, 2)rep(j, 2)rep(k, 2) {
        c[i][k] += a[i][j]*b[j][k];
    }
    return c;
}
S e() { return gen(1, 0, 0, 1); }

int main() {
    int n, q;
    cin >> n >> q;
    
    vector<mint> fib(n+2);
    {
        fib[0] = 0; fib[1] = 1;
        for (int i = 2; i <= n+1; ++i) fib[i] = fib[i-1]+fib[i-2];
    }
    
    vector<int> a(n+1, -1);
    a[0] = 0;
    
    segtree<S, op, e> t(n+1);
    
    auto f = [&](int w, int s) -> mint {
        if (s*2 > w+1) return 0;
        return comb(w+1-s, s);
    };
    
    auto add = [&](int l, int r) {
        int w = r-l;
        S d;
        if (a[r] == -1) {
            rep(i, 2)rep(j, 2) d[i][j] = fib[w+1-i-j];
        }
        else {
            int s = a[r]-a[l];
            if (s == 0) d = gen(1, 0, 1, 0);
            else if (w == 1) {
                if (s == 1) d = gen(0, 1, 0, 0);
            }
            else if (w == 2) {
                if (s == 1) d = gen(1, 1, 0, 1);
            }
            else {
                rep(i, 2)rep(j, 2) d[i][j] = f(w-1-i-j, s-j);
            }
        }
        t.set(r, d);
    };
    
    set<int> is;
    is.insert(0);
    is.insert(n);
    add(0, n);
    
    rep(qi, q) {
        int i, x;
        cin >> i >> x;
        a[i] = x;
        
        if (i < n and x == -1) {
            auto it = is.find(i);
            if (it != is.end()) {
                int l = *prev(it), r = *next(it);
                is.erase(it);
                t.set(i, e());
                add(l, r);
            }
        }
        else {
            auto it = is.find(i);
            if (it == is.end()) it = is.insert(i).first;
            if (i < n) add(i, *next(it));
            add(*prev(it), i);
        }
        
        S s = t.all_prod();
        mint ans = s[0][0]+s[0][1];
        cout << ans.val() << '\n';
    }
    
    return 0;
}

G. Binary Operation

\(16\) 种情况做分讨(实际上只有 \(12\) 种情况)或者 \(\mathrm{DFA}\)