admin管理员组

文章数量:1434900

I'm trying to figure out how double recursions work in the following example:

#include <iostream>
#include <vector>

int F(std::vector<int> &v, int a, int &b) {
    if (a <= 0 || b <= 2) return a;
    b -= 1;
    a = F(v, a - 1, b) + F(v, a, b);
    v.push_back(a);
    return a;
}
    
int main(){
   std::vector<int> v;
   int b = 7;
   F(v, 15, b);
   for(int i=0;i<v.size();i++)
       std::cout<<v[i]<<" ";
}

OUTPUT:

21 33 46 60 75

I understand how 21 got pushed into the vector. But after that, I don't understand which recursion (left or right) is then called, and how to follow that until the correct output.

Could you please explain to me, step by step, how this particular program is executed?

I'm trying to figure out how double recursions work in the following example:

#include <iostream>
#include <vector>

int F(std::vector<int> &v, int a, int &b) {
    if (a <= 0 || b <= 2) return a;
    b -= 1;
    a = F(v, a - 1, b) + F(v, a, b);
    v.push_back(a);
    return a;
}
    
int main(){
   std::vector<int> v;
   int b = 7;
   F(v, 15, b);
   for(int i=0;i<v.size();i++)
       std::cout<<v[i]<<" ";
}

OUTPUT:

21 33 46 60 75

I understand how 21 got pushed into the vector. But after that, I don't understand which recursion (left or right) is then called, and how to follow that until the correct output.

Could you please explain to me, step by step, how this particular program is executed?

Share Improve this question edited Nov 17, 2024 at 1:34 Remy Lebeau 602k36 gold badges508 silver badges853 bronze badges asked Nov 16, 2024 at 22:03 codproecodproe 12111 bronze badges 13
  • 7 The expression you wrote F(v, a - 1, b) + F(v, a, b) contains two subexpressions (the two calls) that can be evaluated in any order. The order is unspecified. Therefore, it does not make much sense to reason about the order of the elements of v. See: en.cppreference/w/cpp/language/eval_order. – Marco Bonelli Commented Nov 16, 2024 at 22:07
  • 4 Whoever wrote the text of this particular exam isn't qualified to do so. That, or the intention is to create a silly "gotcha" question where the only right answer is: you cannot predict the output as the evaluation order of the recursive calls is unspecified. – Marco Bonelli Commented Nov 16, 2024 at 22:18
  • 3 @codproe yes: auto left = F(v, a - 1, b); auto right = F(v, a, b); a = left + right; would be fine. – Marco Bonelli Commented Nov 16, 2024 at 22:28
  • 2 assuming that left recursion is first executed -- Rewrite your code as described by @MarcoBonelli, and then use a debugger to step through your program. Then you can adjust the amount of "detail" you will get, as you will see step-by-step what is occurring. – PaulMcKenzie Commented Nov 16, 2024 at 22:34
  • 1 @codproe yes the task is different, but now the behavior is fully deterministic because you know the order of evaluation and you can correctly predict the output. The original task was simply ill formed. – Marco Bonelli Commented Nov 16, 2024 at 22:47
 |  Show 8 more comments

2 Answers 2

Reset to default 2

C++17:

17 Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced.

The output that you have posted, will be generated only when compiler will perform evaluation of subexpression F(v, a - 1, b) before F(v, a, b) in every evaluation of expression F(v, a - 1, b) + F(v, a, b).

A quick check you can do by swapping the operands (subexpression) - F(v, a, b) + F(v, a - 1, b). With this, you may get a different output.

The recursive call trace for the given expression will be like a binary tree but there is catch here - the parameter b of function F() is a reference. Which means, any change in value b will reflect everywhere it's been referred.

For the given value of parameter a and b (a = 15 and b = 7), once the value of b will become 2, the condition b <= 2 will be satisfied and rest of the recursive calls to F() function will simply return value of a (whatever it is passed as argument in that call to function F()).

Assume, in every call to function F(), when evaluating expression F(v, a - 1, b) + F(v, a, b) expression, compiler evaluates subexpression F(v, a - 1, b) first, this is how the recursion tree will be:

(C1)         [Initial value of args - a = 15, b = 7]  F(v, a, b)  ==> returned value discarded
                                                           |
                                                +-------------------+
                                                |                   |
(C2)                                      F(v, 14, b)         F(v, 15, b) ==> a = 60 + 15 => 75 (pushed and returned)
                                                |  
                                       +-----------------+
                                       |                 |
