3
\$\begingroup\$

Related to this question I am still looking for a solution to Project Euler Problem 566 (see link for a nice simulation also):

Adam plays the following game with his birthday cake.

He cuts a piece forming a circular sector of 60 degrees and flips the piece upside down, with the icing on the bottom. He then rotates the cake by 60 degrees counterclockwise, cuts an adjacent 60 degree piece and flips it upside down. He keeps repeating this, until after a total of twelve steps, all the icing is back on top.

Amazingly, this works for any piece size, even if the cutting angle is an irrational number: all the icing will be back on top after a finite number of steps.

Now, Adam tries something different: he alternates cutting pieces of size \$x=\frac{360}{9}\$ degrees, \$\frac{y=360}{10}\$ degrees and \$z=\frac{360}{\sqrt{11}}\$ degrees. The first piece he cuts has size x and he flips it. The second has size y and he flips it. The third has size z and he flips it. He repeats this with pieces of size x, y and z in that order until all the icing is back on top, and discovers he needs 60 flips altogether.

Let \$F(a,b,c)\$ be the minimum number of piece flips needed to get all the icing back on top for pieces of size \$x=\frac{360}{a}\$ degrees, \$y=\frac{360}{b}\$ degrees and \$z=\frac{360}{\sqrt{c}}\$ degrees.
Let \$G(n) = \sum\limits_{9<=a<b<c<=n} F(a,b,c)\$, for integers a, b and c.

You are given that \$F(9, 10, 11) = 60\$, \$F(10, 14, 16) = 506\$, \$F(15, 16, 17) = 785232\$. You are also given \$G(11) = 60\$, \$G(14) > = 58020\$ and \$G(17) = 1269260\$.

Find \$G(53)\$.

My approach is the following:

  • the cake is always rotated so that the slices start at 0
  • I save all areas that are upside down with tuples of ranges
  • every new slice then checks for all areas, if they are inside the range of the slice
  • all affected areas are joined and rotated around the middle of the piece of cake.

The code seems to be working fine (passing all unit tests), however the performance is so slow that it would take an astronomical amount of time to solve the actual problem.


from math import sqrt

def get_affected_areas(areas:list,angle:float,start_angle:float=0.0)->tuple:
    """function that from a list of areas splits into affected and unaffected areas"""
    areas_affected=[]
    areas_new=[]
    
    for area in areas:
        ## 11 area completely in target space
        if start_angle<=area[0]<area[1]<=angle:
            areas_affected.append(area)
        
        ## 12 area with upperbound in target space
        elif start_angle<area[1]<=angle:
            ar_out,ar_in=(area[0],start_angle),(start_angle,area[1])
            areas_new.append(ar_out)
            areas_affected.append(ar_in)

        ## 13 area with lowerbound in target space
        elif start_angle<=area[0]<angle:
            ar_in,ar_out=(area[0],angle),(angle,area[1])
            areas_new.append(ar_out)
            areas_affected.append(ar_in)

        ## 14 area engulfing targetspace
        elif start_angle<angle<area[1]<area[0]:
            ar_in,ar_out_lower,ar_out_upper=(start_angle,angle),(area[0],start_angle),(angle,area[1])
            areas_new.append(ar_out_lower)
            areas_new.append(ar_out_upper)
            areas_affected.append(ar_in)

        ## 15 uneffected area
        else:
            areas_new.append(area)
            
    return areas_affected,areas_new

def rotate_num(value:float, center:float)->float:
    """function to rotate values around center, when a piece of cake is flipped."""
    dist=center-value
    return center+dist

def get_pos_area(area:tuple, angle:float, angle_start:float=0.0)->tuple:
    """get the positive area values beloning to a negative area"""
    center=(angle+angle_start)/2
    return (rotate_num(area[1],center),rotate_num(area[0],center))

