You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

最大化高低信号重叠:百万级序列的Zigzag近似算法求解

解决百万级序列的Zigzag近似优化问题

这问题挺有意思的——既要处理百万级规模的正整数序列,又要找到以首元素开头的6项Zigzag序列,满足高低信号的符号约束,还得最大化重叠度。我给你梳理下思路,再提供一个线性时间的近似算法,完全适配百万级数据:

先把约束和目标说清楚

首先得明确几个核心点,避免歧义:

  • 序列必须从原序列的首元素a₁开始,后续的z₂z₆是原序列中位置递增的元素(也就是子序列,不然百万级数据根本没法处理)
  • 两种合法的信号模式:
    1. 上升-下降模式:高信号集合H = [z₃-z₂, z₅-z₄]全为正,低信号集合L = [z₄-z₁, z₆-z₃]全为负 → 翻译过来就是:z₃>z₂z₅>z₄z₄ < z₁z₆ < z₃
    2. 下降-上升模式:高信号集合H全为负,低信号集合L全为正 → 也就是:z₃<z₂z₅<z₄z₄ > z₁z₆ > z₃
  • 关于“最大化高低信号重叠度”,这里我们默认是指H中元素的绝对值之和 + L中元素的绝对值之和(这个指标既符合直觉,又容易高效计算,你也可以根据实际需求调整)

线性时间近似算法:贪心+预处理

要处理百万级数据,必须保证算法是O(M)时间复杂度,所以我们用贪心策略,分别计算两种模式下的近似最优解,最后取得分更高的那个。

模式1:上升-下降模式(H正、L负)

我们的贪心思路是尽可能让每个信号的绝对值最大化:

  1. 选z₂:从a₂开始,找后续一段范围内的最小值——这样z₃-z₂的差值能尽可能大
  2. 选z₃:在z₂的位置之后,找范围内的最大值——最大化z₃-z₂
  3. 选z₄:在z₃的位置之后,找所有小于z₁的元素中的最小值——这样z₁-z₄的绝对值最大
  4. 选z₅:在z₄的位置之后,找大于z₄的最大值——最大化z₅-z₄
  5. 选z₆:在z₅的位置之后,找小于z₃的最小值——最大化z₃-z₆的绝对值

如果某一步找不到符合条件的元素(比如z₃之后没有小于z₁的元素),就直接放弃这个模式,只计算模式2。

模式2:下降-上升模式(H负、L正)

思路类似,只是方向相反:

  1. 选z₂:从a₂开始,找范围内的最大值——让z₂-z₃的绝对值尽可能大
  2. 选z₃:在z₂之后,找范围内的最小值——最大化z₂-z₃的绝对值
  3. 选z₄:在z₃之后,找所有大于z₁的元素中的最大值——最大化z₄-z₁
  4. 选z₅:在z₄之后,找小于z₄的最小值——最大化z₄-z₅的绝对值
  5. 选z₆:在z₅之后,找大于z₃的最大值——最大化z₆-z₃

如何选最优解?

计算两种模式的得分:

  • 模式1得分:(z₃-z₂) + (z₅-z₄) + (z₁-z₄) + (z₃-z₆)
  • 模式2得分:(z₂-z₃) + (z₄-z₅) + (z₄-z₁) + (z₆-z₃)

取得分更高的模式对应的序列作为近似解。

百万级数据的适配优化

为了让算法跑得更快,我们可以做两个关键优化:

  1. 预处理前缀/后缀数组
    • 前缀数组:从左到右记录每个位置之前的最小值、最大值
    • 后缀数组:从右到左记录每个位置之后的最小值、最大值
      这样找z₂-z₆的时候,不需要逐个遍历,直接查表就能得到符合条件的最优元素,把每个步骤的时间从O(M)降到O(1),整个算法严格O(M)时间。
  2. 提前剪枝:如果某一步的后缀数组显示后续没有符合条件的元素(比如模式1中z₃之后没有小于z₁的元素),直接放弃该模式,节省计算时间。

为什么这是近似算法?

贪心策略只考虑了局部最优,没有遍历所有可能的组合(那会是O(M^5)的时间,完全不可能处理百万级数据),但对于大规模序列来说,局部最优已经能给出足够好的近似解。如果需要更精确的结果,可以在贪心解的附近做小范围局部搜索(比如调整z₂的位置,在其前后3-5个元素中重新计算得分),这只会增加O(M*K)的时间(K是搜索窗口大小),完全可控。

示例伪代码

