杂题选讲 2

· · 算法·理论

杂题选讲。

另外感谢 @yuhong056 提供好题。

P9820 [ICPC 2020 Shanghai R] Mine Sweeper II

依旧先来一道绿题开开胃。

题意

给定两张 n \times m 的扫雷地图,每个格子要么是地雷要么是空地,空地上会有数字,代表其周围 8 个格子中的地雷个数。

每次修改可以将第二张地图中任意一个格子反转(即地雷变空地,空地变地雷),求一个在修改不超过一半(若总数为奇数则向下取整)的格子的条件下,使第二张地图空地上数字的总和变为第一张的总和的方案。

思路

神秘 Ad-hoc。

先抛一个结论:第一张地图原图和其反图(所有格子反转)中至少有一个是答案。

下面给出证明:

我们把地图数字总和称为代价。首先,操作次数一定是符合要求的。原因很简单,因为把第二张图变为上面说的两种答案,修改次数总和一定等于 n \times m。所以,两种修改方案必然存在一种,使得操作次数满足限制。

然后考虑修改为反图的正确性。我们考虑原图上两个相邻的点对代价产生的贡献,分类讨论(下面说的类型都指原图上的):

所以,反图的代价与正图是相等的。

P1989 【模板】无向图三元环计数

题意

给定一张有 n 个点 m 条边的无向图,求其三元环个数。

思路

我们依旧先说结论。我们可以按照如下方式给这张图定向:

对于一条边 (u, v)

下面给出证明。

我们先证引理:在定向后的有向无环图中,任意节点 x 的出度 outd_x \le \sqrt{2m}。证明如下:

假设节点 u 的出度为 k,即 outd_x = k

根据定向规则,对于 u 的任意出边邻居 v \in Out_u,必有 deg_v \ge deg_u

因为节点 uk 个相邻节点,所以对于这 k 个出邻居 v,它们的无向图度数满足:

deg(v) \ge deg(u) \ge k \quad (\forall v \in Out(u))

显然原无向图中所有节点的度数之和为 2m。考虑到节点 u 及其 k 个出邻居 v_1, v_2, \dots, v_k,这 k+1 个节点的度数和不能超过全图总度数 2m

2m \ge deg_u + \sum_{v \in Out_u} deg_v \ge k + \sum_ {i = 1} ^ k k = k + k^2 > k^2

即:

k < \sqrt{2m}

因此,任意节点的出度满足 outdeg_u = O(\sqrt{m})

根据引理,在定向后的图上,我们可以枚举点 u,直接标记 u 的邻域,然后枚举出点 v,再枚举 v 的出点 w,若 wu 的邻域内,则算入答案。

P7804 [JOI Open 2021] 决算报告 / Financial Report

题意

给定长度为 N 的序列 A,要求在 A 中选出若干下标,使得相邻下标的距离不超过给定值 D,并且使得这些下标对应的元素构成的最长严格上升子序列最长,只需求出其 LIS 长度即可。

思路

首先,可以想到一个朴素的 N ^ 2 dp。设状态为 dp_i 表示以 i 结尾且 A_i 是选出的子序列中的前缀最大值时,子序列的最长长度。如何转移?我们设从 dp_j 转移到当前的 dp_i。我们发现,并不一定需要满足 i - j \le D 的要求,只需区间 (j, i) 中存在若干个 \le A_j 的元素,使得 j 可以通过这些下标来达到 i

所以,如果考虑收集型转移,我们可以提前预处理出一个数组 ok_{i, j}(i \le j),表示 i 是否可以通过若干个下标来到达 j。具体地,我们对于每个 i 都暴力枚举 j,同时记录一个右边界 R,表示满足 i \le k \le jA_k \le A_i 的最大 k。每次判断,如果可以从 R 到达当前枚举的 j,那么 ok_{i, j} 就为真,否则为假。转移时枚举 j \lt i,判断是否可达即可。

但是这样不好优化。于是需要想别的做法。我们考虑先规避掉 A 值的问题。先对 A 按照值从小到大排序,相等的值按原数组中的下标从大到小排序。为什么呢?后面再说。

