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

考虑枚举第一段 b1=i=1r1ai。显然如果一个数 d 可以成为 gcd(b1,b2,bk),那么一定有 b1modd=0,b2modd=0,,bkmodd=0。换言之,在保证 k>1 的情况下,d 对答案有贡献的充要条件是 b1modd=0i=2kbimodd=0,此时一定可以至少将 {ai} 分为满足条件的 2 段。

那么我们仅需考虑 {ai} 的所有前缀 b1=i=1r1ai 和剩余部分 i=r1+1nai,它们两者的所有约数均对答案有贡献,取最大公约数即可。

复杂度 O(nlogA) 级别,A=ai

//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=nx。在这次操作之后,黄金船将会以输入的方式告诉你现在的 n 的二进制分解中有几个 1。
  • 如果某次操作后 n<0,那么你将激怒黄金船并被抓回她的母星每天做十万次马儿跳。
  • 当你猜出 n 的值后,输出一行 ! n 结束这次的猜数。

由于啊船是来自外星的高等智慧生命体,所以她要求你在 30 次操作之内猜出答案。
共有 t 组数据,请在满足上述要求的情况下猜出这 t 个数。
1t5001n109.。
1S,256MB。

我们可以通过至多两次操作减去 nlowbit

  • n 二进制分解中 1 的数量为 c1
  • 进行一次操作 - 1,记 n1 二进制分解中 1 的数量为 c2
  • 显然 -1 后,nlowbit 位之前将不变, lowbit 位将会变成 0, lowbit 之后的为均变成 1,容易推出 nlowbit 是在第 c2c1+2 位。
  • 只需在进行一次 - (1<<(c2-c1+1)-1) 即可减去 lowbit

然而直接这样做需要至多 60 次操作。但我们发现上述第二步操作后啊船给出的 n 的二进制分解中 1 的数量是可以通过操作次数推出的,则可以考虑把上述的第二步操作与下一次的 - 1 合并成 - 1<<(c2-c1+1)——但这样做会使减去最后的一位后多减一个 1,而且至多会进行 31 次操作。

可以考虑实现时维护一个减去 lowbitn 中剩余的 1 的个数,将 lowbit 计入贡献后令该个数 -1,改个数归零后输出答案,可以在不影响正确性的情况下减少一次操作,从而解决问题。

总复杂度 O(tlogn) 级别。

//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=1018 的完全图,点有点权,边有边权。第 i 个节点的点权值为 i,边 (u,v) 的边权值为 gcd(u,v)
t 组数据,每组数据给定整数 l,r,求由节点 lr 构成的完全子图中边权的种类数。
1t1001lr1018l109
2S,256MB。

纪念第一个在赛时想到正解的 div2 E……虽然赛时没写完。

前置知识:整除分块

问题实质是求 [l,r] 中的数两两 gcd 的种类数。考虑枚举 d=gcd(i,j)(li<jr)

对于 dl 时,显然当 2×dr 时,d 对答案有贡献,则有贡献的 d 满足的条件为:d[l,r2]

对于 d<l,考虑 [l,r]d 的倍数的位置。显然其中最小的 d 的倍数为 ld×d,最大的倍数为 rd×d。当满足 ld<rdd 对答案有贡献。由整除分块可知 ldrd 分别只有 lr 种取值,但 r 过大,我们考虑通过整除分块枚举所有 ld 相等的区间 [i,j]

对于 d[i,j],当 d 递增时也有 rd 单调递减成立,则可以考虑在区间 [i,j] 上二分得到最大的满足 ld<rdd,区间 [i,j] 对答案有贡献的数的取值范围即 [i,d]O(l) 地枚举所有区间后再 O(logl) 地累计贡献即可,总复杂度 O(llogl) 级别,可以通过本题。

