Google foobar gearing_up_for_destruction

Question:

I was doing the google foobar challenge but ran out of time on the following challenge i am trying to see what i did wrong.

Challenge

As Commander Lambda’s personal assistant, you’ve been assigned the task of configuring the LAMBCHOP doomsday device’s axial orientation gears. It should be pretty simple – just add gears to create the appropriate rotation ratio. But the problem is, due to the layout of the LAMBCHOP and the complicated system of beams and pipes supporting it, the pegs that will support the gears are fixed in place.

The LAMBCHOP’s engineers have given you lists identifying the placement of groups of pegs along various support beams. You need to place a gear on each peg (otherwise the gears will collide with unoccupied pegs). The engineers have plenty of gears in all different sizes stocked up, so you can choose gears of any size, from a radius of 1 on up. Your goal is to build a system where the last gear rotates at twice the rate (in revolutions per minute, or rpm) of the first gear, no matter the direction. Each gear (except the last) touches and turns the gear on the next peg to the right.

Given a list of distinct positive integers named pegs representing the location of each peg along the support beam, write a function answer(pegs) which, if there is a solution, returns a list of two positive integers a and b representing the numerator and denominator of the first gear’s radius in its simplest form in order to achieve the goal above, such that radius = a/b. The ratio a/b should be greater than or equal to 1. Not all support configurations will necessarily be capable of creating the proper rotation ratio, so if the task is impossible, the function answer(pegs) should return the list [-1, -1].

For example, if the pegs are placed at [4, 30, 50], then the first gear could have a radius of 12, the second gear could have a radius of 14, and the last one a radius of 6. Thus, the last gear would rotate twice as fast as the first one. In this case, pegs would be [4, 30, 50] and answer(pegs) should return [12, 1].

The list pegs will be given sorted in ascending order and will contain at least 2 and no more than 20 distinct positive integers, all between 1 and 10000 inclusive.

Test cases

Inputs:
(int list) pegs = [4, 30, 50]
Output:
(int list) [12, 1]

Inputs:
(int list) pegs = [4, 17, 50]
Output:
(int list) [-1, -1]

My current solution is as follows

def answer(pegs):
    n = len(pegs)
    g = range(n)
    k = pegs[1] - pegs[0]
    for i in range(0,k,2):
        g[0] = i
        for j in range(1,n):
            g[j] = (pegs[j] - pegs[j-1]) - g[j-1]   
        if any(b < 1 for b in g):
            continue
        if 1.0*g[0]/g[-1] == 2.0:
            return [g[0],1]
    return [-1, -1]

I could only get 6 test cases to pass I have now ran out of time but i am curious as to what the right solution was

Asked By: Rajdeep Dosanjh

||

Answers:

I think your solution is along the right lines, but doesn’t allow for a fractional radius.

Note that we can consider your algorithm symbolically, setting g[0]=x, and then computing all the g[j] values in terms of x. It turns out that each g[j] is a linear function of x (with gradient 1 or -1).

You will therefore find that g[-1] = a+mx where m is +1 or -1, and a is an integer.

For a solution to exist you need to solve the equation:

g[0]/g[-1] = 2
x/(a+mx) = 2
x=2(a+mx)
x(1-2m)=2a
x=2a/(1-2m)

so this gives a candidate value of x (as a fraction) which you can then recheck to make sure that no intermediate radius went negative.

Answered By: Peter de Rivaz

If you’re interested in a perfect working solution, this is what I wrote: https://gist.github.com/1lann/be45311db1bd8cbbe6650b0a3e9d1977

It constructs a system of equations where it solves the values for every radius of every gear. Here’s how it computes the solution for 4 pegs for example.

The system of equations would be:

2x + a = peg[1] - peg[0]
a + b = peg[2] - peg[1]
b + x = peg[3] - peg[2]

My program constructs a matrix to represent this:

[
    [2, 1, 0],
    [0, 1, 1],
    [1, 0, 1]
]

It then computes the inverse of the matrix, and then applies it to the distances between the pegs in order to find the radius of every gear. If you’re wondering how the maths work, you can look at: https://www.mathsisfun.com/algebra/systems-linear-equations-matrices.html

Each gear is then verified to have a radius >= 1, and finally the value of x*2 is returned. In order to support fractions (any rational number), all numbers are of a Fraction type.

I did hard code some edge cases, such as when the number of pegs = 2.

Answered By: 1lann

Here’s the working code in python 2.7 for which all the test cases were passed by Google. This is the best solution that I came up with after scratching papers for a while:

