用于寻找最大平衡子阵列的节省空间的算法?

时间:2011-09-11 20:33:04

标签: arrays algorithm binary-data

给定0和1的数组,找到最大子阵列,使得零和1的数量相等。 这需要在O(n)时间和O(1)空间中完成。

我有一个算法在O(n)时间和O(n)空间中完成。它使用前缀和数组,并利用如果0和1的数量相同的事实 sumOfSubarray = lengthOfSubarray / 2

#include<iostream>
#define M 15

using namespace std;

void getSum(int arr[],int prefixsum[],int size) {
    int i;
    prefixsum[0]=arr[0]=0;
    prefixsum[1]=arr[1];
    for (i=2;i<=size;i++) {
        prefixsum[i]=prefixsum[i-1]+arr[i];
    }
}

void find(int a[],int &start,int &end) {
    while(start < end) {
        int mid = (start +end )/2;
        if((end-start+1) == 2 * (a[end] - a[start-1]))
                break;
        if((end-start+1) > 2 * (a[end] - a[start-1])) {
            if(a[start]==0 && a[end]==1)
                    start++; else
                    end--;
        } else {
            if(a[start]==1 && a[end]==0)
                    start++; else
                    end--;
        }
    }
}

int main() {
    int size,arr[M],ps[M],start=1,end,width;
    ;
    cin>>size;
    arr[0]=0;
    end=size;
    for (int i=1;i<=size;i++)
            cin>>arr[i];
    getSum(arr,ps,size);
    find(ps,start,end);
    if(start!=end)
            cout<<(start-1)<<" "<<(end-1)<<endl; else cout<<"No soln\n";
    return 0;
}

10 个答案:

答案 0 :(得分:5)

现在我的算法是O(n)时间和O(Dn)空间,其中Dn是列表中的总体不相符。

此解决方案不会修改列表。

令D为列表中找到的1和0的差异。

首先,让我们在列表中线性地计算并计算D,只是为了看它是如何工作的:

我将以此列表为例:l = 1100111100001110

Element   D
null      0
1         1
1         2   <-
0         1
0         0
1         1
1         2
1         3
1         4
0         3
0         2
0         1
0         0
1         1
1         2
1         3
0         2   <-

找到最长的平衡子阵列相当于在D中找到2个相等的元素,这是更远的appart。 (在本例中,标有箭头的2 2s。)

最长的平衡子阵列是在元素+1的第一次出现和元素的最后出现之间。 (第一个箭头+1和最后一个箭头:00111100001110)

  

<强>注:

     

最长的子阵列将始终位于D的2个元素之间   在[0,Dn]之间,其中Dn是D的最后一个元素。(Dn = 2)   前面的例子)Dn是1和0之间的总不平衡   名单。 (如果Dn为负,则为[Dn,0])

     

在这个例子中,这意味着我不需要“看”3s或4s

     

<强>证明:

     

让Dn&gt; 0。

     

如果存在由P(P> Dn)限定的子阵列。从0 <0 Dn&lt; P,   在达到等于P的D的第一个元素之前,我们达到一个   元素等于Dn。因此,由于列表的最后一个元素等于Dn,因此Dns分隔的最长子阵列比Ps分隔的子阵列更长。因此我们不需要查看Ps

     

出于同样的原因,P不能小于0

     

Dn&lt; 0

的证明是相同的

现在让我们研究D,D不是随机的,2个连续元素之间的差异总是1或-1。 Ans在D和初始列表之间有一个简单的双射。因此我有两个解决这个问题的方法:

  • 第一个是跟踪每个的第一个和最后一个外观 D中的元素介于0和Dn之间(参见备注)。
  • 第二个是将列表转换为D,然后处理D。

第一个解决方案

目前我找不到比第一种方法更好的方法:

首先计算Dn(在O(n)中)。 DN = 2

第二个而不是创建D,创建一个字典,其中键是D的值(在[0和Dn]之间),每个键的值是一对(a,b),其中a是第一次出现的key和b last。

