C. Flush
本题的关键是代入庄家思维!
要让你输掉游戏,最优策略必然是每次选 \(b-1\) 个茶包,因此最终答案为 \(1 + \sum \min(A_i, b-1)\)
可以先将 \(A\) 做升序排序,找到 \(A\) 中第一个大于等于 \(b-1\) 的位置,从这个位置往后的贡献必然都是 \(b-1\),那么它前面的贡献就只能是 \(A_i\) 了。于是还需对 \(A\) 做一遍前缀和

代码实现
#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
问题分析
首先,我们来理解问题的约束条件:
-
相邻茶壶约束:任意相邻的两个茶壶中,至少有一个装的是茶。
- 这意味着,如果我们将装咖啡的茶壶用'C'表示,装茶的茶壶用'T'表示,则序列中不能出现'CC'。
- 这个问题可以被简化为:对N个位置进行填充,要求没有连续两个'C'。
- 满足该条件的填充方式数量是经典的斐波那契数列问题。对于长度为 \(k\) 的序列,填充方式的数量为 \(F_{k+1}\),其中 \(F_n\) 是斐波那契数列,\(F_0=0, F_1=1, F_2=1, \dots\)。
-
咖啡数量约束:若 \(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\) 的值,我们将问题分解为以下两种情况:
-
\(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)分支正是处理这种情况。
-
\(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}\)
浙公网安备 33010602011771号