云计算百科
云计算领域专业知识百科平台

2026-10-31:数对的最大强度。用go语言,给定一个整数数组 nums。需要从数组中选出两个位置不同的元素,组成一个数对。该数对的强度等于:这两个元素的乘积,除以它们的最大公约数的平方。请计算并

2026-10-31:数对的最大强度。用go语言,给定一个整数数组 nums。需要从数组中选出两个位置不同的元素,组成一个数对。该数对的强度等于:这两个元素的乘积,除以它们的最大公约数的平方。请计算并返回所有可能数对中能够得到的最大强度值。

2 <= nums.length <= 2000。

1 <= nums[i] <= 100000。

输入: nums = [2,3,5]。

输出: 15。

解释:

选择 i = 1 和 j = 2,得到强度:

(3 * 5) / gcd(3, 5)2 = 15 / 1 = 15,这是所有数对中的最大值。

题目来自力扣4010。

大体步骤如下:

一、先看清"强度"这个式子的数学本质

数对 (x, y) 的强度定义为:

strength=x⋅ygcd⁡(x,y)2\\text{strength} = \\frac{x \\cdot y}{\\gcd(x,y)^2}strength=gcd(x,y)2x⋅y​

设 g=gcd⁡(x,y)g = \\gcd(x, y)g=gcd(x,y),那么可以写成 x=g⋅a, y=g⋅bx = g \\cdot a,\\ y = g \\cdot bx=g⋅a, y=g⋅b,且 gcd⁡(a,b)=1\\gcd(a,b) = 1gcd(a,b)=1。代入后:

ga⋅gbg2=a⋅b=xg⋅yg\\frac{g a \\cdot g b}{g^2} = a \\cdot b = \\frac{x}{g} \\cdot \\frac{y}{g}g2ga⋅gb​=a⋅b=gx​⋅gy​

