Codeforces Round 940 (Div. 2) and CodeCraft-23
写在前面
比赛地址:https://codeforces.com/contest/1957。
E 什么勾八,面向定理出题是吧。
以及这场犯大病了明明每题都一眼秒了写挂八十万遍导致上大分失败,输!
A
签到。
尽可能凑三角形。
复制复制// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long //============================================================= //============================================================= //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { int n; std::cin >> n; int cnt[110] = {0}; for (int i = 1; i <= n; ++ i) { int a; std::cin >> a; ++ cnt[a]; } int ans = 0; for (int i = 1; i <= 100; ++ i) { if (cnt[i] >= 3) ans += cnt[i] / 3; } std::cout << ans << "\n"; } return 0; }
B
构造。
首先特判 。
对于 ,仅需构造:
此时 中 1 的个数取到上界 。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; //============================================================= int ans[kN]; //============================================================= //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { int n, k; std::cin >> n >> k; if (n == 1) std::cout << k << "\n"; else { int l = log2(k); std::cout << (1 << l) - 1 << " " << k - (1 << l) + 1 << " "; for (int i = 3; i <= n; ++ i) std::cout << 0 << " "; std::cout << "\n"; } } return 0; }
C
DP or 组合数学。
妈的赛时 DP 假了太唐了。以下为赛时组合数学方法。
放棋子等价于删去矩阵中棋子所在的一行一列。于是根据读入的 步先预处理出矩阵还剩下几行几列可填。然后考虑如何求得先手白棋填满 的空矩阵的方案数 。发现仅有白棋可能会出现在 的位置,考虑白棋每步放置的位置,则最终局面可看做选出 个 的位置,再选出 个位置来放置白棋的方案数。于是考虑组合意义:
对于 ,可看做先从矩阵中选出 个 的位置,然后依次从 的矩阵中选出一个 的位置,操作之间是无序的,则其对答案的贡献为:
发现上式右侧可在降序枚举 过程中递推求解。又本题钦定了 ,于是对每组数据都直接套用上述组合意义式大力枚举求解 即可。
总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 3e5 + 10; const int lim = 3e5; const LL p = 1e9 + 7; //============================================================= LL fac[kN]; //============================================================= void Init() { fac[0] = fac[1] = 1; for (int i = 2; i <= lim; ++ i) { fac[i] = fac[i - 1] * i % p; } } LL qpow(LL x_, LL y_) { LL ret = 1; while (y_) { if (y_ & 1) ret = ret * x_ % p; x_ = x_ * x_ % p, y_ >>= 1ll; } return ret; } LL C(LL n_, LL m_) { if (m_ > n_) return 0; return fac[n_] * qpow(fac[m_], p - 2) % p * qpow(fac[n_ - m_], p - 2) % p; } LL Solve(int n_) { LL ret = 1, s = 1; for (int i = 2; i <= n_; i += 2) { s = 1ll * (1ll * i * i - i) % p * s % p; LL delta = C(n_, n_ - i) * s % p * qpow(fac[i / 2], p - 2) % p; ret += delta, ret %= p; } return ret; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); Init(); int T; std::cin >> T; while (T --) { int n, k; std::cin >> n >> k; for (int i = 1; i <= k; ++ i) { int x, y; std::cin >> x >> y; if (x != y) n -= 2; else n -= 1; } std::cout << Solve(n) << "\n"; } return 0; }
补一下好写的一批的 DP。同样记先手白棋填满 的空矩阵的方案数 ,初始化 。
并不考虑此时放的是第几个棋子,而是考虑该矩阵的第 行或列是如何被删去的:
- 若是被放置在 的棋子删去,则仅需考虑如何填满剩下的 的矩阵即可,则贡献为 。
- 否则应有一白棋放置在 或 ,方案数为 ,然后再考虑如何填满剩下的 的矩阵,则贡献为 。
则有转移方程:
预处理后即可 回答每次询问,牛逼。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 3e5 + 10; const int lim = 3e5; const LL p = 1e9 + 7; //============================================================= LL f[kN]; //============================================================= void Init() { f[0] = f[1] = 1; for (int i = 2; i <= lim; ++ i) { f[i] = f[i - 1] + 2ll * (i - 1) * f[i - 2] % p; f[i] %= p; } } LL qpow(LL x_, LL y_) { LL ret = 1; while (y_) { if (y_ & 1) ret = ret * x_ % p; x_ = x_ * x_ % p, y_ >>= 1ll; } return ret; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); Init(); int T; std::cin >> T; while (T --) { int n, k; std::cin >> n >> k; for (int i = 1; i <= k; ++ i) { int x, y; std::cin >> x >> y; if (x != y) n -= 2; else n -= 1; } std::cout << f[n] << "\n"; } return 0; }
D
位运算。
感觉比 C 简单呃呃,可能太套路了。
拆一下这个式子,等价于求三元组 满足:
异或等价于选择一些位置将它们翻转,则上式成立等价于 二进制中最高位的 1(即 位)在 中为 0,则只需检查 所有数二进制该位为 1 的数的数量是否为偶数。考虑枚举位置 ,求合法三元组的数量即求满足条件的区间 的数量。考虑将区间转化为整个数列删去一个前缀删去一个后缀,则仅需考虑整个数列,以及选择的前后缀中该位为 1 的数的奇偶性即可。
考虑预处理 表示前缀 中,它们的第 位中 1 的个数为偶数/奇数的前缀的个数,同理可预处理 表示后缀 中,它们的第 位中 1 的个数为偶数/奇数的后缀的个数。则有:
- 若整个数列第 位为 1 的数量为奇数,则贡献为 。
- 若整个数列第 位为 1 的数量为偶数,则贡献为 。
枚举每位预处理 与 ,然后枚举 通过上式计算贡献即可。
总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; //============================================================= int n, a[kN]; int cntpre[40][kN]; int pre[40][kN][2], suf[40][kN][2]; LL ans; //============================================================= void Init() { for (int i = 0; i <= 30; ++ i) { int s = (1 << i); pre[i][0][0] = suf[i][n + 1][0] = 1; pre[i][0][1] = suf[i][n + 1][1] = 0; for (int j = 1, c = 0; j <= n; ++ j) { c += ((a[j] & s) != 0); cntpre[i][j] = c; pre[i][j][0] = pre[i][j - 1][0] + (c % 2 == 0); pre[i][j][1] = pre[i][j - 1][1] + (c % 2 == 1); } for (int j = n, c = 0; j >= 1; -- j) { c += ((a[j] & s) != 0); suf[i][j][0] = suf[i][j + 1][0] + (c % 2 == 0); suf[i][j][1] = suf[i][j + 1][1] + (c % 2 == 1); } } } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { std::cin >> n; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; Init(); ans = 0; for (int y = 1; y <= n; ++ y) { int bit = log2(a[y]); int c = cntpre[bit][n]; int cpre[2] = {pre[bit][y - 1][0], pre[bit][y - 1][1]}; int csuf[2] = {suf[bit][y + 1][0], suf[bit][y + 1][1]}; if (c % 2 == 0) { ans += 1ll * cpre[0] * csuf[0]; ans += 1ll * cpre[1] * csuf[1]; } else { ans += 1ll * cpre[0] * csuf[1]; ans += 1ll * cpre[1] * csuf[0]; } } std::cout << ans << "\n"; } return 0; }
E
数论。
属于是面向定理出题了。赛时查到了威尔逊定理但是不会求化简后的式子,输!
首先有前置知识:
证明详见上述链接。
对于此题,定义 为从 个元素中选出 个元素组成圆排列的方案数,即有:
上述分子中的连乘式其中恰好有 项,则其中有且仅有一个 的倍数,且它们在模 意义下恰好为 。则上式在模 意义下有:
由威尔逊定理可知,对于所有不为 4 的合数 有 对答案无贡献,否则有:
这式子太典了。以 为质数为例,则对于某个有贡献的 ,其对 贡献为 0,对 贡献为 ,对 贡献为 ……考虑对整个值域进行埃氏筛求质数,在枚举 4 和所有质数的倍数的同时,差分维护它们对上述区间的贡献即可。前缀和还原贡献,并再次求前缀和即可 地回答每次询问。
总时间复杂度 级别,其中 。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e6 + 10; const int lim = 1e6; const LL p = 1e9 + 7; //============================================================= LL d[kN], ans[kN]; bool not_prime[kN]; //============================================================= void Init() { for (int i = 2; i <= lim; ++ i) { for (int j = 1; i * j <= lim; ++ j) { if (j > 1) not_prime[i * j] = true; if (!not_prime[i] || i == 4) { LL delta = 1ll * (i - j % i) % i % p; if (i == 4) delta = 2ll * j % i % p; d[i * j] += delta, d[i * j] %= p; if (i * j + i <= lim) d[i * j + i] += p -delta, d[i * j + i] %= p; } } } for (int i = 1; i <= lim; ++ i) { d[i] += d[i - 1], d[i] %= p; ans[i] += ans[i - 1] + d[i], ans[i] %= p; } } //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; Init(); while (T --) { int n; std::cin >> n; std::cout << ans[n] << "\n"; } return 0; }
写在最后
妈的前期多次犯病导致上大分失败呃呃,什么勾八状态。
学到了什么:
- C:方案数不好 DP 考虑组合意义;更换 DP 枚举的对象可能有助于转移方程的构建。
- D:考虑异或的性质,异或后变大变小,仅需考虑最高位 1。
- E:威尔逊定理;典中典之一类与下取整有关的和式 ,考虑调和级数地枚举 的倍数,则区间 贡献相同。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!