40

I'm a beginner in programming and I'm looking for a nice idea how to generate three integers that satisfy a condition.

Example:

We are given n = 30, and we've been asked to generate three integers a, b and c, so that 7*a + 5*b + 3*c = n. I tried to use for loops, but it takes too much time and I have a maximum testing time of 1000 ms.

I'm using Python 3.

My attempt:

x = int(input())
c = []
k = []
w = []
for i in range(x):
    for j in range(x):
        for h in range(x):
           if 7*i + 5*j + 3*h = x:
              c.append(i)
              k.append(j)
              w.append(h)
if len(c) == len(k) == len(w) 
    print(-1)
else: 
    print(str(k[0]) + ' ' + str(c[0]) + ' ' + str(w[0]))
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Colton Walker
  • 620
  • 6
  • 14
  • Hello and welcome to StackOverflow! Would you mind editing your post and showing us what you've attempted so far? – Daniel Walker Oct 11 '20 at 17:20
  • 12
    `= x` is invalid there. You probably meant `== x`. What is `x`, anyway? Please [edit] the question. For reference, you need to provide a [mre]. BTW welcome to SO! Check out the [tour]. – wjandrea Oct 11 '20 at 17:38
  • 1
    You're also missing a colon after `if len(c) == len(k) == len(w)` – wjandrea Oct 11 '20 at 17:40
  • 7
    Wait, why would `c`, `k`, and `w` ever be different lengths? – wjandrea Oct 11 '20 at 17:44
  • 2
    Your range can go 0-10 rather than 0-30. That comes from `3*h` factor *which can at most be 10 IF others are 0.* – JL Peyret Oct 11 '20 at 17:44
  • 2
    Welcome to StackOverflow! Have you tried actually running this code on your computer? With n=30 this should be extremely fast even without any improvements (about 7ms on my laptop). If this is being run on another computer (for example, automated homework testing), the issue might be elsewhere -- for example, it it is waiting for `input()` but you are supposed to plug in `x` some other way. – exp1orer Oct 11 '20 at 17:48
  • Also append `results.append((i,j,h))` where results is *one* list rather than your 3 lists. – JL Peyret Oct 11 '20 at 17:50
  • Would something like [python constraint](https://pypi.org/project/python-constraint/) be an acceptable solution? – EJoshuaS - Stand with Ukraine Oct 12 '20 at 12:20
  • hi Colton. Can you just state whether a, b, c can be negative? – Fattie Oct 12 '20 at 17:59
  • 2
    @Fattie no, just positive integers – Colton Walker Oct 12 '20 at 19:20
  • 19
    What does randomness have to do in this? Your question says you're asked to find a, b, c that satisfy the condition, but you don't say anything about finding more than one such triplet. Which makes me wonder what the randomness is about, and why you'd bother using lists in the code at all, instead of just `if 7*i + 5*j + 3*h == x: print(i, j, h)`. What's the actual requirement or end-goal? – ilkkachu Oct 12 '20 at 20:02
  • If they are positive integers, then all your `range(1, ...)`s should start at 1 not 0. And are they distinct values or can have same value? This all matters in refining the range on your for-loops: you only need `for i in range(1, 4+1):`, or maybe even `range(1,4)` depending on your answers to those. Only executing the i-for-loop 3 times instead of 30 is 10x speedup, cumulatively multiplied by 6x speedup for the j-for-loop, and 3.33x for the k-for-loop. – smci Oct 13 '20 at 07:00
  • 4
    Also, do you only want to **find one solution** not all solutions? Your question as literally stated implies the former but your code implements the latter. – smci Oct 13 '20 at 07:01
  • 1
    Oh, I interpreted the question as selecting one solution at random, which is a whole different issue – Mooing Duck Oct 13 '20 at 19:10
  • 4
    Why do you have "random" in your title & tags? According to the text of your question, you just want a solution to the equation. "Random" is used to describe entirely different notions. – philipxy Oct 14 '20 at 01:06
  • a=0, b=0, c=10 ;) – Daniel Lee Oct 14 '20 at 11:07
  • 2
    This looks like a comp-sci assignment to me. Help them through their thinking but don't post a full solution. – Moby Disk Oct 14 '20 at 15:18

5 Answers5

82

First, let me note that your task is underspecified in at least two respects:

  1. The allowed range of the generated values is not specified. In particular, you don't specify whether the results may include negative integers.
  2. The desired distribution of the generated values is not specified.