from fractions import Fraction  
def answer(pegs):
    arrLength = len(pegs)
    if ((not pegs) or arrLength == 1):
        return [-1,-1]

    even = True if (arrLength % 2 == 0) else False
    sum = (- pegs[0] + pegs[arrLength - 1]) if even else (- pegs[0] - pegs[arrLength -1])

    if (arrLength > 2):
        for index in xrange(1, arrLength-1):
            sum += 2 * (-1)**(index+1) * pegs[index]

    FirstGearRadius = Fraction(2 * (float(sum)/3 if even else sum)).limit_denominator()

    # now that we have the radius of the first gear, we should again check the input array of pegs to verify that
    # the pegs radius' is atleast 1.
    # since for valid results, LastGearRadius >= 1 and FirstGearRadius = 2 * LastGearRadius
    # thus for valid results FirstGearRadius >= 2

    if FirstGearRadius < 2:
        return [-1,-1]

    currentRadius = FirstGearRadius
    for index in xrange(0, arrLength-2):
        CenterDistance = pegs[index+1] - pegs[index]
        NextRadius = CenterDistance - currentRadius
        if (currentRadius < 1 or NextRadius < 1):
            return [-1,-1]
        else:
            currentRadius = NextRadius

    return [FirstGearRadius.numerator, FirstGearRadius.denominator]

See this image for how I came up with this code:

Image

Answered By: Suresh Lamichhane
from fractions import Fraction

def answer(a):
  l = len(a)
  if(not a or l == 1): return [-1,-1]
  s = (a[l-1] - a[0]) if (l % 2 == 0) else (-a[l-1]-a[0]); 
  if(l > 2): 
      for i in range(1, l-1): s+= 2 * (-1)**(i+1) * a[i]
  v = Fraction(2*(float(s)/3 if (l%2==0) else float(s))).limit_denominator();
  c = v;
  for i in range(0, l-2):
    d = a[i+1] - a[i]
    n = d - c
    if(c < 1 or n < 1): return [-1,-1]
    else: c = n
  return [v.numerator, v.denominator];
Answered By: Val Do

My python code uses fairly basic operations to get things done without brute-forcing it. However I’m lazy and didn’t really comment, so you’re gonna have to figure it out yourself. It passed the foobar solution thing, so it definitely works.

def answer(pegs):
distances = [pegs[x+1]-pegs[x] for x in range(len(pegs)-1)]
x = 0 
for i in distances: #gets d_(n)-d_(n-1)+d_(n-2)...+-d_(1)
    x = i - x
#this tests if firstGearRadius is positive or negative
if len(distances)%2 == 0: #if it's positive
    solution =  [x*-2,1] 
elif len(distances)%2 == 1: #if it's negative
    if x*2 % 3 == 0: #if the numerator is divisible by 3
        solution = [x*2/3,1]
    else:
        solution = [x*2,3]
#finds sizes of the gears
gearSizes = [float(solution[0])/float(solution[1])]
x = gearSizes[0]
for i in distances:
    x = i - x
    gearSizes.append(x)
if any([True for x in gearSizes if x<=1]): #gears must be at least 1 unit radius
    return [-1,-1]
else:
    return solution
Answered By: moothemoo

For a one pass approach

Denote p_0,p_1,...,p_n the positions of the pegs.
Let me define a sequence a_k by the following recurrence relation

a_0 = 0
a_k = a_{k-1}+(-1)^{k+1}(p_k-p_{k-1})

If you compute a_n and simplify you see that this is the same alternating sum of p_k that you see in other answers in which most terms have a coefficient 2 except the first and the last one.

We will see below why it could be convenient to look at this sequence of numbers instead.

If we denote the radii of the gears by r_0,r_1,...,r_n, then they satisfy the equations

r_k = (-1)^k(r_0-a_k)

Also, the condition that the radii are not smaller than 1 is equivalent to the inequalities

r_0 >= a_0 + 1
r_1 <= a_1 - 1
r_2 >= a_2 + 1
r_3 <= a_3 - 1
...

This means that the full sequence of inequality conditions for the radii can be reduced to the single pair of inequalities

max(a_k+1, for k even) <= r_0 <= min(a_k - 1, for k odd)

Finally, the condition of doubling the speed is

(1+2(-1)^{n+1}) r_0 = 2a_n(-1)^{n+1}

So, computing the sequence a_n allows us to get both the answer and at the same time the restrictions on the radii in one pass.

Written in Python the code could look as follows. Feel free to improve it further.

define solution(pegs):
    n = len(pegs)
    if n<2:
        return (-1,-1)
    an = 0 # This contains, at each step the value of the sequence a_k
    pm_one = -1 # The alternating sign in the formulas above.
    # This and the next will get the bounds for the radii.
    max_even_a = -float("inf") 
    min_odd_a = float("inf")
    for i in range(n-1):
        an -= pm_one*(pegs[i+1]-pegs[i])
        pm_one *=-1
        if not i&1:
            min_odd_a = min(min_odd_a, an)
        else:
            max_even_a = max(max_even_a, an)
    # In the formulas above the numerator has a (-1)^{n+1} factor. 
    # Here the sign has been cancelled with the sign of the denominator.
    numerator = 2*an 
    denominator = abs(1+2*pm_one)
    # The inequalities in the explanation are here written as integers.
    # Note that here denominator is positive. So, passing it to the other side
    # doesn't change the sign of the inequality.
    # Of course, the inequalities have here the negated sign and an OR
    # because we are detecting when they fail.
    if numerator < denominator*(max_even_a+1) 
        or numerator > denominator*(min_odd_a-1):
        return (-1,-1)
    # Sometimes the denominator is 3. If it can be cancelled we do so.
    if pm_one == 1 and numerator%3 == 0:
        numerator //=3
        denominator = 1
    return (numerator, denominator)
