Codeforces Round 846
写在前面
比赛地址:https://codeforces.com/contest/1780。
执念就像是记错的二级结论一样可怕的东西。冥冥之中有一种结论错了的感觉,但总是觉得自己曾经亲手推导的东西不可能出错,一边不断纠结着要不要重新推一遍,一边还是犹豫地把求出来的答案写到了试卷上。
一点随想。
还有就是这场打着打着 unrated 了。本来手感挺好,写完 C 看了看 D 也会写,本想直接飞升,突然整这么一出也劲了,我直接开摆。
A
三个奇数或两个奇数一个奇数。
复制复制//By:Luckyblock /* */ #include <cstdio> #include <cctype> #include <vector> #include <algorithm> //============================================================= //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0'; return f * w; } //============================================================= int main() { int T = read(); while (T --) { int n = read(); std::vector <int> odd, even; for (int i = 1; i <= n; ++ i) { int a = read(); if (a % 2) odd.push_back(i); else even.push_back(i); } if (odd.size() >= 3) { printf("YES\n"); for (int i = 0; i < 3; ++ i) printf("%d ", odd[i]); printf("\n"); } else if (odd.size() >= 1 && even.size() >= 2) { printf("YES\n"); printf("%d ", odd[0]); for (int i = 0; i < 2; ++ i) printf("%d ", even[i]); printf("\n"); } else { printf("NO\n"); } } return 0; }
B
考虑枚举第一段 。显然如果一个数 可以成为 ,那么一定有 。换言之,在保证 的情况下, 对答案有贡献的充要条件是 且 ,此时一定可以至少将 分为满足条件的 2 段。
那么我们仅需考虑 的所有前缀 和剩余部分 ,它们两者的所有约数均对答案有贡献,取最大公约数即可。
复杂度 级别,。
//By:Luckyblock /* */ #include <cstdio> #include <cctype> #include <algorithm> #define LL long long const int kN = 2e5 + 10; //============================================================= int n, a[kN]; LL sum[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0'; return f * w; } LL gcd(LL x_, LL y_) { return y_ ? gcd(y_, x_ % y_) : x_; } //============================================================= int main() { // freopen("1.txt", "r", stdin); int T = read(); while (T --) { n = read(); for (int i = 1; i <= n; ++ i) { a[i] = read(); sum[i] = sum[i - 1] + 1ll * a[i]; } LL ans = 1; for (int i = 1; i < n; ++ i) { LL d = gcd(sum[i], sum[n] - sum[i]); ans = std::max(ans, d); } printf("%lld\n", ans); } return 0; }
C
贪心假咯。
虽然题都没了,但还是在这里放一组 hack 数据:
1 11 3 1 1 1 1 1 1 2 2 2 2 2 5 4 2
D
这是一道交互题。
外形怪马黄金船在和你玩一个游戏。她首先确定了一个整数 ,她要求你通过下述操作把这个数猜出来:
- 在一次操作中,你可以输出一行
- x
,表示令 。在这次操作之后,黄金船将会以输入的方式告诉你现在的 的二进制分解中有几个 1。- 如果某次操作后 ,那么你将激怒黄金船并被抓回她的母星每天做十万次马儿跳。
- 当你猜出 的值后,输出一行
! n
结束这次的猜数。由于啊船是来自外星的高等智慧生命体,所以她要求你在 30 次操作之内猜出答案。
共有 组数据,请在满足上述要求的情况下猜出这 个数。
,.。
1S,256MB。
我们可以通过至多两次操作减去 的 lowbit
:
- 记 二进制分解中 1 的数量为 。
- 进行一次操作
- 1
,记 二进制分解中 1 的数量为 。 - 显然 -1 后, 的
lowbit
位之前将不变,lowbit
位将会变成 0,lowbit
之后的为均变成 1,容易推出 的lowbit
是在第 位。 - 只需在进行一次
- (1<<(c2-c1+1)-1)
即可减去lowbit
。
然而直接这样做需要至多 60 次操作。但我们发现上述第二步操作后啊船给出的 的二进制分解中 1 的数量是可以通过操作次数推出的,则可以考虑把上述的第二步操作与下一次的 - 1
合并成 - 1<<(c2-c1+1)
——但这样做会使减去最后的一位后多减一个 1,而且至多会进行 31 次操作。
可以考虑实现时维护一个减去 lowbit
后 中剩余的 1 的个数,将 lowbit
计入贡献后令该个数 -1,改个数归零后输出答案,可以在不影响正确性的情况下减少一次操作,从而解决问题。
总复杂度 级别。
//By:Luckyblock /* */ #include <cstdio> #include <cctype> #include <algorithm> //============================================================= //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0'; return f * w; } //============================================================= int main() { // freopen("1.txt", "r", stdin); int T = read(); while (T --) { int cnt = read(), last = 1, ans = 0; while (cnt) { printf("- %d\n", last); fflush(stdout); int newcnt = read(); ans |= 1 << (newcnt - cnt + 1); last = 1 << (newcnt - cnt + 1); -- cnt; } printf("! %d\n", ans); fflush(stdout); } return 0; }
E
这里有一张 的完全图,点有点权,边有边权。第 个节点的点权值为 ,边 的边权值为 。
组数据,每组数据给定整数 ,求由节点 构成的完全子图中边权的种类数。
,,。
2S,256MB。
纪念第一个在赛时想到正解的 div2 E……虽然赛时没写完。
前置知识:整除分块。
问题实质是求 中的数两两 的种类数。考虑枚举 :
对于 时,显然当 时, 对答案有贡献,则有贡献的 满足的条件为:。
对于 ,考虑 中 的倍数的位置。显然其中最小的 的倍数为 ,最大的倍数为 。当满足 时 对答案有贡献。由整除分块可知 和 分别只有 和 种取值,但 过大,我们考虑通过整除分块枚举所有 相等的区间 。
对于 ,当 递增时也有 单调递减成立,则可以考虑在区间 上二分得到最大的满足 的 ,区间 对答案有贡献的数的取值范围即 。 地枚举所有区间后再 地累计贡献即可,总复杂度 级别,可以通过本题。
或者更进一步地,对于 ,满足上述条件实质上等价于 。枚举区间后 的值固定,则最大的 。注意这样计算出的 可能小于 ,需要特判一下。此时总复杂度变为 级别。
//By:Luckyblock /* */ #include <cmath> #include <cstdio> #include <cctype> #include <algorithm> #define LL long long //============================================================= //============================================================= inline LL read() { LL f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0'; return f * w; } //============================================================= int main() { // freopen("1.txt", "r", stdin); int T = read(); while (T --) { LL l = read(), r = read(), ans = 0, p = 0; if (r / 2 >= l) ans += r / 2 - l + 1; for (LL j = l - 1, i; j; j = i - 1) { i = ceil(1.0 * l / ceil(1.0 * l / j)); LL k = ceil(1.0 * l / j); ans += std::max(0ll, std::min(r / (k + 1), j) - i + 1); } printf("%lld\n", ans); } return 0; }
F
给定一长度为 的数列 ,保证所有数两两不同。求满足: 的三元组 的数量。
,。
1S,256MB。
一种和题解原理相同但可能更好理解(?)的写法。
先排序,以下钦定三元组 中 。然后考虑枚举三元组中的最大值 和最小值 ,如果 ,那么它们对答案的贡献显然为可供选择的 的数量:,则仅需统计 这种互质对的贡献。
考虑求得一个数组 , 代表以 为较大元素的满足条件的三元组的数量。以 为较大元素的数对总数为 ,考虑问题的反面求非互质对的数量。套路地考虑枚举质因数 ,检查 中是否存在 的倍数,并令较大的倍数减去较小的倍数的贡献。具体地,对于小于 的 个与 不互质的 , 的贡献应减去 。
但在枚举质因数 的过程中,可能会出现 重复统计的情况。比如 和 均是 的约数,使得 的贡献被 的倍数减去了两次。
考虑容斥。考虑扩大枚举 的范围,对于一对 ,如果它们共同的质因数为 ,那么减去枚举到它们时 对 的贡献后,应再加上枚举到 的时 对 的贡献,再减去枚举到 的时 对 的贡献,……这样可以保证 的贡献最终只会被减去 1 次。总结一下,我们考虑枚举共同的质因数——如果 仅由 个质数的一次方构成,那么枚举到 时, 对 贡献的符号应为 ,否则枚举到 时不统计贡献。
原理的话……学艺不精没法很好地解释。可以考虑质因数分解为 中任意非空子集的数的集合 ,满足:
上式中 即 应当减去贡献的 的不重不漏的集合,我们的目标是把它们的贡献仅减去 1 次。考虑这个经典式子的意义,达成上述目的,等效于把所有集合 的贡献减去 次。
表示作为 的倍数的 的集合。而 共有 种。我们不妨把上面的式子写的更抽象一点:
即由 个质数的一次方构成的 的个数。由这个式子可知,令 减去所有 的贡献 1 次,等效于先枚举 ,并令 减去所有作为 的倍数的 的贡献 次。
实现时使用埃氏筛,处理出每个数的质因数同时标记不符合枚举条件的 ,再枚举合法的 的倍数更新即可。
总复杂度为 级别。
//By:Luckyblock /* */ #include <cstdio> #include <cctype> #include <vector> #include <algorithm> #define LL long long const int kN = 3e5 + 10; //============================================================= int n, a[kN], pos[kN]; LL ans, f[kN]; bool vis[kN], vis1[kN]; std::vector <int> p[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0'; return f * w; } //============================================================= int main() { // freopen("1.txt", "r", stdin); n = read(); for (int i = 1; i <= n; ++ i) { a[i] = read(); if (i >= 3) f[i] = 1ll * (i - 2) * (i - 1) / 2ll; } std::sort(a + 1, a + n + 1); for (int i = 1; i <= n; ++ i) pos[a[i]] = i; for (int i = 2; i <= a[n]; ++ i) { if (vis1[i]) continue; if (!vis[i]) p[i].push_back(i); int sz = p[i].size(); LL cnt = (pos[i] > 0), sum = pos[i]; for (int j = 2; i * j <= a[n]; ++ j) { vis[i * j] = 1; if (!vis[i]) p[i * j].push_back(i); if (j % i == 0) vis1[i * j] = 1; if (pos[i * j]) { f[pos[i * j]] += (sz % 2 ? -1 : 1) * (cnt * (pos[i * j] - 1ll) - sum); ++ cnt, sum += pos[i * j]; } } } for (int i = 1; i <= n; ++ i) ans += f[i]; printf("%lld\n", ans); return 0; }
G
给定一长度为 的仅由小写字母构成的字符串 ,求 中有多少个子串 ,满足 在 中的出现次数 是 的倍数。两个子串不同当且仅当它们的出现位置不同。
。
3S,512MB。
SAM 板题。
但是板子抄错了哈哈。
建立 SAM 后 link 树上 DP 求得每个状态维护的字符串的出现次数 ,之后枚举状态 ,问题变为求状态 维护的长度为 的字符串 中有多少个 为 的约数。预处理 的约数后二分即可。注意求的是子串数不是子串种类数,上面求得的贡献还需乘 。
总复杂度 级别,不过空间较大,时空限制比较松所以能跑。
不预处理查询的时候再当场求约数也能过。
//By:Luckyblock /* */ #include <cmath> #include <cstdio> #include <cctype> #include <vector> #include <cstring> #include <algorithm> #define LL long long const int kN = 1e6 + 10; //============================================================= int n; char s[kN]; LL ans; std::vector <int> d[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0'; return f * w; } namespace SAM { const int kNode = kN << 1; int nodenum = 1, last = 1, tr[kNode][26], len[kNode], link[kNode]; int sz[kNode]; int edgenum, head[kNode], v[kNode], ne[kNode]; void Insert(int ch_) { int p = last, now = last = ++ nodenum; sz[now] = 1, len[now] = len[p] + 1; for (; p && !tr[p][ch_]; p = link[p]) tr[p][ch_] = now; if (!p) { link[now] = 1; return ; } int q = tr[p][ch_]; if (len[q] == len[p] + 1) { link[now] = q; return ; } int newq = ++ nodenum; memcpy(tr[newq], tr[q], sizeof (tr[q])); len[newq] = len[p] + 1; link[newq] = link[q], link[q] = link[now] = newq; for (; p && tr[p][ch_] == q; p = link[p]) tr[p][ch_] = newq; } void Add(int u_, int v_) { v[++ edgenum] = v_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void Dfs(int u_) { for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; Dfs(v_); sz[u_] += sz[v_]; } int maxl = len[u_], minl = len[link[u_]] + 1; std::vector<int>::iterator i = std::lower_bound(d[sz[u_]].begin(), d[sz[u_]].end(), minl); std::vector<int>::iterator j = std::upper_bound(d[sz[u_]].begin(), d[sz[u_]].end(), maxl); ans += 1ll * sz[u_] * (j - i); } void Solve() { for (int i = 2; i <= nodenum; ++ i) Add(link[i], i); Dfs(1); } } //============================================================= int main() { // freopen("1.txt", "r", stdin); n = read(); scanf("%s", s + 1); for (int i = 1; i <= n; ++ i) { for (int j = 1; i * j <= n; ++ j) { d[i * j].push_back(i); } } for (int i = 1; i <= n; ++ i) SAM::Insert(s[i] - 'a'); SAM::Solve(); printf("%lld\n", ans); return 0; }
写在最后
- 有相同的约数问题可以放在模意义下考虑。
- 问题可以考虑枚举 。
- 容斥的本质是把一个不重不漏的集合的并的贡献,转化成多个集合的交的贡献的和。
2023.1.26 是一个值得铭记的日子。今晚久违的能睡个好觉了。
就这样。
upd:还是睡不着。
顺带吐槽一下为什么只有 \max
却没有 \min
,只能用 \operatorname{min}
这种傻逼写法太难受了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战