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\}\),满足:

\[A_l = \left\{ x \mid (p_i\mid x) \land (x \in a) \right\} \]

\[\{ a_i\} = {\displaystyle \left|\bigcup _{l=1}^{q}A_l\right|=\sum _{l=1}^{n}(-1)^{l+1}\left(\sum _{1\leq u_{1}<\cdots <u_{l}\le q}\left|A_{u_{1}}\cap \cdots \cap A_{u_{l}}\right|\right)} \]

上式中 \(\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\) 种。我们不妨把上面的式子写的更抽象一点:

\[-1 = -\sum_{l=1}^{q}(-1)^{l+1} C_{q}^l = \sum_{l=1}^{q}(-1)^{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} 这种傻逼写法太难受了。

posted @ 2023-01-26 22:53  Luckyblock  阅读(47)  评论(0编辑  收藏  举报