结论:强度 = 两个数各自"约掉公因子之后"的乘积。 所以要让它大,需要两个条件同时满足:

  • 两个数本身尽量大(乘积的分子大);
  • 它们的最大公约数尽量小(除以的 g2g^2g2 小)。
  • 顺带一个副产品:x⋅yx \\cdot yx⋅y 一定能被 gcd⁡(x,y)2\\gcd(x,y)^2gcd(x,y)2 整除,所以代码里做整数除法 xyg2\\frac{x y}{g^2}g2xy​ 不会有任何取整误差,结果是精确的。这一点后面判断剪枝安全性时会用到。

    题目数据范围 n≤2000n \\le 2000n≤2000,两两数对一共约 n(n−1)2≈2×106\\frac{n(n-1)}{2} \\approx 2 \\times 10^62n(n−1)​≈2×106 对,所以"全部枚举"本身是可行的baseline;这份代码的价值在于用一个上界剪枝把绝大多数情况压到远低于这个数。


    二、整体策略:排序 + 构造一个"只依赖乘积"的上界

    暴力枚举的问题是每一对都要算一次 gcd⁡\\gcdgcd。代码的思路是:

    能不能在不算 gcd⁡\\gcdgcd 的前提下,先判断"这一对绝对不可能刷新最优解"?如果能,就直接跳过不做 gcd⁡\\gcdgcd。

    要做到这件事,需要两样东西:

    • 一个便宜的上界:只靠 x⋅yx \\cdot yx⋅y 就能算出"这对的强度最多是多少";
    • 一个单调的枚举顺序:保证一旦上界跌破当前最优,后面所有候选只会更差,可以直接 break,而不是继续遍历。

    排序负责第二件事,全局 gcd⁡\\gcdgcd 负责第一件事。


    三、步骤 1:降序排序(为单调性铺路)

    代码用 slices.SortFunc 配合比较函数 b – a,把 nums 排成从大到小。

    排序不是为了"找最大的两个数"这么简单(最大的两个数不一定是最优解,例如 [15, 10, 6] 中 15×10 的强度只有 6,而 10×6 的强度是 15)。排序的真正目的是让后面双重循环的枚举顺序具备单调性:

    • 外层 i 递增时,x=nums[i]x = nums[i]x=nums[i] 的值递减;
    • 内层遍历 nums[:i],即下标 0,1,…,i−10, 1, \\dots, i-10,1,…,i−1,因为数组已降序,yyy 的值也是递减的,而且恒有 y≥xy \\ge xy≥x。

    于是对固定的 xxx,内层循环中乘积 x⋅yx \\cdot yx⋅y 随着 yyy 从 nums[0] 走到 nums[i-1] 是严格单调不增的。这个性质是 break 能够成立的前提。

    补充一句副作用:这是原地排序,会直接打乱调用方传入的 slice 顺序;如果调用方还依赖原顺序,需要先拷贝一份。


    四、步骤 2:求"全局 gcd",构造合法上界

    代码接着从头到尾把所有元素卷进一个 gcd,得到 allGcd,即整个数组所有元素的公共最大公约数,再平方得到 allGcd2。

    初始值是 0,而 gcd⁡(0,x)=x\\gcd(0, x) = xgcd(0,x)=x(gcd 函数里 a = 0 时循环一次都不进,直接返回 b),所以第一次滚动等价于"把 allGcd 初始化成第一个元素",这是标准的写法。由于 gcd⁡\\gcdgcd 满足交换律和结合律,遍历顺序不影响结果。

    关键的不等式推导:

  • allGcd 整除数组里的每一个元素,所以它也整除 xxx,也整除 yyy;
  • 既然 allGcd 同时整除 xxx 和 yyy,那它必然整除 gcd⁡(x,y)\\gcd(x, y)gcd(x,y);
  • 因此 gcd⁡(x,y)≥allGcd\\gcd(x,y) \\ge \\text{allGcd}gcd(x,y)≥allGcd,进而 gcd⁡(x,y)2≥allGcd2\\gcd(x,y)^2 \\ge \\text{allGcd}^2gcd(x,y)2≥allGcd2;
  • 分子相同、分母更大 ⇒\\Rightarrow⇒ 商更小,于是:
  • x⋅ygcd⁡(x,y)2≤x⋅yallGcd2\\frac{x \\cdot y}{\\gcd(x,y)^2} \\le \\frac{x \\cdot y}{\\text{allGcd}^2}gcd(x,y)2x⋅y​≤allGcd2x⋅y​

    右边就是那个"便宜的上界"——它完全不需要知道这一对具体的 gcd⁡\\gcdgcd,只要一次乘法一次除法就能算出来。而且它是"任意数对通用的",对数组里所有数对同时成立。

    还有个细节值得一提:x⋅yx \\cdot yx⋅y 未必能被 allGcd2\\text{allGcd}^2allGcd2 整除(因为 allGcd 只是 ggg 的下界,不一定等于 ggg),所以 mul / allGcd2 是向下取整的整数除法。向下取整只会让这个上界更小(更紧),而不会让它失效——⌊U⌋≤\\lfloor U \\rfloor \\le⌊U⌋≤ 真实强度 依然成立吗?这里要小心方向:真实强度 ≤xyallGcd2\\le \\frac{xy}{\\text{allGcd}^2}≤allGcd2xy​(实数除法),而真实强度是整数,所以真实强度 ≤⌊xyallGcd2⌋\\le \\lfloor\\frac{xy}{\\text{allGcd}^2}\\rfloor≤⌊allGcd2xy​⌋。是的,因为一个整数若不超过实数 ttt,那它也不超过 ⌊t⌋\\lfloor t \\rfloor⌊t⌋。所以取整后的上界依然是合法上界,剪枝安全。


    五、步骤 3:双重循环 + 上界剪枝

    外层枚举 iii(从 0 到 n−1n-1n−1),x=nums[i]x = nums[i]x=nums[i];内层枚举 j<ij < ij<i,y=nums[j]y = nums[j]y=nums[j]。这样每个无序数对恰好被访问一次,不重不漏。

    内层每一步做三件事:

  • 先算 mul = x * y(一次乘法,极便宜);
  • 算上界 mul / allGcd2,与当前最优 ans 比较:
    • 如果 上界 ≤ ans:说明这对的真实强度 ≤\\le≤ 上界 ≤\\le≤ ans,绝对不可能刷新最优;而且因为 yyy 继续往下走时 mul 只会更小、上界只会更低,后面所有 yyy 也都不可能刷新最优 —— 于是直接 break,连 gcd⁡\\gcdgcd 都不用算。
    • 如果 上界 > ans:说明这对"还有希望",这时才付出代价真正去算 g=gcd⁡(x,y)g = \\gcd(x, y)g=gcd(x,y),再用 mul/(g⋅g)mul/(g \\cdot g)mul/(g⋅g) 更新 ans。
  • ans 初始为 0,而题目保证元素 ≥1\\ge 1≥1,所以第一对的上界一定 >0> 0>0,不会一开始就误剪。
  • 这个 break 的正确性完全建立在第四节的继承和第三节的单调性之上,缺一不可:

    • 没有单调的枚举顺序(比如乱序),即使某一对的上界不达标,也不能推断后面的对不达标,就只能 continue 而非 break,剪枝收益大打折扣;
    • 没有合法上界,break 就可能把真正的答案剪掉。

    六、用 nums = [2, 3, 5] 完整走一遍

  • 降序排序 → [5, 3, 2]。
  • 求全局 gcd:gcd⁡(0,5)=5\\gcd(0,5)=5gcd(0,5)=5,gcd⁡(5,3)=1\\gcd(5,3)=1gcd(5,3)=1,gcd⁡(1,2)=1\\gcd(1,2)=1gcd(1,2)=1,所以 allGcd = 1,allGcd2 = 1。上界退化成"就是乘积本身"。
  • 外层 i = 0:nums[:0] 为空,内层不执行。
  • 外层 i = 1,x=3x = 3x=3:
    • 内层 y=5y = 5y=5:mul = 15,上界 15/1=15>15 / 1 = 15 >15/1=15> ans = 0,有希望 → 算 gcd⁡(3,5)=1\\gcd(3,5) = 1gcd(3,5)=1 → 强度 15/1=1515 / 1 = 1515/1=15 → ans = 15。
  • 外层 i = 2,x=2x = 2x=2:
    • 内层第一个 y=5y = 5y=5:mul = 10,上界 10/1=10≤10 / 1 = 10 \\le10/1=10≤ ans = 15 → 立刻 break,剩下的 y=3y = 3y=3 直接跳过。
  • 返回 15。
  • 验证一下被跳过的两对:(2,5)(2,5)(2,5) 强度 10、(2,3)(2,3)(2,3) 强度 6,确实都小于 15,剪枝没有剪错。


    七、这个剪枝到底有多强?(以及什么时候会失效)

    有意思的是,这个剪枝存在一个"两难"结构,让它在绝大多数输入下都异常高效:

    • 如果数对的 gcd⁡\\gcdgcd 普遍很小 → 强度值很大 → ans 迅速涨到一个很大的数 → 上界很容易就撞上 ans → 剪枝极早触发;
    • 如果数对的 gcd⁡\\gcdgcd 普遍很大 → 说明元素之间共享大量因子 → 全局 allGcd 也倾向于变大 → 上界 xyallGcd2\\frac{xy}{\\text{allGcd}^2}allGcd2xy​ 被压得很低 → 上界本身就不容易超过 ans → 剪枝同样很早触发。

    两个极端都利于剪枝,所以实践中内层循环往往每个 iii 只跑一两次就 break。

    但它并非没有最坏情况:漏洞在于"全局 gcd 小、局部两两 gcd 大"的输入。典型构造是取一批大质数 q1…qmq_1 \\dots q_mq1​…qm​,令数组元素是这些质数的两两乘积 qiqjq_i q_jqi​qj​。任意两个元素共享一个质因子(局部 gcd ≈ 300),但任意三个元素的公共 gcd = 1(全局 allGcd = 1)。此时:

    • 上界 =x⋅y≈105×105=1010= x \\cdot y \\approx 10^5 \\times 10^5 = 10^{10}=x⋅y≈105×105=1010,非常松;
    • 而真实强度只有约 qjqk≈105q_j q_k \\approx 10^5qj​qk​≈105,ans 很小;
    • 于是上界几乎永远大于 ans,剪枝几乎完全失效,内层会跑满,退化成 O(n2)\\mathcal{O}(n^2)O(n2) 次完整枚举。

    即便如此,n=2000n = 2000n=2000 时也就约 2×1062 \\times 10^62×106 次 gcd⁡\\gcdgcd,完全在限制内,所以这个上界分析上是 O(n2)\\mathcal{O}(n^2)O(n2)、实践上远好于 O(n2)\\mathcal{O}(n^2)O(n2),属于"稳赚不赔"的优化。


    八、复杂度分析

    记 $n = $ 数组长度(≤2000\\le 2000≤2000),V=max⁡(nums)V = \\max(nums)V=max(nums)(≤105\\le 10^5≤105)。

    时间复杂度

    • 降序排序:O(nlog⁡n)\\mathcal{O}(n \\log n)O(nlogn)(原地 pdqsort,比较函数 O(1)\\mathcal{O}(1)O(1));
    • 求全局 gcd:nnn 次欧几里得,单次 O(log⁡V)\\mathcal{O}(\\log V)O(logV),合计 O(nlog⁡V)\\mathcal{O}(n \\log V)O(nlogV);
    • 双重循环:迭代次数上界 n(n−1)2=O(n2)\\frac{n(n-1)}{2} = \\mathcal{O}(n^2)2n(n−1)​=O(n2)。每次迭代是 O(1)\\mathcal{O}(1)O(1) 的乘法+比较,只有未触发剪枝的那些迭代才额外付一次 O(log⁡V)\\mathcal{O}(\\log V)O(logV) 的 gcd⁡\\gcdgcd。因此最坏 O(n2log⁡V)\\mathcal{O}(n^2 \\log V)O(n2logV),剪枝生效时显著低于此。

    总时间复杂度:O(nlog⁡n+n2log⁡V)\\mathcal{O}(n \\log n + n^2 \\log V)O(nlogn+n2logV),由双重循环主导,即 O(n2log⁡V)\\mathcal{O}(n^2 \\log V)O(n2logV)。

    由于 V≤105V \\le 10^5V≤105,欧几里得的迭代轮数最多十几轮且常数极小,工程上可以直接视作 O(n2)\\mathcal{O}(n^2)O(n2);代入 n=2000n = 2000n=2000 即约 2×1062 \\times 10^62×106 次基本运算,毫秒级。

    额外空间复杂度

    • 排序是原地的,不分配新数组;nums[:i] 只是复用的切片头(三个机器字),循环中不产生堆分配;
    • 排序递归栈 O(log⁡n)\\mathcal{O}(\\log n)O(logn);
    • ans、allGcd、mul 等均为 O(1)\\mathcal{O}(1)O(1) 的标量。

    总额外空间复杂度:O(log⁡n)\\mathcal{O}(\\log n)O(logn),全来自排序的递归栈;忽略栈则可视为 O(1)\\mathcal{O}(1)O(1)。


    九、两个实现层面的小提醒

  • 整数宽度:mul = x * y 最大可达 101010^{10}1010,已超出 32 位 int。在 64 位平台上 Go 的 int 是 64 位,没问题;若在 32 位平台编译则会溢出,稳妥做法是把中间变量换成 int64(函数返回值已经是 int64,说明作者也考虑到了量级)。
  • 原地修改:如第三节所述,排序会打乱传入的 slice,这是函数的可观察副作用。
  • Go完整代码如下:

    package main

    import (
    "fmt"
    "slices"
    )

    func maxPairStrength(nums []int) int64 {
    slices.SortFunc(nums, func(a, b int) int { return b – a })

    allGcd := 0
    for _, x := range nums {
    allGcd = gcd(allGcd, x)
    }
    allGcd2 := allGcd * allGcd

    ans := 0
    for i, x := range nums {
    for _, y := range nums[:i] {
    mul := x * y
    if mul/allGcd2 <= ans {
    break
    }
    g := gcd(x, y)
    ans = max(ans, mul/(g*g))
    }
    }
    return int64(ans)
    }

    func gcd(a, b int) int {
    for a != 0 {
    a, b = b%a, a
    }
    return b
    }

    func main() {
    nums := []int{2, 3, 5}
    result := maxPairStrength(nums)
    fmt.Println(result)
    }

    在这里插入图片描述

    Python完整代码如下:

    # -*-coding:utf-8-*-

    from math import gcd

    def max_pair_strength(nums):
    # 按从大到小排序
    nums.sort(reverse=True)

    # 计算所有元素的整体 gcd
    all_gcd = 0
    for x in nums:
    all_gcd = gcd(all_gcd, x)

    all_gcd2 = all_gcd * all_gcd

    ans = 0
    for i, x in enumerate(nums):
    for y in nums[:i]:
    mul = x * y

    if mul // all_gcd2 <= ans:
    break

    g = gcd(x, y)
    ans = max(ans, mul // (g * g))

    return ans

    def main():
    nums = [2, 3, 5]
    result = max_pair_strength(nums)
    print(result)

    if __name__ == "__main__":
    main()

    在这里插入图片描述

    C++完整代码如下:

    #include <bits/stdc++.h>
    using namespace std;

    int gcd_int(int a, int b) {
    while (a != 0) {
    int t = b % a;
    b = a;
    a = t;
    }
    return b;
    }

    long long maxPairStrength(vector<int> nums) {
    sort(nums.begin(), nums.end(), greater<int>());

    int allGcd = 0;
    for (int x : nums) {
    allGcd = gcd_int(allGcd, x);
    }
    long long allGcd2 = 1LL * allGcd * allGcd;

    long long ans = 0;
    for (int i = 0; i < (int)nums.size(); ++i) {
    int x = nums[i];
    for (int j = 0; j < i; ++j) {
    int y = nums[j];
    long long mul = 1LL * x * y;

    if (mul / allGcd2 <= ans) {
    break;
    }

    int g = gcd_int(x, y);
    ans = max(ans, mul / (1LL * g * g));
    }
    }
    return ans;
    }

    int main() {
    vector<int> nums = {2, 3, 5};
    long long result = maxPairStrength(nums);
    cout << result << endl;
    return 0;
    }

    在这里插入图片描述

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 2026-10-31:数对的最大强度。用go语言,给定一个整数数组 nums。需要从数组中选出两个位置不同的元素,组成一个数对。该数对的强度等于:这两个元素的乘积,除以它们的最大公约数的平方。请计算并
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!