斐波那契字符串定义如下:

  • 第一个斐波那契字符串是“a”
  • 第二个斐波那契字符串是“bc”
  • 第(n + 2)个斐波那契字符串是前面两个斐波那契字符串的串联。

  • 例如,前几个斐波那契字符串是
    a
    bc
    abc
    bcabc
    abcbcabc
    

    给定一行和一个偏移量,目标是确定该偏移量处的字符。更正式地:



    我编写了以下代码来解决问题:
    #include <iostream>
    #include <string>
    #include <vector>
    
    using namespace std;
    
    int main()
    {
        int k, p;
        string s1 = "a";
        string s2 = "bc";
        vector < int >fib_numb;
        fib_numb.push_back(1);
        fib_numb.push_back(2);
        cin >> k >> p;
        k -= 1;
        p -= 1;
        while (fib_numb.back() < p) {
            fib_numb.push_back(fib_numb[fib_numb.size() - 1] + fib_numb[fib_numb.size() - 2]);
        }
        if (fib_numb[k] <= p) {
            cout << "No solution";
            return 0;
        }
        if ((k - fib_numb.size()) % 2 == 1)
            k = fib_numb.size() + 1;
        else
            k = fib_numb.size();
        while (k > 1) {
            if (fib_numb[k - 2] > p)
                k -= 2;
            else {
                p -= fib_numb[k - 2];
                k -= 1;
            }
        }
        if (k == 1)
            cout << s2[p];
        else
            cout << s1[0];
        return 0;
    }
    

    这是正确的吗?你会怎么做?

    最佳答案

    您可以在不显式计算任何字符串的情况下解决此问题,这可能是解决问题的最佳方法。毕竟,如果要求您计算第50个Fibonacci字符串,则几乎可以肯定会用完内存。 F(50)是12,586,269,025,因此您仅需要12GB的内存即可容纳它!

    解决方案的直觉是,由于斐波那契字符串的每一行都是由前几行的字符组成的,因此您可以将(行,偏移量)对转换为另一行(行,偏移量),其中新行是始终使用比您开始时使用的弦更小的斐波那契弦。如果重复了足够多次,最终您将返回到第0行或第1行的斐波那契字符串,在这种情况下,可以立即读出答案。

    为了使该算法起作用,我们需要建立一些事实。首先,让我们将斐波那契数列定义为零索引;也就是说,顺序是

    F(0)   = 0
    F(1)   = 1
    F(n+2) = F(n) + F(n + 1)
    

    鉴于此,我们知道斐波那契字符串的第n行(一个索引)在其中总共有F(n + 1)个字符。您可以通过归纳快速查看:
  • 第1行的长度为1 = F(2)= F(1 + 1)
  • 第2行的长度为2 = F(3)= F(2 + 1)。
  • 对于某行n + 2,该行的长度由Size(n)+ Size(n + 1)= F(n + 1)+ F(n + 2)= F(n + 3)= F给出((n + 2)+1)

  • 使用此知识,让我们假设我们想要找到斐波那契弦的第七行的第七个字符。我们知道第七行由第五行和第六行的串联组成,因此字符串如下所示:
      R(7) = R(5) R(6)
    

    第五行中的F(5 + 1)= F(6)= 8个字符,这意味着第七行的前八个字符来自R(5)。因为我们要从该行中取出第七个字符,并且由于7≤8,所以我们知道现在需要查看第5行的第七个字符来获得该值。好吧,第5行看起来像是第3行和第4行的串联:
      R(5) = R(3) R(4)
    

    我们想找到这一行的第七个字符。现在,R(3)中有F(4)= 3个字符,这意味着如果我们要查找R(5)的第七个字符,它将位于R(4)部分,而不是R( 3)部分。由于我们正在寻找该行的第七个字符,这意味着我们正在寻找R(4)的7-F(4)= 7-3 =第四个字符,所以现在我们去寻找。同样,R(4)定义为
     R(4) = R(2) R(3)
    

    R(2)中有F(3)= 2个字符,因此我们不想在其中查找该行的第四个字符;它将包含在R(3)中。该行的第四个字符必须是R(3)的第二个字符。让我们看看那里。 R(3)定义为
     R(3) = R(1) R(2)
    

    R(1)中有一个字符,因此此行的第二个字符必须是R(1)的第一个字符,因此我们在那看。但是,我们知道
     R(2) = bc
    

    因此,此字符串的第一个字符是b,这就是我们的答案。让我们看看这是否正确。斐波那契弦的前七行是
    1 a
    2 bc
    3 abc
    4 bcabc
    5 abcbcabc
    6 bcabcabcbcabc
    7 abcbcabcbcabcabcbcabc
    

    果然,如果您查看第七个字符串的第七个字符,您会发现它确实是b。看起来像这样!

    更正式地说,我们感兴趣的递归关系如下所示:
    char NthChar(int row, int index) {
        if (row == 1) return 'a';
        if (row == 2 && index == 1) return 'b';
        if (row == 2 && index == 2) return 'c';
        if (index < Fibonacci(row - 1)) return NthChar(row - 2, index);
        return NthChar(row - 1, index - Fibonacci(row - 1));
    }
    

    当然,现在,这里编写的实现存在问题。因为行索引的最大范围是109,所以我们不可能在所有情况下都计算Fibonacci(row);十分之一斐波纳契数太大了,无法代表!

    幸运的是,我们可以解决这个问题。如果看一张斐波纳契数字表,您会发现F(45)= 1,134,903,170,该数字大于109(并且不小于这个数字的斐波那契数字)。此外,由于我们知道我们关心的索引也必须不大于10亿,因此,如果我们位于第46行或更大的行中,我们将始终将分支放在斐波那契字符串的前半部分。这意味着我们可以将代码重写为
    char NthChar(int row, int index) {
        if (row == 1) return 'a';
        if (row == 2 && index == 1) return 'b';
        if (row == 2 && index == 2) return 'c';
    
        /* Avoid integer overflow! */
        if (row >= 46) return NthChar(row - 2, index);
    
        if (index < Fibonacci(row - 1)) return NthChar(row - 2, index);
        return NthChar(row - 1, index - Fibonacci(row - 1));
    }
    

    在这一点上,我们正在接近解决方案。仍然有一些问题需要解决。首先,上面的代码几乎肯定会炸毁堆栈,除非编译器足够好,可以使用尾部递归消除所有堆栈帧。尽管某些编译器(例如gcc)可以检测到此错误,但是依靠它可能不是一个好主意,因此我们可能应该迭代地重写此递归函数。这是一种可能的实现:
    char NthChar(int row, int index) {
        while (true) {
           if (row == 1) return 'a';
           if (row == 2 && index == 1) return 'b';
           if (row == 2 && index == 2) return 'c';
    
           /* Avoid integer overflow! */
           if (row >= 46 || index < Fibonacci(row - 1)) {
               row -= 2;
           } else {
               index -= Fibonacci(row - 1);
               row --;
           }
        }
    }
    

    但是我们当然可以做得更好。特别是,如果您得到的行号惊人地大(例如十亿),那么不断重复循环从行中减去两个直到小于46就很愚蠢了。只需确定所有减法后最终将变成什么值即可。但是我们可以很容易地做到这一点。如果我们有一个至少为46的偶数行,我们将最终减去2,直到它变成44。如果我们有一个奇数行至少为46,我们最终将结果减去2,直到变成45。我们可以重写上面的代码以显式处理这种情况:
    char NthChar(int row, int index) {
        /* Preprocess the row to make it a small value. */
        if (row >= 46) {
            if (row % 2 == 0)
                row = 45;
            else
                row = 44;
        }
    
        while (true) {
           if (row == 1) return 'a';
           if (row == 2 && index == 1) return 'b';
           if (row == 2 && index == 2) return 'c';
    
           if (index < Fibonacci(row - 1)) {
               row -= 2;
           } else {
               index -= Fibonacci(row - 1);
               row --;
           }
        }
    }
    

    最后一件事情要处理,那就是如果由于字符超出范围而无法解决问题,该怎么办。但是我们可以轻松地解决此问题:
    string NthChar(int row, int index) {
        /* Preprocess the row to make it a small value. */
        if (row >= 46) {
            if (row % 2 == 0)
                row = 45;
            else
                row = 44;
        }
    
        while (true) {
           if (row == 1 && index == 1) return "a"
           if (row == 2 && index == 1) return "b";
           if (row == 2 && index == 2) return "c";
    
           /* Bounds-checking. */
           if (row == 1) return "no solution";
           if (row == 2) return "no solution";
    
           if (index < Fibonacci(row - 1)) {
               row -= 2;
           } else {
               index -= Fibonacci(row - 1);
               row --;
           }
        }
    }
    

    我们有一个可行的解决方案。

    您可能要做的另一种优化是预先计算所需的所有斐波那契数,并将它们存储在一个巨大的数组中。您只需要F(2)至F(44)的斐波那契值,因此您可以执行以下操作:
    const int kFibonacciNumbers[45] = {
        0, 1, 1, 2, 3, 5,
        8, 13, 21, 34, 55, 89,
        144, 233, 377, 610,
        987, 1597, 2584, 4181,
        6765, 10946, 17711, 28657,
        46368, 75025, 121393, 196418,
        317811, 514229, 832040,
        1346269, 2178309, 3524578,
        5702887, 9227465, 14930352,
        24157817, 39088169, 63245986,
        102334155, 165580141, 267914296,
        433494437, 701408733
    };
    

    使用此预先计算的数组,代码的最终版本将如下所示:
    string NthChar(int row, int index) {
        /* Preprocess the row to make it a small value. */
        if (row >= 46) {
            if (row % 2 == 0)
                row = 45;
            else
                row = 44;
        }
    
        while (true) {
           if (row == 1 && index == 1) return "a"
           if (row == 2 && index == 1) return "b";
           if (row == 2 && index == 2) return "c";
    
           /* Bounds-checking. */
           if (row == 1) return "no solution";
           if (row == 2) return "no solution";
    
           if (index < kFibonacciNumbers[row - 1]) {
               row -= 2;
           } else {
               index -= kFibonacciNumbers[row - 1];
               row --;
           }
        }
    }
    

    我尚未对此进行测试;解释唐·克努斯(Don Knuth),我只是证明它是正确的。 :-)但我希望这有助于回答您的问题。我真的很喜欢这个问题!

    10-08 18:41