Write a method that takes as its input an array of integers. For each element in the array, print the product of all the other elements in the array. For example, given the following array:

[1,2,3,4,5]

The output would be:

120, 60, 40, 30, 24

Let’s start with the most literal solution. For each element in the array, loop through all the other elements and multiply them together:

```
int[] j = { 1, 2, 3, 4, 5};
for (int outerIndex = 0; outerIndex < j.Length; outerIndex ++)
{
int product = 1;
for (int innerIndex = 0; innerIndex < j.Length; innerIndex++)
{
if (innerIndex != outerIndex)
{
product *= j[innerIndex];
}
}
Console.WriteLine(product);
}
```

This solution requires an outer for loop to iterate through each element in the array and then an inner for loop to iterate through the other n-1 elements in the array. The asymptotic complexity of this is O(n^2).

Obviously, this isn’t a very good solution, so we can do better. The key observation to make here is that for any given element with value x in the array, the product of all the other elements in the array is simply the total product of all the elements divided by x. We only need to calculate the total product once.

This solution only requires two passes through the array. The first calculates the total product, and the second divides the total product by each element in the array:

```
int[] j = { 1, 2, 3, 4, 5 };
int totalProduct = 1;
for (int i = 0; i < j.Length; i++)
{
totalProduct *= j[i];
}
for (int i = 0; i < j.Length; i++)
{
//assume we check for zeroes beforehand to prevent divide by zero errors
Console.WriteLine(totalProduct / j[i]);
}
```

This solution has O(n) complexity, since it requires only two linear scans of the array.

Now let’s make the problem a little bit more challenging. What if we cannot use division? Assume that the operation is too prohibitively expensive and that we need to find a workaround (a not uncommon scenario in the real world).

We can use an algorithm design technique known as dynamic programming: Break the problem up into smaller sub-problems, solve and store the solution for each one, and then combine the solutions as necessary to arrive at the answer to the original problem. One difference between dynamic programming and the divide and conquer class of algorithms is that dynamic programming stores the solutions to the subproblems, which are then retrieved at a later point in time. This is an optimization that prevents the same sub-problems from being solved multiple times. One example of this approach is calculating the nth element in the fibbonacci sequence. The fibbonacci sequence is a sequence that starts with 0, 1, 1, 2 and where every subsequent number is the sum of the previous two numbers in the sequence. Typically, the solution involves using either recursion or iteration. However, we can use dynamic programming by precomputing the sequence up to some number j. Thus, for all numbers where n < j, instead of having to use recursion or iteration, we can do a simple lookup. For numbers where n > j, we can still reduce the number of computations by summing from j and j-1 instead of from 0 and 1.

With respect to the problem of determining the products without the usage of division, we can use a similar approach. Note that the inefficiency of the first solution is due to the same multiplication operations being carried out multiple times. We can eliminate these redundant calculations by using dynamic programming. For any given element k in the array j of size n, we want to multiply all the numbers to the left of k with all the numbers to the right of k. By precomputing the running product of all the elements in the array in both directions (from left to right and vice versa), we now know the product of all the numbers to the left and right of any element k in the array. The problem can then be solved:

```
int[] j = {1,2,3,4,5};
int[] runningProductLeft = new int[j.Length];
int[] runningProductRight = new int[j.Length];
int product = 1;
//there is no element to the left of the start of the array, so set it to 1
runningProductLeft[0] = 1;
//since we already set the first element of runningProductLeft
//start populating it from index 1
for (int i = 1; i < j.Length; i++)
{
product = product * j[i - 1];
runningProductLeft[i] = product;
}
//we want to populate runningProductRight in reverse by starting from the end of the array
//there is no element to the right of the end of the array, so set it to 1
runningProductRight[j.Length - 1] = 1;
product = 1;
//since we already populated the last element of runningProductRight, start populating from the second to last element in the array
for (int i = j.Length - 2; i >= 0; i--)
{
product = product * j[i + 1];
runningProductRight[i] = product;
}
//now that the running products have been precomputed, printing out the solution becomes trivial
for (int i = 0; i < j.Length; i++)
{
product = runningProductLeft[i] * runningProductRight[i];
Console.WriteLine(product);
}
```

This solution requires three linear scans through the array, so the runtime complexity is still O(n).