Element   D DICTIONNARY
null      0 {0:(0,0)}
1         1 {0:(0,0) 1:(1,1)}
1         2 {0:(0,0) 1:(1,1) 2:(2,2)}
0         1 {0:(0,0) 1:(1,3) 2:(2,2)}
0         0 {0:(0,4) 1:(1,3) 2:(2,2)}
1         1 {0:(0,4) 1:(1,5) 2:(2,2)}
1         2 {0:(0,4) 1:(1,5) 2:(2,6)}
1         3 { 0:(0,4) 1:(1,5) 2:(2,6)}
1         4 {0:(0,4) 1:(1,5) 2:(2,6)}  
0         3{0:(0,4) 1:(1,5) 2:(2,6) }
0         2 {0:(0,4) 1:(1,5) 2:(2,9) }
0         1 {0:(0,4) 1:(1,10) 2:(2,9) } 
0         0 {0:(0,11) 1:(1,10) 2:(2,9) } 
1         1 {0:(0,11) 1:(1,12) 2:(2,9) } 
1         2 {0:(0,11) 1:(1,12) 2:(2,13)}
1         3 {0:(0,11) 1:(1,12) 2:(2,13)} 
0         2 {0:(0,11) 1:(1,12) 2:(2,15)} 

你选择了差异最大的元素:2:(2,15)并且是l [3:15] = 00111100001110(l = 1100111100001110)。

  

时间复杂度:

     

2次传球,第一次传球给Dn,第二次传球给Dn   dictionnary。   找到字典中的最大值。

     

总计是O(n)

     

空间复杂性:

     

D中的当前元素:O(1)字典O(Dn)

     

由于这句话

,我不会在词典中使用3和4      

复杂度是O(n)时间和O(Dn)空间(平均情况下Dn <&lt;&lt;   n)中。

我想这种方法可能比词典更好。

欢迎提出任何建议。

希望有所帮助


第二个解决方案(只是一个想法而不是真正的解决方案)

继续进行的第二种方法是将列表转换为D.(因为从D返回列表很容易就可以了)。 (O(n)时间和O(1)空间,因为我将列表转换到位,即使它可能不是“有效”O(1))

然后从D你需要找到2个相同的元素,这是更远的appart。

它看起来像是在链表中查找最长的循环,Richard Brent algorithm的修改可能会返回最长的循环,但我不知道该怎么做,并且需要O(n)时间和O (1)空间。

找到最长的周期后,返回第一个列表并打印出来。

该算法需要O(n)时间和O(1)空间复杂度。

答案 1 :(得分:4)

不同的方法,但仍然是O(n)时间和记忆。从Neil的建议开始,将0视为-1。

符号:A[0, …, N-1] - 您的大小Nf(0)=0, f(x)=A[x-1]+f(x-1) - 函数

如果您要绘制f,您会看到,您所寻找的是f(m)=f(n), m=n-2k k阳性自然的点。更确切地说,只有x A[x]!=A[x+1](以及数组中的最后一个元素),您必须检查f(x)是否已经发生。不幸的是,现在我发现数组B[-N+1…N-1]没有改进存储此类信息。

完成我的想法:B[x]=-1最初,B[x]=pp = min k: f(k)=x。算法是(仔细检查,因为我很累):

fx = 0
B = new array[-N+1, …, N-1]
maxlen = 0
B[0]=0
for i=1…N-1 :
    fx = fx + A[i-1]
    if B[fx]==-1 :
        B[fx]=i
    else if ((i==N-1) or (A[i-1]!=A[i])) and (maxlen < i-B[fx]):
        We found that A[B[fx], …, i] is best than what we found so far
        maxlen = i-B[fx]

编辑:两个想法(=躺在床上时想出来:P):

1)您可以通过子阵列的长度二进制搜索结果,这将给出O(n log n)时间和O(1)内存算法。让我们使用函数g(x)=x - x mod 2(因为总和为0的子数组总是具有偶数长度)。首先检查,如果整个数组总和为0.如果是 - 我们已经完成,否则继续。我们现在假设0作为起始点(我们知道有这样长度的子阵列和“求和零属性”)和g(N-1)作为终点(我们知道没有这样的子阵列)。我们来做吧

    a = 0
    b = g(N-1)
    while a<b : 
        c = g((a+b)/2)
        check if there is such subarray in O(n) time
        if yes:
            a = c
        if no:
            b = c
    return the result: a (length of maximum subarray)

