稀土掘金AI刷题第246题:小U走排列问题|豆包MarsCode AI刷题

166 阅读9分钟

算法题解析:数轴上点的访问路径总和计算

问题描述

在数轴上有 nn 个点 aia_i,小U初始位于原点 00。她希望按照一定的顺序访问这些点。小U想知道,在所有不同的访问顺序中,走过的路径的总和是多少。每种顺序对应的路径长度等于她从原点出发,依次访问这些点所走的距离之和。

示例:

  • 样例 1:

    输入:n = 3, a = [1, 3, 5]

    输出:50

    解释:

    • 按照顺序 a1,a2,a3a_1, a_2, a_3 访问,路径长度为: ∣1−0∣+∣3−1∣+∣5−3∣=5|1 - 0| + |3 - 1| + |5 - 3| = 5
    • 按照顺序 a1,a3,a2a_1, a_3, a_2 访问,路径长度为: ∣1−0∣+∣5−1∣+∣3−5∣=7|1 - 0| + |5 - 1| + |3 - 5| = 7

    所有的访问顺序有 n!=6n! = 6 种。

任务:

计算所有不同的访问顺序中走过的路径总和。答案需要对 109+710^9 + 7 取模。

解题思路

1. 问题分析

  • 排列组合数量庞大:

    由于访问顺序有 n!n! 种,当 nn 较大时,无法枚举所有排列。

  • 需要找到高效的计算方法:

    我们需要一个时间复杂度低于 O(n!)O(n!) 的算法。

2. 思路概述

  • 拆解路径总和:

    我们可以将所有路径的总和拆解为两部分:

    1. 从原点到第一个访问点的距离之和:

      计算所有排列中,从原点到第一个访问点的距离的总和。

    2. 访问点之间的距离之和:

      计算所有排列中,相邻访问点之间的距离的总和。

  • 利用排列的对称性和数学性质:

    通过数学推导,找到计算总和的公式,避免枚举。

3. 数学推导

3.1 起点到第一个访问点的距离
  • 每个点作为第一个访问点的次数:

    在所有 n!n! 个排列中,每个点作为第一个访问点的次数是 (n−1)!(n-1)!。

  • 总距离贡献:

    所以,从原点到各点的距离之和为:

    D起点=(n−1)!×∑i=1n∣ai∣D_{\text{起点}} = (n-1)! \times \sum_{i=1}^{n} |a_i|

3.2 相邻访问点之间的距离
  • 相邻的点对:

    对于任意两个不同的点 aia_i 和 aja_j,在所有排列中,相邻出现的次数是:

    相邻次数=2×(n−2)!\text{相邻次数} = 2 \times (n - 2)!

    这里乘以 2 是因为 (ai,aj)(a_i, a_j) 和 (aj,ai)(a_j, a_i) 都是有效的相邻对。

  • 相邻点对在排列中的位置数:

    在每个排列中,有 n−1n - 1 个相邻位置,因此总的相邻对出现次数为:

    相邻对总次数=2×(n−2)!×(n−1)\text{相邻对总次数} = 2 \times (n - 2)! \times (n - 1)

  • 总距离贡献:

    所有相邻点对之间的距离总和为:

    D相邻=2×(n−2)!×(n−1)×∑1≤i<j≤n∣ai−aj∣D_{\text{相邻}} = 2 \times (n - 2)! \times (n - 1) \times \sum_{1 \leq i < j \leq n} |a_i - a_j|

  • 简化:

    D相邻=2×(n−1)!×∑1≤i<j≤n∣ai−aj∣D_{\text{相邻}} = 2 \times (n - 1)! \times \sum_{1 \leq i < j \leq n} |a_i - a_j|

    因为 (n−1)×(n−2)!=(n−1)!(n - 1) \times (n - 2)! = (n - 1)!。

3.3 总路径长度
  • 总路径长度为:

    D总=D起点+D相邻=(n−1)!×(∑i=1n∣ai∣+2×∑1≤i<j≤n∣ai−aj∣)D_{\text{总}} = D_{\text{起点}} + D_{\text{相邻}} = (n - 1)! \times \left( \sum_{i=1}^{n} |a_i| + 2 \times \sum_{1 \leq i < j \leq n} |a_i - a_j| \right)

4. 计算方法

4.1 计算 (n−1)!(n - 1)!
  • 由于需要计算大量的阶乘,可以使用循环并取模 109+710^9 + 7 防止溢出。
