【问题标题】:How to add, multiply n numbers to a given range in array and also reverse the range in the array in O(n) time?如何将 n 个数字添加到数组中的给定范围,并在 O(n) 时间内反转数组中的范围?
【发布时间】:2014-01-11 04:33:08
【问题描述】:

假设我们有一个数组 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。 无论如何,欢迎对该算法中的任何优化(例如删除仅加法和乘法的循环)。

【问题讨论】:

  • module them?你是说modulo
  • 是的。 Module 表示取模
  • 你确定这可以在 O(n) 时间内运行吗?
  • 至少建议一些方法来删除加法和乘法的循环。
  • 可以认为是独立循环

标签: java c++ c arrays algorithm


【解决方案1】:

这个答案很长,但恕我直言,我以一种相当容易理解的方式写了它,并做了很多解释,所以请耐心等待。

假设AB 都是整数,则可以在O(N) 时间内解决这个问题。否则,您可以在此时停止阅读。但我认为有必要将算法分解成几个不同的步骤,每个步骤都是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

我们手头的第一个任务是在应用了所有reverse 操作之后,找出数组L 的原始元素的最终顺序。我们甚至不需要将数组反转一次即可达到与反转相同的效果。这可以通过遍历输入字符串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.

困难的部分似乎是需要为每个索引i 对子数组[i..N] 执行C 的模运算。但根据我有限的理解,这是模运算,我们并不需要对每个i 的子数组[i..N] 执行它。但不要相信我的话。我对数论的理解非常初级。

不仅如此,加法和乘法的步骤也可以简化。这里的诀窍是维护 2 个额外的变量,我们称它们为 multiplicativeFactoradditiveConstantmultiplicativeFactor 用于保存我们需要乘以L'[i] 的常数。这最初是1。顾名思义,additiveConstant 变量用于存储在完成L'[i] 乘以multiplicativeFactor 之后需要添加到L'[i] 的任何常量。 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'“继承”(或级联)到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 以获得要添加到@987654422 的最终常量@。这就是上述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;
}

有一个警告我没有谈到。 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 用于计算中涉及的所有变量,应该没问题。

【讨论】:

  • 很好的答案.. 我喜欢解释遇到的事情的方式。我不担心整数溢出,因为我在 java 中使用 BigInteger
  • 哈哈感谢SuperCoder!但请注意,Java BigInteger 中的操作可能不会在恒定时间内运行,因此整个算法可能不会在线性时间内运行
猜你喜欢
  • 1970-01-01
  • 2013-02-23
  • 2023-03-09
  • 2011-07-21
  • 1970-01-01
  • 2013-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多