检查具有某个给定长度L的“求零属性”的子阵列很简单:

    a = 0
    b = L
    fa = fb = 0
    for i=0…L-1:
        fb = fb + A[i]
    while (fa != fb) and (b<N) :
        fa = fa + A[a]
        fb = fb + A[b]
        a = a + 1
        b = b + 1
    if b==N:
        not found
    found, starts at a and stops at b

2) ...你可以修改输入数组吗?如果是,如果O(1)内存完全意味着,你使用没有额外空间(除了常量数量的元素),那么只需将前缀表值存储在输入数组中。不再使用空间(除了一些变量):D

再次,仔细检查我的算法,因为我很累,并且可以完成一个错误。

答案 2 :(得分:2)

像尼尔一样,我觉得考虑字母{±1}而不是{0,1}是有用的。 假设不失一般性,至少与-1s一样多的+1。以下算法,使用 O(sqrt(n log n))位并运行在时间O(n),是由于“AF”

注意:通过假设输入可修改和/或浪费位,此解决方案不会作弊。在此编辑中,此解决方案是唯一一个同时发布O(n)时间和o(n)空间的解决方案。

更简单的版本,使用O(n)位,流式传输前缀和数组,并标记每个值的第一次出现。然后它向后扫描,考虑0和sum之间的每个高度(arr)该高度处的最大子阵列。一些思想揭示了最佳状态(记住这个假设)。在Python中:

sum = 0
min_so_far = 0
max_so_far = 0
is_first = [True] * (1 + len(arr))
for i, x in enumerate(arr):
    sum += x
    if sum < min_so_far:
        min_so_far = sum
    elif sum > max_so_far:
        max_so_far = sum
    else:
        is_first[1 + i] = False

sum_i = 0
i = 0
while sum_i != sum:
    sum_i += arr[i]
    i += 1
sum_j = sum
j = len(arr)
longest = j - i
for h in xrange(sum - 1, -1, -1):
    while sum_i != h or not is_first[i]:
        i -= 1
        sum_i -= arr[i]
    while sum_j != h:
        j -= 1
        sum_j -= arr[j]
    longest = max(longest, j - i)

让空间缩小的诀窍来自于注意到我们正在按顺序扫描is_first,尽管与其构造相反。由于循环变量适合O(log n)位,我们将在每个O(√(n log n))步骤之后计算循环变量的检查点,而不是is_first。这是O(n /√(n log n))= O(√(n / log n))个检查点,总共为O(√(n log n))个比特。通过从检查点重新启动循环,我们按需计算is_first的每个O(√(n log n))位部分。

(PS:may or may not be my fault问题陈述要求O(1)空间。我真诚地道歉,如果是我拉了Fermat并建议我解决问题多了比我想象的要难。)

答案 3 :(得分:2)

如果您的算法确实在所有情况下都有效(请参阅我对您的问题的评论,注意对其进行一些更正),请注意前缀数组是您的常量内存目标的唯一障碍。

检查find函数显示该数组可以用两个整数替换,从而消除了对输入长度的依赖并解决了问题。请考虑以下事项:

  • 您只依赖find函数中前缀数组中的两个值。这些是a[start - 1]a[end]。是的,startend会发生变化,但这是否值得数组呢?
  • 查看循环的进展情况。最后,start会增加,或end只会减少
  • 考虑前面的陈述,如果您要用整数替换a[start - 1]的值,您将如何更新其值?换句话说,对于更改start值的循环中的每个转换,您可以做些什么来相应地更新整数以反映a[start - 1]的新值?
  • 是否可以使用a[end]
  • 重复此过程
  • 事实上,如果a[start - 1]a[end]的值可以用两个整数反映,那么整个前缀数组是否不再用于某个目的?因此不能将其删除吗?

不需要前缀数组和删除输入长度的所有存储依赖关系,您的算法将使用恒定的内存量来实现其目标,从而使其成为O(n)时间和O(1)空间

我希望你根据上面的见解自己解决这个问题,因为这是作业。不过,我在下面提供了一个解决方案供参考:

#include <iostream>
using namespace std;

