繁体   English   中英

减少渐近时间复杂度

[英]Decreasing asymptotic time complexity

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

我正在练习我的算法技巧。 目前,我正试图找到解决这个问题的方法

你有N个计数器,最初设置为0,你有两个可能的操作:

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

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

  • 如果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和由M个整数组成的非空零索引数组A的情况下,返回表示计数器值的整数序列。

序列应返回为:

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

例如,给定:

 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操作发生了多少次,并以某种方式找出将它加到计数器的正确方法,但我无法实现它。

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

有什么想法吗?

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

循环嵌套是问题的症状 ,而不是原因。 这里的基本问题是可能存在ON )非平凡的MAX运算,因此您不能承担超过O1 )的成本超过O1 )。 无论你是否循环,测试或更新所有M计数器的值都需要花费OM ),所以如果你以一种或两种正常情况下的方式实现MAX操作,那么整体成本是oN * M )。

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

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

请注意,这表现出经典的空间与速度之间的权衡。 解决该问题的简单方法具有O1 )内存开销,但在操作数量方面更加渐近复杂。 上面概述的替代解决方案在操作数量上具有更好的渐近复杂性,但是需要ON )存储器开销。

更新:

但正如@Quinchilion正确地观察到的那样,你可以做得更好。 考虑每个计数器的正确当前值是最后一个MAX操作设置的值加上自上一个MAX以来在该计数器上执行的增量数。 没有计数器的价值降低。 因此,我们不需要明确地跟踪MAX操作的时间 - 每个计数器记录的最新值固有地指示是否仍然需要应用最后一个MAX 如果它小于最新MAX时记录的最大计数器值,则必须在增量之前应用该MAX ; 否则,不是。 这可以与上述方法结合以消除对辅助阵列的需要。

我非常感谢@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;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM