#!/usr/bin/env python3 ##N H W ##L_1 R_1 B_1 T_1 L_2 R_2 B_2 T_2 ... L_N R_N B_N T_N ## ##where ## ##* N is the number of pictures on the wall, N <= 6, ##* H is the height above the floor of the center of the first disk. ## This center is also 0.5 feet from the left wall. ##* L_i and R_i are the distances from the left wall to the left and right edges ## of picture number i, 2 <= L_1 < R_1, R_N + 2 <= W, R_{i-1} +2 <= L_i < R_i, ## for i = 2, ... N, ##* B_i and T_i are the distances from the floor to the bottom and top edges ## of picture number i, 2 <= B_i, < T_i, for i = 1, 2, ... N. ## ##For debugging, set to draw result if there is a command line parameter, ##or change the code so DEBUG is 1. ## import sys from math import sqrt, hypot, ceil from collections import namedtuple class V2: # 2D vector def __init__(self, x, y): self.x = float(x) self.y = float(y) def __mul__(self, m): m = float(m) return V2(self.x*m, self.y*m) def __rmul__(self, m): return self*m def __add__(self, v): return V2(self.x + v.x, self.y + v.y) def __sub__(self, v): return V2(self.x - v.x, self.y - v.y) def __neg__(self): return self*(-1.0) def __pow__(self, v): # dot prod is ** return self.x*v.x + self.y*v.y def __abs__(self): # magnitude return hypot(self.x, self.y) def unit(self): # unit vector return self*(1.0/abs(self)) def cc90(self): # CC rotation 90 degrees return V2(-self.y, self.x) def normal(self): # unit normal, 90 deg CC return self.cc90().unit() def __repr__(self): return 'V2({self.x}, {self.y})'.format(**locals()) def tup(v): return (v.x, v.y) UX = V2(1,0) UY = V2(0, 1) ORIGIN = V2(0,0) ######################### basic geometric operations def triSide(hyp, side): return 0 if abs(hyp) < abs(side) else sqrt(hyp*hyp-side*side) def CircleCross(C1, r1, C2, r2, posDir): '''Given circles with center, radius: C1, r1; C2, r2. Use normal to C1-C2 closest in direction to posDir. ''' v = C2-C1 d2 = v**v d = sqrt(d2) u = v*(1/d) n = u.cc90() if n**posDir < 0: n = -n x = .5*(d2 + r1*r1 - r2*r2)/d return C1 + x*u + triSide(r1, x)*n def lineCircleCross(a, u, c, r): '''a +tu crosses circle cneter c, radius r solution in u direction from median''' v = c-a cu = v**u m = abs(v) s = triSide(m, cu) # allow roundofff for tangent ext = triSide(r, s) return a +u*(cu+ext) BLOCKS = [] # list of blocks on wall, increasing x RAD = 0.5 # diameter 1 circles (and exact added to small integers) RAD2 = RAD*RAD class RingPath: ''' turns: list with start, corners, end disk centers R: number of rings in string so far >= len(turns) d: secondary measure of goodness for corner cor is cor.dCen(turns[-1]) W - RAD - path[-1].x for complete path to wall Though a path mutates, it is never replaced, so "is" works ''' def __init__(self, R=1, d=0, turns=None): if turns is None: # no path self.turns = [] self.R = 10**9 self.d = 0.0 else: self.turns = turns self.R = R self.d = d def __bool__(self): return bool(self.turns) def end(self): return self.turns[-1] def badness(self): return (self.R, self.d) def makeBetter(self, rpfrom, cen=None, d=None): ''' want to update self as extension of rpfrom to cen with new dist d, or if way clear to wall, self would be BEST_PATH and calc d. if update, return True. ''' last = rpfrom.end() if self is BEST_PATH: m = W-.5 - last.x dR = int(m) d = m - dR cen = last +UX*dR else: m = abs(cen - last) # should be integer, allow float inaccuracy dR = int(m+.01) R = dR + rpfrom.R improved = not self or (R, d) < self.badness() if improved: self.R = R self.d = d self.turns = rpfrom.turns + [cen] return improved def clearRight(self, bsi): # to other end of edge cy = self.end().y return all([not(b.yl-.5 < cy < b.yh+.5) for b in BLOCKS[bsi:]]) def __repr__(self): return 'RingPath({}, {}, {})'.format(self.R, self.d, self.turns) class Corner: '''v vertex in corner of rect ui unit vector along incoming edge uo unit vector along outgoing edge (provide coord system) p[]: best paths so last circle: p[0] touching incoming edge within 1 of v p[1] touching only at v p[2] touching outgoing edge within 1 of v nextC corner to right of this corner (if uo == UX) or None bi is block index for distinguishing p[i]: calc vLim [] is limiting position in each p range though p[i]'s mutate, no object in self is replaced.''' def __init__(self, v, ui, uo, bi, nextC=None): self.v = v self.ui = ui self.uo = uo self.nextC = nextC self.vLim = (v -uo*.5, v +ui*.5, v +ui*.5 + uo) self.p = (RingPath(), RingPath(), RingPath()) self.bi = bi # block index - allow ref to block def dCen(self, cen, i=2): # used for d, ... return abs(self.vLim[i] - cen) def updateInEdge(self, prevCor):# extend to next edge, inner side '''extend prevCor.p[2] to update self.p[0] Update p[0] if better and return True ''' rpfrom = prevCor.p[2] last = rpfrom.end() cen = last + UX*(int(self.v.x - last.x)) if self.p[0].makeBetter(rpfrom, cen, self.dCen(cen)): dpr('edge update to next corner') self.tryMeet(self, 0) def tryMeet(self, rpcor, ip): # the central geometry! '''find where integral circle line from rpcor[ip] or from starting point if rpCor is None hits near corner self if path clear, and better, update cor from this side, and continue to up to two further tangent circles around corner and other end if nextC is not None, and straight to wall if possible ''' if rpcor: rp = rpcor.p[ip] rpbi = rpcor.bi else: rp = rpStart rpbi = 0 c = rp.end() ui = self.ui uo = self.uo if self != rpcor: vec = self.vLim[0] - c if vec**ui < 0 or vec**uo < 0: return # bad in quadrant if rpcor: veco = self.v - rpcor.v if veco**rpcor.ui <= 0 or veco**rpcor.uo <= 0: return # bad quadrant out cen = None # will be contact point r2 = int(self.dCen(c, 2)) if self.dCen(c, 1) <= r2: # circle line to outer edge cen, ceni = self.makeOCen(rp, rpbi, r2) # if no obstacle r1 = int(self.dCen(c, 1)) if not cen and self.dCen(c, 0) <= r1: # to touch corner cen, ceni = self.makeVCen(rp, rpbi, r1) # if no obstacle if not cen and self != rpcor: # in edge only if from another block if vec**ui > 1: dm = abs(self.vLim[0] - ui - c) # diag to lowest else: dm = vec**uo r0 = int(self.dCen(c, 0)) if dm < r0: cen, ceni = self.makeICen(rp, rpbi, r0) if cen: # test cen and chase propagated improvements if self.p[ceni].makeBetter(rp, cen, self.dCen(cen)): if ceni == 2: if self.nextC: # heading in UX direction if self.p[2].clearRight(self.bi+1): BEST_PATH.makeBetter(self.p[2]) else: self.nextC.updateInEdge(self) # and further else: # further on same corner self.tryMeet(self,ceni) def makeOCen(self, rpfrom, rpbi, r): c = rpfrom.end() cen = lineCircleCross(self.vLim[1], self.uo, c, r) if isClear(c, cen, rpbi, self.bi): return cen, 2 return None, None def makeICen(self, rpfrom, rpbi, r): c = rpfrom.end() cen = lineCircleCross(self.vLim[0], self.ui, c, r) if isClear(c, cen, rpbi, self.bi-1): return cen, 0 return None, None def makeVCen(self, rp, rpBi, r): ''' sure disk r from rp at v; check for obstructing blocks; if OK, calc pv, check for improvement, propogate change ''' v = self.v c = rp.end() cen = CircleCross(v, RAD, c, r, self.ui-self.uo) if isClear(c, cen, rpBi, self.bi): return cen, 1 return None, None def __repr__(self): # not really return '''Corner({}, {}, {}, {}, {}, p[0]={},\n p[1]={}, \n p[2]={})'''.format( self.v, self.ui, self.uo, self.bi, True if self.nextC else None, self.p[0], self.p[1], self.p[2]) Block = namedtuple('Block', ['cll', 'clh', 'chh', 'chl', 'xl', 'xh', 'yl', 'yh']) #corners x low-high, y low-high #xl, xh, yl, yh: x,y extents = low and high def setBlock(verts, bi): xl, xh, yl, yh = verts chh = Corner(V2(xh,yh), UX, -UY, bi) # in toward right, then down chl = Corner(V2(xh,yl), UX, UY, bi) clh = Corner(V2(xl,yh), UY, UX, bi, chh) cll = Corner(V2(xl,yl), -UY, UX, bi, chl) BLOCKS.append(Block(cll, clh, chh, chl, *verts)) def circleIntersects(c, b): '''circle of radius RAD, center c, block b center could be in union of 2 rect, circles at each corner''' for cor in b[:4]: if abs(cor.v-c) < RAD: return True return (b.xl < c.x < b.xh and b.yl-RAD < c.y < b.yh+RAD or b.xl-RAD < c.x < b.xh+RAD and b.yl < c.y < b.yh) def isClear(c1 , c2, bi, bj): '''see if integral circle path between centers c1, c2 in blocks with index bi, bj, are clear of all blocks ''' ## done, not fancy, could be faster v = c2-c1 r = abs(v) n = int(r+.1) # should be int u = v*(1/r) return all([not circleIntersects(c1+u*i, b) for b in BLOCKS[bi:bj+1] for i in range(1, n)]) def toNext(rpCorner, ip): ''' extend rp in corner rpCorner directly to any possible vertex in later blocks, if better than current path rpCorner is None for the initial point ''' bsi = 0 if rpCorner is None else rpCorner.bi+1 # bsi first to go to for b in BLOCKS[bsi:]: for cor in b[:4]: cor.tryMeet(rpCorner, ip) # could hit current block def doCase(param, pert=ORIGIN): ''' in case hit spot right at roundoff error, try perturbations''' global N, W, rpStart, BEST_PATH, caseParam # just change interior of BLOCKS BEST_PATH = RingPath() if isinstance(param, str): caseParam = param param = [int(e) for e in param.split()] else: sList = list(map(str, param)) # all for pic title string sList[2] = sList[2] + ' ' caseParam = ' '.join(sList) BLOCKS[:] = [] (N, H, W, *data) = param for i in range(0,4*N, 4): setBlock(data[i:i+4], i//4) rpStart = RingPath(1, 0, [V2(RAD, H)+pert]) if rpStart.clearRight(0): # straight shot BEST_PATH.makeBetter(rpStart) else: toNext(None, 0) for b in BLOCKS[:N-1]: for cor in b[:4]: for ip, rp in enumerate(cor.p): if rp: toNext(cor, ip) ## ans = sum(BEST_PATH.badness()) return BEST_PATH # whole path for pics eps = 10**-5 def wiggle(param): '''try 3 perturbations''' sols = [] for pert in [(0,0), (0, eps), (eps, 0)]: sols.append(doCase(param, V2(*pert))) sols.sort(key=RingPath.badness) sols.sort(key=RingPath.badness) ans = [sum(rp.badness()) for rp in sols] print(ans[0]) dif = ans[2]-ans[0] if dif > 2*eps: dpr('big perturbation!!!') return sols[0] def solve(param): global DEBUG deb = DEBUG DEBUG = 0 bp = wiggle(param) DEBUG = deb if DEBUG: doPic(bp, lab='SOL') ######## rest except main is for making pictures ################### def rsStr(rp, lab='to'): if lab.endswith('to'): c = rp.end() lab = '{}:({},{})'.format(lab, c.x, y.y) return '{} R={}, d={}, s={}'.format(lab, rp.R, rp.d, caseParam) def doPic(rp, lab='to'): global SCREENSET if DEBUG : if not SCREENSET: setup (width=PICWIDTH, height=PICHEIGHT) SCREENSET = True msg = rsStr(rp, lab=lab) draw(rp, msg) def dpr(s): if DEBUG: print (s, file=sys.stderr) def go(x,y, t = None): if not t: t = getturtle() t.pd() t.goto(x, y) def jump(x, y, t = None): if not t: t = getturtle() t.pu() t.goto(x, y) def seg(x,y, x2, y2, t = None): if not t: t = getturtle() jump(x,y, t=t) go(x2, y2, t=t) def cir(x, y, r, color='black', fill=False, width=1, t = None): if not t: t = getturtle() jump(x+r, y, t=t) t.pd() t.left(90-t.heading()) if fill: t.begin_fill() t.circle(r, steps=360) if fill: t.end_fill() jump(x, y, t=t) def poly(pts, fill=False, t = None): if not t: t = getturtle() jump(*pts[-1], t=t) if fill: t.begin_fill() for v in pts: go(*v, t=t) if fill: t.end_fill() def between(c1, c2, t = None): if not t: t = getturtle() v = c2 - c1 u = v.unit() for i in range(1+int(abs(v)+.1)): c = c1 + u*i cir(c.x, c.y, RAD) def draw(bp, msg): title(msg) if not(bp): dpr(' no path') clearscreen() mult=170 top = max((b.yh for b in BLOCKS)) bot = min((b.yl for b in BLOCKS)) top = 2 +max(bp.turns[0].y , top) bot = -2 + min(bp.turns[0].y, bot) if (top-bot)/W < PICRATIO: setworldcoordinates(0, bot, W, bot + W*PICRATIO) else: setworldcoordinates(0, bot, (top-bot)/PICRATIO, top) mode('world') tracer(False) # no animation ht() color('blue', 'yellow') for b in BLOCKS: L = [tup(cor.v) for cor in b[:4]] poly(L, True) width(2) pencolor('black') for x in [0, W]: seg(x, bot, x, top) turns = bp.turns width(1) if turns: for i in range(1, len(turns)): between(turns[i-1], turns[i]) pencolor('red') e = turns[-1]+RAD*UX tp= e+bp.d*UX seg(e.x, e.y, tp.x,tp.y) update() # input('Press enter after you are done viewing the picture.') C1y = 50 PICHEIGHT = 1000 PICWIDTH=600 PICRATIO = PICHEIGHT/PICWIDTH DEBUG = 0 #1 # for graphics *with IDLE* otherwise uncomment manual input if DEBUG: from turtle import * SCREENSET = False ######## end of graphics extra def main(): s = sys.stdin.read().replace('\n', ' ') solve(s) # draws in IDLE, too if DEBUG is 1 if __name__ == '__main__': main()