void find( int *data, int &start, int &end )
{
    // reflects the prefix sum until start - 1
    int sumStart = 0;

    // reflects the prefix sum until end
    int sumEnd = 0;
    for( int i = start; i <= end; i++ )
        sumEnd += data[i];

    while( start < end )
    {
        int length = end - start + 1;
        int sum = 2 * ( sumEnd - sumStart );

        if( sum == length )
            break;
        else if( sum < length )
        {
            // sum needs to increase; get rid of the lower endpoint
            if( data[ start ] == 0 && data[ end ] == 1 )
            {
                // sumStart must be updated to reflect the new prefix sum
                sumStart += data[ start ];
                start++;
            }
            else
            {
                // sumEnd must be updated to reflect the new prefix sum
                sumEnd -= data[ end ];
                end--;
            }
        }
        else
        {
            // sum needs to decrease; get rid of the higher endpoint
            if( data[ start ] == 1 && data[ end ] == 0 )
            {
                // sumStart must be updated to reflect the new prefix sum
                sumStart += data[ start ];
                start++;
            }
            else
            {
                // sumEnd must be updated to reflect the new prefix sum
                sumEnd -= data[ end ];
                end--;
            }
        }
    }
}

int main() {
    int length;
    cin >> length;

    // get the data
    int data[length];
    for( int i = 0; i < length; i++ )
        cin >> data[i];

    // solve and print the solution
    int start = 0, end = length - 1;
    find( data, start, end );

    if( start == end )
        puts( "No soln" );
    else
        printf( "%d %d\n", start, end );

    return 0;
}

答案 4 :(得分:1)

该算法是O(n)时间和O(1)空间。它可以修改源数组,但它会恢复所有信息。所以它不适用于const数组。如果这个谜题有几个解决方案,那么这个算法会选择离阵列最近的解。或者可以对其进行修改以提供所有解决方案。

<强>算法

变量:

  • p1 - 子阵列开始
  • p2 - 子阵列结束
  • d - 子阵列中1和0的差异

    1. 计算d,如果d==0,则停止。如果d<0,则反转数组,并在找到平衡子阵列后将其反转。
    2. d > 0提前p2:如果数组元素为1,则只递减p2d。否则p2应该传递11*0形式的子数组,其中*是一个平衡的子数组。要使回溯成为可能,11*0?将更改为0?*00(其中?是子数组旁边的值)。然后d递减。
    3. 存储p1p2
    4. Backtrack p2:如果数组元素为1,则只需递增p2。否则我们找到了元素,在第2步中更改了。还原更改并传递11*0形式的子数组。
    5. 提前p1:如果数组元素为1,则只需递增p1。否则p1应该传递0*11
    6. 形式的子数组
    7. 如果p1有所改进,请存储p2p2 - p1
    8. 如果p2位于数组的末尾,请停止。否则继续执行第4步。

enter image description here

如何运作

算法迭代输入数组中平衡子阵列的所有可能位置。对于每个子阵列位置p1p2尽可能保持彼此远离,提供本地最长的子阵列。在所有这些子阵列之间选择具有最大长度的子阵列。

要确定p1的下一个最佳位置,它会前进到第一个位置,其中1和0之间的平衡值会更改为1。 (第5步)。

要确定p2的下一个最佳位置,它会前进到最后一个位置,其中1和0之间的平衡值会更改为1。为了使其成为可能,步骤2检测所有这些位置(从数组的末尾开始)并以这种方式修改数组,以便可以使用线性搜索迭代这些位置。 (第4步)。

执行步骤2时,可能会遇到两种可能的情况。简单的一个:当找到值'1'时;指针p2刚刚进入下一个值,无需特殊处理。但是当找到值“0”时,平衡方向错误,需要通过几位直到找到正确的平衡。所有这些位对算法都不感兴趣,停止p2会产生一个平衡的子阵列,它太短,或者是一个不平衡的子阵列。因此,p2应该传递11*0形式的子阵列(从右到左,*表示任何平衡的子阵列)。在其他方向上没有机会走同样的道路。但是可以临时使用模式11*0中的一些位来允许回溯。如果我们将第一个'1'更改为'0',将第二个'1'更改为最右边的'0'旁边的值,并清除最右边的'0'旁边的值:11*0? -> 0?*00,那么我们就有可能to(first)注意返回途中的模式,因为它以'0'开头,而(second)找到p2的下一个好位置。