Normally, if not specified, one might assume that a uniform distribution on the set of possible solutions to the equation was expected (since it is, in a certain sense, the most random possible distribution on a given set). But a (discrete) uniform distribution is only possible if the solution set is finite, which it won't be if the range of results is unrestricted. (In particular, if (a, b, c) is a solution, then so is (a, b + 3k, c − 5k) for any integer k.) So if we interpret the task as asking for a uniform distribution with unlimited range, it's actually impossible!


On the other hand, if we're allowed to choose any distribution and range, the task becomes trivial: just make the generator always return a = −n, b = n, c = n. Clearly this is a solution to the equation (since −7n + 5n + 3n = (−7 + 5 + 3)n = 1n), and a degenerate distribution that assigns all probability mass to single point is still a valid probability distribution!

If you wanted a slightly less degenerate solution, you could pick a random integer k (using any distribution of your choice) and return a = −n, b = n + 3k, c = n − 5k. As noted above, this is also a solution to the equation for any k. Of course, this distribution is still somewhat degenerate, since the value of a is fixed.

If you want to let all return values be at least somewhat random, you could also pick a random h and return a = −n + h, b = n − 2h + 3k and c = n + h − 5k. Again, this is guaranteed to be a valid solution for any h and k, since it clearly satisfies the equation for h = k = 0, and it's also easy to see that increasing or decreasing either h or k will leave the value of the left-hand side of the equation unchanged.

In fact, it can be proved that this method can generate all possible solutions to the equation, and that each solution will correspond to a unique (h, k) pair! (One fairly intuitive way to see this is to plot the solutions in 3D space and observe that they form a regular lattice of points on a 2D plane, and that the vectors (+1, −2, +1) and (0, +3, −5) span this lattice.) If we pick h and k from some distribution that (at least in theory) assigns a non-zero probability to every integer, then we'll have a non-zero probability of returning any valid solution. So, at least for one somewhat reasonable interpretation of the task (unbounded range, any distribution with full support) the following code should solve the task efficiently:

from random import gauss

def random_solution(n):
    h = int(gauss(0, 1000))  # any distribution with full support on the integers will do
    k = int(gauss(0, 1000))
    return (-n + h, n - 2*h + 3*k, n + h - 5*k)

If the range of possible values is restricted, the problem becomes a bit trickier. On the positive side, if all values are bounded below (or above), then the set of possible solutions is finite, and so a uniform distribution exists on it. On the flip side, efficiently sampling this uniform distribution is not trivial.

One possible approach, which you've used yourself, is to first generate all possible solutions (assuming there's a finite number of them) and then sample from the list of solutions. We can do the solution generation fairly efficiently like this:

  1. find all possible values of a for which the equation might have a solution,
  2. for each such a, find all possible values of b for which there still have a solution,
  3. for each such (a, b) pair, solve the equation for c and check if it's valid (i.e. an integer within the specified range), and
  4. if yes, add (a, b, c) to the set of solutions.

The tricky part is step 2, where we want to calculate the range of possible b values. For this, we can make use of the observation that, for a given a, setting c to its smallest allowed value and solving the equation gives an upper bound for b (and vice versa).

In particular, solving the equation for a, b and c respectively, we get:

  • a = (n − 5b − 3c) / 7
  • b = (n − 7a − 3c) / 5
  • c = (n − 7a − 5b) / 3

Given lower bounds on some of the values, we can use these solutions to compute corresponding upper bounds on the others. For example, the following code will generate all non-negative solutions efficiently (and can be easily modified to use a lower bound other than 0, if needed):

def all_nonnegative_solutions(n):
    a_min = b_min = c_min = 0
    a_max = (n - 5*b_min - 3*c_min) // 7
    for a in range(a_min, a_max + 1):
        b_max = (n - 7*a - 3*c_min) // 5
        for b in range(b_min, b_max + 1):
            if (n - 7*a - 5*b) % 3 == 0:
                c = (n - 7*a - 5*b) // 3
                yield (a, b, c)

We can then store the solutions in a list or a tuple and sample from that list:

from random import choice

solutions = tuple(all_nonnegative_solutions(30))
a, b, c = choice(solutions)

Ps. Apparently Python's random.choice is not smart enough to use reservoir sampling to sample from an arbitrary iterable, so we do need to store the full list of solutions even if we only want to sample from it once. Or, of course, we could always implement our own sampler:

def reservoir_choice(iterable):
    r = None
    n = 0
    for x in iterable:
        n += 1
        if randrange(n) == 0:
           r = x
    return r

a, b, c = reservoir_choice(all_nonnegative_solutions(30))