Answered By: NotDijkstra

a passed solution:

from fractions import Fraction

def solution(p):
    n = len(p)

    if n >= 2:
        r0_n = -2 * (p[n - 1] + reduce(
            lambda a, b: a + b, [0] + [(-1)**i * 2 * p[i]
                                       for i in range(n - 2, 0, -1)]) + (-1)**(n-1)*p[0])

        r0_d = 1 + ((n+1) % 2)*2

        if r0_n < r0_d:
            return [-1, -1]

        r = ['NAN'] * n
        r[0] = float(r0_n) / float(r0_d)
        for i in range(1, n):
            r[i] = p[i] - p[i - 1] - r[i - 1]
            if r[i] < 1:
                return [-1, -1]

        r0 = Fraction(r0_n, r0_d)
        r0.limit_denominator()
        
        return [r0.numerator, r0.denominator]

    return [-1, -1]

some tests:

if __name__ == '__main__':
    print solution([4, 30, 50]), [12, 1]
    print solution([4, 17, 50]), [-1, -1]
    print solution([1, 51]), [100, 3]
    print solution([1, 31]), [20, 1]
    print solution([1, 31, 51, 71]), [20, 1]
    print solution([1, 31, 51, 71, 91]), [20, 1]
    print solution([4, 9, 17, 31, 40]), [4, 1] 

output:

[12, 1] [12, 1]
[-1, -1] [-1, -1]
[100, 3] [100, 3]
[20, 1] [20, 1]
[20, 1] [20, 1]
[20, 1] [20, 1]
[4, 1] [4, 1]

some thinking

#    equaltion         | i
# ---------------------|---
# / r0 + r1 == p1 - p0  [0]
# | r1 + r2 == p2 - p1  [1]
# | r2 + r3 == p3 - p2  [2]
# | r3 + r4 == p4 - p3  [3]
# | r4 + r5 == p5 - p4  [4]
#       r5 == r0/2     [5]
#
#
# / r0 + r1 + 0  + 0  + 0  + 0    = p1 - p0
# | 0  + r1 + r2 + 0  + 0  + 0    = p2 - p1
# | 0  + 0  + r2 + r3 + 0  + 0    = p3 - p2
# | 0  + 0  + 0  + r3 + r4 + 0    = p4 - p3
# | 0  + 0  + 0  + 0  + r4 + r5   = p4 - p4
#  r0 + 0  + 0  + 0  + 0  - 2*r5 = 0
#
# / 1 1 0 0 0  0    / r0    / p1 - p0 
# | 0 1 1 0 0  0 |   | r1 |   | p2 - p1 |
# | 0 0 1 1 0  0 |   | r2 |   | p3 - p2 |
# | 0 0 0 1 1  0 | * | r3 | = | p4 - p3 |
# | 0 0 0 0 1  1 |   | r4 |   | p5 - p4 |
#  1 0 0 0 0 -2 /    r5 /       0    /
Answered By: thiswind

After evaluating most of the solutions in this Q&A, I have put calculated the results.

Gist for coding logic for evaluating correct implementations(mostly correct, but has issues) and performance

Solutions evaluated with links, and time of implementations(since people are finding faster and faster solutions over time). All solutions were verified excepted for ThisWind’s solution

  • NotDijkstra(aka. franklinvp) – June 19, 2020 Easily the most performant
  • Val Do – Feb 16, 2018
  • ThisWind – Has issues with this, and couldn’t get it to verify(5 pass and 5 failed). This could be a potential error on my part.
  • Lamichhane – Aug 11, 2017
  • cbarraford – Mar 10, 2017
  • 1lann – Nov 11, 2016
  • Dayz – Sep 27, 2018

Performance results

Number of test iterations: 10000
solutionValDo :  7.3772001
solutionThisWind :  1.1203797
solutionNotDijkstra :  0.3143226
salutionLamichhane :  6.6052445
solutioncbarraford :  26.4765467
solution1lann :  147.5525556
solutionDayz :  6.619154 
Answered By: Ashitakalax

I got their invitation last night and I worked on this problem for several hours. I created my solution of this problem.

from fractions import Fraction
from sympy import symbols, solve


