算法题:最大连续子数组选择

Posted

技术标签:

【中文标题】算法题:最大连续子数组选择【英文标题】:Algorithms question: Largest contiguous subarray selection 【发布时间】:2021-11-09 19:11:46 【问题描述】:

问。给定两个长度相等的数组 A 和 B,找到索引 [i,j] 的最大可能连续子数组,使得 max(A[i: j])

示例:A = [10, 21, 5, 1, 3],B = [3, 1, 4, 23, 56]

解释:A[4] = 1, A[5] = 3, B[4] = 23, B[5] = 56, max(A[4], A[5])

索引为[4,5](含),最大连续子数组长度为2

我可以在 O(n2) 蛮力方法中做到这一点,但似乎无法降低时间复杂度。有什么想法吗?

【问题讨论】:

什么是 B[i,j] ...? B[i,j] 包含数组 B 中从索引 i 到索引 j 的所有元素 格式不正确。您提到的约束是max(A[i, j]) < B[i, j],但B[i, j] 是整数的子数组,那么您到底是什么意思?就是max(A[i, j])应该小于B[i, j]中的any元素? 是的,但在 B 中的同一索引范围内 divide-and-conquer标签是指算法课程的相应章节吗? 【参考方案1】:

O(n) 解:

将索引j从左向右移动并将i拖到后面,使ij的窗口有效。因此,始终将j 增加 1,然后根据需要将i 增加,以使窗口有效。

为此,请保留一个队列 @9​​87654329@ 的非递增 A 值索引。然后A[Aq[0]] 告诉您窗口中的最大 A 值。同样,为非递减的 B 值保留一个队列。

对于每个新的j,首先根据新的A值和新的B值更新AqBq。然后,当窗口无效时,增加i 并删除Aq[0]Bq[0](如果它们是i)。当ji 都更新时,使用窗口大小j - i - 1 更新结果。

Python 实现:

def solution(A, B):
    Aq = deque()
    Bq = deque()
    i = 0
    maxlen = 0
    for j in range(len(A)):
        while Aq and A[Aq[-1]] < A[j]:
            Aq.pop()
        Aq.append(j)
        while Bq and B[Bq[-1]] > B[j]:
            Bq.pop()
        Bq.append(j)
        while Aq and A[Aq[0]] >= B[Bq[0]]:
            if Aq[0] == i:
                Aq.popleft()
            if Bq[0] == i:
                Bq.popleft()
            i += 1
        maxlen = max(maxlen, j - i + 1)
    return maxlen

与简单的蛮力参考解决方案进行比较的测试结果:

expect:  83   result:  83   same: True
expect: 147   result: 147   same: True
expect: 105   result: 105   same: True
expect:  71   result:  71   same: True
expect: 110   result: 110   same: True
expect:  56   result:  56   same: True
expect: 140   result: 140   same: True
expect: 109   result: 109   same: True
expect:  86   result:  86   same: True
expect: 166   result: 166   same: True

测试代码 (Try it online!)

from timeit import timeit
from random import choices
from collections import deque
from itertools import combinations

def solution(A, B):
    Aq = deque()
    Bq = deque()
    i = 0
    maxlen = 0
    for j in range(len(A)):
        while Aq and A[Aq[-1]] < A[j]:
            Aq.pop()
        Aq.append(j)
        while Bq and B[Bq[-1]] > B[j]:
            Bq.pop()
        Bq.append(j)
        while Aq and A[Aq[0]] >= B[Bq[0]]:
            if Aq[0] == i:
                Aq.popleft()
            if Bq[0] == i:
                Bq.popleft()
            i += 1
        maxlen = max(maxlen, j - i + 1)
    return maxlen

def naive(A, B):
    return max((j - i + 1
                for i, j in combinations(range(len(A)), 2)
                if max(A[i:j+1]) < min(B[i:j+1])),
               default=0)

for _ in range(10):
    n = 500
    A = choices(range(42), k=n)
    B = choices(range(1234), k=n)
    expect = naive(A, B)
    result = solution(A, B)
    print(f'expect: expect:3   result: result:3   same: result == expect')

【讨论】:

用同样的想法打败我 ;) longer code @MBo 太好了,现在我真的不知道下一步该做什么。更正确地测试我的代码,阅读声称 O(n) 没有额外数据结构的答案,或者阅读你的:-) 无需阅读我的 - 类似的想法,更糟糕的实现;) 确实是一个很棒的解决方案。感谢您帮助我解决问题。 N 阶解决方案真的很重要! 很好的解决方案。自己不能做得更好。肯定 +1。【参考方案2】:

我可以看到基于问题,说我们有2个条件:

最大(A[i,j-1]) max(A[i,j]) >= min(B[i,j]) 说maxA是[i,j]中A数组中最大项的索引,minB是[i,j]中B数组中最小项的索引;并且锚点是 min(maxA, minB)

那么我们将有:max(A[i+k,anchor]) >= min(B[i+k,anchor]) ∀ k in [i+1,anchor]。

所以我想出了一个简单的算法,如下所示:

    int extractLongestRange(int n, int[] A, int[] B) 
        // n is size of both array
        int size = 0;
        for(int i = 0; i < n; i++)
            int maxAIndex = i;
            int minBIndex = i;
            for(int j = i; j < n; j++)
                if(A[maxAIndex] < A[j])
                    maxAIndex = j;
                
                if(B[minBIndex] > B[j])
                    minBIndex = j;
                
                if(A[maxAIndex] >= B[minBIndex])
                    if(size < j - i)
                        size = j - i;
                    
                    // here, need to jump back to min of maxAIndex and minBIndex.
                    i = Math.min(maxAIndex, minBIndex);
                    break;
                
                // this case, if j reach the end of array
                if(j == n - 1)
                    if(size < j - i + 1)
                        size = j - i + 1;
                        i = j;
                    
                
            
        
        return size; 

使用这种方法,复杂度为 O(n)。 如果需要,我们可以更改输出以获取其他信息。

【讨论】:

这个解决方案也很棒,是问题的答案。感谢您花一些时间解决这个问题。它对我帮助很大。 但是,arr1[] = 1, 2, 25, 1, 100, arr2[] = 3, 10, 4, 23, 56; 给出了错误的答案。答案 = 2,答案这个函数给出:3 很好,一旦有机会匹配最后两个条件,就必须将 break 移到外部 if 条件。已更新。 @Thinhbk 你能添加一个完整程序的链接以便于测试吗? @Thinhbk 例如like this。其中包含一个示例案例,希望能清楚说明您的方法为何不起作用:A = 1, 1, 1, 1, 2, 2, 2, 2, 2B = 2, 3, 3, 3, 3, 3, 3, 3, 3。你计算 5 而不是 8。【参考方案3】:

这个可以在O(n log(n))解决。

这是我的技术大纲。

我的想法看起来像 A 中最大的“上升水位”,跟踪 A 中出现的“岛屿”和B 中淹没的“岛屿”。并记录从A 出现之后或从B 下沉消失之前的最大交叉点。

我们需要AB 中的两棵平衡二叉树,以及一个优先级事件队列。

区间树需要支持对数“查找并返回包含i(如果存在)的区间”。它还需要支持对数“将i 添加到树,适当扩展/合并间隔,并返回新间隔”。同样是对数“从树中删除i,酌情缩短、拆分或删除其间隔”。

事件的形式可以是“删除B[i]”或“添加A[i]”。优先级队列首先按添加/减去元素的大小排序,然后将B 放在A 之前。 (因此,在从 B 中删除所有大小为 x 的元素之前,我们不会将大小为 x 的元素转换为 A。)

考虑到这一点,这里是如何做到这一点的伪代码。

Put all possible events in the priority queue
Initialize an empty tree of intervals A_tree
Initialize a tree of intervals B_tree containing the entire range
Initialize max interval to (0, -1) (length 0)

For each event (type, i) in queue:
    if event.type = A:
        new_interval = A_tree.add(event.i)
        
        if event.i in B_tree:
            Intersect event.i's A_tree interval with event.i's B_tree interval
            if intersection is longer than max_interval:
                update to new and better max_interval

    else:
        if event.i in A_tree:
            Intersect event.i's A_tree interval with event.i's B_tree interval
            if intersection is longer than max_interval:
                update to new and better max_interval

        B_tree.remove(event.i)

处理任何事件是O(log(n)。有2n = O(n) 事件。所以总时间是O(n log(n))

【讨论】:

以上是关于算法题:最大连续子数组选择的主要内容,如果未能解决你的问题,请参考以下文章

求解最大连续子数组和问题

JavaScript 算法题:从一个数组中找出总和最大的连续子数组

算法题每日一练---第36天:连续子数组的最大和

#yyds干货盘点# leetcode算法题:最大子数组和

数据结构与算法面试题80道

乱序版 ● 剑指offer每日算法题打卡题解——动态规划 (题号42,46,47,48)