BTW, we could make the all_nonnegative_solutions function above a bit more efficient by observing that the (n - 7*a - 5*b) % 3 == 0 condition (which checks whether c = (n − 7a − 5b) / 3 is an integer, and thus a valid solution) is true for every third value of b. Thus, if we first calculated the smallest value of b that satisfies the condition for a given a (which can be done with a bit of modular arithmetic), we could iterate over b with a step size of 3 starting from that minimum value and skip the divisibility check entirely. I'll leave implementing that optimization as an exercise.

Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153
  • 11
    This is a much better solution than the current accepted answer. – Stef Oct 12 '20 at 14:25
  • 3
    It's not clear that the question has anything to do with "random" except that they don't care which solution is returned. – philipxy Oct 14 '20 at 01:07
36
import numpy as np


def generate_answer(n: int, low_limit:int, high_limit: int):
    while True:
        a = np.random.randint(low_limit, high_limit + 1, 1)[0]
        b = np.random.randint(low_limit, high_limit + 1, 1)[0]
        c = (n - 7 * a - 5 * b) / 3.0
        if int(c) == c and low_limit <= c <= high_limit:
            break

    return a, b, int(c)


if __name__ == "__main__":
    n = 30
    ans = generate_answer(low_limit=-5, high_limit=50, n=n)
    assert ans[0] * 7 + ans[1] * 5 + ans[2] * 3 == n
    print(ans)

If you select two of the numbers a, b, c, you know the third. In this case, I randomize ints for a, b, and I find c by c = (n - 7 * a - 5 * b) / 3.0.

Make sure c is an integer, and in the allowed limits, and we are done.

If it is not, randomize again.


If you want to generate all possibilities,

def generate_all_answers(n: int, low_limit:int, high_limit: int):
    results = []
    for a in range(low_limit, high_limit + 1):
        for b in range(low_limit, high_limit + 1):
            c = (n - 7 * a - 5 * b) / 3.0
            if int(c) == c and low_limit <= c <= high_limit:
                results.append((a, b, int(c)))

    return results
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Gulzar
  • 23,452
  • 27
  • 113
  • 201
  • 6
    I would just note that in some "three way" random choices like this, you (usually / often) have to randomly decide **which of the three** is the one determined by the others. – Fattie Oct 12 '20 at 18:02
  • 3
    When the coefficient on `c` is some large number (like `3139293`) instead of `3`, you may want to rethink using this approach. – John Oct 13 '20 at 01:20
  • 4
    Any reason you are using `numpy.random` instead of the standard `random` module? – anrieff Oct 13 '20 at 14:29
  • @anreiff just used to it. Any reason not to? – Gulzar Oct 13 '20 at 14:39
  • 4
    This answer can recognize a solution, but not decide a solution. That is, if a solution is impossible, this program will never tell you so and simply loop forever. The generate all answers is clearly superior, even if you just return at the first result. Randomness just adds an unnecessary complexity and possibly doing too much work for no real benefit – Cruncher Oct 13 '20 at 14:49
  • @Cruncher you are correct. The random answer is there because that is what the OP wanted. At least before some edits. Or at least the what i understood. – Gulzar Oct 13 '20 at 14:50
  • 10
    @Gulzar _"Any reason not to?"_ - Yes: `ModuleNotFoundError: No module named 'numpy'`. It's bad practice to require an external library for something that can be done equally well using the standard library. Actually, the standard library would make the program slightly shorter (`import random; random.randint(low_limit, high_limit + 1)`). – marcelm Oct 13 '20 at 16:41
  • 1
    `np.random.randint(low_limit, high_limit + 1, 1)[0]` would be better written as just `np.random.randint(low_limit, high_limit + 1)`, though that doesn't solve the other issues with this answer. Ilmari Karonen's answer is much better. – user2357112 Oct 13 '20 at 18:12
  • @Fattie actually, since this specific problem is a bounded plane, it doesn't matter which parameter is selected first. – Gulzar Oct 14 '20 at 08:02
  • 1
    @marcelm see https://stackoverflow.com/questions/64350677/is-using-np-random-bad-practice-for-a-single-int/64350732#64350732 if it interests you. Thanks for pointing this out. – Gulzar Oct 14 '20 at 09:54
  • 1
    @Cruncher is perfectly correct. it's an "indeterminate algorithm". – Fattie Oct 14 '20 at 10:03
15

If third-party libraries are allowed, you can use SymPy's diophantine.diop_linear linear Diophantine equations solver:

from sympy.solvers.diophantine.diophantine import diop_linear
from sympy import symbols
from numpy.random import randint

n = 30
N = 8 # Number of solutions needed

# Unknowns
a, b, c = symbols('a, b, c', integer=True)

# Coefficients
x, y, z = 7, 5, 3

# Parameters of parametric equation of solution
t_0, t_1 = symbols('t_0, t_1', integer=True)

solution = diop_linear(x * a + y * b + z * c - n)

if not (None in solution):
  for s in range(N):
    # -10000 and 10000 (max and min for t_0 and t_1)
    t_sub = [(t_0, randint(-10000, 10000)), (t_1, randint(-10000, 10000))]

    a_val, b_val, c_val = map(lambda t : t.subs(t_sub), solution)

    print('Solution #%d' % (s + 1))
    print('a =', a_val, ', b =', b_val, ', c =', c_val)
else:
  print('no solutions')

Output (random):

Solution #1
a = -141 , b = -29187 , c = 48984
Solution #2
a = -8532 , b = -68757 , c = 134513
Solution #3
a = 5034 , b = 30729 , c = -62951
Solution #4
a = 7107 , b = 76638 , c = -144303
Solution #5
a = 4587 , b = 23721 , c = -50228
Solution #6
a = -9294 , b = -106269 , c = 198811
Solution #7
a = -1572 , b = -43224 , c = 75718
Solution #8
a = 4956 , b = 68097 , c = -125049
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
DjaouadNM
  • 22,013
  • 4
  • 33
  • 55
13

Why your solution can't cope with large values of n

You may understand that everything in a for loop with a range of i, will run i times. So it will multiply the time taken by i.

For example, let's pretend (to keep things simple) that this runs in 4 milliseconds:

if 7*a + 5*b + 3*c = n:
    c.append(a)
    k.append(b)
    w.append(c)

then this will run in 4×n milliseconds:

for c in range(n):
    if 7*a + 5*b + 3*c = n:
        c.append(a)
        k.append(b)
        w.append(c)

Approximately:

  • n = 100 would take 0.4 seconds
  • n = 250 would take 1 second
  • n = 15000 would take 60 seconds

If you put that inside a for loop over a range of n then the whole thing will be repeated n times. I.e.

for b in range(n):
    for c in range(n):
        if 7*a + 5*b + 3*c = n:
            c.append(a)
            k.append(b)
            w.append(c)

will take 4n² milliseconds.

  • n = 30 would take 4 seconds
  • n = 50 would take 10 seconds
  • n = 120 would take 60 seconds

Putting it in a third for-loop will take 4n³ milliseconds.

  • n = 10 would take 4 seconds
  • n = 14 would take 10 seconds.
  • n = 24 would take 60 seconds.

Now, what if you halved the original if to 2 milliseconds? n would be able to increase by 15000 in the first case... and 23 in the last case. The lesson here is that fewer for-loops is usually much more important than speeding up what's inside them. As you can see in Gulzar's answer part 2, there are only two for loops which makes a big difference. (This only applies if the loops are inside each other; if they are just one after another you don't have the multiplication problem.)

Artelius
  • 48,337
  • 13
  • 89
  • 105
2

from my perspective, the last number of the three is never a random number. let say you generate a and b first then c is never a random because it should be calculated from the equation

n = 7*a + 5*b + 3*c
c = (7*a + 5*b - n) / -3

this means that we need to generate two random values (a,b) that 7*a + 5*b - n is divisible by 3

import random

n = 30;
max = 1000000;
min = -1000000;

while True:
  a = random.randint(min , max);
  b = random.randint(min , max);
  t = (7*a) + (5*b) - n;
  if (t % 3 == 0) :
    break;

c = (t/-3);

print("A = " + str(a));
print("B = " + str(b));
print("C = " + str(c));
print("7A + 5B + 3C =>")
print("(7 * " + str(a) + ") + (5 * " + str(b) + ") + (3 * " + str(c) + ") = ")
print((7*a) + (5*b) + (3*c));

REPL

Ali Faris
  • 17,754
  • 10
  • 45
  • 70
  • If read the question, which the asker did not put "random" in, "random" in the titile just means "any" & there is no randomness intended in the stochastic sense. See my other comments on this page. – philipxy Oct 15 '20 at 23:20
  • I think "any" and "random" mean the same thing in this case – Ali Faris Oct 16 '20 at 14:33
  • Callling a random routine is a poor thing to do to find a solution to the equation. – philipxy Oct 16 '20 at 21:32