Teaser 3010: Putting game
From The Sunday Times, 31st May 2020 [link] [link]
A putting game has a board with eight rectangular holes, like the example (not to scale), but with the holes in a different order.
If you hit your ball (diameter 4cm) through a hole without touching the sides, you score the number of points above that hole. The sum of score and width (in cm) for each hole is 15; there are 2cm gaps between holes.
I know that if I aim at a point on the board, then the ball’s centre will arrive at the board within 12cm of my point of aim, and is equally likely to arrive at any point in that range. Therefore, I aim at the one point that maximises my long-term average score. This point is the centre of a hole and my average score is a whole number.
(a) Which hole do I aim at?
(b) Which two holes are either side of it?
[teaser3010]




Jim Randell 9:23 am on 30 May 2020 Permalink |
The 4cm diameter ball is not allowed to touch the sides of the hole, so we can consider the outside 2cm of each hole to be “out of bounds”. Which is the same as if the ball was a point, the gaps between holes are extended 2cm in each direction (so they become 6cm wide), and each hole is reduced by a corresponding 2cm on either edge.
To be sure I satisfy all the conditions this Python program constructs all possible orderings for the holes, and then for each ordering looks at possible target positions. It looks for orderings that have a unique maximum targeting point that is at the centre of a hole, and that yields an integer average score per shot. Once an ordering passes these tests we record the hole that the target is in, along with the two holes on either side. This gives a unique target + left/right value (although there are many orderings that contain the three holes appropriately positioned).
It runs in 1.58s, but I think this could be improved with some additional analysis.
from enigma import (irange, subsets, multiset, ordered, printf) # available holes (points) holes = [ 1, 2, 3, 4, 5, 6, 7, 8 ] # layout for a sequence of holes # return a list of: (region size (in mm), points scored) pairs def layout(ps): for (i, p) in enumerate(ps): if i > 0: yield (60, 0) # gap between holes yield (110 - 10 * p, p) # hole size # generate intervals ((a, b), p) from a layout # where a, b are absolute distances, p is number of points scored def intervals(ss): a = b = 0 for (d, p) in ss: if b == 0: (a, b) = (0, d) else: (a, b) = (b, b + d) yield ((a, b), p) # analyse the layout (working in 5mm steps) # return list of (d, (v, r)) values, where: # d = absolute target distance # v + r/240 = max average score def analyse(ss): # determine total length t = sum(d for (d, _) in ss) rs = list() # consider target positions (in 5mm steps) for x in irange(120, t - 120, step=5): # consider each zone r = 0 for ((a, b), p) in intervals(ss): # overlap? if b < x - 120: continue if a > x + 120: break d = min(x + 120, b) - max(x - 120, a) r += p * d rs.append((x, r)) # find maximum average value m = max(r for (_, r) in rs) return list((x, divmod(r, 240)) for (x, r) in rs if r == m) # collect results m = multiset() # choose an order for the holes for ps in subsets(holes, size=len, select="P"): # but ignore the order in the diagram if ps == (6, 3, 8, 7, 2, 5, 1, 4): continue # construct the sequence of (regions, points) ss = list(layout(ps)) # analyse it rs = analyse(ss) # there should only be one max position if len(rs) != 1: continue (x, (v, r)) = rs[0] # and the average score should be an integer if r != 0: continue # find the interval x is in for ((a, b), p) in intervals(ss): # and check it is centred if p > 0 and a < x < b and b - x == x - a: # find the holes on either side i = ps.index(p) z = ps[i - 1:i + 2] printf("[{ps} -> target = {x} mm, avg pts = {v}; segment = {z}]") m.add((p, ordered(ps[i - 1], ps[i + 1]))) # output solution for ((x, (l, r)), n) in m.most_common(): printf("centre = {x}, left/right = {l}/{r} [{n} solutions]")Solution: (a) You should aim at the centre of the 5 point hole. (b) The 6 point and 8 point holes are either side of the 5 point hole.
Here is a diagram of the arrangement:
The “out of bounds” areas are indicated in red, and we score zero points if the centre of the ball lands in these.
In the 24cm section centred on the 5 point hole we see that there is a 3cm zone where we can score 6 points, a 6cm zone where we can score 5 points, and a 3cm zone where we can score 8 points.
The average expected score is thus: (6×3 + 5×6 + 8×3) / 24 = 72 / 24 = 3.
LikeLike
Jim Randell 6:28 pm on 30 May 2020 Permalink |
Here is the program adapted to just consider the centre + left/right values. It finds there is only one segment that must appear in any solution (or its reverse), and this gives the required answer. It runs in 54ms.
from enigma import (irange, subsets, peek, printf) # available holes (points) holes = [ 1, 2, 3, 4, 5, 6, 7, 8 ] # layout for a sequence of holes (in mm) # return a list of: (region size, points scored) pairs def layout(ps): for (i, p) in enumerate(ps): if i > 0: yield (60, 0) # gap between holes yield (110 - 10 * p, p) # hole # generate intervals ((a, b), p) from a layout # where a, b are absolute distances, p is the number of points scored def intervals(ss): a = b = 0 for (d, p) in ss: if b == 0: (a, b) = (0, d) else: (a, b) = (b, b + d) yield ((a, b), p) # analyse the layout (working in 5mm steps) # return list of (d, (v, r)) values, where: # d = absolute target distance # v + r/240 = max average score def analyse(ss): # determine total length t = sum(d for (d, _) in ss) rs = list() # consider target positions (in 5mm steps) for x in irange(120, t - 120, step=5): # consider each zone r = 0 for ((a, b), p) in intervals(ss): # overlap? if b < x - 120: continue if a > x + 120: break d = min(x + 120, b) - max(x - 120, a) r += p * d rs.append((x, r)) # find maximum average value m = max(r for (_, r) in rs) return list((x, divmod(r, 240)) for (x, r) in rs if r == m) # find triples with integer scores max average scores at the centre of the centre hole # choose the centre hole and left/right holes for (X, L, R) in subsets(holes, size=3, select="P"): if not (L < R): continue # create the layout ss = list(layout((L, X, R))) # analyse it rs = analyse(ss) # there should be only one max position if len(rs) != 1: continue (x, (v, r)) = rs[0] # and the max average score should be an integer if r != 0: continue # and it should be centred on X ((a, b), _) = peek(intervals(ss), 2) if a < x < b and b - x == x - a: printf("segment = ({L}, {X}, {R}) -> target = {x} mm, avg pts = {v}")I suppose really I should demonstrate that we can construct an ordering containing the required segment that satisfies all the requirements, but as my first program finds lots I don’t think it is necessary.
LikeLike
Robert Brown 10:02 am on 30 May 2020 Permalink |
If the middle & adjacent slots are too small, the ±12 cm range goes into the next slot, which is undefined. This reduces the solution space and quickly leads to what appears to be a unique result. But if you reduce the smaller adjacent slot by 1 and increase the other one, it still works. This would be valid if the smaller slot was the first one on the board, so a possible duplicate result?
LikeLike
Jim Randell 1:11 pm on 30 May 2020 Permalink |
@Robert: I don’t think it is possible for the target area to extend beyond the centre hole and the holes on either side. The smallest holes are 7cm and 8cm and the distance from the centre of one to the edge of the other is always greater than 12cm, so I think we only need to consider centre + left/right configurations. (Which I’m looking at to give a more efficient program).
LikeLike
Robert Brown 10:52 am on 30 May 2020 Permalink |
Further to above. The ‘not to scale’ drawing masks the fact that low value slots are quite wide. I assume there are 8 values as per the drawing, with corresponding slot widths.
So there is a solution where the mid value is low, and the adjacent values can take one of two different pairs (they have the same sum), and where the total length measured from the centre of the mid slot is > 12 in each case. Definitely two sets of results, doesn’t matter where they are on the board. Maybe I’m missing something?
LikeLike
Jim Randell 6:55 pm on 1 June 2020 Permalink |
I’ve updated the diagram with a scale drawing. Including a ball.
LikeLike
Robert Brown 12:17 pm on 30 May 2020 Permalink |
This is not a good place to have a conversation. You have my email address, but I don’t have yours!
Both of the above sets of results work, but each post has typos which I cannot correct. An in depth exploration reveals 5 different solutions, all with 3 different L M R values between 1 & 8, and with average values of 3, 4 or 5. In each case the minimum distance from the centre of the middle value (where he aims for) is at least 15.5 cm before going into the next (unknown) slot, plenty of room for the 2cm radius ball to have it’s centre up to 12 cm from that centre. So no need for any of the slots to be the first one on the board.
LikeLike
Jim Randell 6:55 pm on 1 June 2020 Permalink |
@Robert: It’s a limitation of WordPress.com I’m afraid, but if there are any corrections you would like to make, just post a followup comment, and I will use it to correct errors in the original.
LikeLike
Frits 1:14 pm on 15 August 2025 Permalink |
Based on Jim’s diagram of the arrangement.
# number of holes, sum score+width, gap, ball diameter, aim deviation N, T, G, B, Z = 8, 15, 2, 4, 12 cm = lambda pts: T - pts - 2 * G # length of deviation range dev_cm = 2 * Z rng = set(range(1, N + 1)) # look for a solution L, M, R for M in rng: mid_cm = cm(M) mid_total = mid_cm * M # remaining cm to use for left and right holes rem_cm = dev_cm - mid_cm - 2 * G - 2 * B rem_rng = sorted(rng - {M}) for R in rem_rng: right_cm = cm(R) # aim as far right as possible in order to maximise the score right = min(rem_cm, right_cm) # but aim must also be at the centre of a hole if 2 * right != rem_cm: continue mid_right_total = mid_total + right * R _, r = divmod(mid_right_total, dev_cm) left_total_needed = dev_cm - r while True: L, r = divmod(left_total_needed, right) if L >= R: break if not r and L != M: print(f"answer: (a) {M}, (b) {L} and {R}") left_total_needed += dev_cmLikeLike