注意到,如果有一个集合,其中相邻的任意两个下标都满足差不大于 D,此时要转移到一个新下标 i,满足 i 大于集合中最大下标且与其相差不超过 D,那么这个集合中所有下标的 dp 值都可以作为 dp_i 的转移方案。

于是我们考虑使用并查集来存储集合。再开一个 set 来存储当前已经处理完的下标(这里指原下标),用于快速找到当前点 i 左边且离当前下标最近的下标 j。如果 j 满足要求,那么 i 可以加入 j 的集合,此时更新 dp_i 为这个集合的答案的最大值。此时前面提到的排序规则的作用就体现出来了:值相同时按照 ID 从大到小排序,意味着值相同时,ID 更大的元素会出现在前面,寻找 j 时就会发现,更大下标的元素不在 i 的左边,从而规避了 ID 较小的元素错误地从 ID 较大的元素转移过来。如何求解最大值?我们每次合并的时候,将大的下标合并到小的去,这样可以保证每次 find 时可以找到最左端点 L。找到后,直接查询 [L, j] 的区间最大值即可。为什么查询 [L, j] 呢?显然这个集合的所有下标在 set 中是连续的,所以这个区间内只有这个集合的答案,其他都为非法值。初始需要将没有处理的答案赋上 - \infty。注意此时还需要将 set 中最小的大于 i 的下标 k 并入集合,如果 k - i \le D 的话。

P8078 [WC2022] 秃子酋长

题意

给定一个排列 A,每次查询:把区间 [l, r] 排序后,相邻两个数在原序列内位置差之和。

思路

回滚莫队。

你会发现这题线段树比较难实现,区间合并很难,但是又考虑到这题时限较宽(5s),可以过 O(n \sqrt n) 做法,所以考虑莫队。

若我们考虑普通莫队,可以对每个数存它的的出现位置,再开一个链表维护所有位置的有序列表,每次扩展区间的时候,就可以在链表中找到当前数的前驱后继,贡献容易算出。这样是 O(n \sqrt n \log n) 的。稍微算一算,炸了。

但是我们注意到,因为我们对每个数分别存了出现位置,所以,在不考虑 set 的情况下,我们的删除操作可以做到 O(1)。于是可以想到只删除不增加的回滚莫队,时间复杂度 O(n \sqrt n)

本题卡常。

[ABC405G] Range Shuffle Query

题意

给定长为 N 的序列 AQ 次询问,每次询问 (l, r, x),表示子段 A_l, A_{l + 1}, \dots, A_r 中,去掉 \ge x 的元素后,将剩下的子段中的元素重排,能够得到的不同序列数量。

思路

首先,答案可以表示为

\frac {(\sum \limits _ {i = 1} ^ x cnt_i)!} {\prod \limits _ {i = 1} ^ x (cnt_i!)}

于是,问题转化为求区间内小于等于给定值的出现个数。

但是,如果直接莫队+树状数组动态维护的话,时间复杂度是 O(n \sqrt n \log_2 n) 的,炸了。于是我们需要换一种思路。

注意到,时间复杂度的瓶颈在于,每次扩展/收缩区间都需要 \log_2n 的时间,而总共有 n \sqrt n 次端点移动。但是查询总共只有 n 次,所以我们可以换一种修改很快,查询可以稍慢一点的数据结构。

能够想到值域分块。修改时只需修改当前点和块内的总和/积,查询时枚举完整块的和/积再并上非完整块部分的即可。

此时,我们修改的时间复杂度来到了 O(1),而查询是 O(\sqrt n),但总时间复杂度降了下来,O(n \sqrt n),这也是一种平衡吧。

[ABC259Ex] Yet Another Path Counting

题意

给定 n \times n 的矩阵 A_{i, j},求:满足起点和终点的数相等的不同的路径数量。

思路

显然,对于任意两个点 (x_1, y_1), (x_2, y_2),我们可以 O(1) 地求出答案:\text C _ {|x_1 - x_2|} ^ {|x_1 - x_2| + |y_1 - y_2|}。若点集大小为 S,则这个做法是 O(S ^ 2) 的。