def flip_affected_areas(areas_affected:list,angle:float,start_angle:float=0.0)->list:
    """
    function that takes a list of negative areas.
    eturns the negative areas after the region has been flipped.
    """
    ## 21 get new positive areas
    area_affected_positive=[]

    # get all areas, that are not negative in the target area. 
    # these (to be precise their rotated values) are the ones that will become negative after being flipped 
    for ea in areas_affected:
        ar_pos=get_pos_area(area=ea,angle=angle,angle_start=start_angle)
        area_affected_positive.insert(0,ar_pos)
        
    ## 22 get negative areas from positives -- if no areas, all negative
    areas_affected_flipped=[]
    pos=start_angle

    for ap in area_affected_positive:
        ar_neg=(pos,ap[0])
        areas_affected_flipped.append(ar_neg)
        pos=ap[1]
        
    try: 
        ar_neg=(area_affected_positive[-1][1],angle)

        areas_affected_flipped.append(ar_neg)

        ## 23 deleting empty areas
        areas_affected_flipped=[an for an in areas_affected_flipped if an[0]!=an[1]]

    except (IndexError):
        # this error happens if area_affected_positive is empty sinc there exist no positive areas. 
        # this is not problematic, as in this case no areas are appended to areas_new.
        areas_affected_flipped.append((start_angle,angle))
    
    return areas_affected_flipped


def sort_pieces(piece)->float:
    """function for sorting pieces based on second value (upper bound)"""
    return piece[1]

def f(
    a:int,
    b:int,
    c:int,
    max_counter:int=1_269_260,
    log_threshold:int=1e6,
    epsilon:float=1e-10
    )->int:
    """"
    function to run a while loop of rotating cake pieces of sizes a,b,c cyclically
    returns number of steps needed until the upperside of the cake is on top again
    max_counter set by default to 1.2M as this is the expected result of g(17)
    """
    
    # 0 Initialize an empty vector to store negative areas.
    # all negative areas will be collected and deducted based on cake piece rotation
    # when the list areas is empty this means there exist no negative areas
    # hence then the cake is perfectly upside up
    areas=[]

    # angle length
    angles=(360/a,360/b,360/sqrt(c))

    # counter of the iterations. will also be used to change the angles applied at each step
    counter=0

    # start angle is always at 0, since the cake will be rotated. this drastically reduces complexity
    start_angle=0.0

    # termination criteria for while loop
    cake_unfinished=True
    
    while cake_unfinished:
        # get current angle
        angle=angles[counter%3]

        # # 1 identify all affected areas as well as uneffected areas and put into new arrays
        areas_affected,areas_new=get_affected_areas(
            areas=areas,
            angle=angle,
            start_angle=start_angle
            )

        # 2 flip affected areas 
        areas_affected_flipped=flip_affected_areas(
            areas_affected=areas_affected,
            angle=angle,
            start_angle=start_angle
            )
        
        # 3 join effected and uneffected areas
        areas_new+=areas_affected_flipped

        # 4 rotate cake (north the cake)

        ## 41 add degrees (cake needs to be rotated the amount of the next angle)
        angle_rotate=angles[(counter+1)%3]
        areas_new=[((a[0]+angle_rotate)%360,(a[1]+angle_rotate)%360) for a in areas_new]

        ## 42 sort for next iteration
        areas_new.sort(key=sort_pieces)

        # 5 update for next iteration
      
        ## 51 throw out "tiny" areas, as they occur due to rounding issues
        areas=[a for a in areas_new if abs(a[0]-a[1])>epsilon]
        
        ## 52 count up for next iteration
        counter+=1

        # give periodical logs to confirm program is still running
        if (counter%log_threshold)==0:
            print(f"cake {a,b,c} at counter {counter}")
            pass

        # checkout when cake is finished
        if len(areas)==0:
            cake_unfinished=False
            print(f"cake {a,b,c} finalized after {counter} iterations!")
            return counter
        # checkout in case cake does not finish in time
        elif counter>max_counter: 
            cake_unfinished=False
            print(f"cake not finilized after {counter} iterations!")
            return 0

# tests for f
assert f(a=9,b=10,c=11)==60
assert f(a=10,b=14,c=16)==506
assert f(a=15,b=16,c=17)==785_232

def g(
    upper_bound:int,
    lower_bound:int=9,
    max_counter:int=1269260,
    log_threshold:int=1e6,
    epsilon:float=1e-10
    )->int:
    """function iterating over all possible combinations of f() within specified upper_bound """
    print(f"\n---------------------------------\nStarting g with upper bound of {upper_bound}\n---------------------------------\n")

    sum=0

    # generate all possibilities for f(a,b,c) and sum the results into sum.
    for a in range(lower_bound,upper_bound-1):
        print(f"\n-----------\na now at {a}\n-----------\n")
        for b in range(a+1,upper_bound):
            for c in range(b+1,upper_bound+1):
                
                res=f(a=a,b=b,c=c,max_counter=max_counter,log_threshold=log_threshold,epsilon=epsilon)
                # if f() aborts due to reaching max_counter, it returns 0
                if res==0:
                    print(f"Error for f({(a,b,c)}) - max_counter {max_counter} reached")
                    return res
                else:
                    sum+=res

    print(f"\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nFinished g with upper_bound {upper_bound}! Resulting sum = {sum}\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n")
    return sum

