java - 如何将n个数字相加,相乘到数组中的给定范围,并以O(n)时间反转数组中的范围?

标签 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。
无论如何,将欢迎此算法中进行任何优化(例如删除仅用于加法和乘法的循环)。

最佳答案

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

假设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的任何常量。 additiveConstant0结合。

    为了更具体地了解这一点,我们设置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'的 future 元素中。让我们稍微表达一下这些操作:
  • 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;
    }
    

    我没有谈到一个警告。 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)时间运行。

    再一次,如果您担心整数溢出,假设Lint数组,则可以将long long用于计算中涉及的所有变量,应该没问题。

    关于java - 如何将n个数字相加,相乘到数组中的给定范围,并以O(n)时间反转数组中的范围?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21058575/

    相关文章:

    java - 避免双循环

    java - 带有 Jersey 2 的 Atmosphere PubSub

    c++ - 为什么 std::move 会阻止 RVO(返回值优化)?

    c++ - strdup 导致堆损坏

    string - 为什么我的C程序打印这样?我不明白字符串是如何工作的吗?

    java - StringBuilder 和正则表达式解析

    java - 洗一副牌直到得到特定数字

    c++ - 如何在 C++ 中查找和避免未初始化的原始成员?

    c - 迭代指针与字符数组

    c - 在 C 中初始化数组时出现段错误