-1

My while loop works fine with 10_000, but it takes time to load with 100_000 and doesn't work with 10_000_000.

I don't understand why, it's a machine, and it should be fast no matter the numbers. So I suppose there is a mistake in my code, but for me, everything looks good.

implementing this enter image description here

Console.WriteLine(SumSequenceElements(10_000_000));

static double SumSequenceElements(int n)
{
   int i = 1;
   double sum = 0;
   while (i <= n)
   {
      int j = 0;
      double power = 1;
      while (j < i + 1)
      {
         power *= -1;
         j++;
      }

      sum += power / (i * (i + 1));
      i++;
   }

   return sum;
}
UkFLSUI
  • 5,509
  • 6
  • 32
  • 47
Vita
  • 55
  • 7
  • 12
    "I don't understand why, it's a machine, and it should be fast no matter the numbers" - um, really not. Your code executes the inner loop roughly (n^2)/2 times. When n is 10 million, that's a *lot* of iterations. – Jon Skeet Aug 05 '23 at 09:43
  • 8
    Hint: there's a much, *much* simpler way of working out (-1)^(i+1). You really don't need to iterate i+1 times to evaluate that. – Jon Skeet Aug 05 '23 at 09:44
  • 6
    In comp sci and specifically in software engineering, the concept of computational complexity (how long something takes and how much memory it takes) as a function of its inputs is called [Big O Notation](https://en.wikipedia.org/wiki/Big_O_notation). Your algorithm shown in your example is **O** (**n** squared). That's a computational complexity to avoid, because it makes your programs astonishingly slow for large values of **n**. This concept is worth studying. – O. Jones Aug 05 '23 at 09:51
  • You should have debugged your code properly. You would have seen what the issue was. – jmcilhinney Aug 05 '23 at 09:56
  • Is this an assignment? Usually the whole point of such an assignment is for you to understand the complexity behind computations and for you to come up with a "smarter" solution. Like simplification of a formula. – JHBonarius Aug 05 '23 at 10:26
  • 1
    "I don't understand why, it's a machine, and it should be fast no matter the numbers." - man, try logic. By your logic no one would EVER need to buy a supercomputer or a hardware upgrade because "it is a machine, it should be fast no matter the numbers". No need to ever upgrade. Real world - as you SHOULD know - does not work like that. Makes me wonder why you think people do hardware upgrades, like ever? – TomTom Aug 05 '23 at 11:34
  • Yep, that is an assignment, and I need to use a while loop because I was told to. If there are any simpler decisions I'm happy, but they are not for my case, unfortunately. "Usually the whole point of such an assignment is for you to understand the complexity behind computations and for you to come up with a "smarter" solution." If someone wants me to come up, let them tell me this, not only mean silently. Anyway, what's up with you people? O. Jones, thank you, Jon Skeet thanks. Everybody else, did I ask you about your thoughts on what I should have done or already known? So rude. – Vita Aug 05 '23 at 12:10
  • @Vita: "did I ask you about your thoughts", yes, you did, by asking here.... – Luuk Aug 05 '23 at 15:33
  • I asked about the loop, not about me and my knowledge – Vita Aug 06 '23 at 17:57

3 Answers3

7

(-1)^k is +1 if k is even and -1 if k is odd. Now, we have k = i + 1. I.e., whenever i is even, then k is odd and vice versa. Or, in other words, (-1)^(i+1) is -1 if i even and +1 if i is odd.

You can test whether an int is even by looking at the remainder of the division by 2. C#'s Modulus operator % is yielding this remainder.

static double SumSequenceElements(int n)
{
   int i = 1;
   double sum = 0;
   while (i <= n)
   {
      double power = i % 2 == 0 ? -1 : +1;
      sum += power / (i * (i + 1));
      i++;
   }

   return sum;
}

By avoiding the inner loop, the computational complexity decreases from O(n^2) to O(n), i.e., from quadratic to linear. A made a little benchmark on my PC. With n = 1 million, both of my methods (above and below) take 2 ms to run, your method takes about 16 minutes on my PC. This is exactly the expected speed increase of 1/2 a million which corresponds to the average number of times the inner loop is running for 1 iteration of the outer loop.

Btw., ?: is the ternary conditional operator.

Since the sign of this +/-1 power is switching between + and - at each iteration. You can also write:

static double SumSequenceElements(int n)
{
   int i = 1;
   double power = 1; // Since we start with i = 1
   double sum = 0;
   while (i <= n)
   {
      sum += power / (i * (i + 1));
      power *= -1; // Change the sign of power
      i++;
   }

   return sum;
}

This does the same calculation as you did, but instead of using a nested loop which starts at power = 1 every time, we calculate it continuously in the outer loop.

power *= -1 uses a Compound assignment to calculate power = power * -1.


Update: Source of numerical errors

As part of the calculation, we compute i * (i + 1). i is an int with a range of ~ -2 billions to +2 billions. We get an arithmetic overflow when i is >= 46340 (which is ~sqrt(2^31)).

There are two possibilities to fix the error:

  1. Declare i as long. Works with n < ~ 3 billions ~ sqrt(2^63).
  2. Calculate i * (i + 1.0) instead. Note that we add 1.0 instead of 1. This switches from int arithmetic to double arithmetic with Max(double) = ~1.7 × 10^308.

Another improvement we can make is to reverse the loop. Adding small numbers to a much larger number leads to loss of precision for floating point numbers. Therefore we should start by adding the small fractions.

By using long and 1.0 we are not limited by the 2 billions limit for n and not for the ~3 billions limit for i * (i + 1). My ultimate solution also uses the reversed loop for increased precision:

static double SumSequenceElements(long n)
{
    long i = n;
    double power = i % 2 == 0 ? -1 : +1;
    double sum = 0;
    while (i >= 1) {
        sum += power / (i * (i + 1.0));
        power *= -1; // Change the sign of power
        i--;
    }

    return sum;
}

This version is somewhat faster than the forward loop, because the loop conditions tests i versus the constant value 1, where as the forward loop tests it versus the variable n.

Tested for n = 10,000,000,000. Result: 0.38629436111989063 in ~21.8 seconds.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • Thank you, your decisions are good. Unfortunately, I have about 5 similar assignments, and there are different power numbers, not just change signs +-. And also I was told to use a while statement for raising to power. From the comments above I've got that the inner loop will not work, so maybe I will make it outside, Idk, but thank you anyway) – Vita Aug 05 '23 at 12:20
  • Sorry, out of curiosity I tried your solutions, and it didn't work with my numbers either... – Vita Aug 05 '23 at 12:27
  • Your inner loop does work. The problem is, that it will loop in average n/2 times per outer loop. Together with the outer loop this gives n * n/2 repetitions. So for 1,000,000 you get 500,000,000,000 iterations. – Olivier Jacot-Descombes Aug 05 '23 at 13:00
  • My second solution uses a while loop to calculate the power. It reuses the already existing outer loop for this. But there are more efficient ways with fewer iterations to calculate the power in a loop: [The most efficient way to implement an integer based power function pow(int, int)](https://stackoverflow.com/questions/101439/the-most-efficient-way-to-implement-an-integer-based-power-function-powint-int). Btw.: My methods take 2 ms for n = 1 million, your takes 16 minutes on my PC – Olivier Jacot-Descombes Aug 05 '23 at 13:14
  • haha, that's why I couldn't wait for it... I tried your method, and it returns a wrong value as a sum, it's a weekend so I didn't think why. I cannot use anything else but while, especially the pow function – Vita Aug 06 '23 at 18:01
  • 1
    I found the source of the error. It is due to arithmetic overflow for large numbers. See my update in my answer. – Olivier Jacot-Descombes Aug 07 '23 at 10:33
  • oh, it still gives wrong due to my test expected result (10 000 000 should be 0.38629436111988125), but I already cheated by returning it without executing the program, if even here people struggle with loops, how could I do 6 tasks in 2 hours 100% correctly? thank you anyway, your solution is the best, I appreciate spending time) – Vita Aug 07 '23 at 20:07
  • Note I tested 10 billions, not 10 millions. – Olivier Jacot-Descombes Aug 07 '23 at 21:06
  • Yeah, I noticed, but how would it help with 10 m?)) – Vita Aug 08 '23 at 12:00
  • For 10 m I get 0.38629436111988125 with the forward loop and 0.38629436111988563 with the reverse loop. [WolframAlpha](https://www.wolframalpha.com/input?i=sum%28%28%28-1%29%5E%28i%2B1%29%29%2F%28i*%28i%2B1%29%29%29+for+i+%3D+1+to+10000000) yields 0.386294361119885618835464242791 which is closer the the reverse loop version and thus confirms its higher precision. Your expected result is less precise. (Hint: click "more digits" in the WolphramAlpha link). – Olivier Jacot-Descombes Aug 08 '23 at 14:18
2

"I don't understand why, it's a machine, and it should be fast no matter the numbers"

When checking how much time the loop is needing, you can change code to:

static double SumSequenceElements(int n)
{
   int i = 1;
   double sum = 0;
   DateTime d = DateTime.Now;
   while (i <= n)
   {
      if (i % 100000 == 0) {
         Console.WriteLine($"{(DateTime.Now-d).TotalSeconds:N5}: {i}");
         d=DateTime.Now;
      }
      int j = 0; //.......(rest of code unchanged)

Which produced output like:

10,66730: 100000
31,94547: 200000
53,36376: 300000

You can see that every step takes more time. A simple conclusion that can be drawn is that the while (j<i+1){ ... } is taking more time when the value of i is getting larger..

Feel free to do some more debugging on this to expand your knowledge on this.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
Luuk
  • 12,245
  • 5
  • 22
  • 33
1

Olivier's answer is excellent, and I would advise using that, but looking at this I got curious and found a constant time approximation, detailed below.

First, the fraction 1/i(i+1) (Forgive my lack of LaTeX) is equal to 1/i + 1/(i+1) - 2/(i+1). This can be determined via partial fraction decomposition, and confirmed by recombining the fraction. Then, the sum becomes:

1/1 + 1/2 - 1/2 - 1/3 + 1/3 + ... + (-1)^(n+1)*(1/(n+1)) - 2H_n - 1. The first bits telescope, and all but the first and last term cancel to give: 1 + (-1)^(n+1)*(1/(n+1)) - 2H_n - 1, simplifying further to: (-1)^(n+1)*(1/(n+1)) - 2H_n.

At this point, an approximation for the nth harmonic number (some here) can be applied to whatever precision is required and the sum obtained in constant time.