4.2 计算 ∑i=1n∣ai∣\sum_{i=1}^{n} |a_i|
  • 直接累加所有点到原点的距离。
4.3 计算 ∑1≤i<j≤n∣ai−aj∣\sum_{1 \leq i < j \leq n} |a_i - a_j|
  • 为了高效计算点对之间的距离之和,可以先对数组 aa 进行排序,然后利用前缀和计算。
4.4 前缀和计算点对距离之和
  • 步骤:

    1. 排序数组 aa。

    2. 初始化前缀和 prefix_sum=0\text{prefix_sum} = 0 和结果 S=0S = 0。

    3. 遍历数组,对于第 ii 个元素:

      增量=ai×i−prefix_sum\text{增量} = a_i \times i - \text{prefix_sum} S+=增量S += \text{增量} prefix_sum+=ai\text{prefix_sum} += a_i

5. 示例演算

以样例 1 为例:

  • 输入:

    n=3n = 3,a=[1,3,5]a = [1, 3, 5]

  • 计算 (n−1)!=2(n - 1)! = 2

  • 计算 ∑i=1n∣ai∣=∣1∣+∣3∣+∣5∣=9\sum_{i=1}^{n} |a_i| = |1| + |3| + |5| = 9

  • 排序数组:

    a=[1,3,5]a = [1, 3, 5]

  • 计算点对距离之和:

    • 初始化 prefix_sum=0\text{prefix_sum} = 0,S=0S = 0

    • 第 1 个元素(i=0i = 0):

      增量=1×0−0=0\text{增量} = 1 \times 0 - 0 = 0 S+=0⇒S=0S += 0 \Rightarrow S = 0 prefix_sum+=1⇒prefix_sum=1\text{prefix_sum} += 1 \Rightarrow \text{prefix_sum} = 1

    • 第 2 个元素(i=1i = 1):

      增量=3×1−1=2\text{增量} = 3 \times 1 - 1 = 2 S+=2⇒S=2S += 2 \Rightarrow S = 2 prefix_sum+=3⇒prefix_sum=4\text{prefix_sum} += 3 \Rightarrow \text{prefix_sum} = 4

    • 第 3 个元素(i=2i = 2):

      增量=5×2−4=6\text{增量} = 5 \times 2 - 4 = 6 S+=6⇒S=8S += 6 \Rightarrow S = 8 prefix_sum+=5⇒prefix_sum=9\text{prefix_sum} += 5 \Rightarrow \text{prefix_sum} = 9

  • 总点对距离之和 ∑i<j∣ai−aj∣=S=8\sum_{i<j} |a_i - a_j| = S = 8

  • 计算总路径长度:

    D总=2×(9+2×8)=2×(9+16)=2×25=50D_{\text{总}} = 2 \times (9 + 2 \times 8) = 2 \times (9 + 16) = 2 \times 25 = 50

    与样例输出一致。

代码实现

MOD = 10**9 + 7

def solution(n, a):
    # 计算 (n - 1)! % MOD
    fact = 1
    for i in range(2, n):
        fact = fact * i % MOD

    # 排序数组
    a.sort()

    # 计算 S1 = sum(|a_i|)
    S1 = sum(abs(ai) for ai in a) % MOD

    # 计算 S2 = sum_{i<j} |a_i - a_j|
    prefix_sum = 0
    S2 = 0
    for i in range(n):
        S2 = (S2 + a[i] * i - prefix_sum) % MOD
        prefix_sum = (prefix_sum + a[i]) % MOD

    # 计算总路径长度
    total = fact * (S1 + 2 * S2) % MOD

    return total

# 测试样例
print(solution(3, [1, 3, 5]))    # 输出: 50
print(solution(4, [1, 2, 4, 7])) # 输出: 324
print(solution(2, [2, 6]))       # 输出: 16