我们还有一个做法:确定一个起点,然后 O(n ^ 2) dp。

但是,如果我们直接枚举颜色,对所有的颜色都用同一种方法暴力求的话,是 O(n ^ 4) 的,TLE。

考虑根号分治。对于点集大小 \le B 的颜色,易知其最多有 B 个,用第一种求法,时间复杂度 O(B \times S ^ 2) \le O(n ^ 2 B)

对于点集大小 \gt B 的颜色,这时候显然就不能直接 O(S ^ 2) 做了,此时 O(n ^ 2) 的 dp 更优。下面证明这部分的时间复杂度。

设点集大小大于 B 的颜色共有 C 种,则所有满足条件的点集大小之和 \sum \limits _ {S_i \gt B} S_i \gt C \times B,又总共只有 n ^ 2 个点,所以 \sum \limits _ {S_i \gt B} S_i \le n ^ 2,结合上面两式,得 C \lt \frac {n ^ 2} B,则这部分的时间复杂度为 O(n ^ 2 C) \lt O(\frac {n ^ 4} B)

综上,总时间复杂度为 O(n ^ 2 B + \frac {n ^ 4} B),由均值不等式知,n ^ 2 B + \frac {n ^ 4} B \ge 2n ^ 3,在 B = n 时等号成立。证毕。

所以,将 n 作为阈值,根号分治即可。

P6880 [JOI 2020 Final] 奥运公交 / Olympic Bus

题意

给定一个含有 N 个点,M 条边的有向图,点的编号从 1N。每条边从 U_i 指向 V_i,经过这条边的代价为 C_i。图中可能存在重边。

在最开始时,我们可以翻转至多一条边,即让这条边从 U_i\to V_i 永久变为 V_i\to U_i,并产生 D_i 的代价。

你要从点 1 到点 N,再从点 N 回到点 1,你想知道,通过翻转至多一条边,能得到的最小代价和为多少?

数据范围:

思路

容易看出,答案 = \min\Big(dist(1\to N)+dist(N\to 1),\ \min_i\big[D_i+dist'_i(1\to N)+dist'_i(N\to 1)\big]\Big),其中 dist'_i 表示把第 i 条边反转后的最短路。

如果对每条边都反转、重新跑两遍 Dijkstra,时间复杂度是 O(N^2M) 的,这显然是 TLE 的。所以需要避免对很多边重新跑最短路。

首先,我们对于每一个点 x,求出 1 \to x, N \to x, x \to 1, x \to N 的最短路,这个可以通过建反图得到。

然后,对于一条边,分类讨论:

若这条边在某一条最短路径(1 \to NN \to 1)上,那么如果翻转这条边,可能破坏原来的最短路,需要重新计算。但是显然不能跑堆优化 Dijkstra。因为这图是稠密图,M = N ^ 2,这种情况下 O(N ^ 2) 的朴素 Dijikstra 会比堆优化更快。由于最短路径上的边数是 O(N) 的,所以这部分时间复杂度为 O(N ^ 3)

若这条边不在任何一条最短路径上,那么翻转这条边不会使答案变大。我们设当前边为 (u, v, w),此时答案为:

\min\Big(dist(1\to N), dist(1 \to v) + w + dist(u \to N)\Big) + \min \Big(dist(N \to 1), dist(N \to v) + w + dist(u \to 1)\Big)

但是这样会有一个问题:如果 1 \to v 的最短路径恰好包含当前边 (u, v, w),那么 dist(1 \to v) 不就是错的了吗?事实上,这不会影响答案。下面我们来讨论这个问题。

假设 1 \to v 最短路的最后一条边确为 (u, v, w),即 dist(1, v) = dist(1, u) + w代入入上式:

dist(1,v)+w+dist(u,N)=dist(1,u)+2w+dist(u,N)\ge dist(1,u)+dist(u,N)\ge dist(1,N)

你会发现这个答案一定不比最初的最短路优。所以这并不会影响答案。