def create_equations(_pegs, _isEven, smbls, r0):
    eq = None
    temp = symbols('temp')
    if _isEven:
        for i in range(len(smbls)):
            if i == 0:
                eq = -r0 - r0 / 2 - smbls[i] + temp
            if 0 < i < len(smbls):
                eq = eq.subs(temp, (-1) ** (i + 1) * 2 * smbls[i] + temp)
            if i == len(smbls) - 1:
                eq = eq.subs(temp, - smbls[i])
    else:
        for i in range(len(smbls)):
            if i == 0:
                eq = -r0 + r0 / 2 - smbls[i] + temp
            if 0 < i < len(smbls):
                eq = eq.subs(temp, (-1) ** (i + 1) * 2 * smbls[i] + temp)
            if i == len(smbls) - 1:
                eq = eq.subs(temp, smbls[i])
    return eq


def create_symbols(len_pegs):
    smbls = []
    for i in range(len_pegs):
        smbls.append(symbols("P" + str(i)))
    r0 = symbols("r0")
    return smbls, r0


def answer(pegs):

    # first check
    len_pegs = len(pegs)
    if (not pegs) or len_pegs == 1:
        return [-1, -1]

    isEven = True if (len_pegs % 2 == 0) else False

    # create list of symbols used in equation based on list length
    smbls, r0 = create_symbols(len_pegs)

    # the function returns an equation based on the following equation
    # for even list length : (0 = r0 + rn -Pn + 2Pn-1 - 2Pn-2 + 2Pn-3 ... - P1 + P0)
    # for odd list length : (0 = r0 - rn -Pn + 2Pn-1 - 2Pn-2 + 2Pn-3 ... + P1 - P0)
    #  where rn = r0/2
    equation = create_equations(pegs, isEven, smbls, r0)

    # substituting values of variables in the equation
    for i in range(len_pegs):
        equation = equation.subs(smbls[i], pegs[i])

    # solving the equation and simplifying float values to simple fraction
    radius_g1 = float(solve(equation)[0])
    radius_g1 = Fraction(radius_g1).limit_denominator()

    # finally we check if radius of any gear is less than one as required
    radius_cur = radius_g1
    for i in range(0, len_pegs-1):
        dist_c = pegs[i + 1] - pegs[i]
        radius_next = dist_c - radius_cur
        if radius_cur < 1 or radius_next < 1:
            return [-1, -1]
        else:
            radius_cur = radius_next

    # return the end results [numerator, denominator]
    return [radius_g1.numerator, radius_g1.denominator]


if __name__ == "__main__":
    # try some examples
    print(answer([4, 30, 50, 56]))
    print(answer([4, 17, 50]))
Answered By: Nouman Ahsan

Adding my recursive solution here.


def solutionNader(pegs):
    
    """
        returns the radius of cog i
        ri = Xi -/+ r0
        also sets the ans for the fractional requirement.
    """
    def helper(X,i):
        if i == len(pegs)-1: # last cog, we need to make sure its half of r0
            X = 2*(pegs[i]-pegs[i-1]-X)
            if i%2:
                ans[0],ans[1] = [X,3] if (X)%3 else [X/3,1]
                return X/6.0
            else:
                ans[0],ans[1] = [-X,1]
                return -X/2
        #recursively calculate the radius of next cog
        r_next = helper(pegs[i]-pegs[i-1]-X,i+1) if i>0 else helper(0,1)

        #radius of r = gap bitween the pegs - r of next cog        
        r = pegs[i+1]-pegs[i]-r_next 

        if r < 1: 
            ans[0],ans[1] = [-1,-1]
            raise Exception('Invalid Cog')
        return r

    try:
        ans = [-1,-1]
        helper(0,0)
    finally:
        return ans

Kudos to @Ashitakalax for the performance tests

Number of test iterations: 10000

  • solutionNotDijkstra : 0.0879838466644
  • solutionNader : 0.119354009628
  • solutionThisWind : 0.224207162857
  • solutionDayz : 1.24098587036
  • salutionLamichhane : 1.32456707954
Answered By: Nader

I got this problem in Jan of 2020. After completing it, I wanted to see if anyone else did it my way, but it doesn’t look like it.

The positions of the pegs, along with the first radius, implies what the last gear radius will be. There is a linear relationship between the first gear radius and the last gear radius, as well as boundaries on the size of the first gear radius:

This is the function I wrote that will take in the peg positions and the starting gear radius and calculate the implied last gear radius:

def implied_last_radius(pegs, start_radius):
    diffs = [start_radius] + [x - y for x, y in zip(pegs[1:], pegs[:-1])]
    for i in range(1, len(diffs)):
        diffs[i] = diffs[i] - diffs[i - 1]

    return diffs[-1]

The problem states that all gears must has a radius >= 1, and the last gear must be half the size of the first gear. This puts a lower-bound on the first gear of a radius of 2 (i.e. any valid result with a first gear having a radius less than 2 will result in a last gear with a radius less than 1, which isn’t allowed). This also puts an upper bound on the first gear size, because the gear in the second peg must also have a minimum radius of 1. Therefore:

first_radius_diff = pegs[1] - pegs[0]
first_radius_range = (2, first_radius_diff - 1)

We can calculate the implied radius of each of the first radii using the above function. For the pegs [4, 30, 50] The answer is:

First Radii = (2, 25)
Implied Last Radii = (-4, 19)

That is, if the first radius is 2, the last radius must be -4 (invalid), and if the first radius is 25, the last radius is 19. The linear relationship can be modelled as y = mx + b:

m = (implied_radii[1] - implied_radii[0]) / (first_radius_range[1] - first_radius_range[0])
b = implied_radii[0] - m * first_radius_range[0]

Now we have a relation between the first and last gear radius sizes. We simply compute the first radius when the last radius is half the value:

first_radius = b / (0.5 - m)

Lastly, we check to see if that value is within the ranges of allowed first gear radius range, but also critically that it does not require any gears on any pegs that have a radius less than 1:

def solvable(pegs, start_radius, radius_range):
    diffs = [start_radius] + [x - y for x, y in zip(pegs[1:], pegs[:-1])]
    for i in range(1, len(diffs)):
        diffs[i] = diffs[i] - diffs[i - 1]

    return radius_range[0] <= start_radius <= radius_range[1] and all([diff >= 1 for diff in diffs])

The last tricky part that got me was converting to a simplified fractional form. This was easily solved by mapping all of the numerical values to a fractions.Fraction class. If first gear radius passed the solvable test, return the numerator and denominator, else return [-1, -1].

Answered By: mystery_lektro

This is the solution I came up with, I did it on 30 March 2021 and it passed both verify solution.py and submit solution.py.

Solution (pegs.py)

import numpy as np
from fractions import Fraction


def solution(pegs):
    # constraint I: gear[i] + gear[i+1] == distance[i]
    # constraint II: gear[0] == 2 * gear[-1]
    # constraint III: gear[0] >= 1
    #
    # I ensures that the gears fit into the spaces of the pegs
    # II ensures that the total gear ration is 2:1
    # III ensures that the gears have at least a radius of 1

    pegs = np.array(pegs)

    n = len(pegs)
    gear_1 = [-1, -1]
    distances = np.zeros(n - 1)
    system_equations = np.zeros([n, n + 1])

    # calculate the distances
    for i in range(n - 1):
        distances[i] = pegs[i + 1] - pegs[i]

    # The first equation is about constraint II
    system_equations[0, 0] = 1
    system_equations[0, -2] = -2

    # The other equations are about constraint I
    for i in range(n - 1):
        system_equations[i + 1, i] = 1
        system_equations[i + 1, i + 1] = 1
        system_equations[i + 1, -1] = distances[i]

    # check for solvability
    rank_a = np.linalg.matrix_rank(system_equations[:, 0:n])
    rank_ac = np.linalg.matrix_rank(system_equations)

    # System of linear equation has no solution (not full rank)
    if rank_a != rank_ac:
        return gear_1

    # The system of linear equations has indefinite solutions (not expected)
    if rank_ac < n:
        return gear_1

    # Solve the system of linear equations
    gears = np.linalg.inv(system_equations[:, 0:n]).dot(system_equations[:, n])

    # Check validity of the solution
    if is_no_solution(distances, gears):
        return gear_1

    # convert gear 1 to integer fraction
    frac_gear_1 = Fraction(gears[0]).limit_denominator()
    gear_1[0] = int(frac_gear_1.numerator)
    gear_1[1] = int(frac_gear_1.denominator)

    # return the solution
    return gear_1


# Check if the solution is valid
def is_no_solution(distances, gears):
    # Check constraint I
    for i in range(len(distances)):
        if gears[i] + gears[i + 1] - distances[i] > 0.01:
            return True

    # Check constraint II
    if gears[0] != gears[-1] * 2:
        return True

    # Check constraint III
    for gear in gears:
        if gear < 1:
            return True

    # no constraint has been violated thus the solution is valid
    return False

Testing(test_pegs.py)

from unittest import TestCase

from pegs import solution


class Test(TestCase):
    def test_solution(self):
        # provided test-cases
        self.assertEqual([12, 1], solution([4, 30, 50]))
        self.assertEqual([-1, -1], solution([4, 17, 50]))

        # my own test-cases
        self.assertEqual([10, 3], solution([5, 10]))

        # test-cases from Stack Overflow @thiswind
        self.assertEqual([100, 3], solution([1, 51]))
        self.assertEqual([20, 1], solution([1, 31]))
        self.assertEqual([20, 1], solution([1, 31, 51, 71]))
        self.assertEqual([20, 1], solution([1, 31, 51, 71, 91]))
        self.assertEqual([4, 1], solution([4, 9, 17, 31, 40]))
