【问题标题】:Algorithm: array with odd and even numbers算法:奇偶数数组
【发布时间】:2017-05-07 06:29:53
【问题描述】:

给定一个包含 n 个元素的数组,我想计算数组的最大范围,其中奇数和偶数一样多。

示例

输入:

2 4 1 6 3 0 8 10 4 1 1 4 5 3 6

预期输出:

12

我尝试了什么

我尝试使用以下步骤:

  • 将所有奇数更改为 1,将所有偶数更改为 -1
  • 检查所有可能的子数组,并为每个子数组计算 1 和 -1 值的总和。
  • 取这些子数组中总和为 0 的最大子数组

但这有O(n^2)的时间复杂度。

问题

我怎样才能在 O(n) 中做到这一点?

【问题讨论】:

  • 奇数怎么能等于偶数呢?
  • 奇数个数等于偶数个数...
  • 我对数组进行了转换,使赔率为 1,偶数为 -1,但我想不出办法。
  • 请记住在发布问题时添加您尝试的内容

标签: arrays algorithm time-complexity sub-array


【解决方案1】:

给定:数组a

任务:找到奇数和偶数个数为偶数的最大子数组

解决方案 O(n)

  1. 在java中用-1替换奇数和用1替换偶数时,为每个累积和创建一个最高条目的哈希映射m:

    Map<Integer, Integer> m = new HashMap<>();
    int sum = 0;
    for (int i = 0; i < a.length; i++) {
      sum += a[i] % 2 == 0 ? 1 : -1;
      m.put(sum, i);
    }
    
  2. 通过在java中使用m查找最大距离来找到总和为0的最大子数组:

    int bestStart = -1, bestEnd = -1; // indexes, so end inclusive
    sum = 0;
    for (int i = 0; i < a.length; i++) {
      Integer end = m.get(sum);
      sum += a[i] % 2 == 0 ? 1 : -1;
      if (end != null && end - i > bestEnd - bestStart) {
        bestStart = i;
        bestEnd = end;
      }
    }
    

基于观察,您可以使用 cumSum[y] - cumSum[x - 1] 获得从 x 到 y 的总和(在将元素转换为 1 和 -1 之后)。因此,如果我们希望 this 为 0,那么它们需要相同(如果 x = 0 则 cumSum = 0,则 cumSum[-1] 未定义,请注意)。

【讨论】:

  • 是的,但是有什么方法可以在没有哈希图的情况下完成?
  • 是的,你可以只使用一个数组,你知道总和不能小于-n并且大于n,所以你可以创建一个大小为2n+1的数组并获取索引和+n。
  • 这样就行了!非常感谢!
【解决方案2】:

这是一个常见的动态规划问题。我们可以通过迭代列表并同时更新最优解来维护次优解。它类似于查找数组的最大元素。将第一个元素设置为最大值,并在需要时在每次迭代中更新它。这个问题只需要多一点。

我们需要 5 个指针(整数,实际上是 5 个)。开始指针、结束指针和当前指针,maxend,maxstart。将startcurrent 指针设置为数组的开头。当接下来的元素遵守规则(奇偶交替)时,增加current 指针。一旦他们不遵守规则,将结束指针设置为当前指针。比较end-start指针的差异,如果大于maxend-maxstart则改变maxend和maxstart继续这个操作。 最后,您可以打印 maxstart 和 maxend 之间的数组部分。

【讨论】:

  • 对不起,这个问题有点不清楚,请参阅示例...您的解决方案不起作用,因为奇数和偶数不应该是必要的交替。例如:o o e e (o=odd,e=even) 是满足要求的子数组...
  • 啊哈,我认为他们应该交替进行。有空我会更新答案
【解决方案3】:

使用 1 表示奇数,使用 -1 表示偶数的方法确实是正确的。我们将这些值称为增量(因此包括减量)。

以示例输入为例,您可以将这些增量可视化如下:

  2   5   6   0   8   3   4   5   2   7   4   8   2   6   6   5   7 
 -1   1  -1  -1  -1   1  -1   1  -1   1  -1  -1  -1  -1  -1   1   1   

然后您可以将其下方的这些增量的累积总和可视化:

  2   5   6   0   8   3   4   5   2   7   4   8   2   6   6   5   7 
 -1   1  -1  -1  -1   1  -1   1  -1   1  -1  -1  -1  -1  -1   1   1   
0  -1   0  -1  -2  -3  -2  -3  -2  -3  -2  -3  -4  -5  -6  -7  -6  -5

 \     / \
   \ /     \
             \
               \ 
                 \     / \     / \     / \   
                   \ /     \ /     \ /     \ 
                                             \
                                               \
                                                 \
                                                   \
                                                     \             /
                                                       \         /
                                                         \     /
                                                           \ /