C ++代码:

#include <cstddef>
#include <bitset>

static const size_t N = 270;

void findLargestBalanced(std::bitset<N>& a, size_t& p1s, size_t& p2s)
{
    // Step 1
    size_t p1 = 0;
    size_t p2 = N;
    int d = 2 * a.count() - N;
    bool flip = false;

    if (d == 0) {
        p1s = 0;
        p2s = N;
        return;
    }

    if (d < 0) {
        flip = true;
        d = -d;
        a.flip();
    }

    // Step 2
    bool next = true;
    while (d > 0) {
        if (p2 < N) {
            next = a[p2];
        }

        --d;
        --p2;

        if (a[p2] == false) {
            if (p2+1 < N) {
                a[p2+1] = false;
            }

            int dd = 2;
            while (dd > 0) {
                dd += (a[--p2]? -1: 1);
            }

            a[p2+1] = next;
            a[p2] = false;
        }
    }

    // Step 3
    p2s = p2;
    p1s = p1;

    do {
        // Step 4
        if (a[p2] == false) {
            a[p2++] = true;
            bool nextToRestore = a[p2];
            a[p2++] = true;

            int dd = 2;
            while (dd > 0 && p2 < N) {
                dd += (a[p2++]? 1: -1);
            }

            if (dd == 0) {
                a[--p2] = nextToRestore;
            }
        }
        else {
            ++p2;
        }

        // Step 5
        if (a[p1++] == false) {
            int dd = 2;
            while (dd > 0) {
                dd += (a[p1++]? -1: 1);
            }
        }

        // Step 6
        if (p2 - p1 > p2s - p1s) {
            p2s = p2;
            p1s = p1;
        }
    } while (p2 < N);

    if (flip) {
        a.flip();
    }
}

答案 5 :(得分:0)

对数组中的所有元素求和,然后diff =(array.length - sum)将是0和1的差异。

  1. 如果diff等于array.length / 2,则最大子数组= array。
  2. 如果diff小于array.length / 2那么有1比0更多。
  3. 如果diff大于array.length / 2,那么0比1更多。
  4. 对于案例2&amp; 3,初始化两个指针,启动&amp;结束指向数组的开头和结尾。如果我们有更多1,则根据array [start] = 1或array [end] = 1向内移动指针(start ++或end--),并相应地更新sum。在每一步检查sum =(end - start)/ 2.如果此条件为真,则start和end表示最大子数组的范围。

    这里我们最终做了两次数组传递,一次计算总和,一次向内移动指针。我们正在使用常量空间,因为我们只需要存储sum和两个索引值。

    如果有人想要敲一些伪代码,那么你会受到欢迎:)

答案 6 :(得分:0)

这是一个看起来像是缩放O(n)的动作脚本解决方案。虽然它可能更像是O(n log n)。它绝对只使用O(1)内存。

警告我还没有检查它是多么完整。我可能会错过一些案例。

protected function findLongest(array:Array, start:int = 0, end:int = -1):int {
    if (end < start) {
        end = array.length-1;
    }

    var startDiff:int = 0;
    var endDiff:int = 0;
    var diff:int = 0;
    var length:int = end-start;
    for (var i:int = 0; i <= length; i++) {
        if (array[i+start] == '1') {
            startDiff++;
        } else {
            startDiff--;
        }

        if (array[end-i] == '1') {
            endDiff++;
        } else {
            endDiff--;
        }

        //We can stop when there's no chance of equalizing anymore.
        if (Math.abs(startDiff) > length - i) {
            diff = endDiff;
            start = end - i;
            break;
        } else if (Math.abs(endDiff) > length - i) {
            diff = startDiff;
            end = i+start;
            break;
        }
    }

    var bit:String = diff > 0 ? '1': '0';
    var diffAdjustment:int = diff > 0 ? -1: 1;

    //Strip off the bad vars off the ends.
    while (diff != 0 && array[start] == bit) {
        start++;
        diff += diffAdjustment;
    }

    while(diff != 0 && array[end] == bit) {
        end--;
        diff += diffAdjustment;
    }

    //If we have equalized end. Otherwise recurse within the sub-array.
    if (diff == 0)
        return end-start+1;
    else
        return findLongest(array, start, end);      

}