(C3)                             F(v, 13, b)        F(v, 14, b) ==> a = 46 + 14 => 60 (pushed and returned)
                                       |
                              +-----------------+
                              |                 |
(C4)                    F(v, 12, b)        F(v, 13, b) ==> a = 33 + 13 => 46 (pushed and returned)
                              |
                     +-----------------+
                     |                 |
(C5)           F(v, 11, b)        F(v, 12, b) ==> a = 21 + 12 => 33 (pushed and returned)
                     |
value of b calcul-   |
ated to 2   +-----------------+     +-val returned-+
            |                 |     |              v
(C6)   F(v, 10, 2)        F(v, 11, 2) ==> a = 10 + 11 => 21 (pushed and returned)   
           \     |             /              ^
            \    +------------/-val returned--+
             \_______________/
                     |
                     v
              Both these calls will return the value of
              parameter a received (i.e. 10 and 11) because 
              value of parameter b is 2 now.

              Stack will windup from here.
              All the pending calls to function F() (right subtree's), in the 
              stack, will just return the value of parameter a.

Hence, the output you are getting:

21 33 46 60 75

In short

The precedence rules of C++ do not define if the left or the right term of your double recursion is called first. In fact, depending on the order chosen by the compiler, you may obtain different results with the same code.

In depth demonstration

From your OP:

if (a <= 0 || b <= 2) return a;

Above is the base case that helps keep the recursion from recursing forever.

a = F(v, a - 1, b) + F(v, a, b);
v.push_back(a);

assuming that left recursion is first executed

The first term is call (per assumption). Initially b=7, a=15. Then since a is immediately decremented in the call and b is decremented inside F, both a and b decrease together. Since a-b > 2, the base case will hit b == 2 before a <=0.
In the recursion, the first term is called again and again until b == 2. Only then is the 2nd term called and now a is not decremented in the call. Inside the 2nd F, the first F is called again (per your assumption). This time, b is still 2 as it is not decremented until after the base case. In your debugger, be sure to observe the Call Stack and watch the recursions unwind.

One tricky item. The arg a when calling the first F is one less than the arg a when calling the second F. Just keep point in mind.

I was curious how different the results might be. The variations are controlled by an added a-1 flag, flag_am1.
flag_am1 = 1: Deterministic with the a-1 term evaluated first.
flag_am1 = 0: Deterministic with the a-1 term evaluated second.
flag_am1 = -1: Identical to the OP
flag_am1 = -2: Similar to the OP except that the order of the two functions are swapped.
Note that for the flag_am1 = -2 case,

#include <iostream>
#include <vector>

int F(std::vector<int>& v, int a, int& b, int flag_am1) {
   // std::cout << flag_am1 << ": ENTER: a=" << a << " b=" << b << std::endl;
   if (a <= 0 || b <= 2)
   {
      std::cout << flag_am1 << ": EXIT:  a=" << a << " b=" << b << std::endl;
      return a;
   }
   b -= 1;
   if (flag_am1 == 1)
   {
      // std::cout << "1.0: a=" << a << " b=" << b << std::endl;
      a = F(v, a - 1, b, flag_am1);
      std::cout << "1.1: a=" << a << " b=" << b << std::endl;
      a += F(v, a, b, flag_am1);
      std::cout << "1.2: a=" << a << " b=" << b << std::endl;
   }
   else if (flag_am1 == 0)
   {
      // std::cout << "0.0: a=" << a << " b=" << b << std::endl;
      a = F(v, a, b, flag_am1);
      std::cout << "0.1: a=" << a << " b=" << b << std::endl;
      a += F(v, a - 1, b, flag_am1);
      std::cout << "0.2: a=" << a << " b=" << b << std::endl;
   }
   else if (flag_am1 == -1) // like the OP
   {
      std::cout << "-1.0: a=" << a << " b=" << b << std::endl;
      a = F(v, a - 1, b, flag_am1) + F(v, a, b, flag_am1);
      std::cout << "-1.1: a=" << a << " b=" << b << std::endl;
   }
   else{ // flag_am1 == -2
      std::cout << "-2.0: a=" << a << " b=" << b << std::endl;
      a = F(v, a, b, flag_am1) + F(v, a - 1, b, flag_am1);
      std::cout << "-2.1: a=" << a << " b=" << b << std::endl;
   }
   v.push_back(a);
   std::cout << " ** v.push_back(" << a << ")" << std::endl;
   return a;
}

int main() {
   std::vector<int> v{};
   int b = 7;

   v.resize(0);
   b = 7;
   std::cout << "\t *** a-1 flag is 1" << std::endl;
   F(v, 15, b, 1);
   for (int i = 0;i < v.size();i++)
   {
      std::cout << v[i] << " ";
   }
   std::cout << "\n" << std::endl;

   v.resize(0);
   b = 7;
   std::cout << "\t *** a-1 flag is 0" << std::endl;
   F(v, 15, b, 0);
   for (int i = 0;i < v.size();i++)
   {
      std::cout << v[i] << " ";
   }
   std::cout << "\n" << std::endl;

   v.resize(0);
   b = 7;
   std::cout << "\t *** a-1 flag is -1 (effectively like the OP)" << std::endl;
   F(v, 15, b, -1); // like original OP
   for (int i = 0;i < v.size();i++)
   {
      std::cout << v[i] << " ";
   }
   std::cout << "\n" << std::endl;

   v.resize(0);
   b = 7;
   std::cout << "\t *** a-1 flag is -2 (similar to OP but two F terms reversed)" << std::endl;
   F(v, 15, b, -2); // like original OP except the a-1 term is 2nd
   for (int i = 0;i < v.size();i++)
   {
      std::cout << v[i] << " ";
   }
   std::cout << "\n" << std::endl;
}

The important thing to remember when looking at the four sets of outputs, for cases flag_am1 = -1 or = -2, your results may vary.

Output:

         *** a-1 flag is 1
1: EXIT:  a=10 b=2
1.1: a=10 b=2
1: EXIT:  a=10 b=2
1.2: a=20 b=2
 ** v.push_back(20)
1.1: a=20 b=2
1: EXIT:  a=20 b=2
1.2: a=40 b=2
 ** v.push_back(40)
1.1: a=40 b=2
1: EXIT:  a=40 b=2
1.2: a=80 b=2
 ** v.push_back(80)
1.1: a=80 b=2
1: EXIT:  a=80 b=2
1.2: a=160 b=2
 ** v.push_back(160)
1.1: a=160 b=2
1: EXIT:  a=160 b=2
1.2: a=320 b=2
 ** v.push_back(320)
20 40 80 160 320 

         *** a-1 flag is 0
0: EXIT:  a=15 b=2
0.1: a=15 b=2
0: EXIT:  a=14 b=2
0.2: a=29 b=2
 ** v.push_back(29)
0.1: a=29 b=2
0: EXIT:  a=28 b=2
0.2: a=57 b=2
 ** v.push_back(57)
0.1: a=57 b=2
0: EXIT:  a=56 b=2
0.2: a=113 b=2
 ** v.push_back(113)
0.1: a=113 b=2
0: EXIT:  a=112 b=2
0.2: a=225 b=2
 ** v.push_back(225)
0.1: a=225 b=2
0: EXIT:  a=224 b=2
0.2: a=449 b=2
 ** v.push_back(449)
29 57 113 225 449

         *** a-1 flag is -1 (effectively like the OP)
-1.0: a=15 b=6
-1.0: a=14 b=5
-1.0: a=13 b=4
-1.0: a=12 b=3
-1.0: a=11 b=2
-1: EXIT:  a=10 b=2
-1: EXIT:  a=11 b=2
-1.1: a=21 b=2
 ** v.push_back(21)
-1: EXIT:  a=12 b=2
-1.1: a=33 b=2
 ** v.push_back(33)
-1: EXIT:  a=13 b=2
-1.1: a=46 b=2
 ** v.push_back(46)
-1: EXIT:  a=14 b=2
-1.1: a=60 b=2
 ** v.push_back(60)
-1: EXIT:  a=15 b=2
-1.1: a=75 b=2
 ** v.push_back(75)
21 33 46 60 75

         *** a-1 flag is -2 (similar to OP but two F terms reversed)
-2.0: a=15 b=6
-2.0: a=15 b=5
-2.0: a=15 b=4
-2.0: a=15 b=3
-2.0: a=15 b=2
-2: EXIT:  a=15 b=2
-2: EXIT:  a=14 b=2
-2.1: a=29 b=2
 ** v.push_back(29)
-2: EXIT:  a=14 b=2
-2.1: a=43 b=2
 ** v.push_back(43)
-2: EXIT:  a=14 b=2
-2.1: a=57 b=2
 ** v.push_back(57)
-2: EXIT:  a=14 b=2
-2.1: a=71 b=2
 ** v.push_back(71)
-2: EXIT:  a=14 b=2
-2.1: a=85 b=2
 ** v.push_back(85)
29 43 57 71 85

本文标签: cDouble recursion with vectorsStack Overflow