def find_optimal_zigzag(a):
    M = len(a)
    if M < 6:
        return None  # 元素数量不足,无法生成6项序列
    
    z1 = a[0]
    # 预处理前缀最小/最大值数组
    prefix_min = [0] * M
    prefix_max = [0] * M
    prefix_min[0] = prefix_max[0] = z1
    for i in range(1, M):
        prefix_min[i] = min(prefix_min[i-1], a[i])
        prefix_max[i] = max(prefix_max[i-1], a[i])
    
    # 预处理后缀最小/最大值数组
    suffix_min = [0] * M
    suffix_max = [0] * M
    suffix_min[-1] = suffix_max[-1] = a[-1]
    for i in range(M-2, -1, -1):
        suffix_min[i] = min(suffix_min[i+1], a[i])
        suffix_max[i] = max(suffix_max[i+1], a[i])
    
    # 计算模式1(上升-下降)的得分和序列
    score1 = -float('inf')
    seq1 = None
    # 找z2:位置1到M-5的最小值(留足够位置给后续元素)
    z2_val = prefix_min[M-5]
    z2_pos = a.index(z2_val, 1, M-4)  # 找到第一个出现该最小值的位置
    # 找z3:z2_pos之后到M-3的最大值
    z3_val = suffix_max[z2_pos+1] if z2_pos+1 <= M-3 else None
    if z3_val is not None:
        z3_pos = a.index(z3_val, z2_pos+1, M-2)
        # 找z4:z3_pos之后到M-2的小于z1的最小值
        z4_val = None
        z4_pos = -1
        for i in range(z3_pos+1, M-1):
            if a[i] < z1:
                if z4_val is None or a[i] < z4_val:
                    z4_val = a[i]
                    z4_pos = i
        if z4_pos != -1:
            # 找z5:z4_pos之后到M-1的大于z4_val的最大值
            z5_val = suffix_max[z4_pos+1] if z4_pos+1 <= M-2 else None
            if z5_val is not None and z5_val > z4_val:
                z5_pos = a.index(z5_val, z4_pos+1, M-1)
                # 找z6:z5_pos之后的小于z3_val的最小值
                z6_val = None
                z6_pos = -1
                for i in range(z5_pos+1, M):
                    if a[i] < z3_val:
                        if z6_val is None or a[i] < z6_val:
                            z6_val = a[i]
                            z6_pos = i
                if z6_pos != -1:
                    seq1 = [z1, z2_val, z3_val, z4_val, z5_val, z6_val]
                    score1 = (z3_val-z2_val) + (z5_val-z4_val) + (z1-z4_val) + (z3_val-z6_val)
    
    # 计算模式2(下降-上升)的得分和序列
    score2 = -float('inf')
    seq2 = None
    # 找z2:位置1到M-5的最大值
    z2_val = prefix_max[M-5]
    z2_pos = a.index(z2_val, 1, M-4)
    # 找z3:z2_pos之后到M-3的最小值
    z3_val = suffix_min[z2_pos+1] if z2_pos+1 <= M-3 else None
    if z3_val is not None:
        z3_pos = a.index(z3_val, z2_pos+1, M-2)
        # 找z4:z3_pos之后到M-2的大于z1的最大值
        z4_val = None
        z4_pos = -1
        for i in range(z3_pos+1, M-1):
            if a[i] > z1:
                if z4_val is None or a[i] > z4_val:
                    z4_val = a[i]
                    z4_pos = i
        if z4_pos != -1:
            # 找z5:z4_pos之后到M-1的小于z4_val的最小值
            z5_val = suffix_min[z4_pos+1] if z4_pos+1 <= M-2 else None
            if z5_val is not None and z5_val < z4_val:
                z5_pos = a.index(z5_val, z4_pos+1, M-1)
                # 找z6:z5_pos之后的大于z3_val的最大值
                z6_val = None
                z6_pos = -1
                for i in range(z5_pos+1, M):
                    if a[i] > z3_val:
                        if z6_val is None or a[i] > z6_val:
                            z6_val = a[i]
                            z6_pos = i
                if z6_pos != -1:
                    seq2 = [z1, z2_val, z3_val, z4_val, z5_val, z6_val]
                    score2 = (z2_val-z3_val) + (z4_val-z5_val) + (z4_val-z1) + (z6_val-z3_val)
    
    # 返回得分更高的序列
    if score1 > score2:
        return seq1
    else:
        return seq2

注意:伪代码中的index方法是简化版,实际工程中可以通过预处理记录每个值的位置,避免重复遍历,进一步提升效率。

内容的提问来源于stack exchange,提问作者Iraj Shokouhi Bahar

火山引擎 最新活动