请注意,具有相同数量的偶数和奇数的范围对应于增量总和为 0 的范围。您可以通过选择开始/结束点来识别此类范围,以便开始之前的累积和为等于该范围结束后的累积和。这就像在上面的“图形”中画一条水平线并取最远的交叉点。

例如,总和中第一次出现两个 -1 值表示带有[5, 6] 的子数组是有效范围(其中奇数和偶数的数量相等)。寻找其他这样的范围,我们可以发现取最左边和最右边的 -3 会产生更大的结果:[3 4 5 2 7 4]。我们也可以将 -2 作为边界值:[8 3 4 5 2 7]

我们还可以看到,最长范围必须对应于介于 0 和 end-sum 之间的总和(示例中为 -5)。以不在此范围内的为例:示例中的 -6。由于 0 和 -5 都在 -6 的同一侧,我们确信使用 -5 可以获得更好的结果(在图中将水平线向上移动)。对于所有超出 0 范围和最终总和的中间总和值都是如此。

可以得出的另一个结论是,总是有可能找到左端点与方向变化对应的最优解。在示例中,第一个点的总和为 -3。

算法

您可以创建一个递归算法,只要找到符合上述规则的点就会递归:

  • 添加起点值之前的总和在0和最终总和的范围内
  • 起始位置的增量与之前的不同(或之前没有),并且与从开始到结束的整体方向相反。这对应于上图中的一个山谷,但当最终总和为正时,它会是一个山顶。

当总和等于最终总和时,递归停止。这会立即产生大小。这个大小返回给调用者(递归树中的上一级),并且数组的末尾被缩短,直到最终的总和等于在该递归级别上查看的总和。这再次导致大小。这两种尺寸中最好的一个返回给调用者,......等等。

算法中不创建数组,递归生成的调用堆栈除外。但如果数组是完全随机的,那么递归调用的平均次数应该很少,因为总和在统计上的期望值为 0。

时间复杂度是O(n),因为总和的计算显然是O(n),另外两个循环要么移动起点,要么移动终点数组的一个方向的结束索引,不再访问相同的元素。

JavaScript 实现

此代码使用最简单的 JavaScript 语法,因此算法清晰:

function value(x) {
    // Return 1 when the given value is odd, else -1
    return (x % 2) || -1;
}

function largestRange(a) {
    var sign, sumEnd, end;

    // Calculate final sum
    sumEnd = 0;
    for (end = 0; end < a.length; end++) {
        sumEnd = sumEnd + value(a[end]);
    }
    // ... and its sign (1 or -1 or 0)
    sign = Math.sign(sumEnd);

    function recurse(start, sumStart) {
        var sum, i, size, val;
        // End of recursion:
        if (sumStart === sumEnd) return end - start;
        sum = sumStart
        for (i = start; sum !== sumEnd; i++) {
            val = value(a[i]);
            // Got closer to sumEnd, and now moving away from it
            if (val !== sign && Math.sign(sum - sumStart) == sign) break;
            sum = sum + val;
        }
        // Get longest range size for this particular sum
        size = recurse(i, sum);
        // Get range size for sumStart
        while (sumEnd !== sumStart) {
            end--;
            sumEnd = sumEnd - value(a[end]);
        }
        // Retain the best of both:
        if (end - start > size) size = end - start;
        return size;
    }
    // Initiate the recursion and return result
    return recurse(0, 0);
}

// Sample input
var a = [2, 5, 6, 0, 8, 3, 4, 5, 2, 7, 4, 8, 6, 6, 5, 7];
// Calculate
var size = largestRange(a); 
// Output size of longest range
console.log(size);

性能

由于随机数组的预期总和预计接近 0,因此大多数中间总和值有可能超出 0 和最终总和的范围,这意味着它们不会占用太多时间和空间。

我做了一些性能测试,将它与为每个遇到的总和创建带有键的哈希的解决方案进行比较。对于较短的输入数组,这些算法更快,但对于较大的数组(如 1000 个条目),上述算法表现更好。当然,可以调整基于散列的解决方案以考虑我上面确定的规则,然后它对于更大的数组也会表现得更好。递归带来了一些哈希映射没有的开销。

但是当您对 cme​​ts 表示有兴趣看到没有哈希映射的解决方案时,我选择了这个解决方案。

【讨论】:

    猜你喜欢
    • 2021-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-10
    • 2020-12-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多