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
考虑枚举第一段 \(b_1 = \sum_{i=1}^{r_1}a_{i}\)。显然如果一个数 \(d\) 可以成为 \(\gcd(b_1, b_2\dots,b_k)\),那么一定有 \(b_1\bmod d=0, b_2\bmod d=0, \dots, b_k\bmod d=0\)。换言之,在保证 \(k>1\) 的情况下,\(d\) 对答案有贡献的充要条件是 \(b_1\bmod d=0\) 且 \(\sum_{i=2}^{k} b_{i} \bmod d=0\),此时一定可以至少将 \(\{a_i\}\) 分为满足条件的 2 段。
那么我们仅需考虑 \(\{a_i\}\) 的所有前缀 \(b_1 = \sum_{i=1}^{r_1}a_{i}\) 和剩余部分 \(\sum_{i=r_1+1}^{n} a_i\),它们两者的所有约数均对答案有贡献,取最大公约数即可。
复杂度 \(O(n\log A)\) 级别,\(A = \sum{a_i}\)。
//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
这是一道交互题。
外形怪马黄金船在和你玩一个游戏。她首先确定了一个整数 \(n\),她要求你通过下述操作把这个数猜出来:
- 在一次操作中,你可以输出一行
- x
,表示令 \(n=n-x\)。在这次操作之后,黄金船将会以输入的方式告诉你现在的 \(n\) 的二进制分解中有几个 1。- 如果某次操作后 \(n<0\),那么你将激怒黄金船并被抓回她的母星每天做十万次马儿跳。
- 当你猜出 \(n\) 的值后,输出一行
! n
结束这次的猜数。由于啊船是来自外星的高等智慧生命体,所以她要求你在 30 次操作之内猜出答案。
共有 \(t\) 组数据,请在满足上述要求的情况下猜出这 \(t\) 个数。
\(1\le t\le 500\),\(1\le n\le 10^9\).。
1S,256MB。
我们可以通过至多两次操作减去 \(n\) 的 lowbit
:
- 记 \(n\) 二进制分解中 1 的数量为 \(c_1\)。
- 进行一次操作
- 1
,记 \(n-1\) 二进制分解中 1 的数量为 \(c_2\)。 - 显然 -1 后,\(n\) 的
lowbit
位之前将不变,lowbit
位将会变成 0,lowbit
之后的为均变成 1,容易推出 \(n\) 的lowbit
是在第 \(c_2 - c_1 + 2\) 位。 - 只需在进行一次
- (1<<(c2-c1+1)-1)
即可减去lowbit
。
然而直接这样做需要至多 60 次操作。但我们发现上述第二步操作后啊船给出的 \(n\) 的二进制分解中 1 的数量是可以通过操作次数推出的,则可以考虑把上述的第二步操作与下一次的 - 1
合并成 - 1<<(c2-c1+1)
——但这样做会使减去最后的一位后多减一个 1,而且至多会进行 31 次操作。
可以考虑实现时维护一个减去 lowbit
后 \(n\) 中剩余的 1 的个数,将 lowbit
计入贡献后令该个数 -1,改个数归零后输出答案,可以在不影响正确性的情况下减少一次操作,从而解决问题。
总复杂度 \(O(t\log n)\) 级别。
//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
这里有一张 \(n=10^{18}\) 的完全图,点有点权,边有边权。第 \(i\) 个节点的点权值为 \(i\),边 \((u,v)\) 的边权值为 \(\gcd(u,v)\)。
\(t\) 组数据,每组数据给定整数 \(l,r\),求由节点 \(l\sim r\) 构成的完全子图中边权的种类数。
\(1\le t\le 100\),\(1\le l\le r\le 10^{18}\),\(l\le 10^9\)。
2S,256MB。
纪念第一个在赛时想到正解的 div2 E……虽然赛时没写完。
前置知识:整除分块。
问题实质是求 \([l, r]\) 中的数两两 \(\gcd\) 的种类数。考虑枚举 \(d = \gcd(i, j) (l\le i<j\le r)\):
对于 \(d\ge l\) 时,显然当 \(2\times d \le r\) 时,\(d\) 对答案有贡献,则有贡献的 \(d\) 满足的条件为:\(d\in \left[l, \left\lfloor\frac{r}{2}\right\rfloor\right]\)。
对于 \(d< l\),考虑 \([l,r]\) 中 \(d\) 的倍数的位置。显然其中最小的 \(d\) 的倍数为 \(\left\lceil \frac{l}{d} \right\rceil \times d\),最大的倍数为 \(\left\lfloor \frac{r}{d} \right\rfloor \times d\)。当满足 \(\left\lceil \frac{l}{d} \right\rceil < \left\lfloor \frac{r}{d} \right\rfloor\) 时 \(d\) 对答案有贡献。由整除分块可知 \(\left\lceil \frac{l}{d} \right\rceil\) 和 \(\left\lfloor \frac{r}{d} \right\rfloor\) 分别只有 \(\sqrt{l}\) 和 \(\sqrt{r}\) 种取值,但 \(r\) 过大,我们考虑通过整除分块枚举所有 \(\left\lceil \frac{l}{d} \right\rceil\) 相等的区间 \([i,j]\)。
对于 \(d\in [i,j]\),当 \(d\) 递增时也有 \(\left\lfloor \frac{r}{d} \right\rfloor\) 单调递减成立,则可以考虑在区间 \([i,j]\) 上二分得到最大的满足 \(\left\lceil \frac{l}{d} \right\rceil < \left\lfloor \frac{r}{d} \right\rfloor\) 的 \(d\),区间 \([i,j]\) 对答案有贡献的数的取值范围即 \([i, d]\)。\(O(\sqrt{l})\) 地枚举所有区间后再 \(O(\log l)\) 地累计贡献即可,总复杂度 \(O(\sqrt{l}\log l)\) 级别,可以通过本题。
或者更进一步地,对于 \(d\in [i,j]\),满足上述条件实质上等价于 \(\left(\left\lceil \frac{l}{d} \right\rceil + 1\right)\times d \le r\)。枚举区间后 \(\left\lceil \frac{l}{d} \right\rceil\) 的值固定,则最大的 \(d = \min\left(\frac{r}{\left(\left\lceil \frac{l}{d} \right\rceil + 1\right)}, j\right)\)。注意这样计算出的 \(d\) 可能小于 \(i\),需要特判一下。此时总复杂度变为 \(O(\sqrt{l})\) 级别。
//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
给定一长度为 \(n\) 的数列 \(a\),保证所有数两两不同。求满足:\(\gcd\left( \operatorname{min}(a_i, a_j, a_k), \max(a_i, a_j, a_k) \right) = 1\) 的三元组 \((i, j, k)\) 的数量。
\(3\le n\le 3\times 10^5\),\(1\le a_i\le 3\times 10^5\)。
1S,256MB。
一种和题解原理相同但可能更好理解(?)的写法。
先排序,以下钦定三元组 \((i,j,k)\) 中 \(i<j<k\)。然后考虑枚举三元组中的最大值 \(a_k\) 和最小值 \(a_i\),如果 \(\gcd(a_i, a_k) = 1\),那么它们对答案的贡献显然为可供选择的 \(a_j\) 的数量:\(k - i - 1\),则仅需统计 \((a_i, a_k)\) 这种互质对的贡献。
考虑求得一个数组 \(f\),\(f_k\) 代表以 \(a_k\) 为较大元素的满足条件的三元组的数量。以 \(a_k\) 为较大元素的数对总数为 \(\frac{(i-2)(i-1)}{2}\),考虑问题的反面求非互质对的数量。套路地考虑枚举质因数 \(g\),检查 \(a\) 中是否存在 \(g\) 的倍数,并令较大的倍数减去较小的倍数的贡献。具体地,对于小于 \(a_k\) 的 \(c\) 个与 \(a_k\) 不互质的 \(a_i\),\(f_k\) 的贡献应减去 \(c\times (k - 1) - \sum i\)。
但在枚举质因数 \(g\) 的过程中,可能会出现 \(a_i\) 重复统计的情况。比如 \(g = 2\) 和 \(g = 3\) 均是 \(a_i = 6\) 的约数,使得 \(a_i=6\) 的贡献被 \(a_i\) 的倍数减去了两次。
考虑容斥。考虑扩大枚举 \(g\) 的范围,对于一对 \((a_i,a_k)\),如果它们共同的质因数为 \((p_1, p_2,\dots, p_q)\),那么减去枚举到它们时 \(a_i\) 对 \(f_k\) 的贡献后,应再加上枚举到 \(p_1\times p_2,p_1\times p_3,\cdots p_{q-1}\times p_q\) 的时 \(a_i\) 对 \(f_k\) 的贡献,再减去枚举到 \(p_1\times p_2\times p_3,p_1\times p_2\times p_4,\cdots p_{q-2}\times p_{q-1}\times p_q\) 的时 \(a_i\) 对 \(f_k\) 的贡献,……这样可以保证 \(a_i\) 的贡献最终只会被减去 1 次。总结一下,我们考虑枚举共同的质因数——如果 \(g\) 仅由 \(l\) 个质数的一次方构成,那么枚举到 \(g\) 时,\(a_i\) 对 \(f_k\) 贡献的符号应为 \((-1)^{l}\),否则枚举到 \(g\) 时不统计贡献。
原理的话……学艺不精没法很好地解释。可以考虑质因数分解为 \((p_1, p_2, \dots, p_q)\) 中任意非空子集的数的集合 \(\{ a_i\}\),满足:
上式中 \(\left|\bigcup _{l=1}^{q}A_l\right|\) 即 \(f_k\) 应当减去贡献的 \(a_i\) 的不重不漏的集合,我们的目标是把它们的贡献仅减去 1 次。考虑这个经典式子的意义,达成上述目的,等效于把所有集合 \(\left|A_{u_1}\cap \cdots \cap A_{u_l}\right|\) 的贡献减去 \((-1)^l\) 次。
\(\left|A_{u_1}\cap \cdots \cap A_{u_l}\right|\) 表示作为 \(p_{u_1}\times,\dots, p_{u_l}\) 的倍数的 \(a_i\) 的集合。而 \(p_{u_1}\times,\dots, p_{u_l}\) 共有 \(C_q^l\) 种。我们不妨把上面的式子写的更抽象一点:
\(C_q^l\) 即由 \(l\) 个质数的一次方构成的 \(g\) 的个数。由这个式子可知,令 \(f_k\) 减去所有 \(a_i\) 的贡献 1 次,等效于先枚举 \(g\),并令 \(f_k\) 减去所有作为 \(g\) 的倍数的 \(a_i\) 的贡献 \((-1)^l\) 次。
实现时使用埃氏筛,处理出每个数的质因数同时标记不符合枚举条件的 \(g\),再枚举合法的 \(g\) 的倍数更新即可。
总复杂度为 \(O(n\ln\ln n)\) 级别。
//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
给定一长度为 \(n\) 的仅由小写字母构成的字符串 \(s\),求 \(s\) 中有多少个子串 \(t\),满足 \(t\) 在 \(s\) 中的出现次数 \(\operatorname{cnt}(t)\) 是 \(|t|\) 的倍数。两个子串不同当且仅当它们的出现位置不同。
\(1\le n\le 10^6\)。
3S,512MB。
SAM 板题。
但是板子抄错了哈哈。
建立 SAM 后 link 树上 DP 求得每个状态维护的字符串的出现次数 \(sz\),之后枚举状态 \(u\),问题变为求状态 \(u\) 维护的长度为 \([\operatorname{len}(\operatorname{link}_{u}) + 1, \operatorname{len}(u)]\) 的字符串 \(t\) 中有多少个 \(|t|\) 为 \(sz_u\) 的约数。预处理 \(1\sim n\) 的约数后二分即可。注意求的是子串数不是子串种类数,上面求得的贡献还需乘 \(sz_u\)。
总复杂度 \(O(n\ln \ln n + n\log n)\) 级别,不过空间较大,时空限制比较松所以能跑。
不预处理查询的时候再当场求约数也能过。
//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;
}
写在最后
- 有相同的约数问题可以放在模意义下考虑。
- \(\gcd\) 问题可以考虑枚举 \(\gcd\)。
- 容斥的本质是把一个不重不漏的集合的并的贡献,转化成多个集合的交的贡献的和。
2023.1.26 是一个值得铭记的日子。今晚久违的能睡个好觉了。
就这样。
upd:还是睡不着。
顺带吐槽一下为什么只有 \max
却没有 \min
,只能用 \operatorname{min}
这种傻逼写法太难受了。