综上,对于在最短路上的边,重新跑 N ^ 2 Dijkstra;对于不在最短路上的边,直接把初始最短路和翻转后经过这条边的答案取 \min 即可。

这里放一份代码,主要是这题太恶心了。

:::info[参考代码]

#include <bits/stdc++.h>

using namespace std;
using ll = long long;
using pii = pair<int, int>;

const int MAXN = 2e2 + 10, MAXM = 5e4 + 10;
const ll INF = 1e18;

struct SEdge {
  ll u, v, w, d;
} E[MAXM];

struct Edge {
  ll w, id;
  bool operator<(const Edge &oth) const {
    return w < oth.w;
  }
  bool operator<=(const Edge &oth) const {
    return w <= oth.w;
  }
} g[2][MAXN][MAXN][2];

ll n, m, dis[2][2][MAXN], tdis[2][MAXN], pre[MAXN];
bool vis[MAXN], flag[2][MAXM];

void chkmin(Edge arr[2], Edge w) {
  if (w <= arr[0]) {
    arr[1] = arr[0], arr[0] = w;
  } else if (w < arr[1]) arr[1] = w;
}

void addedge(int u, int v, Edge w) {
  chkmin(g[0][u][v], w);
  chkmin(g[1][v][u], w);
}

int findu(bool graph, bool nor1) {
  int u = 0;
  for (int i = 1; i <= n; i++) {
    if (!vis[i] && dis[nor1][graph][i] < dis[nor1][graph][u]) {
      u = i;
    }
  }
  return u;
}

int tfindu(bool nor1) {
  int u = 0;
  for (int i = 1; i <= n; i++) {
    if (!vis[i] && tdis[nor1][i] < tdis[nor1][u]) {
      u = i;
    }
  }
  return u;
}

void getpath(int s, bool nor1) {
  if (!pre[s]) return;
  flag[nor1][pre[s]] = 1;
  getpath(E[pre[s]].u, nor1);
}

void dij(int s, bool graph, bool nor1) {
  fill(dis[nor1][graph], dis[nor1][graph] + n + 1, INF);
  fill(vis, vis + n + 1, 0), fill(pre + 1, pre + n + 1, 0);
  dis[nor1][graph][s] = 0;
  for (int i = 1; i <= n; i++) {
    int u = findu(graph, nor1);
    vis[u] = 1;
    for (int v = 1; v <= n; v++) {
      if (dis[nor1][graph][v] > dis[nor1][graph][u] + g[graph][u][v][0].w) {
        dis[nor1][graph][v] = dis[nor1][graph][u] + g[graph][u][v][0].w, pre[v] = g[graph][u][v][0].id;
      }
    }
  }
  if (!graph) getpath(n + 1 - s, nor1);
}

void dij(int s, bool nor1) {
  fill(tdis[nor1], tdis[nor1] + n + 1, INF);
  fill(vis + 1, vis + n + 1, 0);
  tdis[nor1][s] = 0;
  for (int i = 1; i <= n; i++) {
    int u = tfindu(nor1);
    vis[u] = 1;
    for (int v = 1; v <= n; v++) {
      tdis[nor1][v] = min(tdis[nor1][v], tdis[nor1][u] + g[0][u][v][0].w);
    }
  }
}

int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m;
  for (int _ : {0, 1}) {
    for (int __ : {0, 1}) {
      for (int u = 1; u <= n; u++) {
        for (int v = 1; v <= n; v++) {
          g[_][u][v][__].w = INF;
        }
      }
    }
  }
  for (int i = 1; i <= m; i++) {
    auto &[u, v, w, d] = E[i];
    cin >> u >> v >> w >> d;
    addedge(u, v, {w, i});
  }
  dij(1, 0, 0), dij(n, 0, 1), dij(1, 1, 0), dij(n, 1, 1);
  ll ans = dis[0][0][n] + dis[1][0][1];
  for (int i = 1; i <= m; i++) {
    auto &[u, v, w, d] = E[i];
    if (flag[0][i] || flag[1][i]) {
      ll duv = g[0][u][v][0].w, dvu = g[0][v][u][0].w;
      g[0][v][u][0].w = min(dvu, w);
      if (g[0][u][v][0].w == w) g[0][u][v][0].w = g[0][u][v][1].w;
      dij(1, 0), dij(n, 1);
      g[0][u][v][0].w = duv, g[0][v][u][0].w = dvu;
      ans = min(ans, d + tdis[0][n] + tdis[1][1]);
    } else {
      ans = min(ans, d + min(dis[0][0][n], dis[0][0][v] + dis[1][1][u] + w) + min(dis[1][0][1], dis[1][0][v] + dis[0][1][u] + w));
    }
  }
  cout << (ans < INF ? ans : -1);
  return 0;
}

