一道由数学作业延伸出的构造题

先放原题:

将正整数1,2,3,,301, 2, 3, \dots, 30划分为kk组,使得每组内任意两数之和不等于任何完全平方数。求kk的最小值。(《培优新方法》八年级数学 §23 第3题)

嗯,对手搓暴力枚举来说数据确实大了点。抱着“反正也不可能求出正解”的心态,我尝试遍历了这些数,在第ii遍历时,将符合要求的数依次加入第ii组。具体来讲,如果数gg和第ii组里的任何数之和都不是完全平方数,则将gg加入该组并在随后的遍历中忽略。最终一共遍历了33遍才将所有数分完,所以我大剌剌填了33上去。往后一翻——答案就是33

然后某人发表暴论:“这有点像一道编程题。我在一本讲程序设计的书上看到过。”我同意。于是就有了这篇帖。

现在问题变成了:

输入正整数nn,将集合{1,2,,n}\{1, 2, \dots, n\}划分为kk个子集S1,S2,,SkS_1, S_2, \dots, S_k,有i,j,SiSj=\forall i, j, S_i \cap S_j = \emptyset,且有a,bSi,a+bx2 (xN)\forall a, b \in S_i, a + b \neq x^2 \space (x \in \mathbb{N})。输出最小的kk及具体划分方案(所以别想打表)

首先我们证明上面贪心算法的正确性。令第ii趟遍历构成集合SiS_i。为了保证SiS_i符合要求,即a,bSi,a+bx2 (xN)\forall a, b \in S_i, a + b \neq x^2 \space(x \in \mathbb{N}),对于扫描到的任意两个冲突的数,它们一定不能处于同一集合中,也即至少需要一个新集合来处理冲突的数,不会使最终结果变差。而我们将任意两个不冲突的数都尽可能放入同一集合中(应收尽收),综上这能够保证kk最小。

我们可以预处理出不超过(2n1)(2n-1)的完全平方数。由于我们采用顺序遍历,易知扫描到数gg时,只需检查完全平方数x[g,2g]x \in [g, 2g],查询数(xg)(x-g)是否在SiS_i中即可。用简化版的并查集实现集合的查询和合并。

哦,你说时间复杂度?我没能准确地算出来。根据之前尝试的结果,数据在2×1052\times 10^5以内时,时间都还可以接受。但若假设每个数平均会在遍历tt遍之后被划分到一个集合中;易知每次检查数gg,都将扫描约2gg=(21)g\sqrt{2g}-\sqrt{g} = (\sqrt{2}-1)\sqrt{g}个数,共花费q=(21)g=1ngq = (\sqrt{2}-1)\sum\limits_{g=1}^{n}{\sqrt{g}}的时间;二分查找花费log\log级别的时间,故最终大约花费O(t(nlogn+q))\mathrm{O}(t(n\log\sqrt{n}+q))的时间。

这应该不是最高效的算法。(是,我明白,这的确挺暴力的,但我也想不出其他办法。)欢迎从数轮角度分析并提出更快的解法。

我也想过二分kk的值(很明显答案具有单调性),但不妙的是,至今我仍未想出check ()函数怎么写。如何高效地判定在分ansans组时能否满足要求呢?

或者,有无可能……直接推导出某一数学结论呢?

总之,上述解法的代码实现如下:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
#include <bits/stdc++.h> using namespace std; const int N = 1024000; int n, ans, x, cnt, q; int fa[N], nums[N]; bool solved[N]; void solve (int g) { int p = upper_bound (nums + 1, nums + 1 + q, g) - nums; // 最小的大于g的完全平方数 for (int i = p; i <= q && nums[i] < g<<1; ++i) if (fa[nums[i] - g] == fa[x]) // 分组冲突 return; fa[g] = fa[x]; // 合并 solved[g] = 1; ++cnt; } int main () { scanf ("%d", &n); for (int i = 1; i <= n; ++i) fa[i] = i; // 初始化并查集 for (int i = 1; i * i <= n<<1; ++i) nums[++q] = i * i; // 预处理完全平方数 while (cnt < n) { ++ans, x = 1; // 新一轮遍历 while (solved[x]) ++x; for (int i = x; i <= n; ++i) if (!solved[i]) solve (i); } printf ("%d", ans); // 我懒,所以就不去重并输出具体方案了(雾 return 0; }
  • 2021年9月11日