Answered By: KasparJohannes

I just finished my solution and it isn’t the most efficient, but it is the most didactic that I could do. When doing this solution, I focused on learning, instead of efficiency.

Like the others have said, we have a linear equation. See the thiswind’s answer to understand it better. If, even after his answer the linear equation isn’t clean, take a look at these classes on linear algebra.

Note that I’ll keep all my values in fractions, so I don’t need to convert decimals to fractions at the end. You may think that use float numbers is better, but if we found a repeating decimals, like 33.333333…, we won’t be able to convert it to fractions properly, because of the precision of float numbers. See:

number = 100/3 # 33.333333...
print(number) # 33.333333333333336
print(Fraction(number)) # Fraction(4691249611844267, 140737488355328)

So, I made some functions to help us in the future to solve the linear system.

The first one returns the result of subtracting two lines. For example: [1, 2, 3, 4, 5] – [2, 3, 2, 3, 5] = [-1, -1, 1, 2, 0]

def difference_of_lines(line1, line2):
    if (len(line1) != len(line2)):
        return -1

    aux = range(len(line1))

    for i in range(len(line1)):
        aux[i] = line1[i] - line2[i]

    return aux

And the second one returns the result of multiplying a number by each item of a line. For example: [1, 2, 3, 4, 5] * 2 = [2, 4, 6, 8, 10]

def multiply_row_by_number(row, number):
    aux = []

    for r in row:
        aux.append(r*number)

    return aux

Now, let’s make the function that solves our linear system:

def solve_linear_system(A, B):
    for i in range(len(A)):

        # let's turn the diagonal equals to 1
        B[i] = B[i] / A[i][i]
        A[i] = multiply_row_by_number(A[i], Fraction(1)/A[i][i])

        # now let's make all values on the column i be zero, except the cell (i, i), that must be 1
        for k in range(len(A)):
            if k == i:
                continue

            B[k] = B[k] - A[k][i] * B[i]
            A[k] = difference_of_lines(A[k], multiply_row_by_number(A[i], A[k][i]))

    return B

See this post about how to solve linear systems in python to understand this function better.

Great! Now that we can solve linear systems, we need to create the matrices A and B such that Ax = B.

def make_matrices(pegs):
    tam = len(pegs)

    # make it easier to access specific positions
    A = [[] * (tam)] * (tam)
    B = range(tam)

    B[0] = Fraction(pegs[1] - pegs[0])

    # Example for len(pegs) == 3
    # r_0 + r_1 = p_1 - p_0
    # r_1 + r_2 = p_2 - p_1
    # r_1 - 2*r_2 = 0
    # So:
    # A = [[1, 1, 0], [0, 1, 1], [1, 0, -2]]
    # B = [pegs[1] - pegs[0], pegs[2] - pegs[1], 0]

    for i in range(0, tam-1):
        A[i] = [Fraction(0)] * (tam)
        A[i][i] = Fraction(1)
        A[i][i+1] = Fraction(1)
        B[i] = Fraction(pegs[i+1] - pegs[i])

    A[-1] = [Fraction(0)] * (tam)
    A[-1][0] = Fraction(1)
    A[-1][-1] = Fraction(-2)
    B[-1] = Fraction(0)

    return A, B

Now, we can use our previous functions to make the solution function.

def solution(pegs):
    A, B = make_matrices(pegs)

    sizes = solve_linear_system(A, B)

    for s in sizes:
        if s < 1:
            return [-1, -1]

    return [sizes[0].numerator, sizes[0].denominator]

Some tests:

assert(solution([4, 30, 50]) == [12, 1])
assert(solution([4, 17, 50]) == [-1, -1])
assert(solution([i for i in range(500, 10001, 500)]) == [1000, 3])
assert(solution([1, 51]) == [100, 3])
assert(solution([1, 31]) == [20, 1])
assert(solution([1, 31, 51, 71]) == [20, 1])
assert(solution([1, 31, 51, 71, 91]) == [20, 1])
assert(solution([4, 9, 17, 31, 40]) == [4, 1])

Please, feel free to kindly point any grammar errors or make any questions 😀

Got this challenge in October 2021.

Arrived at the following reasoning:

total_length = 1r[0] + 2r[1] + 2r[2] + 2r[3] ... + 0.5r[0]

Refactor for

1.5r[0] = total_length - 2(r[1]+r[2]+r[3]+...)

Then, given that there are known distances between pegs:

d[0], d[1], d[2] ... d[n]

There is a system of linear equations:

r[1] = d[1]-r[2] 
r[2] = d[3]-r[3] 
r[3] = d[4]-r[4] 
... 
r[n] = d[n] - 0.5r[0]

Refactoring to express everything via r[0] gives us the following pattern for odd number of gaps:

1.5r[0] = total_length - 2(d[1]+d[3]+d[5]...) when number of gaps is odd
1.5r[0] = total_length - 2(d[1]+d[3]+d[5]...-0.5r[0]) when number of gaps is even.

Solve for r:

r[0] = (total_length - 2(d[1]+d[3]+d[5]...)/(0.5+num_gaps%2)
# will be .../1.5 when odd and .../0.5 when even

This makes sense, because when we reduce the equation with even number of gaps, the phrase (-0.5x) appears an odd number of times, and vice versa. When we extract 0.5r[0] from 2(d[1]+d[3]+…), it turns into 1r[0]. We’re also sampling every other gap, because the oddly positioned gaps (with even indices, since we’re starting at 0) will always cancel out once the equation is completely expanded.
After that, the only thing remains: check that all the radii are more than 1. We can use the same system of linear equations.

The eventual code is like this:

import fractions from Fraction

def solution(pegs):
    #skipping input validation (make sure that it's a sorted sequence of 2 or more positive integers)
    pegs = map(float, pegs)
    num_gaps = len(pegs)-1
    gaps= list(pegs[i+1]-pegs[i] for i in range(num_gaps)
    if any(gaps[i] < 2 for i in range(num_gaps)):
        return (-1,-1)
    total_length = pegs[-1]-pegs[0]
    indices = list(i*2+1 for i in range(num_gaps/2))
    evens = list(gaps[i] for i in indices)
    divisor = 0.5+num_gaps%2
    result = (total_length-2*sum(evens))/divisor
    radii = range(len(pegs))
    radii[0] = result
    for i in radii[1:]:
        radii[i]=dists[i-1]-radii[i-1]
    if result<2 or any(i<1 for i in radii):
        return (-1,-1)
    else:
        result = Fraction(result).limit_denominator()
        return (result.numerator, result.denominator())

Passed all tests and submitted successfully.

I’m not sure this is the best solution for performance, but it’s definitely very straightforward and has potential for code golf.

I tried using .as_integer_ratio(), but it didn’t work out, because it doesn’t play nicely with division by 3, so it would not pass tests, but using Fraction with .limit_denominator() while the rest of the code is identical gives different results.

Answered By: Nomenator

Just finished my solution:

def validate(pegs, radius):
  for i in range(1, len(pegs)):
    if radius < 1:
      return False
    radius = pegs[i] - (pegs[i - 1] + radius)
  return True

def solution(pegs):
  if len(pegs) < 2:
    return [-1,-1]
  
  summ = -pegs[0]
  sign = 1
  for i in range(1, len(pegs) - 1):
    summ += pegs[i] * 2 * sign
    sign = -sign
  summ += pegs[-1] * sign
  
  first_r = summ * 2
  divisor = 1

  if len(pegs) % 2 == 0:
    if first_r % 3 == 0:
      first_r /= 3
    else:
      divisor = 3

  if not validate(pegs, first_r / divisor):
    return [-1, -1]
  return [first_r, divisor]

Benchmarks result:

solutionValDo : 0.407623799983412
solutionThisWind : 0.14491929998621345 
solutionNotDijkstra : 0.07515150000108406
solutionLamichhane : 0.40241099998820573
solutioncbarraford : 0.7571794000105001
solution1lann : 7.518031900050119
solutionDayz : 0.3909747999859974
solutionPlayermet : 0.05188179999822751 <--
Answered By: Playermet

There are sufficient answers here, however I made a validator which might be useful. It just generates random valid peg positions, applies the provided solution and checks if the result is correct.

from random import randrange

class DoomsDayMachineSolutionValidator:
    def _validate_answer(self, peg_positions, first_radius):
        current_radius = first_radius
        for peg_position, next_position in zip(peg_positions, peg_positions[1:]):
            current_radius = next_position - peg_position - current_radius
        if 2 * current_radius == first_radius:
            return True
        return False

    def _get_random_radius(self):
        return randrange(0, 1000) / 4

    def _get_random_n(self):
        return randrange(2, 20)

    def _get_radiuses(self):
        first_radius = randrange(2, 100, 2)
        radiuses_random = [self._get_random_radius() for n in range(self._get_random_n())]
        rs = [first_radius] + radiuses_random + [int(first_radius / 2)]
        newList = [x / 4 for x in rs]
        return newList

    def _get_peg_positions_from_radiuses(self, radiuses):
        peg_positions = [self._get_random_radius()]
        for radius, next_radius in zip(radiuses, radiuses[1:]):
            peg_positions.append(peg_positions[-1] + radius + next_radius)
        return peg_positions

    def _get_test_case(self):
        return self._get_peg_positions_from_radiuses(self._get_radiuses())

    def run(self, solution_function, number_of_tests=1000):
        for i in range(number_of_tests):
            pos = self._get_test_case()
            sol = solution_function(pos)

            if self._validate_answer(pos, sol[0] / sol[1]) is False and sol != [-1, -1]:
                print(pos, sol)
                return False
        return True

validator = DoomsDayMachineSolutionValidator()
print(validator.run(solution))
Answered By: cheesus

Got this question on March 2023; came to see afterwards what solutions others came up with.

I did the same setting up a system of equations like others, but didn’t see anyone really optimize their answers for this specific question and also have decent explanations.

From @thiswind’s answer above

#    equation          | i
# ---------------------|---
# / r0 + r1 == p1 - p0  [0]
# | r1 + r2 == p2 - p1  [1]
# | r2 + r3 == p3 - p2  [2]
# | r3 + r4 == p4 - p3  [3]
# | r4 + r5 == p5 - p4  [4]
#       r5 == r0/2     [5]
#
#
# / r0 + r1 + 0  + 0  + 0  + 0    = p1 - p0
# | 0  + r1 + r2 + 0  + 0  + 0    = p2 - p1
# | 0  + 0  + r2 + r3 + 0  + 0    = p3 - p2
# | 0  + 0  + 0  + r3 + r4 + 0    = p4 - p3
# | 0  + 0  + 0  + 0  + r4 + r5   = p4 - p4
#  r0 + 0  + 0  + 0  + 0  - 2*r5 = 0
#
# / 1 1 0 0 0  0    / r0    / p1 - p0 
# | 0 1 1 0 0  0 |   | r1 |   | p2 - p1 |
# | 0 0 1 1 0  0 |   | r2 |   | p3 - p2 |
# | 0 0 0 1 1  0 | * | r3 | = | p4 - p3 |
# | 0 0 0 0 1  1 |   | r4 |   | p5 - p4 |
#  1 0 0 0 0 -2 /    r5 /       0    /

From here, we can just implement a full solver for r0, but given the matrix structure, we can do a simpler solver.

If we subtract the previous row from each row (skipping the last one), we get something like so:

(let d_n = p_n+1 – p_n; example: d_0 = p1 – p0)

# / 0 1 0 0 0   2    / r0    / d0 - 0                       
# | 0 0 1 0 0  -2 |   | r1 |   | d1 - d0                      |
# | 0 0 0 1 0   2 |   | r2 |   | d2 - (d1 - d0)               |
# | 0 0 0 0 1  -2 | * | r3 | = | d3 - (d2 - (d1 - d0))        |
# | 0 0 0 0 0   3 |   | r4 |   | d4 - (d3 - (d2 - (d1 - d0))) |
#  1 0 0 0 0  -2 /    r5 /    0                            /

Note that the ‘3’ above will be either a ‘3’ or ‘-1’ depending on len(input) being odd/even:

# / 0 1 0 0   2    / r0    / d0 - 0                
# | 0 0 1 0  -2 |   | r1 |   | d1 - d0               |
# | 0 0 0 1   2 | * | r2 |   | d2 - (d1 - d0)        |
# | 0 0 0 0  -1 |   | r3 | = | d3 - (d2 - (d1 - d0)) |
#  1 0 0 0  -2 /    r5 /    0                     /

If we put all the numbers in a matrix so that:

# / 0 1 0 0 0  2  d0                    
# | 0 0 1 0 0 -2 (d1-d0)                |
# | 0 0 0 1 0  2 (d2-(d1-d0))           |
# | 0 0 0 0 1 -2 (d3-(d2-(d1-d0)))      |
# | 0 0 0 0 0  3 (d4-(d3-(d2-(d1-d0)))) |
#  1 0 0 0 0 -2  0                     /

we can call that ‘3’ or ‘-1’ value ‘matrix[-2][-2]’

Taking the last two rows:

matrix[-2][-2]*r5 = matrix[-2][-1]
r0 + matrix[-1][-2]*r5 = matrix[-1][-1]

Filling in the values and since matrix[-1][-2] will always be ‘-2:

matrix[-2][-2]*r5 = (d4-(d3-(d2-(d1-d0))))
r0 -2*r5 = 0

Solving for r0 yields:

r0 = 2*(d4-(d3-(d2-(d1-d0))))/matrix[-2][-2]

Implementation:

def solution(x):
    n = len(x)

    if n < 2:
        return -1, -1

    # Calculate differences between distances
    diff = [0] * (n-1)
    for i in range(n-1):
        diff[i] = x[i+1] - x[i]

    # Calculate "d_n - (d_n-1 - (d_n-2 - (d-n-3 - ...)))"
    val = diff[0]
    for i in range(n-2):
        val = diff[i+1] - val

    # The denominator is 3 or -1
    denom = 3 if n % 2 == 0 else -1

    # Use fractions.Fraction to reduce fraction to positive ints
    from fractions import Fraction
    s = Fraction(2*val, denom)
    if s < 2:
        return -1, -1
    return s.numerator, s.denominator

Might be missing some edge cases and the first two loops can be combined, but that’s what I think should be the optimal solution.

Answered By: th0m4s
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.