代码详解

  1. 计算 (n−1)!(n - 1)! 模 MODMOD:

    fact = 1
    for i in range(2, n):
        fact = fact * i % MOD
    
    • 由于 (n−1)!(n - 1)! 在后续计算中会多次使用,先计算出来。
    • 每次乘法后取模,防止数值过大。
  2. 排序数组 aa:

    a.sort()
    
    • 排序后的数组方便计算点对之间的距离之和。
  3. 计算所有点到原点的距离之和 S1S1:

    S1 = sum(abs(ai) for ai in a) % MOD
    
    • 对每个点 aia_i,计算 ∣ai∣|a_i|,累加得到 S1S1。
  4. 计算点对之间的距离之和 S2S2:

    prefix_sum = 0
    S2 = 0
    for i in range(n):
        S2 = (S2 + a[i] * i - prefix_sum) % MOD
        prefix_sum = (prefix_sum + a[i]) % MOD
    
    • 解释:

      • a[i]∗ia[i] * i 表示当前元素与前面 ii 个元素的乘积之和。
      • prefix_sumprefix_sum 是前 ii 个元素的累加和。
      • 差值 a[i]∗i−prefix_suma[i] * i - prefix_sum 即为当前元素与前面所有元素的差值之和。
    • 举例:

      • 对于排序后的数组 [a0,a1,...,an−1][a_0, a_1, ..., a_{n-1}],

        ∑j=0i−1(ai−aj)=ai×i−∑j=0i−1aj\sum_{j=0}^{i-1} (a_i - a_j) = a_i \times i - \sum_{j=0}^{i-1} a_j

  5. 计算总路径长度:

    total = fact * (S1 + 2 * S2) % MOD
    
    • 根据之前推导的公式,计算总路径长度。
  6. 返回结果:

    return total
    

时间复杂度分析

  • 排序: O(nlog⁡n)O(n \log n)
  • 计算 (n−1)!(n - 1)!: O(n)O(n)
  • 计算 S1S1 和 S2S2: O(n)O(n)
  • 总时间复杂度: O(nlog⁡n)O(n \log n)

总结

  • 关键思路:

    • 利用数学推导,将所有排列的路径总和转化为可以计算的公式。
    • 通过排序和前缀和技巧,高效地计算点对之间的距离之和。
  • 技巧运用:

    • 排列组合: 理解排列中元素出现的位置和次数。
    • 数学归纳和公式推导: 将复杂的求和问题转化为可计算的表达式。
    • 前缀和: 快速计算数列中元素的累加和,减少重复计算。

可能的扩展

  • 考虑起点不在原点的情况:

    如果小U初始位置不在原点,而是在某个位置 x0x_0,那么公式需要调整。

  • 加入更多的约束条件:

    比如访问顺序有特定的限制,或者路径不能重复经过某些点,问题将更加复杂。

  • 多维空间的扩展:

    如果点不在数轴上,而是在二维或三维空间,计算点对之间的距离总和将需要新的方法。


练习与思考

  1. 为什么在计算相邻点对的次数时,需要乘以 (n−1)(n - 1)?

    • 因为在每个排列中,有 n−1n - 1 个相邻位置,每个位置都可能是任意两个点的组合。
  2. 如果点的位置可以为负数,算法是否适用?

    • 适用,因为绝对值和差值计算都可以处理负数。
  3. 能否在不排序的情况下计算 ∑i<j∣ai−aj∣\sum_{i<j} |a_i - a_j|?

    • 在未排序的情况下,无法利用前缀和优化计算,需要 O(n2)O(n^2) 的时间复杂度,不适用于大的 nn。
  4. 在计算 (n−1)!(n - 1)! 时,是否可以使用递归方式?

    • 可以,但需要注意递归深度和效率,循环计算更简单直接。

结语

这道题目展示了如何通过数学推导和算法优化,解决一个看似需要暴力枚举的问题。通过理解排列中元素出现的次数,以及相邻元素对的组合数量,我们能够高效地计算出所有访问顺序的路径总和。这种方法对于其他涉及全排列和路径计算的问题也具有参考价值。


附录:完整代码

MOD = 10**9 + 7

def solution(n, a):
    # 计算 (n - 1)! % MOD
    fact = 1
    for i in range(2, n):
        fact = fact * i % MOD

    # 排序数组
    a.sort()

    # 计算 S1 = sum(|a_i|)
    S1 = sum(abs(ai) for ai in a) % MOD

    # 计算 S2 = sum_{i<j} |a_i - a_j|
    prefix_sum = 0
    S2 = 0
    for i in range(n):
        S2 = (S2 + a[i] * i - prefix_sum) % MOD
        prefix_sum = (prefix_sum + a[i]) % MOD

    # 计算总路径长度
    total = fact * (S1 + 2 * S2) % MOD

    return total

# 测试样例
print(solution(3, [1, 3, 5]))    # 输出: 50
print(solution(4, [1, 2, 4, 7])) # 输出: 324
print(solution(2, [2, 6]))       # 输出: 16