给定一个数组V,我们需要找到两个索引(i,j),使得V[j]> V[i]且(j-i)最大。

11
给定一个数组V,我们需要找到两个索引(i,j)使得V[j] > V[i]且(j - i)最大。
暴力法很简单,对于每个值i(范围从1到n),我们将值j(范围从i+1到n)的值进行比较。我们跟踪到目前为止的最大(j-i)来找到最终答案。
这种方法的时间复杂度为O(n^2)。有没有人有改进时间复杂度的建议?

@spacevillain,如果列表是[4, 3, 2, 1],你的方法将行不通,因为在非递减排序后,“i”将为3,“j”将为0,这是不正确的,因为“j”应该大于“i”。 - Kay
@Kay,V[i]有什么限制? - Ivan Bianko
@IvanBenko - 如果你指的是数组的值没有限制,那么没有问题。我们只需要找到一对索引(i,j),使得A[j] > A[i],且j-i最大化。 - Kay
[8, 7, 5, 4, 2, 1, 9, 6, 3]是一个非常棘手的列表进行搜索。 - Neil
我可以在O(n)的时间复杂度内找到j-i的一个好的下限。首先,找到两个连续的升序条目。然后,从数组末尾开始向前查找大于较低条目的条目。接着,从较低条目开始向前查找小于上限条目的条目。 - Neil
5个回答

3

以下是一个可在线性时间内解决问题的方法。

  1. 使用简单的正向扫描数组计算递增位置i的堆栈S,使得min A [1..i-1] > i
  2. 反向遍历列表。
  3. 当当前元素大于堆栈S顶部给出的值时:检查是否有新记录并弹出堆栈顶部。

Python的快速实现:

def notsurewhattonamethis(A):
    assert(A)
    S = [0]
    for i,v in enumerate(A):
        if v<A[S[-1]]:
            S.append(i)
    best = (-1,-1)
    for i,v in reversed(list(enumerate(A))):
        while v>A[S[-1]]:
            j = S.pop()
            d = i - j
            if d > best[1]-best[0]:
                best = (j,i)
            if not S: return best
    return best

不错,尽管它仍然具有O(N^2)的时间*空间复杂度。 - Neil
@Neil:你为什么会关心时间*空间复杂度呢? - Nabb
仅仅因为原始问题没有明确说明O(N)空间复杂度是否可接受。(鉴于被接受的答案,我认为是可以接受的。) - Neil

2
如果您已知道数组元素的限制(否则请参见下面的更新),我可以向您推荐具有时间复杂度O(n*log(MaxN))和空间复杂度O(MaxN)的算法,其中MaxN = Max(V[i])。对于这个算法,我们需要一个结构,它可以在1到N之间以时间复杂度O(log(N))获取数组中的最小值,并以时间复杂度O(log(N))更新数组元素。Fenwick tree可以完成这些技巧。让我们把这个结构称为最小化器。然后我们需要做以下几件事情:
  1. 按给定顺序v[i]迭代所有元素,并将值i放在minimizator的v[i]位置。
  2. 对于每个元素v[i],在v[i-1]和1之间使用minimizator找到最小值(这是小于v[i]的元素的最小索引)
  3. 记住i和找到的小于v[i]的元素的最小索引之间的最大差异。

好的。我尝试写一些伪代码

prepare array (map values)
init minimizator

ansI = -1
ansJ = -1

for i from 0 to v.length-1
  minIndexOfElementLessThanCurrent = get min value from 1 to v[i]-1 (inclusive) using minimizator
  set to minimizator v[i] position value i

  if minIndexOfElementLessThanCurrent is exists
    if ansJ - ansI < i - minIndexOfElementLessThanCurrent 
      ansJ = i
      ansI = minIndexOfElementLessThanCurrent

C++ 实现:
class FenwickTree
{
    vector<int> t;
    int n;

public:

    static const int INF = 1000*1000*1000;

    void Init (int n)
    {
        this->n = n;
        t.assign (n, INF);
    }

