1

I'm currently trying to create an algorithm that generate all combinations of K financial products for a portfolio where each stock has a proportion ranging from 1/Z to N/Z.

I am dividing my portfolio in Z shares than finding all the assignments of those shares to K financial products so that each has at least 1 and at most N shares (N is chosen so my arbitrary limit for proportions is (N/Z)%).

At the end, i should get a list of list where each sub-list contains K elements, doesn't contain an element greater than N and sums to Z.

Example: K = 16, N = 32, Z = 64 [..., [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], [5, 1, 3, 18, 2, 4, 2, 4, 6, 2, 4, 4, 1, 4, 1, 3] ,...]

Two examples among many, here are two sets of 16 number between 1 and 32 so that their sum is equal to 64.

However i can't seem to find a way to obtain this result.

Thanks you!

2 Answers2

0

Note up front: I'm assuming that you know about graphs, not the kind of X/Y lines from school math but the kind with the vertices and edges. If you're not familiar with that, I'd recommend Steven Skiena's Algorithm Design Manual, which (at least to me) was eye-opening.

Now, imagine a graph where every edge represents one value. The graph forms a tree of height K. You then search the tips of that tree for vertices that have the sum Z for all edges from the root. Running this algorithm surely works, but for larger N, Z and K, this might be inefficient.

First thing to optimize is that you don't build the actual graph but only traverse it implicitly. This avoids lots of overhead for things you don't really need.

Next thing to reconsider in the primitive algorithm is that 4+1 and 1+4 is probably just one solution in your domain. In order to avoid generating those, only traverse to cheaper or equally cheap edges. IOW, after picking the 1 in the first step, you can't pick the 4, so that duplicate solution is automatically discarded. Of course you could also go from low to high, that doesn't matter.

Then, you can start pruning further: For example, if you have two more steps to take and need a sum of ten from those, but your last edge had the value three, according to above rule you can only reach at most six, so you know that there are no solutions in this branch of the tree. Similarly if you are already over the value Z you're looking for, there's no need to recurse further.

Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
0

Introduction

Finding the combinations can be done using a simple counting algorithm. Use an array of "digits" where each digit has the value 1 through N. The number of digits in the array is K. Updating the array to find the next valid solution takes amortized O(1) time, since in most cases only a few digits at the end of the array change.

The process of updating the array is similar to normal counting:

  • increment the rightmost digit (the least significant digit)
  • if it exceeds its maximum, set it back to its minimum, and carry into the next digit
  • repeat until no carry is necessary
  • if there's a carry out of the most significant digit, then terminate the search

Generating the combinations of K values is a little more complicated because the minimum and maximum digit values are not fixed numbers. But the concept is the same.

The minimum value for a digit has the following constraints:

  • the digit shall not be less than 1
  • the digit shall not be less than the digit to its left (this prevents duplicate combinations)
  • the digit shall be large enough that the sum of the array can be greater than or equal to Z

The maximum value for a digit has the following constraints:

  • the digit shall not be greater than N
  • the digit shall be small enough that the sum of the array can be less than or equal to Z

Example

The following example assumes the input parameters are K=16, N=32, Z=64. The algorithm uses two arrays to generate solutions:

  • The first is the digitArray, which contains K elements, each of which has a value between 1 and N. After each update of the digitArray, the sum of the elements is always Z.
  • The second array is the prefixSumArray. The prefixSumArray is only needed as an optimization. Without it, computing the sum of the "blue" digits would take O(K) time.

The digitArray is conceptually divided into three subarrays as shown below. In the description that follows, the green element is the element being processed. Blue elements are either untouched (while scanning right-to-left), or have been finalized (while scanning left-to-right). Red elements are waiting to be assigned a new value.

enter image description here

Two consecutive solutions to the problem are shown below. It is the transformation from one to the other that this example strives to illustrate.

[1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 11, 11, 12, 12]
[1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4,  4,  4,  5, 32]

The initial state of the digit array, with the corresponding prefix sum array:

enter image description here

The code starts by incrementing the right-most digit. That digit will always overflow, and generate a carry, because it only has one possible value (which is the value that makes the array sum equal to Z). Setting the digit to its minimum value is deferred.

enter image description here