:::

P5355 [Ynoi Easy Round 2017] 由乃的玉米田

题意

给你一个序列 a,长度为 n,有 m 次操作,每次询问一个区间是否可以选出两个数它们的差为 x,或者询问一个区间是否可以选出两个数它们的和为 x,或者询问一个区间是否可以选出两个数它们的乘积为 x ,或者询问一个区间是否可以选出两个数它们的商为 x(没有余数) ,这四个操作分别为操作 1,2,3,4

选出的这两个数可以是同一个位置的数。

思路

我们一个一个来看。

首先,显然对于乘法,在知道区间内所有数是否出现的情况下,我们可以 O(\sqrt x) 枚举两个数,再判断是否存在。如果考虑在线,只能主席树,但是显然会挂。所以我们考虑离线,那么就是莫队模板了。这样是 O(n \sqrt n + m \sqrt V) 的。

然后,对于加减法,我们可以通过 bitset 来记录每个数是否出现,查询时只需 bitset 移位判断即可。这样是 O(\frac {N ^ 2} {32}) 的。

以上三个操作是另一个题(即双倍经验):P3674 小清新人渣的本愿。

最后来看除法。我们设一个阈值 B。如果 x > B,我们考虑直接枚举一个数,如果能够找到另一个数使得两数作商没有余数且值为 x,答案就是 yuno 了。这个和上面所有的操作一起做莫队。

如果 x \le B,我们记录 last _ {i, k},表示在区间 [1, i] 中,满足作商结果为 x 的二元组 (a, b) 中,a 在数组中下标的最大值,这里我们钦定 pos_a \lt pos_b。于是,只需判断是否满足 l \le last _ {r, x} 即可。

如何维护?你可能会想到预处理,但是这样空间复杂度是 O(NB) 的,128 MB 的空间使你意识到这似乎不太行依旧 Ynoi 特色。我们依然把他离线下来,对每个 x 分开存储,按照 r 从小到大排序,然后从 1n 维护即可。这样可以省去大小为 n 的一维,这样空间就只有 O(B) 了。

CF1364E X-OR

题意

交互题。有一个 0n - 1 的排列 p,初始时你不知道。你可以通过询问两个 1n 中的下标 (i, j),交互库会返回 p_i \mathbin{|} p_j,即 p_ip_j 按位或的结果。求这个排列。