# tests for g
assert g(upper_bound=11)==60
assert g(upper_bound=14)==58_020
assert g(upper_bound=17)==1_269_260

How can I change (areas of) my code to make it run more efficiently?

What else I've tried

One possible improvement would be joining two areas like (0,1) and (1,5) into (0,5), to reduce the time it takes to iterate over all the areas (by reducing the areas). For this I wrote a function rationalize_areas() that can be seen below. It runs inside a while loop but it is just making the code run much longer. Some other adjustments need to be made to the code if the function should be included.

def rationalize_areas(area:list,epsilon:float=1e-10)->list:
    """given a list of tuples joins tuples when applicable"""
    max_idx=len(area)

    for i1 in range(0,max_idx-1):
        for i2 in range(i1+1,max_idx):

            if -epsilon<(area[i1][0]-area[i2][1])<epsilon:
                area.append((area[i2][0],area[i1][1]))
                del area[i1],area[i2-1]
                return True
            if -epsilon<(area[i1][1]-area[i2][0])<epsilon:
                area.append((area[i1][0],area[i2][1]))
                del area[i1],area[i2-1]
                return True
    
    return False
\$\endgroup\$
1
  • 2
    \$\begingroup\$ This is a 100% difficulty problem on Project Euler that less than 200 people have solved. Unless you have the correct mathematical approach, nothing else matters. \$\endgroup\$
    – qwr
    Commented Jun 29 at 2:07

1 Answer 1

-1
\$\begingroup\$

your code for definition needs a lot of cycles (this makes it slow); to make it faster you need to use more efficient formulas or a different language like c or c++. If you want to use python, I suggest you to.

  1. Use a library like Numba or something similar
  2. try Pypy is a JIT compiler;
  3. I rewrote function g() like return sum([f(a = a,b = b,c = c,max_counter = max_counter,log_threshold = log_threshold, epsilon = epsilon) for a in range(lower_bound, upper_bound-1) for b in range(a+1,upper_bound) for c in range(b+1,upper_bound+1)]);
  4. remove unused variables in flip_affected_areas(), rotate_num() and other part of your code;
  5. optimizes the print()'s use, sometimes they ask for data that is not necessary for the calculations;
  6. use space in your code.
\$\endgroup\$
4
  • \$\begingroup\$ Please avoid quoting articles of clearly low quality such as "13 Ways". He applies a correct \$O(n^2)\$ str catenation answer to the wrong situation. Filterfalse is nice because it's lazy, but there's no speedup, rather he used num100 vs num500 workloads, plus a wrong indent caused early exit without looping. The LRU cache is fabulous, but it's cheating to use that global cache across benchmark timings. Tip9's map() is nice, except it's lazy and he didn't use list(output) to draw values from that generator. It's like he saw a good timing and didn't stop to think what was behind it. \$\endgroup\$
    – J_H
    Commented May 26 at 16:39
  • \$\begingroup\$ @toolic thanks for reporting the spelling error. I didn't quite understand the other comment. @J_H from the article I understood the potential of sum(), however, yes the article is of low quality, but a starting point. \$\endgroup\$
    – AsrtoMichi
    Commented May 26 at 20:15
  • \$\begingroup\$ Oh sure, definitely better to let the cPython interpreter's C code grovel over list elements with sum() (or let numpy do it), than to patiently wait for bytecode interpretation of a for loop to accomplish the same thing. That said, always opt for a small memory footprint generator being summed, instead of allocating a giant list. // I was distressed by the veneer of "look, I measured this! You can believe me!" when the results were so obviously unbelievable, and the things being tested diverged so much from what the narrative prose suggested. \$\endgroup\$
    – J_H
    Commented May 26 at 21:21
  • \$\begingroup\$ Nothing in this answer will actually help solve the problem because it's fundamentally a number theory problem, not a coding one. \$\endgroup\$
    – qwr
    Commented Jun 29 at 2:09

Not the answer you're looking for? Browse other questions tagged or ask your own question.