    int GetMin (int i)
    {
        int res = INF;
        for (; i >= 0; i = (i & (i+1)) - 1)
            res = min (res, t[i]);
        return res;
    }

    void Update (int i, int value)
    {
        for (; i < n; i = (i | (i+1)))
            t[i] = min (t[i], value);
    }
};

pair<int, int> Solve(const vector<int>& v)
{
    int maxElement = 0;
    for(int i = 0; i < v.size(); i++)
        maxElement = max(maxElement, v[i]);

    FenwickTree minimizator;
    minimizator.Init(maxElement+1);

    int ansI = -1, ansJ = -1;
    for(int i = 0; i < v.size(); i++)
    {
        int minLeftIndex = minimizator.GetMin(v[i]-1);      
        minimizator.Update(v[i], i);

        if(minLeftIndex == FenwickTree::INF) continue; // no left elements less than v[i]

        if(ansJ - ansI < i - minLeftIndex)
        {           
            ansJ = i;
            ansI = minLeftIndex;
        }
    }
    return make_pair(ansI, ansJ);
}

更新: 如果元素类型不是整数(例如双精度浮点数),或者数组元素的最大值太大(例如10^9),我们可以将数组值映射到整数集1..N中(它不会影响结果),然后时间复杂度应为O(n * log(n))。
如果元素是整数,就有一个O(max(maxN, n))的解法。因此,如果maxN <= n,则复杂度为O(N)。我们只需要在常数时间O(1)内回答查询“从1到N获取最小值”即可: 1. 创建大小为maxN的数组。 2. 数组的第m[i]个元素是源数组V中值为i的最小索引。 3. 使用动态规划创建相同大小的数组,其中数组元素r[i]是 1<=j<=i 的 m[j] 的最小值。递推式为 r[i] = min(r[i-1], m[i])。
这个算法的主要思想与上面相同,只是使用数组 r 来找到从 1v[i] 的最小值。
C++ implementation:

pair<int, int> Solve(const vector<int>& v)
{
    int maxElement = 0;
    for(int i = 0; i < v.size(); i++)
        maxElement = max(maxElement, v[i]);

    vector<int> minimum(maxElement + 1, v.size() + 1);
    for(int i = 0; i < v.size(); i++)
        minimum[v[i]] = min(minimum[v[i]], i); // minimum[i] contains minimum index of element i

    for(int i = 1; i < minimum.size(); i++)
        minimum[i] = min(minimum[i-1], minimum[i]); // now minimum[i] contains minimum index between elements 1 and i

    int ansI = -1, ansJ = -1;
    for(int i = 0; i < v.size(); i++)
    {
        int minLeftIndex = minimum[v[i]-1];      

        if(minLeftIndex >= i) continue; // no left elements less than v[i]

        if(ansJ - ansI < i - minLeftIndex)
        {           
            ansJ = i;
            ansI = minLeftIndex;
        }
    }
    return make_pair(ansI, ansJ);
}

如果元素是双倍的,或者是其他的(非常大的整数),我们无法在线性时间内将元素映射到集合1..N中(或者可以吗?)。我只知道一个O(n*log(n))的解决方案(排序元素等)。

1
算法加一分,实现差一分。最好用伪代码编写。 - Andriy Tylychko

2

Java实现在线性时间内运行。

public class MaxIndexDifference {

public static void main(String[] args) {
     System.out.println(betweenTwoElements(2, 3, 6, 10, 4));
}

private static int betweenTwoElements(int... nums) {
    int numberOfElements = nums.length;
    int maxDifference = -1, minIndex = 0, maxIndex = 0;

    int[] lMin = new int[numberOfElements];
    int[] rMax = new int[numberOfElements];

    /* Construct lMin such that stores the minimum value (to the left)  from (nums[0], nums[1], ... nums[i])*/

    lMin[0] = nums[0];
    for (int i = 1; i < numberOfElements; i++) {
        lMin[i] = Math.min(nums[i], lMin[i -1]);
    }
    /* Construct RMax[] such that RMax[j] stores the maximum value (to the right) from (arr[j], arr[j+1], ..arr[n-1]) */
    rMax[numberOfElements - 1] = nums[numberOfElements - 1];
    for (int i = numberOfElements-2; i >= 0; i--) {
        rMax[i] =  Math.max(nums[i], rMax[i + 1]);
    }
    /* Traverse both arrays from left to right to find optimum maxIndex - minIndex This process is similar to merge() of MergeSort */
    while (minIndex < numberOfElements && maxIndex < numberOfElements) {
        if (lMin[minIndex] < rMax[maxIndex]) {
            maxDifference = Math.max(maxDifference, maxIndex - minIndex);
            maxIndex = maxIndex +1;
        } else {
            minIndex = minIndex +1;
        }           
    }   
    return maxDifference;
}
}