### 思路 神秘交互题。 一眼看上去没一点思路啊,怎么办。但是,由于位运算有一些神秘的性质,具体来说,有这么一个恒等式: $$ (x \mathbin{|} a_1) \mathbin{\&} (x \mathbin{|} a_2) \mathbin{\&} \dots (x \mathbin{|} a_k) = x \mathbin{|} (a_1 \mathbin{\&} a_2 \mathbin{\&} \dots a_k) $$ 证明就不写了。看到这个公式,你想到了什么?我什么也没想到。 如果我们钦定一个 $\{ a_k \}$,使得其中所有元素的按位与结果为 $0$,那么我们只需 $k$ 次询问,就可以得到 $x$ 的值。 那么如何确定一个满足条件的 $\{ a_k \}$ 呢?首先重复元素显然是没用的。注意到对于二进制下的一位,如果 $\{ a_k \}$ 中有任何一个这一位为 $0$,那么整个序列按位与的结果在这一位上就为 $0$。 这里先抛一个结论:我们可以随机在 $1$ 到 $n$ 中选择不超过 $15$ 个数,这些数的按位与结果几乎一定为 $0$。为什么呢?我们发现,如果按位与结果有一位上是 $1$,那么要满足这 $k$ 个数中每个数这一位都是 $1$,这样的概率是 $\frac 1 {2 ^ k}$。当 $k$ 取 $15, 20$ 左右时,几乎已经不可能出现一位为 $1$ 的情况,更别说 $2$ 位及以上的可能性了。 好了。我们现在知道,询问 $15$ 次能够求出一个数的值,但是我们显然不能对每个数都这样求,我们需要一点策略。注意到,$0$ 和所有数按位或都不会改变另一个数的数值,所以我们可以考虑先求出 $0$ 的位置,之后直接用别的数和这个位置查询来获得结果即可。 如何求出 $0$ 的位置?我们可以考虑令 $x$ 初始为任意数,同时记录位置 $pos$,随后枚举所有 $1$ 到 $n$ 内除 $x$ 以外的数 $y$,如果 $x \mathbin{|} y = x$,这意味着二进制表示下 $y$ 是 $x$ 的子集,也就相当于 $y$ 是从 $x$ 中去除若干个 $1$ 得到的数。此时直接令 $x \leftarrow y$ 即可。 最后 $pos$ 就是 $0$ 的位置,我们只需拿其他数与这个数进行查询操作即可。 这套算法显然可以得到正确结果,这里主要说一下为什么操作次数不会超过给定的阈值。首先我们随机指定一个位置,我们需要找到这个数,花费 $15$ 次操作,最坏情况下,也就是 $0$ 在最末尾的情况,对每个数都需要查询 $\text {or}$ 和,另外还需要 $15 \log_2 n$ 次询问来得知 $\log_2 n$ 个数的值,也就是 $165$ 次。最后需要 $n - 1$ 操作来获知其他每个数的值。总共加起来 $4270$ 左右,看起来卡的比较死的时候会炸,但是我们可以一开始把我们查询的顺序 shuffle 一下,这样就很难被卡了。 最后注意,求出 $0$ 位置后不能直接输出 `!`,因为得到其他数的值的时候还需要询问。可以先问完,拿一个容器存一下。 --- ## CF1562F Tubular Bells ### 题意 交互题。有一个长度为 $n$ 的排列 $A$,其中 $\forall 1 \le i \le n, l \le A_i \le r$,且保证 $r - l + 1 = n$。现在你只知道 $n$,你需要求出这个排列。最多询问 $n + 5000$ 次,每次询问可以用二元组 $(x, y)$ 表示:询问 $\text{lcm} (A_x, A_y)$。 ### 思路 依旧神秘交互题。 根据上一题的经验,很快就可以猜到应该有这么一个结论: $$ \gcd (\text {lcm} (x, a_1), \text {lcm} (x, a_2), \dots, \text {lcm} (x, a_k)) = \text {lcm} (x, \gcd(a_1, a_2, \dots, a_k)) $$ 事实上这是对的。读者若感兴趣,可以自己证明,这里不做讲解。 也就是说,只要找到序列 $\{ a_k \}$,使得 $\gcd (A_{a_1}, A_{a_2}, \dots, A_{a_k}) = 1$,那么就可以通过 $k$ 次询问来获知一个数的值。 我们考虑暴力。显然这里有一个 $\frac {n (n - 1)} 2$ 次查询的暴力方法。对于每个数,我们记录下这个数与其他所有数的 $\text {lcm}$ 结果,除了 $n = 3$ 的情况下,容易知道,对于一个数,把它和其他所有数取 $\text {lcm}$ 后再做 $\gcd$,结果一定为 $1$。$n = 3$ 的情况下面单独讨论。因为一定其他数中一定存在相邻数。这个方法可以通过 $n \le 100$ 时的情况。事实上,$n \le 100$ 的情况必须要这么做,具体原因后面说明。 我们讨论 $n = 3$ 时的情况。容易知道,此时一定能够求出两个正确的数,且一定为最大和最小值。因为计算这两个的时候,上面的结论适用。但是,对于中间这个数,它旁边两个数的 $\gcd$ **不一定为 1**。原因很显然,考虑 $A = \{ 8, 9, 10 \}$ 的情况,这时,中间的数按照上面的方法会被计算为 $\text {lcm} (9, 2) = 18$,这是错误的。 对于 $n \gt 100$ 的情况,我们需另想办法。根据上一题的经验,我们可以考虑随机化。随机若干个数,使他们 $\gcd$ 结果不为 $1$ 的概率极小。 事实上,有这么一个公式:随机 $k$ 个正整数,他们 $\gcd$ 结果为 $1$ 的概率是 $\frac 1 {\zeta (k)}$。 如果感兴趣的话,可以看看证明。如果只是为了做这道题,那么你只需要知道,$k$ 取 $20$ 左右时,这个概率就已经非常接近 $1$。 :::info[证明] 以下证明来自 Qwen 3.7-Plus。 首先,黎曼函数($\zeta$ 函数)的定义: $$ \zeta(k) = \sum_{n=1}^{\infty} \frac{1}{n^k} = \prod _ {p \text{ is prime}} \frac 1 {1 - p^{-k}} $$ $k$ 个正整数 $a_1, a_2, \dots, a_k$ 互素(即 $\gcd = 1$),等价于**不存在任何一个素数 $p$ 能同时整除这 $k$ 个数**。 1. **单个素数的概率**: 在正整数中,能被素数 $p$ 整除的数的比例(自然密度)是 $\frac{1}{p}$。 假设这 $k$ 个数是“独立”选取的,那么素数 $p$ **同时**整除这 $k$ 个数的概率是: $$ P(p \text{ 整除所有数}) = \left(\frac{1}{p}\right)^k = \frac{1}{p^k} $$ 2. **单个素数“不”整除的概率**: 因此,素数 $p$ **不**同时整除这 $k$ 个数的概率是: $$ 1 - \frac{1}{p^k} $$ 3. **所有素数都不整除的概率**: 在数论中,不同素数的整除性质在渐近意义下是**相互独立**的。因此,没有任何素数能同时整除这 $k$ 个数的总概率,就是所有素数对应概率的乘积: $$ P(\gcd = 1) = \prod_{p \text{ is prime}} \left( 1 - \frac{1}{p^k} \right) $$ 4. **联系黎曼 $\zeta$ 函数**: 回忆一下黎曼 $\zeta$ 函数的**欧拉乘积公式**: $$ \zeta(k) = \sum_{n=1}^{\infty} \frac{1}{n^k} = \prod_{p \text{ is prime}} \frac{1}{1 - p^{-k}} $$ 对比一下就会发现,我们刚才求出的概率乘积,恰好是欧拉乘积公式的**倒数**。 $$ P(\gcd = 1) = \frac{1}{\prod_{p} (1 - p^{-k})^{-1}} = \frac{1}{\zeta(k)} $$ ::: 然后,如果我们已经知道了一个数 $x$ 和他的位置 $p$,如何才能求出其他所有数?若能够找到一个数,它与其他所有数互质,那么容易知道,直接对其他所有位置查询与这个位置的 $\text {lcm}$,再除以这个数即可得到答案。那么这个数要满足什么条件呢?首先他要是个质数,其次,不能有数是他的倍数。也就是说,要大于中位数。 依然考虑随机选择。我们注意到,$2 \times 10 ^ 5$ 内质数的出现频率约为 $9 \%$,随机到大于一半的数的概率是 $50 \%$,这两个相乘,就得到了随机到满足条件的数的概率,即 $4.5 \%$。这样,大约只需要随机 250 个数。 接下来就要提到前文的问题了。为什么 $n \le 100$ 时必须要暴力呢?因为上面的方法中,需要随机质数,但是你会发现,有一个长度为 $87$ 的空档,在这个区间里你找不到质数。所以为了避免风险,对于 $n \le 100$ 的情况,我们使用暴力做法,且 $n \le 100$ 刚好是暴力能过的临界点。