减少渐近时间复杂度

时间:2016-06-07 21:10:00

标签: c algorithm performance time-complexity

首先,对于相当广泛的问题感到抱歉。

我正在练习我的算法技巧。目前,我正在尝试找到this problem的解决方案。

  

你有N个计数器,最初设置为0,你有两个   他们可能采取的行动:

     
      
  • 增加(X) - 计数器X增加1,
  •   
  • 最大计数器 - 所有计数器都设置为任何计数器的最大值。
  •   
     

给出了M个整数的非空零索引数组A.这个   array表示连续操作:

     
      
  • 如果A [K] = X,使得1≤X≤N,则操作K增加(X),
  •   
  • 如果A [K] = N + 1,则操作K为最大计数器。
  •   
     

例如,给定整数N = 5且数组A使得:

A[0] = 3
A[1] = 4
A[2] = 4
A[3] = 6
A[4] = 1
A[5] = 4
A[6] = 4
     

每次连续操作后计数器的值为:

(0, 0, 1, 0, 0)
(0, 0, 1, 1, 0)
(0, 0, 1, 2, 0)
(2, 2, 2, 2, 2)
(3, 2, 2, 2, 2)
(3, 2, 2, 3, 2)
(3, 2, 2, 4, 2) 
     

目标是在所有操作之后计算每个计数器的值。

     

假设给出以下声明:

struct Results {
  int * C;
  int L;
};
     

写一个函数:

struct Results solution(int N, int A[], int M);
     

给定整数N和非空零索引数组A.   由M个整数组成,返回一个表示整数的序列   计数器的值。

     

序列应返回为:

     
      
  • 结构结果(在C中)或
  •   
  • 整数向量(在C ++中)或
  •   
  • 记录结果(以Pascal表示)或
  •   
  • 整数数组(使用任何其他编程语言)。
  •   
     

例如,给定:

A[0] = 3
A[1] = 4
A[2] = 4
A[3] = 6
A[4] = 1
A[5] = 4
A[6] = 4
     

该函数应返回[3, 2, 2, 4, 2],如上所述。

     

假设:

     

N和M是[1..100,000]范围内的整数;每个元素   数组A是[1..N + 1]范围内的整数。

     

复杂度:

     
      
  • 预期的最坏情况时间复杂度为O(N + M);
  •   
  • 预期的最坏情况空间复杂度为O(N),超出输入存储(不计入输入参数所需的存储空间)。
  •   
     

可以修改输入数组的元素。

以下是我在中的尝试:

struct Results solution(int N, int A[], int M) {
    struct Results result;
    int i, j,
        maxCounter = 0;

    result.C = calloc(N, sizeof(int));

    result.L = N;

    for (i = 0; i < M; i++) {
        if (A[i] <= N) {
            result.C[A[i] - 1]++;
            if (result.C[A[i] - 1] > maxCounter) {
                maxCounter = result.C[A[i] - 1];
            }
        } else {
            for (j = 0; j < N; j++) {
                result.C[j] = maxCounter;
            }
        }
    }

    return result;
}

解决方案可以正常工作,但在某些性能测试中失败了。问题是由于嵌套循环,此算法为O(N*M)。问题表明最坏情况下的复杂性应该是O(N+M)

我有一些关于如何不需要循环的想法,比如存储max counters操作发生了多少次,并以某种方式找出将它加到计数器上的正确方法,但我无法实现它。 / p>

另一种可能性是找到一种方法,使用像struct.C之类的东西一次设置memset中的所有元素,但我认为这会是作弊。

有什么想法吗?

3 个答案:

答案 0 :(得分:3)

您可以随时更新maxCounter值,而不是在实际需要时从头开始计算。有点类似的策略可能适用于将所有计数器设置为当前最大值的问题。如果您跟踪重置计数器的值,但只有在真正需要时才这样做,该怎么办?

答案 1 :(得分:3)

循环嵌套是问题的症状,而不是原因。这里的基本问题是可能存在 O N)非平凡MAX操作,因此您无法承担超过 O 1)他们的费用超过 O 1)。无论您是否循环,测试或更新所有M计数器的值都会花费 O M),因此如果您在一个MAX中执行N操作在正常情况下需要执行其中一个或两个的方式,然后总成本为 o M * MAX)。

假设您在更新计数器时跟踪全局最大值,就像您一样,您可以以不立即处理所有计数器的方式实现MAX。相反,您可以记录最近应用MAX操作的时间,当时的全局最大值,以及上次应用MAX操作时的每个计数器记录到那个柜台

