如何添加,将n个数字乘以数组中的给定范围,并在O(n)时间内反转数组中的范围?

时间:2014-01-11 04:33:09

标签: java c++ c arrays algorithm

假设我们有一个数组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。 无论如何,在这个算法中的任何优化(比如删除循环以仅添加和乘法)都会受到欢迎。

1 个答案:

答案 0 :(得分:2)

这个答案很长,但是我已经用相当多的解释以一种相当容易理解的方式写了它,所以请暂时忍受我。

假设O(N)A都是整数,可以在B时间内解决此问题。否则你可以在此时停止阅读。但我认为有必要将算法分解为几个不同的步骤,每个步骤都是O(N)。所以整体仍然是O(N)

问题中最困难的部分可能是弄清楚如何在线性时间内完成此步骤:

    if ith letter of S is 'R'
        reverse L[i...N]

如果我们只是继续盯着原始算法,我们将确信即使其他步骤可以在线性时间内实现,这一步也可以从不在线性时间内完成。但事实并非如此。我们该怎么做呢?我想到的方法是从双端队列/双端队列数据结构中借用一个想法。由于我们知道数组L有多长,我们只维护3个变量,leftmostrightmostisReversed

  • leftmost将保留当前最左侧未使用的L数组索引,因此leftmost初始化为1,因为我们正在使用一个索引数组,如您所述问题(术语'未使用'将在后面解释)。
  • rightmost将保留L数组当前最右侧未使用的索引,因此初始化为N L的长度。
  • isReversed用于指示阵列是否正在反转。这已初始化为false

我们的第一项任务是在应用了所有L操作后计算出数组reverse的原始元素的最终顺序。我们甚至不需要反转阵列一次以达到与反转相同的效果。这可以通过遍历输入字符串S一次,并在所有反向操作之后确定数组L的哪个元素应该位于每个位置来完成。为简单起见,我们创建了一个新的整数数组L',在应用所有反向操作后将保留L的最终原始元素,并尝试填写L'

假设我们处于索引iS[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个额外的变量,让他们称之为multiplicativeFactoradditiveConstantmultiplicativeFactor用于保存我们需要乘以L'[i]的常量。这最初是1。顾名思义,additiveConstant变量用于存储L'[i]乘以L'[i]后我们需要添加到multiplicativeFactor的任何常量。 additiveConstant已初始化为0

要以更具体的方式查看此内容,请设置A = 3B = 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} multiplicativeConstantadditiveConstant整数溢出,以及中间计算。如果Lint数组,我们很幸运,因为我们可以使用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;
}

这解决了multiplicativeConstantadditiveConstant变量的溢出问题(但不会阻止中间计算和其他变量的溢出),并完成我们的算法。我相信这是正确的,但请亲自验证。我无法解释模块化算术的东西,因为我只知道如何使用它,所以你必须自己查找。在旁注中,A % CB % 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来计算所涉及的所有变量,你应该是细