Un détail tout de même, ça semble évident mais ça peut demander un peu de concentration pour ne pas l'oublier en court de route : tant que vous n'êtes pas arrivé à vos fins, c'est à dire que vous n'avez pas assez d'informations, il ne faut pas donner le moindre indice qui laisserait soupçonner que vous n'appréciez pas ce démarchage.
Éviter par exemple des phrases comme « Au fait, comment avez-vous eu mon numéro de téléphone ? » Si votre interlocuteur commence à soupçonner que son appel vous dérange, il risque de ne pas vous croire si vous vous prétendez intéressé, et l'appel ne durera pas longtemps. Pas assez longtemps pour obtenir les infos que vous souhaitez en tout cas.
Ces démarcheurs savent très bien que ce qu'ils font est illégal, même s'ils prétendent parfois le contraire. Et sachant qu'ils peuvent être poursuivis, ou avoir diverses emmerdes comme être radiés de la plate-forme CPF, ils commencent à être assez méfiants.
L'autre jour, j'ai oublié ce principe et demandé directement à une démarcheuse où elle travaillait. Elle m'a fait répéter la question, et après avoir compris ma demande, elle a raccroché sans y mettre la moindre forme. Ni refus, ni excuse, ni au-revoir, juste raccroché. Si ce n'est pas de la méfiance, ça…
À titre d'exemple, j'ai pu piéger une entreprise de formation professionnelle qui se livrait au démarchage téléphonique. Sans doute sous-traité, mais ce n'est pas mon problème, les agissements d'un sous-traitant engagent évidemment la responsabilité de son donneur d'ordre.
Je me suis montré intéressé par une formation en langue anglaise, très utile pour mon travail. Le démarcheur m'a alors fait rappeler par un responsable des inscriptions, et je m'attendais au pire, c'est à dire à ce qu'il me demande mon numéro de sécurité sociale et qu'il réinitialise avec mon aide mon mot de passe pour accéder lui-même à mon compte CPF et m'inscrire à sa formation. Ç'aurait été une arnaque selon les règles, mais non, c'était un peu plus honnête, il m'a juste demandé mon adresse électronique, celle inscrite sur le compte CPF, ce qui lui a suffit à me pré-inscrire à des formations.
Vérification faite, les formations apparaissaient bien comme des propositions à valider, avec un organisme de formation tout à fait identifiable, en tout cas par la plate-forme CPF. J'en avais assez, et la fin de la discussion téléphonique a été assez intéressante :
C'est bon, je vois bien les formations. Je vous remercie, j'ai toutes les informations qu'il me faut.
Attendez, je peux vous expliquer la façon dont les formations vont se passer.
Ce ne sera pas nécessaire, vraiment, j'ai tout ce qu'il me faut.
Non, je ne vous ai pas expliqué…
Si si, j'en sais assez je vous dit.
Assez pour quoi ?
Assez pour signaler votre démarchage illicite.
Vous n'étiez pas intéressé par la formation ?
Non, désolé, j'avais juste besoin d'informations pour le signalement.
Vous nous avez tendu un piège alors ?
C'est ça. Ça vous a plu ?
Mais c'est dégueulasse ce que vous faites !
Ah non, moi j'aide à faire respecter la loi. Ce que vous faites en revanche, c'est illégal. Si j'ai besoin d'une formation, je suis assez grand pour aller la chercher tout seul. Allez, bonne journée.
from__future__importannotationsimportenumimportioimportitertoolsfromtypingimportIterable,Iterator,List,Tupleimportnumpyasnpimportnumpy.typingasnptCoords=Tuple[int,int]classMaterial(enum.Enum):AIR=enum.auto()ROCK=enum.auto()SAND=enum.auto()classSlice:def__init__(self,array:npt.ArrayLike,leak:Coords)->None:self.matrix:npt.NDArray=np.array(array)self.leak=leakdefpour(self)->Iterator[bool]:"""Inject a unit of sand. Yields False if it was blocked, True if it fell into the void. Stops when the leak is clogged up by a mountain of sand."""# List of coordinates to pour sand fromsources:List[Coords]=[]# Start at the original leaky,x=self.leakwhileTrue:# Our unit of sand is injected at current coordinates (y, x)whiley<self.matrix.shape[0]-1:y_=y+1forx_in(x,x-1,x+1):ifself.matrix[y_,x_]isMaterial.AIR:# Sand can flow down:# * current coordinate is a good place to pour next# sand unit from;sources.append((y,x))# * current sand unit falls down one level.y=y_x=x_breakelse:# Ways down exhausted, sand is blockedself.matrix[y,x]=Material.SAND# Get out of the descent loopbreakelse:# Descent ended, sand fell all the way downyieldTruecontinue# Descent did not end, sand got blockedyieldFalseifnotsources:# Nowhere to pour sand from, even the leak is clogged upbreak# Pour next sand unit from last good placey,x=sources.pop()def__str__(self)->str:result=io.StringIO()forlineinself.matrix:forpointinline:ifpointisMaterial.AIR:result.write(' ')elifpointisMaterial.ROCK:result.write('█')elifpointisMaterial.SAND:result.write('░')else:assertFalse# we covered all casesresult.write('\n')returnresult.getvalue()defadd_segment(self,start:Coords,end:Coords)->None:y1,x1=starty2,x2=endify1==y2:x1,x2=min(x1,x2),max(x1,x2)ys=(y1for_inrange(x1,x2+1))xs=(xforxinrange(x1,x2+1))elifx1==x2:y1,y2=min(y1,y2),max(y1,y2)ys=(yforyinrange(y1,y2+1))xs=(x1for_inrange(y1,y2+1))fory,xinzip(ys,xs):self.matrix[y,x]=Material.ROCKdefimport_segments(lines:Iterable[str])->Tuple[List[Coords],List[Tuple[Coords,Coords]]]:points:List[Coords]=[]segments:List[Tuple[Coords,Coords]]=[]defcoords(word:str)->Coords:parts=word.split(',')iflen(parts)!=2:raiseValueError("unexpected number of coordinates")returnint(parts[1]),int(parts[0])forlineinlines:words=line.rstrip().split(' -> ')segment_points=[coords(word)forwordinwords]points.extend(segment_points)segments.extend(itertools.pairwise(segment_points))returnpoints,segmentsdefimport_slice1(lines:Iterable[str])->Slice:points,segments=import_segments(lines)y_leak,x_leak=0,500points.append((y_leak,x_leak))xmin=min(xfor_,xinpoints)-1xmax=max(xfor_,xinpoints)+1ymin=min(yfory,_inpoints)ymax=max(yfory,_inpoints)ifymin<0:raiseValueError("unexpected negative coordinate")xshift=xminmatrix=np.full(((ymax+1),xmax-xshift+1),Material.AIR)result=Slice(matrix,(0,500-xshift))for(y1,x1),(y2,x2)insegments:result.add_segment((y1,x1-xshift),(y2,x2-xshift))returnresultdefimport_slice2(lines:Iterable[str])->Slice:points,segments=import_segments(lines)y_leak,x_leak=0,500points.append((y_leak,x_leak))xmin=min(xfor_,xinpoints)xmax=max(xfor_,xinpoints)ymin=min(yfory,_inpoints)ymax=max(yfory,_inpoints)+2# including the floor# Update xmin and xmax to accomodate a pyramid of sandxmin=min(xmin,x_leak-(ymax-y_leak))xmax=max(xmax,x_leak+(ymax-y_leak))ifymin<0:raiseValueError("unexpected negative coordinate")xshift=xminmatrix=np.full(((ymax+1),xmax-xshift+1),Material.AIR)result=Slice(matrix,(0,500-xshift))for(y1,x1),(y2,x2)insegments:result.add_segment((y1,x1-xshift),(y2,x2-xshift))result.add_segment((ymax,xmin-xshift),(ymax,xmax-xshift))returnresultdefsolve1(lines:Iterable[str])->int:"""Solve part 1 of today's puzzle"""slice_=import_slice1(lines)fori,falleninenumerate(slice_.pour()):iffallen:returniraiseValueError("Simulation ended with unexpected sand leak clogged")defsolve2(lines:Iterable[str])->int:"""Solve part 2 of today's puzzle"""slice_=import_slice2(lines)fori,falleninenumerate(slice_.pour()):iffallen:raiseValueError("Simulation ended with unexpected sand under the floor")returni+1
Visiblement, il y a une optimisation possible : une si un grain de sable, lâché du point de départ, tombe à un endroit, on peut lâcher ce grain et les suivants de cet endroit, jusqu'à ce qu'il soit ensablé…
Je me demande s'il ne serait pas possible de résoudre la partie 2 bien plus vite, en calculant l'aire des zones protégées par des rochers.
En effet, le nombre d'unités de sables tombées, c'est l'aire d'une triangle plein, moins celle des rochers qu'il contient, moins l'aire des zones protégées par ces derniers.
Ceci dit, déterminer la zone protégée par l'ensemble des rochers, c'est loin d'être évident. La zone protégée par un segment horizontal, c'est facile, c'est un triangle dessous. La zone protégée par un segment vertical, c'est facile, c'est rien du tout. Mais quand on commence à avoir des segments horizontaux et verticaux qui se touchent, ça devient compliqué.
Allez, une autre piste, sans doute pas beaucoup plus simple. On dirait qu'on peut déterminer la zone protégée par des rochers, non pas seulement par calcul, mais aussi par une simulation bizarre : faire couler de l'air depuis le bas vers le haut et regarder où il s'accumule. Mais ça pose le problème d'introduire cet air : il faut qu'il s'accumule et qu'il traverse en même temps les segments…
from__future__importannotationsimportenumimportioimportitertoolsfromtypingimportIterable,List,Tupleimportnumpyasnpimportnumpy.typingasnptCoords=Tuple[int,int]classMaterial(enum.Enum):AIR=enum.auto()ROCK=enum.auto()SAND=enum.auto()classSlice:def__init__(self,array:npt.ArrayLike,leak:Coords)->None:self.matrix:npt.NDArray=np.array(array)self.leak=leakdefpour(self)->bool:"""Inject a unit of sand. Return False if it was blocked, True if it fell into the void."""y,x=self.leakwhiley<self.matrix.shape[0]-1:y_=y+1forx_in(x,x-1,x+1):ifself.matrix[y_,x_]isMaterial.AIR:# Sand can flow downy=y_x=x_breakelse:# Blockedself.matrix[y,x]=Material.SANDreturnFalse# Sand fell all the way down into the voidreturnTruedef__str__(self)->str:result=io.StringIO()forlineinself.matrix:forpointinline:ifpointisMaterial.AIR:result.write(' ')elifpointisMaterial.ROCK:result.write('█')elifpointisMaterial.SAND:result.write('░')else:assertFalse# we covered all casesresult.write('\n')returnresult.getvalue()defadd_segment(self,start:Coords,end:Coords)->None:y1,x1=starty2,x2=endify1==y2:x1,x2=min(x1,x2),max(x1,x2)ys=(y1for_inrange(x1,x2+1))xs=(xforxinrange(x1,x2+1))elifx1==x2:y1,y2=min(y1,y2),max(y1,y2)ys=(yforyinrange(y1,y2+1))xs=(x1for_inrange(y1,y2+1))fory,xinzip(ys,xs):self.matrix[y,x]=Material.ROCKdefimport_segments(lines:Iterable[str])->Tuple[List[Coords],List[Tuple[Coords,Coords]]]:points:List[Coords]=[]segments:List[Tuple[Coords,Coords]]=[]defcoords(word:str)->Coords:parts=word.split(',')iflen(parts)!=2:raiseValueError("unexpected number of coordinates")returnint(parts[1]),int(parts[0])forlineinlines:words=line.rstrip().split(' -> ')segment_points=[coords(word)forwordinwords]points.extend(segment_points)segments.extend(itertools.pairwise(segment_points))returnpoints,segmentsdefimport_slice1(lines:Iterable[str])->Slice:points,segments=import_segments(lines)y_leak,x_leak=0,500points.append((y_leak,x_leak))xmin=min(xfor_,xinpoints)-1xmax=max(xfor_,xinpoints)+1ymin=min(yfory,_inpoints)ymax=max(yfory,_inpoints)ifymin<0:raiseValueError("unexpected negative coordinate")xshift=xminmatrix=np.full(((ymax+1),xmax-xshift+1),Material.AIR)result=Slice(matrix,(0,500-xshift))for(y1,x1),(y2,x2)insegments:result.add_segment((y1,x1-xshift),(y2,x2-xshift))returnresultdefimport_slice2(lines:Iterable[str])->Slice:points,segments=import_segments(lines)y_leak,x_leak=0,500points.append((y_leak,x_leak))xmin=min(xfor_,xinpoints)xmax=max(xfor_,xinpoints)ymin=min(yfory,_inpoints)ymax=max(yfory,_inpoints)+2# including the floor# Update xmin and xmax to accomodate a pyramid of sandxmin=min(xmin,x_leak-(ymax-y_leak))xmax=max(xmax,x_leak+(ymax-y_leak))ifymin<0:raiseValueError("unexpected negative coordinate")xshift=xminmatrix=np.full(((ymax+1),xmax-xshift+1),Material.AIR)result=Slice(matrix,(0,500-xshift))for(y1,x1),(y2,x2)insegments:result.add_segment((y1,x1-xshift),(y2,x2-xshift))result.add_segment((ymax,xmin-xshift),(ymax,xmax-xshift))returnresultdefsolve1(lines:Iterable[str])->int:"""Solve part 1 of today's puzzle"""slice_=import_slice1(lines)foriinrange(10000):ifslice_.pour():returniraiseValueError("Simulantion did not end within expected limit")defsolve2(lines:Iterable[str])->int:"""Solve part 2 of today's puzzle"""slice_=import_slice2(lines)foriinrange(100000):_=slice_.pour()ifslice_.matrix[slice_.leak]isMaterial.SAND:returni+1raiseValueError("Simulantion did not end within expected limit")
Pour la définition de aoc.group_lines(lines: str), cf. hier
from__future__importannotationsimportitertoolsfromtypingimportIterable,Iterator,List,Optional,Tuple,UnionimportaocclassElement:def__init__(self,value:Union[List[Element],int]):self.value=valuedef__lt__(self,other:Element)->bool:ifisinstance(self.value,int)andisinstance(other.value,int):returnself.value<other.valueifisinstance(self.value,list)andisinstance(other.value,list):forself_elt,other_eltinzip(self.value,other.value):ifself_elt<other_elt:returnTrueifother_elt<self_elt:returnFalse# Ran out of elements on one of the listsreturnlen(self.value)<len(other.value)ifisinstance(self.value,int):returnElement([self])<otherifisinstance(other.value,int):returnself<Element([other])assertFalse# should not happen: we covered all casesdef__eq__(self,other:object)->bool:ifnotisinstance(other,Element):returnNotImplementedifisinstance(self.value,int)andisinstance(other.value,int):returnself.value==other.valueifisinstance(self.value,list)andisinstance(other.value,list):return(len(self.value)==len(other.value)andall(self_elt==other_eltforself_elt,other_eltinzip(self.value,other.value)))ifisinstance(self.value,int):returnElement([self])==otherifisinstance(other.value,int):returnself==Element([other])assertFalse# should not happen: we covered all casesdef__str__(self)->str:ifisinstance(self.value,int):returnstr(self.value)elifisinstance(self.value,list):return'[{}]'.format(','.join(str(element)forelementinself.value))assertFalse# should not happen: we covered all casesdefimport_element(chars:Iterable[str])->Element:element:Optional[Element]=Nonelists:List[List[Element]]=[]# List[List[Element]]current_list:Optional[List[Element]]=Nonecurrent_int:Optional[int]=Noneforcharinchars:ifchar=='[':ifcurrent_intisnotNone:raiseValueError("unexpected beginning of list")ifcurrent_listisnotNone:lists.append(current_list)current_list=[]elifchar==']':ifcurrent_listisNone:raiseValueError("unexpected end of list")# If we were parsing an int, add it before closing current listifcurrent_intisnotNone:current_list.append(Element(current_int))current_int=Noneiflists:# We are closing a sub-listprev_list=lists.pop()prev_list.append(Element(current_list))current_list=prev_listelse:# We are closing the top-level listelement=Element(current_list)current_list=Noneelifchar.isdecimal():value=int(char)ifcurrent_intisNone:current_int=valueelse:current_int=10*current_int+valuecontinueelifchar==',':ifcurrent_listisNone:raiseValueError("unexpected separator")ifcurrent_intisnotNone:current_list.append(Element(current_int))current_int=Noneelifchar=='\n':passelse:raiseValueError("unexpected character")ifelementisNone:raiseValueError("nothing to parse")returnelementdefimport_pairs(lines:Iterable[str])->Iterator[Tuple[Element,Element]]:forgroupinaoc.group_lines(lines):elements=[import_element(line)forlineingroup]iflen(elements)!=2:raiseValueError("unexpected group length")yield(elements[0],elements[1])defimport_elements(lines:Iterable[str])->Iterator[Element]:forlineinlines:iflineandline!='\n':yieldimport_element(line)defsolve1(lines:Iterable[str])->int:"""Solve part 1 of today's puzzle"""pairs=import_pairs(lines)returnsum(i+1fori,(elt1,elt2)inenumerate(pairs)ifelt1<elt2)defsolve2(lines:Iterable[str])->int:"""Solve part 2 of today's puzzle"""elements:Iterable[Element]=import_elements(lines)div1=import_element("[[2]]")div2=import_element("[[6]]")elements=sorted(itertools.chain(elements,(div1,div2)))key=1fori,elementinenumerate(elements):ifelement==div1orelement==div2:key*=i+1returnkey
Au fait, vu les visualisations, vous avez évidemment remarqué que loin d'être une gorge de rivière, nous avions plutôt affaire à l'île de Numenor une montagne en forme d'étoile, n'est-ce pas ?
from__future__importannotationsimportitertoolsimportmathimportrefromtypingimportCallable,Iterable,Iterator,List,TupleclassTest:def__init__(self,div:int,rcpt1:int,rcpt2:int):self.div=divself.rcpt1=rcpt1self.rcpt2=rcpt2def__call__(self,n:int):ifn%self.div==0:returnself.rcpt1else:returnself.rcpt2classMonkey:def__init__(self,number:int,items:List[int],op:Callable[[int],int],test:Test)->None:self.number=numberself.items=itemsself.op=opself.test=testself.activity=0_re_monkey=re.compile(r'^Monkey (\d+):?\n?$')_re_items=re.compile(r'^\s*Starting items: (.*)\n?$')_re_op=re.compile(r'^\s*Operation: (.*)\n?$')_re_test=re.compile(r'^\s*Test: divisible by (\d+)\n?$')_re_true=re.compile(r'^\s*If true: throw to monkey (\d+)\n?$')_re_false=re.compile(r'^\s*If false: throw to monkey (\d+)\n?$')@classmethoddefimport_lines(class_,lines:Iterable[str])->Monkey:lines=iter(lines)number=int(class_._re_monkey.match(next(lines)).group(1))items=[int(word)forwordinclass_._re_items.match(next(lines)).group(1).split(', ')]op=import_op(class_._re_op.match(next(lines)).group(1))div=int(class_._re_test.match(next(lines)).group(1))rcpt1=int(class_._re_true.match(next(lines)).group(1))rcpt2=int(class_._re_false.match(next(lines)).group(1))test=Test(div,rcpt1,rcpt2)returnclass_(number,items,op,test)defgroup_key()->Callable[[str],int]:key=0defaux(line:str):nonlocalkeyifline.rstrip()=='':key+=1returnkeyreturnauxdefgroup_lines(lines:Iterable[str])->Iterable[Iterable[str]]:for_,groupinitertools.groupby(lines,group_key()):yield(lineforlineingroupifline.rstrip()!="")_re_op_unary=re.compile("^new = old ([+*]) (\d+)$")_re_op_binary=re.compile("^new = old ([+*]) old$")defimport_op(line:str)->Callable[[int],int]:if(m:=_re_op_unary.match(line))isnotNone:arg=int(m.group(2))ifm.group(1)=='+':returnlambdaold:old+argifm.group(1)=='*':returnlambdaold:old*argraiseValueError("unrecognized operator")if(m:=_re_op_binary.match(line))isnotNone:ifm.group(1)=='+':returnlambdaold:2*oldifm.group(1)=='*':returnlambdaold:old**2raiseValueError("unrecognized operator")raiseValueError("unrecognized operation arity")classGame:def__init__(self,monkeys:dict[int,Monkey])->None:self.monkeys=monkeysdefround(self)->None:formonkeyinself.monkeys.values():foriteminmonkey.items:item=monkey.op(item)item//=3rcpt=monkey.test(item)monkey.activity+=1self.monkeys[rcpt].items.append(item)monkey.items=[]@classmethoddefimport_lines(class_,lines:Iterable[str])->Game:monkeys={}forgroupingroup_lines(lines):monkey=Monkey.import_lines(group)monkeys[monkey.number]=monkeyreturnclass_(monkeys)defbusiness(self)->int:activity=sorted(monkey.activityformonkeyinself.monkeys.values())returnactivity[-1]*activity[-2]classGame2(Game):def__init__(self,*args,**kwargs)->None:super().__init__(*args,**kwargs)self.div=math.lcm(*(monkey.test.divformonkeyinself.monkeys.values()))defround(self)->None:formonkeyinself.monkeys.values():foriteminmonkey.items:item=monkey.op(item)%self.divrcpt=monkey.test(item)monkey.activity+=1self.monkeys[rcpt].items.append(item)monkey.items=[]defsolve1(lines:Iterable[str])->int:"""Solve part 1 of today's puzzle"""game=Game.import_lines(lines)for_inrange(20):game.round()returngame.business()defsolve2(lines:Iterable[str])->int:"""Solve part 2 of today's puzzle"""game=Game2.import_lines(lines)for_inrange(10000):game.round()returngame.business()
Ma première idée fonctionnait sur l'exemple mais pas sur les données réelles : parcourir récursivement une matrice de proche en proche en évitant simplement les endroits où on est déjà passé, ça atteint vite la limites de longueur de la pile d'appels.
Du coup, j'ai fait du Dijkstra. Enfin quelque chose inspiré de son algorithme en tout cas. Ça pourrait être fait de façon entièrement itérative, mais ça ne valait pas la peine, ça reste raisonnable en récursif.
Une fois qu'on a ça, la deuxième partie du puzzle ne pose pas spécialement de problème. Il y a tout de même deux façons de l'implémenter, une bête et méchante (on prend la première solution et on l'applique plusieurs fois), et une un rien plus futée.
fromtypingimportIterable,Iterator,List,Optional,Set,TupleimportnumpyasnpCoords=Tuple[int,int]classMap:def__init__(self,matrix:np.ndarray)->None:self.matrix=matrixself.ly,self.lx=matrix.shapedefneighs(self,y:int,x:int)->Iterator[Coords]:"""Yield neighbours that are reachable from the given coordinates, considering movement rules (no climbing)"""for(dx,dy)in((-1,0),(1,0),(0,-1),(0,1)):y_=y+dyx_=x+dxify_<0ory_>=self.lyorx_<0orx_>=self.lx:continueifself.matrix[y_,x_]-self.matrix[y,x]<=1:yieldy_,x_def_distances(self,starts:Iterable[Coords],end:Coords,distances:np.ndarray)->None:"""Update distances matrix by computing walking distance from possible starts"""ifstarts==[]:# Nothing left to explorereturnnexts=[]# List[Coords]forstartinstarts:start_dist=distances[start]ifstart_dist<0:raiseValueError('cannot compute distances from uncharted point')forneighinself.neighs(*start):neigh_dist=distances[neigh]ifneigh_dist<0orstart_dist+1<neigh_dist:# This point has either never been checked before, or has# been but we have a shorter path to itdistances[neigh]=start_dist+1nexts.append(neigh)# Update distances from the points we have just updatedself._distances(nexts,end,distances)defmin_dist(self,starts:List[Coords],end:Coords)->int:distances=np.full_like(self.matrix,-1)"""Return the minimal distance to reach end, starting from starts"""forstartinstarts:distances[start]=0self._distances(starts,end,distances)returndistances[end]defimport_lines(lines:Iterable[str])->Tuple[Map,Coords,Coords]:matrix=[]# type: List[List[int]]start=Noneend=Nonefory,lineinenumerate(lines):matrix.append([])forx,charinenumerate(line.rstrip()):ifchar=='S':start=(y,x)height=0elifchar=='E':end=(y,x)height=25else:height=ord(char)-ord('a')matrix[-1].append(height)ifstartisNoneorendisNone:raiseValueError("no start or end position found")returnMap(np.array(matrix)),start,enddefsolve_both(lines:Iterable[str])->Tuple[int,int]:"""Solve part 1 of today's puzzle"""map_,start,end=import_lines(lines)min1=map_.min_dist([start],end)min2=map_.min_dist([coordsforcoords,height# type: ignoreinnp.ndenumerate(map_.matrix)ifheight==0],end)returnmin1,min2
Je n'ai pas encore résolu le problème de ce jour, dimanche c'est famille, mais ça va venir. 🙂
En lisant l'énoncé, tout de même, je m'étais dit que ça devait faire des nombres qui monteraient bien vite. En y réfléchissant, je pense que j'aurais tout seul pensé à travailler modulo leur PGCD.
Pour le code d'opération, l'eval vient tout de suite en tête bien sûr, mais ça me donne quand même quelques boutons, d'écrire du code qui a l'air d'un trou de sécurité. Réflexe professionnel je pense. À suivre, je dois toujours coder ça de toute façon.
J'ai pas modélisé un CPU complet comme Tanguy, c'est un peu l'enclume pour écraser la mouche.
Ça reste à voir, ça. Il y a eu un AoC où on n'arrêtait pas de ressortir un processeur bizarroïde pour l'enrichir de nouvelles instructions et variantes d'instructions existantes.
Il y a un risque pour que ce ne soit pas la dernière fois que nous aurons à bidouiller avec du code machine de communicateur elfique…
Oui, la seconde partie apparaît alors avoir rentré la bonne réponse à la première partie. Et les données d'entrée et donc les bonnes réponses sont associées à une identité en effet.
Sans surprise, j'ai implémenté un CPU selon les spécifications fournies. Mais pour ce qui est de faire quelque chose à partir des états qu'il atteint à des cycles donnés, je me suis amusé à y introduire ce que j'ai appelé un débogueur, je vous laisse découvrir ça.
importiofromenumimportEnumfromtypingimportCallable,Iterable,Iterator,List,Optional,TupleclassOperation(Enum):addx=('addx',1,2)noop=('noop',0,1)def__init__(self,word:str,nargs:int,cycles:int)->None:self.word=wordself.nargs=nargsself.cycles=cyclesclassInstruction:def__init__(self,op:Operation,*args:int)->None:self.op=opiflen(args)!=op.nargs:raiseValueError("operator {} expect {} arguments".format(op,op.nargs))self.args=argsclassCPU:def__init__(self,program:Iterable[Instruction],debug:Optional[Callable[[int,int],None]]=None)->None:self.X=1self.cycle=1self.program=programself.debug=debugdef_apply(self,instruction:Instruction)->None:ifinstruction.opisOperation.addx:self.X+=instruction.args[0]elifinstruction.opisOperation.noop:passdef_cycle(self)->None:ifself.debugisnotNone:self.debug(self.cycle,self.X)self.cycle+=1defrun(self)->None:last_instruction=Noneforinstructioninself.program:for_inrange(instruction.op.cycles):self._cycle()self._apply(instruction)defimport_program(lines:Iterable[str])->Iterator[Instruction]:forlineinlines:words=line.split()op=Operation[words[0]]args=[int(word)forwordinwords[1:]]yieldInstruction(op,*args)classStrengthSum:def__init__(self)->None:self.strength=0def__call__(self,cycle:int,value:int)->None:ifcyclein(20,60,100,140,180,220):self.strength+=cycle*valueclassCRTDrawer:def__init__(self)->None:self.crt=io.StringIO()@staticmethoddefposition(cycle:int)->int:return(cycle-1)%40def__call__(self,cycle:int,value:int)->None:position=self.position(cycle)ifposition==0:self.crt.write('\n')ifvalue-1<=position<=value+1:self.crt.write('█')else:self.crt.write(' ')classMultiDebug:def__init__(self,*debugs:Callable[[int,int],None])->None:self.debugs=debugsdef__call__(self,cycle:int,value:int)->None:fordebuginself.debugs:debug(cycle,value)defsolve_both(lines:Iterable[str])->Tuple[int,str]:"""Solve part 1 of today's puzzle"""program=import_program(lines)strength_report=StrengthSum()crt_drawer=CRTDrawer()cpu=CPU(program,debug=MultiDebug(strength_report,crt_drawer))cpu.run()returnstrength_report.strength,crt_drawer.crt.getvalue()
Bon, en fait, pas besoin d'énumérer de façon aussi laborieuse bien sûr :
from__future__importannotationsfromenumimportEnumfromtypingimportIterable,Iterator,Set,TupleCoords=Tuple[int,int]classDirection(Enum):O=(0,0)U=(0,1)D=(0,-1)L=(-1,0)R=(1,0)def__init__(self,dx,dy):self.dx=dxself.dy=dyclassKnot:def__init__(self,x:int,y:int):self.x=xself.y=y@propertydefcoords(self)->Coords:return(self.x,self.y)defmove(self,direction:Direction)->None:self.x+=direction.dxself.y+=direction.dydefdist(self,other:Knot)->int:returnmax(abs(self.x-other.x),abs(self.y-other.y))deffollow(self,other:Knot)->None:ifself.dist(other)<=1:returnifself.x<other.x:self.x+=1ifself.x>other.x:self.x-=1ifself.y<other.y:self.y+=1ifself.y>other.y:self.y-=1classRope:def__init__(self,n_knots:int)->None:self.knots=[Knot(0,0)for_inrange(n_knots)]@propertydefhead(self)->Knot:returnself.knots[0]@propertydeftail(self)->Knot:returnself.knots[-1]defmove(self,direction:Direction)->None:self.head.move(direction)foriinrange(1,len(self.knots)):leader=self.knots[i-1]follower=self.knots[i]follower.follow(leader)defimport_lines(lines:Iterable[str])->Iterator[Direction]:forlineinlines:word1,word2=line.split()direction=Direction[word1]repeat=int(word2)for_inrange(repeat):yielddirectiondefsolve_both(lines:Iterable[str])->Tuple[int,int]:"""Solve both parts of today's puzzle"""rope=Rope(10)visited1=set()# type: Set[Coords]visited2=set()# type: Set[Coords]fordirectioninimport_lines(lines):rope.move(direction)visited1.add(rope.knots[1].coords)visited2.add(rope.tail.coords)returnlen(visited1),len(visited2)
#! /usr/bin/python3# Advent of Code 2022, day 9from__future__importannotationsfromenumimportEnumfromtypingimportIterable,Iterator,Set,TupleCoords=Tuple[int,int]classDirection(Enum):O=(0,0)U=(0,1)D=(0,-1)L=(-1,0)R=(1,0)UL=(-1,1)UR=(1,1)DL=(-1,-1)DR=(1,-1)def__init__(self,dx,dy):self.dx=dxself.dy=dyclassKnot:def__init__(self,x:int,y:int):self.x=xself.y=y@propertydefcoords(self)->Coords:return(self.x,self.y)defmove(self,direction:Direction)->None:self.x+=direction.dxself.y+=direction.dydefdist(self,other:Knot)->int:""""Chebyshev distance! (Not really used in my code, actually)"""returnmax(abs(self.x-other.x),abs(self.y-other.y))defdirection_to(self,other:Knot)->Direction:dx=other.x-self.xdy=other.y-self.yifdx<-1:ifdy<0:returnDirection.DLifdy==0:returnDirection.Lifdy>0:returnDirection.ULifdx==-1:ifdy<-1:returnDirection.DLif-1<=dy<=1:returnDirection.Oifdy>1:returnDirection.ULifdx==0:ifdy<-1:returnDirection.Dif-1<=dy<=1:returnDirection.Oifdy>1:returnDirection.Uifdx==1:ifdy<-1:returnDirection.DRif-1<=dy<=1:returnDirection.Oifdy>1:returnDirection.URifdx>1:ifdy<0:returnDirection.DRifdy==0:returnDirection.Rifdy>0:returnDirection.URassertFalse# cannot happen, all cases were covereddeffollow(self,other:Knot)->None:self.move(self.direction_to(other))classRope:def__init__(self,n_knots:int)->None:self.knots=[Knot(0,0)for_inrange(n_knots)]@propertydefhead(self)->Knot:returnself.knots[0]@propertydeftail(self)->Knot:returnself.knots[-1]defmove(self,direction:Direction)->None:self.head.move(direction)foriinrange(1,len(self.knots)):leader=self.knots[i-1]follower=self.knots[i]follower.follow(leader)defimport_lines(lines:Iterable[str])->Iterator[Direction]:forlineinlines:word1,word2=line.split()direction=Direction[word1]repeat=int(word2)for_inrange(repeat):yielddirectiondefsolve_both(lines:Iterable[str])->Tuple[int,int]:"""Solve both parts of today's puzzle"""rope=Rope(10)visited1=set()# type: Set[Coords]visited2=set()# type: Set[Coords]fordirectioninimport_lines(lines):rope.move(direction)visited1.add(rope.knots[1].coords)visited2.add(rope.tail.coords)returnlen(visited1),len(visited2)
J'aurais bien aimé éviter d'énumérer tous les cas de mouvement de suivi, mais je n'ai rien trouvé d'astucieux pour éviter cela.
J'espère que dans vos solutions perso, vous aurez pensé à utiliser, en arrivant à la deuxième partie, à utiliser une seule corde pour simuler les deux…
Une petite optimisation pour la première partie : si on arrive à une hauteur de 9, pas la peine de regarder plus loin, aucun arbre ne sera plus haut que ça.
Mais bon, la seconde partie est bien plus coûteuse en temps de toute façon.
Alors qu'on a enfin quitté le camp pour aller chercher des caramboles, je ne peux m'empêcher que ça procrastine encore :
– Voilà, le verger c'est par là…
– Oh, regardez, la forêt que nous avons planté il y a quelques années, on pourrait y construire une cabane !
– Oui, super, ça nous changera les idées. Lutin drone-opérateur, tu nous cartographie ça ?
– Patron, venez nous donner un coup de main pour calculer le meilleur emplacement pour notre cabane au lieu d'essayer de régler votre communicateur. On vous l'a déjà dit, il est défectueux, et vous aurez tout le temps pour le réparer plus tard. On a plus important à faire là !
– Et les fruits pour les rennes alors ?
– Les quoi ? Ah, ça… On n'y est pas encore, soyez patient. Vous êtes pressé, vous avez un rendez-vous qui approche ou quoi ?
# Attention
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au journal Piéger les démarcheurs abusifs. Évalué à 10.
Un détail tout de même, ça semble évident mais ça peut demander un peu de concentration pour ne pas l'oublier en court de route : tant que vous n'êtes pas arrivé à vos fins, c'est à dire que vous n'avez pas assez d'informations, il ne faut pas donner le moindre indice qui laisserait soupçonner que vous n'appréciez pas ce démarchage.
Éviter par exemple des phrases comme « Au fait, comment avez-vous eu mon numéro de téléphone ? » Si votre interlocuteur commence à soupçonner que son appel vous dérange, il risque de ne pas vous croire si vous vous prétendez intéressé, et l'appel ne durera pas longtemps. Pas assez longtemps pour obtenir les infos que vous souhaitez en tout cas.
Ces démarcheurs savent très bien que ce qu'ils font est illégal, même s'ils prétendent parfois le contraire. Et sachant qu'ils peuvent être poursuivis, ou avoir diverses emmerdes comme être radiés de la plate-forme CPF, ils commencent à être assez méfiants.
L'autre jour, j'ai oublié ce principe et demandé directement à une démarcheuse où elle travaillait. Elle m'a fait répéter la question, et après avoir compris ma demande, elle a raccroché sans y mettre la moindre forme. Ni refus, ni excuse, ni au-revoir, juste raccroché. Si ce n'est pas de la méfiance, ça…
# Exemple : démarchage au CPF
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au journal Piéger les démarcheurs abusifs. Évalué à 10.
À titre d'exemple, j'ai pu piéger une entreprise de formation professionnelle qui se livrait au démarchage téléphonique. Sans doute sous-traité, mais ce n'est pas mon problème, les agissements d'un sous-traitant engagent évidemment la responsabilité de son donneur d'ordre.
Je me suis montré intéressé par une formation en langue anglaise, très utile pour mon travail. Le démarcheur m'a alors fait rappeler par un responsable des inscriptions, et je m'attendais au pire, c'est à dire à ce qu'il me demande mon numéro de sécurité sociale et qu'il réinitialise avec mon aide mon mot de passe pour accéder lui-même à mon compte CPF et m'inscrire à sa formation. Ç'aurait été une arnaque selon les règles, mais non, c'était un peu plus honnête, il m'a juste demandé mon adresse électronique, celle inscrite sur le compte CPF, ce qui lui a suffit à me pré-inscrire à des formations.
Vérification faite, les formations apparaissaient bien comme des propositions à valider, avec un organisme de formation tout à fait identifiable, en tout cas par la plate-forme CPF. J'en avais assez, et la fin de la discussion téléphonique a été assez intéressante :
[^] # Re: Optimisation
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 14. Évalué à 3.
Avec ladite optimisation :
[^] # Re: Beaucoup de temps pour animer et afficher joli
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 14. Évalué à 3.
Bravo pour l'implémentation de mon idée !
# Optimisation
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 14. Évalué à 3.
Visiblement, il y a une optimisation possible : une si un grain de sable, lâché du point de départ, tombe à un endroit, on peut lâcher ce grain et les suivants de cet endroit, jusqu'à ce qu'il soit ensablé…
# Pistes pour la partie 2
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 14. Évalué à 4.
Je me demande s'il ne serait pas possible de résoudre la partie 2 bien plus vite, en calculant l'aire des zones protégées par des rochers.
En effet, le nombre d'unités de sables tombées, c'est l'aire d'une triangle plein, moins celle des rochers qu'il contient, moins l'aire des zones protégées par ces derniers.
Ceci dit, déterminer la zone protégée par l'ensemble des rochers, c'est loin d'être évident. La zone protégée par un segment horizontal, c'est facile, c'est un triangle dessous. La zone protégée par un segment vertical, c'est facile, c'est rien du tout. Mais quand on commence à avoir des segments horizontaux et verticaux qui se touchent, ça devient compliqué.
Allez, une autre piste, sans doute pas beaucoup plus simple. On dirait qu'on peut déterminer la zone protégée par des rochers, non pas seulement par calcul, mais aussi par une simulation bizarre : faire couler de l'air depuis le bas vers le haut et regarder où il s'accumule. Mais ça pose le problème d'introduire cet air : il faut qu'il s'accumule et qu'il traverse en même temps les segments…
# En Python, modélisé
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 14. Évalué à 4. Dernière modification le 14 décembre 2022 à 11:40.
[^] # Re: python, en trichant
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 13. Évalué à 6.
On peut tricher en moins craignos. Un indice :
# En Pypthon
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 13. Évalué à 5.
Pour la définition de
aoc.group_lines(lines: str)
, cf. hier# Étoile
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 12. Évalué à 4.
Au fait, vu les visualisations, vous avez évidemment remarqué que loin d'être une gorge de rivière, nous avions plutôt affaire à
l'île de Numenorune montagne en forme d'étoile, n'est-ce pas ?# En Python, avec un parseur d'opération
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 11. Évalué à 4.
# En Python
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 12. Évalué à 4.
Ma première idée fonctionnait sur l'exemple mais pas sur les données réelles : parcourir récursivement une matrice de proche en proche en évitant simplement les endroits où on est déjà passé, ça atteint vite la limites de longueur de la pile d'appels.
Du coup, j'ai fait du Dijkstra. Enfin quelque chose inspiré de son algorithme en tout cas. Ça pourrait être fait de façon entièrement itérative, mais ça ne valait pas la peine, ça reste raisonnable en récursif.
Une fois qu'on a ça, la deuxième partie du puzzle ne pose pas spécialement de problème. Il y a tout de même deux façons de l'implémenter, une bête et méchante (on prend la première solution et on l'applique plusieurs fois), et une un rien plus futée.
[^] # Re: À la chasse aux singes, j'envoie le Python !
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 11. Évalué à 3.
Euh, modulo leur PPCM évidemment !
[^] # Re: À la chasse aux singes, j'envoie le Python !
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 11. Évalué à 4.
Je n'ai pas encore résolu le problème de ce jour, dimanche c'est famille, mais ça va venir. 🙂
En lisant l'énoncé, tout de même, je m'étais dit que ça devait faire des nombres qui monteraient bien vite. En y réfléchissant, je pense que j'aurais tout seul pensé à travailler modulo leur PGCD.
Pour le code d'opération, l'eval vient tout de suite en tête bien sûr, mais ça me donne quand même quelques boutons, d'écrire du code qui a l'air d'un trou de sécurité. Réflexe professionnel je pense. À suivre, je dois toujours coder ça de toute façon.
[^] # Re: En Python, modélisé
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 9. Évalué à 4.
Eh oh, je sais que j'écris du code-fleuve, mais pas la peine de se moquer non plus !
[^] # Re: Plus simple
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 10. Évalué à 3.
Ça reste à voir, ça. Il y a eu un AoC où on n'arrêtait pas de ressortir un processeur bizarroïde pour l'enrichir de nouvelles instructions et variantes d'instructions existantes.
Il y a un risque pour que ce ne soit pas la dernière fois que nous aurons à bidouiller avec du code machine de communicateur elfique…
[^] # Re: Où est la partie 2 ?
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 10. Évalué à 4.
Oui, la seconde partie apparaît alors avoir rentré la bonne réponse à la première partie. Et les données d'entrée et donc les bonnes réponses sont associées à une identité en effet.
# En Python, modélisé
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 10. Évalué à 5. Dernière modification le 10 décembre 2022 à 13:03.
Sans surprise, j'ai implémenté un CPU selon les spécifications fournies. Mais pour ce qui est de faire quelque chose à partir des états qu'il atteint à des cycles donnés, je me suis amusé à y introduire ce que j'ai appelé un débogueur, je vous laisse découvrir ça.
[^] # Re: En Python, modélisé
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 9. Évalué à 4. Dernière modification le 09 décembre 2022 à 18:05.
Tu peux te passer de la position initiale dans l'ensemble des positions visitées : après le premier mouvement, la queue de la corde y sera toujours.
# Coin coin
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 9. Évalué à 4.
Le problème parle de corde à nœuds, mais m'évoquerait plutôt une canne et ses canetons, pas vous ?
[^] # Re: En Python, modélisé
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 9. Évalué à 3. Dernière modification le 09 décembre 2022 à 16:58.
Bon, en fait, pas besoin d'énumérer de façon aussi laborieuse bien sûr :
[^] # Re: En Python, modélisé
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 9. Évalué à 3.
Ce serait sans doute plus joli avec des coordonnées indexées (une liste de coordonnées en somme) plutôt que nommées.
Mais ce n'est de toute façon pas très compréhensible, de cette façon-là.
# En Python, modélisé
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 9. Évalué à 4.
J'aurais bien aimé éviter d'énumérer tous les cas de mouvement de suivi, mais je n'ai rien trouvé d'astucieux pour éviter cela.
J'espère que dans vos solutions perso, vous aurez pensé à utiliser, en arrivant à la deuxième partie, à utiliser une seule corde pour simuler les deux…
[^] # Re: python procédural, moche mais efficace
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 8. Évalué à 4.
Une petite optimisation pour la première partie : si on arrive à une hauteur de 9, pas la peine de regarder plus loin, aucun arbre ne sera plus haut que ça.
Mais bon, la seconde partie est bien plus coûteuse en temps de toute façon.
# Procrastination
Posté par 🚲 Tanguy Ortolo (site web personnel) . En réponse au message Avent du Code, jour 8. Évalué à 3.
Alors qu'on a enfin quitté le camp pour aller chercher des caramboles, je ne peux m'empêcher que ça procrastine encore :
– Voilà, le verger c'est par là…
– Oh, regardez, la forêt que nous avons planté il y a quelques années, on pourrait y construire une cabane !
– Oui, super, ça nous changera les idées. Lutin drone-opérateur, tu nous cartographie ça ?
– Patron, venez nous donner un coup de main pour calculer le meilleur emplacement pour notre cabane au lieu d'essayer de régler votre communicateur. On vous l'a déjà dit, il est défectueux, et vous aurez tout le temps pour le réparer plus tard. On a plus important à faire là !
– Et les fruits pour les rennes alors ?
– Les quoi ? Ah, ça… On n'y est pas encore, soyez patient. Vous êtes pressé, vous avez un rendez-vous qui approche ou quoi ?