1

具有O(N)复杂度的算法:

#!/usr/bin/perl

use strict;
use warnings;

sub dump_list { print join(", ", map sprintf("%2d", $_), @_), "\n" }

for (0..20) {
    # generate a random list of integers with some convenient bias:
    my @l = (map int(rand(20) + 20 - $_), 0..19);

    my $max = $l[-1];
    my $min = $l[0];

    my @max;
    for my $l (reverse @l) {
        $max = $l if $l > $max;
        push @max, $max;
    }
    @max = reverse @max;

    my @min;
    for my $l (@l) {
        $min = $l if $l < $min;
        push @min, $min;
    }

    my $best_i = 0;
    my $best_j = -1;
    my $best   = -1;

    my $j = 0;
    for my $i (0..$#l) {
        while ($j < @l) {
            last unless $max[$j] > $min[$i];
            $j++;
            if ($j - $i > $best) {
                $best = $j - 1 - $i;
                $best_i = $i;
                $best_j = $j - 1;
            }
        }
    }

    print "list: "; dump_list @l;
    print "idxs: "; dump_list 0..$#l;
    print "best: $best, i: $best_i, j: $best_j\n\n";
}

更新: 回应Nohsib的请求:

假设你有一个随机数列表 (a[0], a[1], a[2], a[3]..., a[N-1])

第一步是找到每个数字左边的最大值 mas max[i] = maximum(a[0], a[1], ..., a[i]) 和右边的最小值 min[i] = minimum(a[i], a[i+1], ..., a[N-1]).

一旦你有了这些数组,找到满足 a[j] < a[k] 且最大化 k-j 的区间就几乎是轻而易举的。

试着用一些随机列表在纸上操作,你很容易就能找到背后的逻辑。


太棒了!我猜如果您使用单个 while 循环,而不是 for 和 while 循环,它仍然可以工作,就像 while (i<n & j<n) 一样,并在 $max[$j] > $min[$i] 时增加 j,否则增加 i。目前,使用您的方法,对于像 [1,2,3,4,4,4,4,4,4,4] 这样的列表,仍可能获得 O(n^2) 的时间复杂度。但通过上述建议的更改,您可以将时间复杂度降低到 O(n)。感谢您提出的想法。我会在有时间时尝试编写伪代码。 - Kay
@Kay:由于$j在for循环内部没有被重置为0,因此在最坏情况下它已经是O(N)了。在最坏情况下,while循环内的代码将执行2*N次,因此它是O(N)。 - salva

0

我不确定你想要什么,你的问题的第一段与第二段相矛盾。因此,我将给出两个答案,一个针对我能想到的每种解释,虽然都可能不是你的意思。

第一种解释:在j > i的约束条件下搜索V [j] - V [i]的最大值。
这本质上是找到最小值和最大值,但除此之外,还有一个索引的限制。这本身并不意味着不能使用这个想法;对于任何选择的V [i],您只需要在V [i + 1..n]上找到最大值,并且您不需要每次重新计算这些最大值,从而导致以下算法:

  • 以O(n)的时间计算后缀最大值数组
  • 在O(n)中找到具有最大差异的一对(V [i],suffixmax [i + 1])

第二种解释:在V [j]> V [i]的约束条件下搜索最大的j-i。
我无法想出什么好办法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接