2021CCPC上海省赛题解ABCDEGHIJK
A. 小 A 的点面论
题意
给定两相异的非零向量(x1,y1,z1),(x2,y2,z2) (0≤xi,yi,zi≤10)(x_1,y_1,z_1),(x_2,y_2,z_2)\ \ (0\leq x_i,y_i,z_i\leq 10)(x1,y1,z1),(x2,y2,z2) (0≤xi,yi,zi≤10),求一个向量(x,y,z) (−200≤x,y,z≤200)(x,y,z)\ \ (-200\leq x,y,z\leq 200)(x,y,z) (−200≤x,y,z≤200)垂直于这两个向量.上述xi,yi,zi,x,y,z∈Zx_i,y_i,z_i,x,y,z\in\mathbb{Z}xi,yi,zi,x,y,z∈Z.
思路I
输出(x1,y1,z1)×(x2,y2,z2)=(∣y1z1y2z2∣,−∣x1z1x2z2∣,∣x1y1x2y2∣)(x_1,y_1,z_1)\times (x_2,y_2,z_2)=\left(\begin{vmatrix}y_1 & z_1 \\ y_2&z_2\end{vmatrix},-\begin{vmatrix}x_1 & z_1 \\ x_2&z_2\end{vmatrix},\begin{vmatrix}x_1 & y_1 \\ x_2&y_2\end{vmatrix}\right)(x1,y1,z1)×(x2,y2,z2)=(∣∣y1y2z1z2∣∣,−∣∣x1x2z1z2∣∣,∣∣x1x2y1y2∣∣)即可.
代码I -> 2021CCPC上海省赛-A(计算几何+叉积)
void solve() {
int x1, y1, z1, x2, y2, z2; cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
int ans1 = y1 * z2 - y2 * z1;
int ans2 = x2 * z1 - x1 * z2;
int ans3 = x1 * y2 - x2 * y1;
cout << ans1 << ' ' << ans2 << ' ' << ans3;
}
int main() {
solve();
}
思路II
因欲求向量坐标范围不超过200200200,可O(n3)O(n^3)O(n3)暴力枚举(x,y,z)(x,y,z)(x,y,z),检查它与(xi,yi,zi) (i=1,2)(x_i,y_i,z_i)\ \ (i=1,2)(xi,yi,zi) (i=1,2)的点积是否为000即可.
代码II -> 2021CCPC上海省赛-A(计算几何+暴力+点积)
void solve() {
int x1, y1, z1, x2, y2, z2; cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
for (int x = -200; x <= 200; x++) {
for (int y = -200; y <= 200; y++) {
for (int z = -200; z <= 200; z++) {
if (x1 * x + y1 * y + z1 * z == 0 && x2 * x + y2 * y + z2 * z == 0) {
cout << x << ' ' << y << ' ' << z;
return;
}
}
}
}
}
int main() {
solve();
}
C. 小 A 的期末考试
题意
给定n (1≤n≤100)n\ \ (1\leq n\leq 100)n (1≤n≤100)个同学的学号s (1≤s≤n)s\ \ (1\leq s\leq n)s (1≤s≤n)和分数a (0≤a≤100)a\ \ (0\leq a\leq 100)a (0≤a≤100)和小A的学号m (1≤m≤n)m\ \ (1\leq m\leq n)m (1≤m≤n).设所有同学初始平均分为avgavgavg.现有操作:①若小A成绩低于606060分,将其改为606060分;②若小A外的其他同学分数≥avg\geq avg≥avg,将其分数−=2-=2−=2,但分数不低于000分.按学号升序输出每个同学最后的分数.
代码 -> 2021CCPC上海省赛-C(模拟)
void solve() {
int n, m; cin >> n >> m;
vii stus;
double avg = 0; // 平均分
for (int i = 0; i < n; i++) {
int s, a; cin >> s >> a;
stus.push_back({ s,a });
avg += a;
}
avg /= n;
sort(all(stus));
for (auto& [s, a] : stus) {
if (s == m) a = max(a, 60);
else a = max(0, a - (cmp(a, avg) >= 0 ? 2 : 0));
}
for (auto& [s, a] : stus) cout << a << ' ';
}
int main() {
solve();
}
E. Zztrans 的庄园
题意
有555种等级的鱼,分别用符号D,C,B,A,SD,C,B,A,SD,C,B,A,S表示,售价分别为16,24,54,80,1000016,24,54,80,1000016,24,54,80,10000元.每次钓鱼需要花232323元购买鱼饵.现给定n (1<n≤100)n\ \ (1<n\leq 100)n (1<n≤100)种鱼的等级和调到的概率,求钓k (1≤k≤100)k\ \ (1\leq k\leq 100)k (1≤k≤100)次收益的期望,误差不超过1e−41\mathrm{e}-41e−4.
思路
先求钓一次的净收益的期望,再乘kkk.
代码 -> 2021CCPC上海省赛-E(期望)
map<char, int> prices = { {'D',16},{'C',24},{'B',54},{'A',80},{'S',10000} };
void solve() {
int n, k; cin >> n >> k;
double ans = 0;
while (n--) {
char t; double p; cin >> t >> p;
ans += (prices[t] - 23) * p;
}
cout << fixed << setprecision(8) << ans * k;
}
int main() {
solve();
}
G. 鸡哥的雕像
题意
给定一个长度为n (2≤n≤1e5)n\ \ (2\leq n\leq 1\mathrm{e}5)n (2≤n≤1e5)的序列a1,⋯ ,an (1≤ai≤1e9)a_1,\cdots,a_n\ \ (1\leq a_i\leq 1\mathrm{e}9)a1,⋯,an (1≤ai≤1e9).对每个i∈[1,n]i\in[1,n]i∈[1,n],输出除了aia_iai外的其他元素之积,答案对998244353998244353998244353取模.
思路
注意到998244353<1e9998244353<1\mathrm{e}9998244353<1e9,而998244353998244353998244353在模998244353998244353998244353下不存在逆元,故不能用求逆元的方式解决.
维护前缀积pre[]pre[]pre[]和后缀积suf[]suf[]suf[],则对每个i,ans=pre[i−1]∗suf[i+1]i,ans=pre[i-1]*suf[i+1]i,ans=pre[i−1]∗suf[i+1].
代码 -> 2021CCPC上海省赛-G(前缀积+后缀积)
const int MAXN = 1e5 + 5;
const int MOD = 998244353;
int n;
int a[MAXN];
int pre[MAXN], suf[MAXN]; // 前缀积、后缀积
void solve() {
cin >> n;
pre[0] = 1;
for (int i = 1; i <= n; i++) {
cin >> a[i];
pre[i] = (ll)pre[i - 1] * a[i] % MOD;
}
suf[n + 1] = 1;
for (int i = n; i >= 1; i--) suf[i] = (ll)suf[i + 1] * a[i] % MOD;
for (int i = 1; i <= n; i++) cout << (ll)pre[i - 1] * suf[i + 1] % MOD << ' ';
}
int main() {
solve();
}
J. Alice and Bob-1
题意
有n (1≤n≤5000)n\ \ (1\leq n\leq 5000)n (1≤n≤5000)个元素a1,⋯ ,an (−1e9≤ai≤1e9)a_1,\cdots,a_n\ \ (-1\mathrm{e}9\leq a_i\leq 1\mathrm{e}9)a1,⋯,an (−1e9≤ai≤1e9),Alice和Bob轮流取走一个元素,Alice先手.取完所有元素后,两人拥有的价值定义为各自取的元素之和的绝对值.设Alice和Bob拥有的价值分别为AAA和BBB,Alice希望A−BA-BA−B尽量大,Bob希望A−BA-BA−B尽量小,两人都采取最优策略,求A−BA-BA−B.
思路
贪心策略:两人轮流取当前的最大元素.
[证] Alice希望A−BA-BA−B尽量大,Bob希望A−BA-BA−B尽量小,都等价于两人希望自己拥有的价值尽量大.
设Alice和Bob拥有的价值分别为∣A∣|A|∣A∣和∣B∣|B|∣B∣.
因价值有绝对值,故所有数取反不影响答案,不妨设S=∑i=1nai≥0\displaystyle S=\sum_{i=1}^n a_i\geq 0S=i=1∑nai≥0…
ans=∣A∣−∣B∣=∣A∣−∣S−A∣={S,A≥S2A−S,0<A<S−S,A≤0ans=|A|-|B|=|A|-|S-A|=\begin{cases}S,A\geq S \\ 2A-S,0<A<S \\ -S,A\leq 0\end{cases}ans=∣A∣−∣B∣=∣A∣−∣S−A∣=⎩⎨⎧S,A≥S2A−S,0<A<S−S,A≤0.作图知:该分段函数单调增,故证.
先将a[]a[]a[]升序排列.因集合元素可能全正、全负、有正有负,但答案都能归结为两种情况:①最大值被Alice取走,使得ansansans去掉绝对值后正得更多;②最大值被Bob取走,使得ansansans去掉绝对值后负得更多,两种情况取max\maxmax即可.
代码 -> 2021CCPC上海省赛-J(贪心)
void solve() {
int n; cin >> n;
vi a(n + 1);
ll sum = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
sum += a[i];
}
sort(a.begin() + 1, a.end());
ll sum1 = 0, sum2 = 0;
for (int i = 1; i <= n; i += 2) sum1 += a[i];
for (int i = n; i >= 1; i -= 2) sum2 += a[i];
cout << max(abs(sum1) - abs(sum - sum1), abs(sum2) - abs(sum - sum2));
}
int main() {
solve();
}
D. Zztrans 的班级合照
题意 (3 s3\ \mathrm{s}3 s)
nnn(偶数)个人按如下要求排队:排成人数相同的两排,每排从左往右身高不减,且第二排同学身高不低于第一排对应位置的同学的身高.现给定将同学按身高升序排列后每个同学的排名(身高相同的同学排名相同),求排队方案数,答案对998244353998244353998244353取模.
第一行输入一个偶数n (2≤n≤5000)n\ \ (2\leq n\leq 5000)n (2≤n≤5000).第二行输入nnn个整数a1,⋯ ,an (1≤ai≤n)a_1,\cdots,a_n\ \ (1\leq a_i\leq n)a1,⋯,an (1≤ai≤n),分别表示每个同学的身高排名.
思路
记录每个身高iii的人数cnt[i]cnt[i]cnt[i]后将原数组去重,则相同的身高的人任意排,答案乘上人数的阶乘即可.
考虑所有身高都不同时的情况.注意到任意时刻第二排的人数不超过第一排的人数,dp[i][j]dp[i][j]dp[i][j]表示排完前iii个人,且第一排比第二排多jjj个人的方案数,则最终答案为dp[n][n/2]dp[n][n/2]dp[n][n/2].
用sumsumsum记录当前排完的人数.对每个身高的人数iii,枚举第一排比第二排多的人数jjj,显然它不超过min{n2,sum}\min\left\{\dfrac{n}{2},sum\right\}min{2n,sum}.因第二排的人数不超过第一排的人数,故还需满足j≥sum−jj\geq sum-jj≥sum−j.按上一个状态dp[sum−i][]dp[sum-i][]dp[sum−i][]中第一排比第二排多的人数分类,不妨设dp[sum][]dp[sum][]dp[sum][]中第一排还需补kkk个人,则可从dp[sum−i][j−k]dp[sum-i][j-k]dp[sum−i][j−k]转移到dp[sum][j]dp[sum][j]dp[sum][j],枚举k∈[0,min{i,j}]k\in[0,\min\{i,j\}]k∈[0,min{i,j}]即可.
代码 -> 2021CCPC上海省赛-D(DP+组合计数)
const int MAXN = 5005;
const int MOD = 998244353;
int n;
int cnt[MAXN]; // cnt[i]表示身高为i的人数
int dp[MAXN][MAXN]; // dp[i][j]表示排完前i个人,且第一排比第二排多j个人的方案数
int fac[MAXN], ifac[MAXN];
void init() { // 预处理阶乘
fac[0] = 1;
for (int i = 1; i < MAXN; i++) fac[i] = (ll)fac[i - 1] * i % MOD;
}
void solve() {
init();
cin >> n;
for (int i = 0; i < n; i++) {
int x; cin >> x;
cnt[x]++;
}
vi h; // 去重后的身高
int ans = 1;
for (int i = 1; i <= n; i++) {
if (cnt[i]) {
h.push_back(cnt[i]);
ans = (ll)ans * fac[cnt[i]] % MOD; // 相同身高的人任意排
}
}
int sum = 0; // 当前排完的人数
dp[0][0] = 1; // i=0时只有j=0是合法方案
for (auto i : h) { // 枚举每个身高的人数
sum += i;
// 第一排的人数比第二排多的人数不超过min{当前排完的人数,总人数的一半}
for (int j = min(n / 2, sum); j >= sum - j; j--) {
for (int k = 0; k <= min(i, j); k++) // 枚举上一个状态站第一排的人数
dp[sum][j] = ((ll)dp[sum][j] + dp[sum - i][j - k]) % MOD;
}
}
ans = (ll)ans * dp[n][n / 2] % MOD;
cout << ans;
}
int main() {
solve();
}
H. 鸡哥的 AI 驾驶
题意 (4 s4\ \mathrm{s}4 s)
数轴上有若干辆车,每辆车有三个参数:位置、速度、型号.型号相同的两车在同一位置时不会发生事故,不同型号的辆车在同一位置时会发生事故.求一个时间t s.t. [0,t]t\ s.t.\ [0,t]t s.t. [0,t]时间内未发生事故,(t,t+1](t,t+1](t,t+1]时间内发生了事故.
第一行输入两个整数n,k (1≤k≤n≤1e5)n,k\ \ (1\leq k\leq n\leq 1\mathrm{e}5)n,k (1≤k≤n≤1e5),分别表示车数、型号数.接下来nnn行每行输入三个整数p,v,t (−1e9≤p,v≤1e9,1≤t≤k)p,v,t\ \ (-1\mathrm{e}9\leq p,v\leq 1\mathrm{e}9,1\leq t\leq k)p,v,t (−1e9≤p,v≤1e9,1≤t≤k).数据保证初始时任意两车不在同一位置.
输出时间ttt,若不会发生事故,输出−1-1−1.
思路
若会发生事故,则时间越久越可能发生事故,故是否发生事故的性质具有二段性,可二分出其分界点.
考虑如何check.显然两不同型号的车发生事故的充要条件是它们的相互位置发生改变,即直观上它们互相穿过了对方.注意到每辆车不发生事故的移动范围是数轴上该型号的车最左边与最右边的位置之间的线段,则某型号的车离开该范围也会发生事故.
考察二分时间的范围.显然耗时最久的是从x=−1e9x=-1\mathrm{e}9x=−1e9以速度v=1v=1v=1走到x=1e9x=1\mathrm{e}9x=1e9,耗时t=2e9t=2\mathrm{e}9t=2e9,则边界可取[0,2e9+1][0,2\mathrm{e}9+1][0,2e9+1],其中+1+1+1是为了退出循环后断定l=2e9l=2\mathrm{e}9l=2e9是否有解.
代码 -> 2021CCPC上海省赛-H(二分)
const int MAXN = 1e5 + 5;
int n, k;
struct Car {
int p, v, t; // 位置、速度、型号
bool operator<(const Car& B)const { return p < B.p; }
}cars[MAXN];
pii segs[MAXN]; // 每一段型号相同的车两端点的车的编号
pair<ll, int> pos[MAXN]; // 车移动后的位置、编号
bool check(int ti) {
for (int i = 1; i <= n; i++) pos[i] = { cars[i].p + (ll)cars[i].v * ti,i }; // 末位置
sort(pos + 1, pos + n + 1);
for (int i = 1; i <= n; i++) {
if (i != 1 && pos[i].first == pos[i - 1].first && cars[pos[i].second].t != cars[pos[i - 1].second].t)
return false; // 型号不同的两车在同一位置
if (i < segs[pos[i].second].first || i > segs[pos[i].second].second) return false; // 超出最大移动范围
}
return true;
}
void solve() {
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> cars[i].p >> cars[i].v >> cars[i].t;
if (k == 1) { // 只有一种型号不会发生事故
cout << -1;
return;
}
sort(cars + 1, cars + n + 1); // 按位置升序排列
// 预处理segs[]
for (int i = 1; i <= n; i++) { // 左端点
if (cars[i].t == cars[i - 1].t) segs[i].first = segs[i - 1].first;
else segs[i].first = i;
}
for (int i = n; i >= 1; i--) { // 右端点
if (cars[i].t == cars[i + 1].t) segs[i].second = segs[i + 1].second;
else segs[i].second = i;
}
int l = 0, r = 2e9 + 1; // 注意+1,否则l=2e9时无法判断是否有解
while (l < r) {
int mid = (ll)l + r + 1 >> 1; // 注意这里会爆int
if (check(mid)) l = mid;
else r = mid - 1;
}
cout << (l == 2e9 + 1 ? -1 : l);
}
int main() {
solve();
}
B. 小 A 的卡牌游戏
题意 (2 s2\ \mathrm{s}2 s)
一副nnn张卡的卡组恰包含aaa张A卡、bbb张B卡、ccc张C卡.现给出nnn次三选一的机会,三张卡分别来自三个种类,玩家需从三张卡中选一张加入自己的卡组,使得卡组强度尽量大.每张卡有一个强度值,卡组的强度是所有卡的强度之和.求卡组强度的最大值.
第一行输入四个整数n,a,b,c (1≤a,b,c≤n≤5000,a+b+c=n)n,a,b,c\ \ (1\leq a,b,c\leq n\leq 5000,a+b+c=n)n,a,b,c (1≤a,b,c≤n≤5000,a+b+c=n).接下来nnn行每行输入三个整数描述一个三选一的机会,其中第iii行输入三个整数ai,bi,ci (1≤ai,bi,ci≤1e9)a_i,b_i,c_i\ \ (1\leq a_i,b_i,c_i\leq 1\mathrm{e}9)ai,bi,ci (1≤ai,bi,ci≤1e9),分别表示该次选择中A卡、B卡、C卡的强度.
思路I
先考虑只有A卡和B卡的情况.注意不能贪心地选择强度前aaa大的A卡和强度前bbb大的B卡,因为选择间不独立.考虑先确定B卡的选择,剩下的选A卡.对两次三选一的机会(ai,bi)(a_i,b_i)(ai,bi)和(aj,bj)(a_j,b_j)(aj,bj),若选bib_ibi,则只能再选aja_jaj,同理选bjb_jbj只能再选aia_iai,则选前者更优的充要条件是:bi+aj>bj+aib_i+a_j>b_j+a_ibi+aj>bj+ai,即bi−ai>bj−ajb_i-a_i>b_j-a_jbi−ai>bj−aj.故将三选一的机会按bi−aib_i-a_ibi−ai降序排列后贪心地选前几个即可.
考虑有A、B、C卡的情况.dp[i][j]dp[i][j]dp[i][j]表示表示前iii次选择中有jjj次选择C卡的最大强度,其中的(i−j)(i-j)(i−j)次选A卡或B卡按照上述贪心策略选即可.总时间复杂度O(n2)O(n^2)O(n2).
代码I -> 2021CCPC上海省赛-B(贪心+DP)
const int MAXN = 5005;
int n, A, B, C;
ll dp[MAXN][MAXN]; // dp[i][j]表示前i次选择中有j次选择C卡的最大强度
struct Card {
int a, b, c;
bool operator<(const Card& B)const {
if (b - a != B.b - B.a) return b - a > B.b - B.a;
else return c > B.c;
}
}cards[MAXN];
void solve() {
cin >> n >> A >> B >> C;
for (int i = 1; i <= n; i++) cin >> cards[i].a >> cards[i].b >> cards[i].c;
sort(cards + 1, cards + n + 1);
for (int i = 0; i <= n; i++)
for (int j = i + 1; j <= C; j++) dp[i][j] = -INFF; // 初始化非法状态
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= min(i, C); j++) { // 枚举选C卡的次数
if (j) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + cards[i].c); // 选C卡
if (i - j <= B) dp[i][j] = max(dp[i][j], dp[i - 1][j] + cards[i].b); // 选B卡
else dp[i][j] = max(dp[i][j], dp[i - 1][j] + cards[i].a); // 选A卡
}
}
cout << dp[n][C];
}
int main() {
solve();
}
思路II
各边容量都为111,费用为卡牌强度的负值,转化为求最小费用流,最终答案为最小费用的负值.
代码II -> 2021CCPC上海省赛-B(费用流)
namespace SPFA_Cost_Flow {
static const int MAXN = 5005, MAXM = 1e5 + 10; // 边开两倍
int n, m, s, t; // 点数、边数、源点、汇点
int head[MAXN], edge[MAXM], capa[MAXM], cost[MAXM], nxt[MAXM], idx; // capa[i]表示边i的容量,cost[i]表示边i的费用
int min_capa[MAXN]; // min_capa[i]表示到节点i的所有边的容量的最小值
int dis[MAXN]; // dis[i]表示源点到节点i的最短路
int pre[MAXN]; // pre[i]表示节点i的前驱边的编号
bool state[MAXN]; // SPFA中记录每个节点是否在队列中
void add(int a, int b, int c, int d) { // 建边a->b,容量为c,费用为d
edge[idx] = b, capa[idx] = c, cost[idx] = d, nxt[idx] = head[a], head[a] = idx++; // 正向边
edge[idx] = a, capa[idx] = 0, cost[idx] = -d, nxt[idx] = head[b], head[b] = idx++; // 反向边,流量初始为0,费用为正向边的相反数
}
bool spfa() { // 返回是否找到增广路
memset(dis, INF, so(dis));
memset(min_capa, 0, so(min_capa));
qi que;
que.push(s);
dis[s] = 0, min_capa[s] = INF; // 源点处的流量无限制
while (que.size()) {
int u = que.front(); que.pop();
state[u] = false;
for (int i = head[u]; ~i; i = nxt[i]) {
int v = edge[i];
if (capa[i] && dis[v] > dis[u] + cost[i]) { // 边还有容量
dis[v] = dis[u] + cost[i];
pre[v] = i; // 记录前驱边
min_capa[v] = min(min_capa[u], capa[i]);
if (!state[v]) {
que.push(v);
state[v] = true;
}
}
}
}
return min_capa[t]; // 汇点的流量非零即可以到达汇点,亦即存在增广路
}
pll EK() { // first为最大流、second为最小费用
pll res(0, 0);
while (spfa()) { // 当前还有增广路
int tmp = min_capa[t];
res.first += tmp, res.second += (ll)tmp * dis[t];
for (int i = t; i != s; i = edge[pre[i] ^ 1])
capa[pre[i]] -= tmp, capa[pre[i] ^ 1] += tmp; // 正向边减,反向边加
}
return res;
}
}
using namespace SPFA_Cost_Flow;
int A, B, C;
void solve() {
memset(head, -1, so(head));
s = 0, t = MAXN - 1; // 超级源点、超级汇点
cin >> n >> A >> B >> C;
// 所有汇点向超级汇点连边,容量为每种卡的数量,费用为0
add(n + 1, t, A, 0), add(n + 2, t, B, 0), add(n + 3, t, C, 0);
for (int i = 1; i <= n; i++) {
int a, b, c; cin >> a >> b >> c;
add(s, i, 1, 0); // 超级源点向源点连边,容量为1,费用为0
// 各源点向对应的汇点连边,容量为1,费用为强度的负值
add(i, n + 1, 1, -a), add(i, n + 2, 1, -b), add(i, n + 3, 1, -c);
}
cout << -EK().second;
}
int main() {
solve();
}
K. Alice and Bob-2
题意 (15 s15\ \mathrm{s}15 s)
给定一些只包含小写字母的字符串,Alice和Bob两人轮流取字符,Alice先手,不能操作者败.每次有两种操作:①选择一个非空的字符串,取走其中任一个字符;②选择一个非空的字符串,取走其中任两个相异的字符.
有t (1≤t≤10)t\ \ (1\leq t\leq 10)t (1≤t≤10)组测试数据.每组测试数据第一行输入一个整数n (1≤n≤10)n\ \ (1\leq n\leq 10)n (1≤n≤10),表示字符串个数.接下来nnn行每行输入一个长度不超过404040且只包含小写字母的字符串sss.
对每组测试数据,输出最后的胜利者.
思路
显然SG函数.注意到字符串"aabb"和字符串"ccdd"对答案的贡献是相同的,即本问题中两字符串本质不同当且仅当它们所含的字符种类数不同或所含的字符种类数相同且相同字符的数目不同.考虑记搜,将本质相同的字符串用哈希值表示.
事实上,长度为lenlenlen的本质不同的字符串的个数为P(len)P(len)P(len),其中P(i)P(i)P(i)表示iii的无序分拆数,故要求的SG函数只有∑i=1nP(i)=215308\displaystyle\sum_{i=1}^n P(i)=215308i=1∑nP(i)=215308个.
代码 -> 2021CCPC上海省赛-K(SG函数+记搜+哈希)
namespace Hash {
const int Base = 131, MOD = 1e9 + 7;
umap<int, int> mp; // 记录哈希值对应的下标
int idx = 0;
int get_hash(vi& a) {
int res = 0;
for (auto i : a) {
if (i) res = ((ll)res * Base + i) % MOD;
else break;
}
return res;
}
int get_idx(int a) {
if (mp.count(a)) return mp[a];
else return mp[a] = idx++;
}
};
using namespace Hash;
int cnt[30]; // 每个字母出现的次数
umap<int, int> SG;
int get_mex(set<int>& s) {
int mex = 0;
for (auto i : s) {
if (i == mex) mex++;
else break;
}
return mex;
}
int get_SG(vi a) {
sort(all(a), greater<int>()); // 注意排序
int ha = get_hash(a);
if (SG.count(ha)) return SG[ha]; // 搜过
set<int> tmpSG; // 存已求出的SG函数值
// 删除一个字符
for (int i = 0; i < a.size(); i++) { // 枚举要删除的字符
if (a[i]) { // 还有这种字符
a[i]--; // 删除一个字符
tmpSG.insert(get_SG(a));
a[i]++; // 恢复现场
}
else break; // 没有这种字符
}
// 删除两个相异的字符
for (int i = 0; i < a.size(); i++) { // 枚举第一个要删除的字符
if (!a[i]) break; // 没有这种字符
for (int j = i + 1; j < a.size(); j++) { // 枚举第二个要删除的字符
if (!a[j]) break;
a[i]--, a[j]--; // 删除两个字符
tmpSG.insert(get_SG(a));
a[i]++, a[j]++; // 恢复现场
}
}
return SG[ha] = get_mex(tmpSG);
}
void solve() {
int ans = 0; // 各SG函数的异或和
CaseT{
for (int i = 0; i < 26; i++) cnt[i] = 0; // 清空
string s; cin >> s;
for (auto ch : s) cnt[ch - 'a']++;
vi tmp;
for (int i = 0; i < 26; i++)
if (cnt[i]) tmp.push_back(cnt[i]);
ans ^= get_SG(tmp);
}
cout << (ans ? "Alice" : "Bob") << endl;
}
int main() {
solve();
}
I. 对线
题意 (12 s12\ \mathrm{s}12 s)
有三排长度为nnn的兵线,每排兵线从左往右编号1∼n1\sim n1∼n,同排同编号位置对齐.现有如下四个操作:
①0 x l r (x∈{1,2,3},1≤l≤r≤n)0\ x\ l\ r\ \ (x\in\{1,2,3\},1\leq l\leq r\leq n)0 x l r (x∈{1,2,3},1≤l≤r≤n),表示询问第xxx排从lll位置到rrr位置间的士兵数,答案对998244353998244353998244353取模.
②1 x l r y (x∈{1,2,3},1≤l≤r≤n,1≤y≤1e9)1\ x\ l\ r\ y\ \ (x\in\{1,2,3\},1\leq l\leq r\leq n,1\leq y\leq 1\mathrm{e}9)1 x l r y (x∈{1,2,3},1≤l≤r≤n,1≤y≤1e9),表示第xxx排从lll位置到rrr位置都增加yyy个士兵.
③2 x y l r (x,y∈{1,2,3},1≤l≤r≤n)2\ x\ y\ l\ r\ \ (x,y\in\{1,2,3\},1\leq l\leq r\leq n)2 x y l r (x,y∈{1,2,3},1≤l≤r≤n),表示交换第xxx排和第yyy排区间[l,r][l,r][l,r]上的士兵.
④3 x y l r (x,y∈{1,2,3},1≤l≤r≤n)3\ x\ y\ l\ r\ \ (x,y\in\{1,2,3\},1\leq l\leq r\leq n)3 x y l r (x,y∈{1,2,3},1≤l≤r≤n),表示将第xxx排区间[l,r][l,r][l,r]上的士兵复制一份加到第yyy排对应位置.
思路
线段树节点维护:①1×41\times 41×4的矩阵sum[][]sum[][]sum[][],其中sum[1][x]sum[1][x]sum[1][x]表示第xxx排当前区间的区间和;②4×44\times 44×4的矩阵lazy[][]lazy[][]lazy[][],表示矩阵乘法的懒标记,初始化为单位矩阵.整体思路:每个4×44\times 44×4的矩阵左上角的3×33\times 33×3矩阵的每一列维护兵线的每一排,4×44\times 44×4的矩阵的第444行维护操作.
①操作,答案为[l,r][l,r][l,r]的区间和的a[1][x]a[1][x]a[1][x]元素.
②操作,根据Gauss消元解线性方程组中的"一行的若干倍加到另一行上",不妨取矩阵的第444行都为111,则该操作等价于将第444行的yyy倍加到第xxx行.如x=1x=1x=1时,转移矩阵[100001000010y001]\begin{bmatrix}1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ y&0&0&1\end{bmatrix}⎣⎡100y010000100001⎦⎤;x=2x=2x=2时,转移矩阵[1000010000100y01]\begin{bmatrix}1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&y&0&1\end{bmatrix}⎣⎡1000010y00100001⎦⎤;x=3x=3x=3时,转移矩阵.[10000100001000y1]\begin{bmatrix}1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&y&1\end{bmatrix}⎣⎡10000100001y0001⎦⎤.
③操作,根据Gauss消元解线性方程组中的"交换两行",易知转移矩阵即单位矩阵交换第xxx行和第yyy行的结果.如x=1,y=3x=1,y=3x=1,y=3时,转移矩阵[0010010010000001]\begin{bmatrix}0&0&1&0 \\ 0&1&0&0 \\ 1&0&0&0 \\ 0&0&0&1\end{bmatrix}⎣⎡0010010010000001⎦⎤.
④操作,根据Gauss消元解线性方程组中的"一行的若干倍加到另一行上",易知转移矩阵即单位矩阵的a[x][y]a[x][y]a[x][y]元素+1+1+1.如x=1,y=3x=1,y=3x=1,y=3时,转移矩阵[1010010000100001]\begin{bmatrix}1&0&1&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&0&1\end{bmatrix}⎣⎡1000010010100001⎦⎤.
注意输入输出量大.
代码 -> 2021CCPC上海省赛-I(线段树维护矩阵)
namespace FastIO {
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++) // 重写getchar()
#define pc(ch) (p - buf2 == SIZE ? fwrite(buf2, 1, SIZE, stdout), p = buf2, *p++ = ch : *p++ = ch) // 重写putchar()
char buf[1 << 23], * p1 = buf, * p2 = buf;
template<typename T>
void read(T& x) { // 数字快读
x = 0;
T sgn = 1;
char ch = gc();
while (ch < '0' || ch > '9') {
if (ch == '-') sgn = -1;
ch = gc();
}
while (ch >= '0' && ch <= '9') {
x = (((x << 2) + x) << 1) + (ch & 15);
ch = gc();
}
x *= sgn;
}
const int SIZE = 1 << 21;
int stk[40], top;
char buf1[SIZE], buf2[SIZE], * p = buf2, * s = buf1, * t = buf1;
template<typename T>
void print_number(T x) {
p = buf2; // 复位指针p
if (!x) {
pc('0');
return;
}
top = 0; // 栈顶指针
if (x < 0) {
pc('-');
x = ~x + 1; // 取相反数
}
do {
stk[top++] = x % 10;
x /= 10;
} while (x);
while (top) pc(stk[--top] + 48);
}
template<typename T>
void write(T x) { // 数字快写
print_number(x);
fwrite(buf2, 1, p - buf2, stdout);
}
};
using namespace FastIO;
const int MAXN = 3e5 + 5;
const int MOD = 998244353;
template<typename T>
struct Matrix {
static const int MAXSIZE = 5;
int n, m; // 行数、列数
T a[MAXSIZE][MAXSIZE]; // 下标从1开始
Matrix() :n(0), m(0) { memset(a, 0, so(a)); }
Matrix(int _n, int _m) :n(_n), m(_m) { memset(a, 0, so(a)); }
void init_identity() { // 初始化为单位矩阵
assert(n == m);
memset(a, 0, so(a));
for (int i = 1; i <= n; i++) a[i][i] = 1;
}
Matrix<T> operator+(const Matrix<T>& B)const {
assert(n == B.n), assert(m == B.m);
Matrix<T> res(n, n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
res.a[i][j] = ((ll)a[i][j] + B.a[i][j]) % MOD;
}
return res;
}
Matrix<T> operator-(const Matrix<T>& B)const {
assert(n == B.n), assert(m == B.m);
Matrix<T> res(n, n);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
res.a[i][j] = ((a[i][j] - B.a[i][j]) % MOD + MOD) % MOD;
}
return res;
}
Matrix<T> operator*(const Matrix<T>& B)const {
assert(m == B.n);
Matrix<T> res(n, B.m);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= B.m; j++) {
for (int k = 1; k <= m; k++)
res.a[i][j] = ((ll)res.a[i][j] + (ll)a[i][k] * B.a[k][j]) % MOD;
}
}
return res;
}
Matrix<T> operator^(int k)const { // 快速幂
assert(n == m);
Matrix<T> res(n, n);
res.init_identity(); // 单位矩阵
Matrix<T> tmpa(n, n); // 存放矩阵a[][]的乘方
memcpy(tmpa.a, a, so(a));
while (k) {
if (k & 1) res = res * tmpa;
k >>= 1;
tmpa = tmpa * tmpa;
}
return res;
}
Matrix<T>& operator=(const Matrix<T>& B) {
memset(a, 0, so(a));
n = B.n, m = B.m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) a[i][j] = B.a[i][j];
return *this;
}
bool operator==(const Matrix<T>& B)const {
if (n != B.n || m != B.m) return false;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
if (a[i][j] != B.a[i][j]) return false;
}
return true;
}
void print() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) cout << a[i][j] << " \n"[j == m];
}
};
Matrix<ll> Imatrix = Matrix<ll>(4, 4); // 单位矩阵
struct Node {
int l, r;
Matrix<ll> sum; // 区间和
Matrix<ll> lazy; // 矩阵乘法标记
}SegT[MAXN << 2];
void push_up(int u) {
SegT[u].sum = SegT[u << 1].sum + SegT[u << 1 | 1].sum;
}
void build(int u, int l, int r) {
SegT[u].l = l, SegT[u].r = r;
SegT[u].sum = Matrix<ll>(4, 4);
SegT[u].lazy = Matrix<ll>(4, 4);
SegT[u].lazy.init_identity(); // 初始化为单位矩阵,表示无修改
if (l == r) {
SegT[u].sum.a[1][4] = 1;
return;
}
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
push_up(u);
}
void push_down(int u) {
if (SegT[u].lazy == Imatrix) return; // 无懒标记
SegT[u << 1].sum = SegT[u << 1].sum * SegT[u].lazy;
SegT[u << 1].lazy = SegT[u << 1].lazy * SegT[u].lazy;
SegT[u << 1 | 1].sum = SegT[u << 1 | 1].sum * SegT[u].lazy;
SegT[u << 1 | 1].lazy = SegT[u << 1 | 1].lazy * SegT[u].lazy;
SegT[u].lazy.init_identity(); // 清空为单位矩阵
}
void modify(int u, int l, int r, Matrix<ll>& mul) {
if (l <= SegT[u].l && SegT[u].r <= r) {
SegT[u].sum = SegT[u].sum * mul;
SegT[u].lazy = SegT[u].lazy * mul;
return;
}
push_down(u);
int mid = SegT[u].l + SegT[u].r >> 1;
if (l <= mid) modify(u << 1, l, r, mul);
if (r > mid) modify(u << 1 | 1, l, r, mul);
push_up(u);
}
Matrix<ll> query(int u, int l, int r) {
if (l <= SegT[u].l && SegT[u].r <= r) return SegT[u].sum;
push_down(u);
int mid = SegT[u].l + SegT[u].r >> 1;
Matrix<ll> res(4, 4);
if (l <= mid) res = res + query(u << 1, l, r);
if (r > mid) res = res + query(u << 1 | 1, l, r);
return res;
}
void solve() {
Imatrix.init_identity();
int n, q; read(n), read(q);
build(1, 1, n);
while (q--) {
int op; read(op);
if (op == 0) { // 询问第x排[l,r]的区间和
int x, l, r; read(x), read(l), read(r);
write(query(1, l, r).a[1][x]), putchar('\n');
}
else if (op == 1) { // 第x排[l,r]+=y
int x, l, r, y; read(x), read(l), read(r), read(y);
Matrix<ll> mul(4, 4);
mul.init_identity();
mul.a[4][x] = y;
modify(1, l, r, mul);
}
else if (op == 2) { // 交换第x排和第y排的[l,r]
int x, y, l, r; read(x), read(y), read(l), read(r);
Matrix<ll> mul(4, 4);
mul.init_identity();
mul.a[x][x] = mul.a[y][y] = 0;
mul.a[x][y] = mul.a[y][x] = 1;
modify(1, l, r, mul);
}
else { // 第y排[l,r]+=第x排[l,r]
int x, y, l, r; read(x), read(y), read(l), read(r);
Matrix<ll> mul(4, 4);
mul.init_identity();
mul.a[x][y]++;
modify(1, l, r, mul);
}
}
}
int main() {
solve();
}