或者更进一步地,对于 d[i,j],满足上述条件实质上等价于 (ld+1)×dr。枚举区间后 ld 的值固定,则最大的 d=min(r(ld+1),j)。注意这样计算出的 d 可能小于 i,需要特判一下。此时总复杂度变为 O(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(min(ai,aj,ak),max(ai,aj,ak))=1 的三元组 (i,j,k) 的数量。
3n3×1051ai3×105
1S,256MB。

一种和题解原理相同但可能更好理解(?)的写法。

先排序,以下钦定三元组 (i,j,k)i<j<k。然后考虑枚举三元组中的最大值 ak 和最小值 ai,如果 gcd(ai,ak)=1,那么它们对答案的贡献显然为可供选择的 aj 的数量:ki1,则仅需统计 (ai,ak) 这种互质对的贡献。

考虑求得一个数组 ffk 代表以 ak 为较大元素的满足条件的三元组的数量。以 ak 为较大元素的数对总数为 (i2)(i1)2,考虑问题的反面求非互质对的数量。套路地考虑枚举质因数 g,检查 a 中是否存在 g 的倍数,并令较大的倍数减去较小的倍数的贡献。具体地,对于小于 akc 个与 ak 不互质的 aifk 的贡献应减去 c×(k1)i

但在枚举质因数 g 的过程中,可能会出现 ai 重复统计的情况。比如 g=2g=3 均是 ai=6 的约数,使得 ai=6 的贡献被 ai 的倍数减去了两次。

考虑容斥。考虑扩大枚举 g 的范围,对于一对 (ai,ak),如果它们共同的质因数为 (p1,p2,,pq),那么减去枚举到它们时 aifk 的贡献后,应再加上枚举到 p1×p2,p1×p3,pq1×pq 的时 aifk 的贡献,再减去枚举到 p1×p2×p3,p1×p2×p4,pq2×pq1×pq 的时 aifk 的贡献,……这样可以保证 ai 的贡献最终只会被减去 1 次。总结一下,我们考虑枚举共同的质因数——如果 g 仅由 l 个质数的一次方构成,那么枚举到 g 时,aifk 贡献的符号应为 (1)l,否则枚举到 g 时不统计贡献。


原理的话……学艺不精没法很好地解释。可以考虑质因数分解为 (p1,p2,,pq) 中任意非空子集的数的集合 {ai},满足:

Al={x(pix)(xa)}

{ai}=|l=1qAl|=l=1n(1)l+1(1u1<<ulq|Au1Aul|)

上式中 |l=1qAl|fk 应当减去贡献的 ai不重不漏的集合,我们的目标是把它们的贡献仅减去 1 次。考虑这个经典式子的意义,达成上述目的,等效于把所有集合 |Au1Aul| 的贡献减去 (1)l 次。

|Au1Aul| 表示作为 pu1×,,pul 的倍数的 ai 的集合。而 pu1×,,pul 共有 Cql 种。我们不妨把上面的式子写的更抽象一点:

1=l=1q(1)l+1Cql=l=1q(1)lCql

Cql 即由 l 个质数的一次方构成的 g 的个数。由这个式子可知,令 fk 减去所有 ai 的贡献 1 次,等效于先枚举 g,并令 fk 减去所有作为 g 的倍数的 ai 的贡献 (1)l 次。


实现时使用埃氏筛,处理出每个数的质因数同时标记不符合枚举条件的 g,再枚举合法的 g 的倍数更新即可。

总复杂度为 O(nlnlnn) 级别。

//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,满足 ts 中的出现次数 cnt(t)|t| 的倍数。两个子串不同当且仅当它们的出现位置不同。
1n106
3S,512MB。

SAM 板题。

但是板子抄错了哈哈。

建立 SAM 后 link 树上 DP 求得每个状态维护的字符串的出现次数 sz,之后枚举状态 u,问题变为求状态 u 维护的长度为 [len(linku)+1,len(u)] 的字符串 t 中有多少个 |t|szu 的约数。预处理 1n 的约数后二分即可。注意求的是子串数不是子串种类数,上面求得的贡献还需乘 szu

总复杂度 O(nlnlnn+nlogn) 级别,不过空间较大,时空限制比较松所以能跑。

不预处理查询的时候再当场求约数也能过。

//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} 这种傻逼写法太难受了。

posted @   Luckyblock  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示