[英]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),超出输入存储(不计入输入参数所需的存储)。
可以修改输入数组的元素。
这是我在c中的尝试:
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值,您可以在正确的轨道上,而不是在实际需要时从头开始计算它。 有点类似的策略可能适用于将所有计数器设置为当前最大值的问题。 如果您跟踪重置计数器的值,但只有在确实需要时才这样做,该怎么办?
循环嵌套是问题的症状 ,而不是原因。 这里的基本问题是可能存在O ( N
)非平凡的MAX
运算,因此您不能承担超过O ( 1
)的成本超过O ( 1
)。 无论你是否循环,测试或更新所有M
计数器的值都需要花费O ( M
),所以如果你以一种或两种正常情况下的方式实现MAX
操作,那么整体成本是o ( N
* M
)。
假设您在更新计数器时跟踪全局最大值,就像您一样,您可以以不立即处理所有计数器的方式实现MAX
。 相反,您可以记录最近应用MAX
操作的时间,此时的全局最大值,以及MAX
操作最后一次应用于该计数器的每个计数器记录。
在这种情况下,无论何时在特定计数器上执行更新,您都可以检查是否存在先前需要应用的MAX
操作,以及要应用的值。 所有这些都是O ( 1
),因此每次更新的成本仍为O ( 1
)。 MAX
操作本身只需更新两个标量,因此也是O ( 1
)。 因此处理所有指令花费O ( M
)。 最后,您必须通过计数器一次通过以应用任何剩余的未应用的MAX
操作; 对于N
计数器中的每一个,这花费O ( 1
)。 总成本: O ( N
+ M
)。
请注意,这表现出经典的空间与速度之间的权衡。 解决该问题的简单方法具有O ( 1
)内存开销,但在操作数量方面更加渐近复杂。 上面概述的替代解决方案在操作数量上具有更好的渐近复杂性,但是需要O ( N
)存储器开销。
更新:
但正如@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.