在这种情况下,每当您对特定计数器执行更新时,您都可以检查是否存在先需要应用的先前1操作以及要应用的值。所有这些都是 O 1),因此每次更新的费用仍为 O MAX)。 1操作本身只需要更新两个标量,因此 O M)也是如此。因此,处理所有指令需要花费 O MAX)。最后,您必须通过计数器一次以应用任何剩余的未应用1操作;每个N计数器的成本 O N)。总费用: O M + 1)。

请注意,这表现出经典的空间与速度之间的权衡。这个问题的简单方法有 O N)内存开销,但在操作次数方面更为渐近复杂。上面提到的替代解决方案在操作数量上具有更好的渐近复杂性,但需要 O MAX)内存开销。

<强>更新

但正如@Quinchilion正确观察到的那样,你可以做得更好。考虑每个计数器的正确当前值是上一个MAX操作设置的值加上自上一个MAX以来在该计数器上执行的增量数。没有计数器的价值下降。因此,我们不需要明确跟踪MAX操作的时间 - 每个计数器记录的最新值固有地指示是否仍然需要应用最后一个MAX。如果它小于最新MAX时记录的最大计数器值,则必须在增量之前应用@Override public boolean onTouchEvent(MotionEvent event) { if(isTouchable) { int maskedAction = event.getActionMasked(); if (maskedAction == MotionEvent.ACTION_DOWN) { this.setTextColor(resources.getColor(R.color.octane_orange)); initialClick = event.getX(); } else if (maskedAction == MotionEvent.ACTION_UP) { this.setTextColor(defaultTextColor); endingClick = event.getX(); checkIfSwipeOrClick(initialClick, endingClick, range); } else if(maskedAction == MotionEvent.ACTION_CANCEL) this.setTextColor(defaultTextColor); } return true; } ;否则,不是。这可以与上述方法结合使用,以消除对辅助阵列的需求。

答案 2 :(得分:1)

我非常感谢@Quinchilion和@JohnBollinger对这个问题的全面分析。

我找到了一个通过正确性和性能测试的答案。在这里,完全评论:

struct Results solution(int N, int A[], int M) {
    struct Results result;

    int i,
        maxCounter = 0,
        lastMaxCounter = 0;

    result.C = calloc(N, sizeof(int));

    result.L = N;

    /* One way or another, We have to iterate over all the counter  
     * operations input array, which gives us an O(M) time...
     */
    for (i = 0; i < M; i++) {
        /* There is a little gotcha here. The counter operations
         * input array is 1-based, while our native C arrays are 0-based.
         * So, in order check if we have a `increment` or a `max counters`
         * operation, we have to consider the interval ]0, N].
         */
        if (A[i] > 0 && A[i] <= N) {
            /* This is an `increment` operation.
             *
             * First we need to check if there is no pending `max counter`
             * operation in this very counter.
             * This is done by checking if the value of current counter is
             * **less than** the value of `lastMaxCounter`.
             */
            if (result.C[A[i] - 1] < lastMaxCounter) {
                /* If it is, means that we have to discard the counter's
                 * current value and increment it from the value of
                 * `lastMaxCounter`.
                 */
                result.C[A[i] - 1] = lastMaxCounter + 1;
            } else {
                /* If it ain't, we just increment this counter's value.
                 */
                result.C[A[i] - 1]++;
            }

            /* We also want to keep track of the maximum counter value.
             */
            if (result.C[A[i] - 1] > maxCounter) {
                maxCounter = result.C[A[i] - 1];
            }
        } else {
            /* This is a `max counter` operation.
             * 
             * What we need to do is buffer the current `maxCounter`
             * in order to be able to update the counters later.
             */
            lastMaxCounter = maxCounter;
        }
    }

    /* At this point, if all counters have been incremented at least 
     * once after the last `max counter` operation, we are good to go.
     *
     * Since this is a rather pretentious assumption, we need to
     * double check it.
     *
     * We iterate over all counters, checking if any of them is lower
     * than the buffered `lastMaxCounter`. If it is, it means that no
     * `increment` was performed on this counter after the last
     * `max counter`, so this means that its value should be equal
     * to `lastMaxCounter`.
     *
     * This is an O(N) operation.
     *
     * So, the algorithm's time complexity is O(N) + O(M) = O(N+M)
     * and the space complexity is O(N).
     */
    for (i = 0; i < N; i++) {
        if (result.C[i] < lastMaxCounter) {
            result.C[i] = lastMaxCounter;
        }
    }

    return result;
}