The code then increments the second digit (from the right). That results in an array with a partial sum of 53, and one unknown digit at the end. That last digit must be at least 13 (it can't be less than the digit to its left). A value of 13 in the last digit brings the sum to 66, which is greater than Z. So the second digit has overflowed, and will be set to its minimum.

enter image description here

After incrementing the third digit, the partial sum is 41. The two red digits must be at least 12 each, bringing the sum to 65. The third digit has overflowed.

enter image description here

The fourth digit also overflows, but this time let's demonstrate that by computing the maximum value for the digit. The maximum is
(Z - sum_of_blue) / (count_of_red + 1) = (64 - 18) / (3 + 1) = 11
So it overflows after 11.

enter image description here

The fifth digit has a maximum of (64 - 15) / (4 + 1) = 9. So incrementing it from 3 to 4 does not cause an overflow.

enter image description here

Now the code needs to set all of the red digits to their minimum values (updating the prefix sum array at the same time). The minimum for a digit is found by assuming that all the digits to the right are equal to N. So the minimum is
(Z - sum_of_blue) - (N * count_of_red) = (64 - 19) - (32 * 3) = -51
That's less than the digit to the left, so the minimum is 4.

enter image description here

The next digit has a minimum value of (64 - 23) - (32 * 2) = -23, so that digit is set to 4.

enter image description here

The next digit has a minimum value of (64 - 27) - (32 * 1) = 5

enter image description here

And the final digit has a minimum value of (64 - 32) - (32 * 0) = 32

enter image description here

And the final result is the next solution in the sequence.

enter image description here

Code

Here's a sample implementation in C:

#include <stdio.h>
#include <stdbool.h>

void showArray(int count, int digitArray[], int K)
{
    printf("%5d: [%2d", count, digitArray[0]);
    for (int i = 1; i < K; i++)
        printf(", %2d", digitArray[i]);
    printf("]\n");
}

// The 'computeMinimums' function sets the red elements in the 'digitArray' 
// to their minimum allowed value, while updating the 'prefixSumArray'. 
// The 'start' parameter is the index of the first red element.

void computeMinimums(int digitArray[], int prefixSumArray[], int start, int K, int N, int Z)
{
    int leftDigit = 1;   // value of the element to the left of the current digit
    int leftSum = 0;     // sum of the elements to the left of the current digit
    if (start > 0)
    {
        leftDigit = digitArray[start-1];
        leftSum = prefixSumArray[start-1];
    }
    int rightSum = (K - start - 1) * N;  // maximum sum for the red digits

    for (int i = start; i < K; i++)
    {
        // compute the minimum value for the current digit
        int minDigit = Z - leftSum - rightSum;
        if (minDigit < leftDigit)
            minDigit = leftDigit;

        // prepare for the next digit
        leftDigit = minDigit;
        leftSum += minDigit;
        rightSum -= N;

        // update the current element in the arrays
        digitArray[i] = minDigit;
        prefixSumArray[i] = leftSum;
    }
}

bool nextSolution(int digitArray[], int prefixSumArray[], int K, int N, int Z)
{
    // starting with the rightmost digit, scan backwards through the array
    // to find a digit that can be incremented without overflowing
    for (int i = K-1; i >= 0; i--)
    {
        // compute the maximum value for the current digit
        int leftSum = prefixSumArray[i] - digitArray[i];
        int maxDigit = (Z - leftSum) / (K - i);
        if (maxDigit > N)
            maxDigit = N;

        // Increment the current digit, and if it doesn't overflow,
        // then there is a solution.
        // Update the prefix sum array, and fill in the red array elements.
        digitArray[i]++;
        if (digitArray[i] <= maxDigit)
        {
            prefixSumArray[i]++;
            computeMinimums(digitArray, prefixSumArray, i+1, K, N, Z);
            return true;
        }
    }

    // all of the digits overflowed, so there are no more solutions
    return false;
}

int main(void)
{
    int K = 16;
    int N = 32;
    int Z = 64;
    int digitArray[K];
    int prefixSumArray[K];

    // fill the digitArray with the first solution, and initialize the prefix sums
    computeMinimums(digitArray, prefixSumArray, 0, K, N, Z);
    for (int count = 1; ; count++)
    {
        // output the current solution
        showArray(count, digitArray, K);

        // generate the next solution, end the loop if there are no more
        if (!nextSolution(digitArray, prefixSumArray, K, N, Z))
            break;
    }
}
user3386109
  • 34,287
  • 7
  • 49
  • 68