假设我们有一个数组L [] = {4,1,2,6}。我们需要做的是将一个由字符A,R,M组成的字符串S作为输入,并应用以下算法:
for i from 1 to N do
if ith letter of S is 'R'
reverse L[i...N]
else if ith letter of S is 'A'
add A to all numbers of L[i..N].
else if ith letter of S is 'M'
multiply B to all numbers of L[i..N].
for all number in L[i..N], module them by C.
print L[i]
end
如何优化此算法以使其在O(n)时间内运行?数组的长度以及字符串是n。 无论如何,在这个算法中的任何优化(比如删除循环以仅添加和乘法)都会受到欢迎。
答案 0 :(得分:2)
这个答案很长,但是我已经用相当多的解释以一种相当容易理解的方式写了它,所以请暂时忍受我。
假设O(N)
和A
都是整数,可以在B
时间内解决此问题。否则你可以在此时停止阅读。但我认为有必要将算法分解为几个不同的步骤,每个步骤都是O(N)
。所以整体仍然是O(N)
。
问题中最困难的部分可能是弄清楚如何在线性时间内完成此步骤:
if ith letter of S is 'R'
reverse L[i...N]
如果我们只是继续盯着原始算法,我们将确信即使其他步骤可以在线性时间内实现,这一步也可以从不在线性时间内完成。但事实并非如此。我们该怎么做呢?我想到的方法是从双端队列/双端队列数据结构中借用一个想法。由于我们知道数组L
有多长,我们只维护3个变量,leftmost
,rightmost
和isReversed
。
leftmost
将保留当前最左侧未使用的L
数组索引,因此leftmost
初始化为1
,因为我们正在使用一个索引数组,如您所述问题(术语'未使用'将在后面解释)。rightmost
将保留L
数组当前最右侧未使用的索引,因此初始化为N
L
的长度。isReversed
用于指示阵列是否正在反转。这已初始化为false
。我们的第一项任务是在应用了所有L
操作后计算出数组reverse
的原始元素的最终顺序。我们甚至不需要反转阵列一次以达到与反转相同的效果。这可以通过遍历输入字符串S
一次,并在所有反向操作之后确定数组L
的哪个元素应该位于每个位置来完成。为简单起见,我们创建了一个新的整数数组L'
,在应用所有反向操作后将保留L
的最终原始元素,并尝试填写L'
。
假设我们处于索引i
和S[i] == 'R'
,因此我们设置isReversed = true
表示我们正在撤消子阵列[i..N]
。当isReversed == true
时,我们知道子数组[i..N]
正在被反转,因此L'[i]
处的元素应该是最右边未使用的元素,其索引为rightmost
。因此,我们按L'[i] = L[rightmost]
(rightmost
)设置1
和减少 rightmost = rightmost - 1
。相反,如果isReversed == false
我们没有反转子数组[i..N]
,那么L'[i]
处的元素应该是最左边的未使用元素,其索引为leftmost
。因此,我们按L'[i] = L[leftmost]
(leftmost
)设置1
和增量 leftmost = leftmost - 1
。后续reverse
将否定isReversed
的价值。
因此,当前的算法在C ++中看起来像这样(我假设你对C ++没问题,因为你的一个问题标签是C ++):
// set up new array L'
int Lprime[N+1];
int leftmost = 1;
int rightmost = N;
bool isReversed = false;
for (int i = 1; i <= N; i++) {
if (S[i] == 'R') {
// negate isReversed
isReversed = !isReversed;
}
if (isReversed) {
Lprime[i] = L[rightmost];
rightmost = rightmost - 1;
} else {
Lprime[i] = L[leftmost];
leftmost = leftmost + 1;
}
}
请确认这是正确的,尽管我认为是这样。
现在我们来看看原始算法的其余部分:
else if ith letter of S is 'A'
add A to all numbers of L[i..N].
else if ith letter of S is 'M'
multiply B to all numbers of L[i..N].
for all number in L[i..N], module them by C.
对于每个索引C
,似乎需要在子代[i..N]
上按i
执行模数。但基于我有限的理解,这是模运算,我们并不需要在每个[i..N]
的子阵列i
上执行它。但是不要相信我的话。我对数论的理解非常简陋。
不仅如此,添加和乘法的步骤也可以简化。这里的诀窍是保留2个额外的变量,让他们称之为multiplicativeFactor
和additiveConstant
。 multiplicativeFactor
用于保存我们需要乘以L'[i]
的常量。这最初是1
。顾名思义,additiveConstant
变量用于存储L'[i]
乘以L'[i]
后我们需要添加到multiplicativeFactor
的任何常量。 additiveConstant
已初始化为0
。
要以更具体的方式查看此内容,请设置A = 3
,B = 5
。假设S
是字符串"AMMAAM"
。这意味着以下内容(注意:我们暂时忽略模C
):
1
处设置L'[1] = L'[1] + 3;
2
处设置L'[2] = (L'[2] + 3) * 5;
3
处设置L'[3] = ((L'[3] + 3) * 5) * 5;
4
处设置L'[4] = (((L'[4] + 3) * 5) * 5) + 3;
5
处设置L'[5] = ((((L'[5] + 3) * 5) * 5) + 3) + 3
6
处设置L'[6] = (((((L'[6] + 3) * 5) * 5) + 3) + 3) * 5
观察先前字符'A'
和'M'
&#34;的影响结转&#34; (或级联)到L'
的未来元素。让我们稍微改变一下这些操作:
L'[1] = L'[1] + 3
L'[2] = 5 * L'[2] + (3 * 5)
L'[3] = 5 * 5 * L'[3] + (3 * 5 * 5)
L'[4] = 5 * 5 * L'[4] + (3 * 5 * 5 + 3)
L'[5] = 5 * 5 * L'[5] + (3 * 5 * 5 + 3 + 3)
L'[6] = 5 * 5 * 5 * L'[6] + (3 * 5 * 5 + 3 + 3) * 5
我们开始看到一些模式。
L'[i]
的乘法因子始终是B
的幂。添加A
对此乘法因素没有任何影响。乘法因子存储在我们上面描述的multiplicativeConstant
变量中L'[i]
乘以额外的B
时,需要将A
乘以B
所得的所有常数相乘以获得要添加到L'[i]
的最终常量。这是上述additiveConstant
变量的目的。L'[i]
添加到additiveConstant
之前完成L'[i]
的乘法因此,每个L'[i]
的最终值可以表示为multiplicativeConstant * L'[i] + additiveConstant;
,算法的第二个主要部分如下所示:
int multiplicativeConstant = 1;
int additiveConstant = 0;
for (int i = 1; i <= N; i++) {
if (S[i] == 'A') {
additiveConstant += A;
} else if (S[i] == 'M') {
multiplicativeConstant *= B;
// need to multiply all the constants by B as well
additiveConstant *= B;
}
Lprime[i] = multiplicativeConstant * Lprime[i] + additiveConstant;
}
有一点需要注意,我没有谈过。 {strong} multiplicativeConstant
和additiveConstant
的整数溢出,以及中间计算。如果L
是int
数组,我们很幸运,因为我们可以使用long long
来避免溢出。否则,我们必须小心中间计算不会溢出。
那么modulo C
操作呢?实际上,他们会将L'[i]
中的每个值保持在[0..C-1]
范围内。基于我对数论的有限理解,我们可以像这样执行模运算来达到同样的效果:
int multiplicativeConstant = 1;
int additiveConstant = 0;
for (int i = 1; i <= N; i++) {
if (S[i] == 'A') {
additiveConstant = (additiveConstant + (A % C)) % C;
} else if (S[i] == 'M') {
multiplicativeConstant = (multiplicativeConstant * (B % C)) % C;
// need to multiply all the constants by B as well
additiveConstant = (additiveConstant * (B % C)) % C;
}
Lprime[i] = ((multiplicativeConstant * (Lprime[i] % C)) % C + additiveConstant) % C;
}
这解决了multiplicativeConstant
和additiveConstant
变量的溢出问题(但不会阻止中间计算和其他变量的溢出),并完成我们的算法。我相信这是正确的,但请亲自验证。我无法解释模块化算术的东西,因为我只知道如何使用它,所以你必须自己查找。在旁注中,A % C
和B % C
部分可以完成一次,其结果存储在变量中。
最后,把所有东西放在一起:
// set up new array L'
int Lprime[N+1];
int leftmost = 1;
int rightmost = N;
bool isReversed = false;
for (int i = 1; i <= N; i++) {
if (S[i] == 'R') {
// negate isReversed
isReversed = !isReversed;
}
if (isReversed) {
Lprime[i] = L[rightmost];
rightmost = rightmost - 1;
} else {
Lprime[i] = L[leftmost];
leftmost = leftmost - 1;
}
}
int multiplicativeConstant = 1;
int additiveConstant = 0;
// factor out A % C and B % C
int aModC = A % C;
int bModC = B % C;
for (int i = 1; i <= N; i++) {
if (S[i] == 'A') {
additiveConstant = (additiveConstant + aModC) % C;
} else if (S[i] == 'M') {
multiplicativeConstant = (multiplicativeConstant * bModC) % C;
// need to multiply all the constants by B as well
additiveConstant = (additiveConstant * bModC) % C;
}
Lprime[i] = ((multiplicativeConstant * (Lprime[i] % C)) % C + additiveConstant) % C;
}
// print Lprime
总体上O(N)
时间运行。
再一次,如果你担心整数溢出,假设L
是一个int
数组,你可以使用long long
来计算所涉及的所有变量,你应该是细