答案 7 :(得分:0)

我认为以下列方式存在O(1)的算法是不可能的。假设您在每一位上迭代ONCE。这需要一个需要O(log n)空间的计数器。可能有人可能认为n本身是问题实例的一部分,那么你有一个长度为k的二进制字符串的输入长度:k + 2-log k。无论你如何看待它们,你需要一个额外的变量,如果你需要一个索引到那个数组,已经使它成为非O(1)。

通常你没有这个问题,因为你有一个大小为n的问题,输入n个大小的log k,这加起来就是nlog k。这里长度log k的变量只是O(1)。但是这里我们的log k只有1.所以我们只能引入一个长度恒定的帮助变量(我的意思是真的不变,无论n多大都必须限制它。)

这里有一个问题就是可以看到问题的描述。在计算机理论中,你必须非常小心你的编码。例如。如果你切换到一元编码,你可以使NP问题多项式(因为那时输入大小比n-ary(n> 1)编码大指数。

对于n,输入的大小仅为2-log n,必须小心。当你在O(n)的情况下说话时 - 这实际上是一个O(2 ^ n)的算法(这不是我们需要讨论的问题 - 因为人们可以争论n本身是否是描述的一部分或者不是)。

答案 8 :(得分:0)

我有这个算法在O(n)时间和O(1)空间运行。

它利用简单的“缩小然后扩展”技巧。代码中的注释。

public static void longestSubArrayWithSameZerosAndOnes() {
    // You are given an array of 1's and 0's only.
    // Find the longest subarray which contains equal number of 1's and 0's
    int[] A = new int[] {1, 0, 1, 1, 1, 0, 0,0,1};
    int num0 = 0, num1 = 0;

    // First, calculate how many 0s and 1s in the array
    for(int i = 0; i < A.length; i++) {
        if(A[i] == 0) {
            num0++;
        }
        else {
            num1++;
        }
    }
    if(num0 == 0 || num1 == 0) {
        System.out.println("The length of the sub-array is 0");
        return;
    }

    // Second, check the array to find a continuous "block" that has
    // the same number of 0s and 1s, starting from the HEAD and the
    // TAIL of the array, and moving the 2 "pointer" (HEAD and TAIL)
    // towards the CENTER of the array
    int start = 0, end = A.length - 1;
    while(num0 != num1 && start < end) {
        if(num1 > num0) {
            if(A[start] == 1) {
                num1--; start++;
            }
            else if(A[end] == 1) {
                num1--; end--;
            }
            else {
                num0--; start++;
                num0--; end--;
            }
        }
        else if(num1 < num0) {
            if(A[start] == 0) {
                num0--; start++;
            }
            else if(A[end] == 0) {
                num0--; end--;
            }
            else {
                num1--; start++;
                num1--; end--;
            }
        }
    }
    if(num0 == 0 || num1 == 0) {
        start = end;
        end++;
    }

    // Third, expand the continuous "block" just found at step #2 by
    // moving "HEAD" to head of the array and "TAIL" to the end of
    // the array, while still keeping the "block" balanced(containing
    // the same number of 0s and 1s
    while(0 < start && end < A.length - 1) {
        if(A[start - 1] == 0 && A[end + 1] == 0 || A[start - 1] == 1 && A[end + 1] == 1) {
            break;
        }
        start--;
        end++;
    }
    System.out.println("The length of the sub-array is " + (end - start + 1) + ", starting from #" + start + " to #" + end);

}

答案 9 :(得分:-1)

线性时间,恒定空间。如果我错过了任何错误,请告诉我 在python3中测试。

def longestBalancedSubarray(A):
    lo,hi = 0,len(A)-1
    ones = sum(A);zeros = len(A) - ones
    while lo < hi:
        if ones == zeros: break
        else:
            if ones > zeros:
                if A[lo] == 1: lo+=1; ones-=1
                elif A[hi] == 1: hi+=1; ones-=1
                else: lo+=1; zeros -=1
            else:
                if A[lo] == 0: lo+=1; zeros-=1
                elif A[hi] == 0: hi+=1; zeros-=1
                else: lo+=1; ones -=1
    return(A[lo:hi+1])