Mercurial > hg > python
comparison nono.py @ 71:eea4bcb657bc simple
Pruned back to pass 0
| author | Henry Thompson <ht@markup.co.uk> |
|---|---|
| date | Fri, 18 Jul 2025 21:54:39 +0100 |
| parents | 578cb569d815 |
| children | 15e540c190d5 |
comparison
equal
deleted
inserted
replaced
| 70:578cb569d815 | 71:eea4bcb657bc |
|---|---|
| 6 import sys | 6 import sys |
| 7 | 7 |
| 8 Red='[31m' | 8 Red='[31m' |
| 9 eRed='[39m' | 9 eRed='[39m' |
| 10 RedFmt=Red+'%s'+eRed | 10 RedFmt=Red+'%s'+eRed |
| 11 | |
| 12 def interleave(*args): | |
| 13 for vals in zip(*args): | |
| 14 yield from vals | |
| 15 | 11 |
| 16 class Run: | 12 class Run: |
| 17 '''What we're looking for, converted eventually into what | 13 '''What we're looking for, converted eventually into what |
| 18 we've found. | 14 we've found. |
| 19 "left" and "right" make sense for Rows, for Columns read "above" and "below"''' | 15 "left" and "right" make sense for Rows, for Columns read "above" and "below"''' |
| 94 return '%s|'%('|'.join(str(c) for c in self)) | 90 return '%s|'%('|'.join(str(c) for c in self)) |
| 95 | 91 |
| 96 def myPrintSize(self): | 92 def myPrintSize(self): |
| 97 return sum((len(str(run)) if isinstance(run,Run) else 1) for run in self.runs) | 93 return sum((len(str(run)) if isinstance(run,Run) else 1) for run in self.runs) |
| 98 | 94 |
| 99 #=========v-old-v============ | |
| 100 | |
| 101 def resetAllRuns(self): | |
| 102 # compute the set of all possible layouts for runs | |
| 103 self.rn=len(self.runs) | |
| 104 rtot=sum(self.runs) | |
| 105 self.allRuns=list(self.seedList(0,0,self.margin0,self.marginN+1, | |
| 106 sum(1+self.runs[k] for k in range(self.rn))-1)) | |
| 107 self.nar=len(self.allRuns) | |
| 108 | |
| 109 def seedList(self,i,j,pos0,posN,runLen): | |
| 110 """ | |
| 111 :param i: starting skip before next run | |
| 112 :type i: 0 if pos==0 else 1 | |
| 113 :param j: next run number | |
| 114 :type j: int | |
| 115 :param pos0: left margin | |
| 116 :type pos0: int | |
| 117 :param posN: right margin | |
| 118 :type posN: int | |
| 119 :return: list of runlens separated, possibly prefixed, with skips as negative integers | |
| 120 """ | |
| 121 bound=posN-(pos0+runLen)+1 | |
| 122 dprint('s',i,j,pos0,posN,runLen,bound) | |
| 123 if j==self.rn: | |
| 124 yield [] | |
| 125 return | |
| 126 r=self.runs[j] | |
| 127 for v in range(i,bound): | |
| 128 for sub in self.seedList(1,j+1,pos0+v+r,posN,runLen-(r+1)): | |
| 129 yield [-v,r]+sub | |
| 130 | |
| 131 def step(self,pos): | |
| 132 if self.runs==[]: | |
| 133 return 0 | |
| 134 scratch=[0 if c.val is None else c.val for c in self] | |
| 135 wins=0 | |
| 136 changed=0 | |
| 137 for k,runs in enumerate(self.allRuns): | |
| 138 if (self.m.loop,self.cLet,pos,k)==(1,'R',7,5): | |
| 139 print(self.m) | |
| 140 dprint('=====pass %s.%s.%s.%s======'%(self.m.loop,self.cLet,pos,k)) | |
| 141 if self.onepass(self.margin0,self.marginN+1,scratch,runs.copy()): | |
| 142 wins+=1 | |
| 143 dprint(wins, scratch) | |
| 144 maybeSeq=None | |
| 145 inSeq=None | |
| 146 j=None | |
| 147 for i in range(self.n): | |
| 148 if scratch[i]>=wins: | |
| 149 # If blobby in _every_ pass, then must be a blob | |
| 150 if inSeq is None: | |
| 151 inSeq=i if maybeSeq is None else maybeSeq | |
| 152 dprint('s1',inSeq) | |
| 153 maybeSeq=None | |
| 154 if self[i].val is None: | |
| 155 dprint('s1a',i) | |
| 156 self.newBlob(i,True) # Check cross-vector | |
| 157 j=i | |
| 158 changed=1 | |
| 159 elif self[i].val is True: | |
| 160 pass | |
| 161 else: | |
| 162 eprint("Shouldn't happen: attempt to blob where x already present! %s at %s"%(self,i),err=101) | |
| 163 elif inSeq is not None: | |
| 164 if self[i].val is not True: | |
| 165 inSeq=None | |
| 166 maybeSeq=None | |
| 167 dprint('s2',i-1,j) | |
| 168 if j is not None: | |
| 169 # Only if we actually added a blob at some point | |
| 170 dpush('b_s2') | |
| 171 self.checkNew(i-1) | |
| 172 dpop('b_s2') | |
| 173 elif self[i].val is True and maybeSeq is None: | |
| 174 dprint('s3',i) | |
| 175 maybeSeq=i | |
| 176 dprint('s4',inSeq,i) | |
| 177 if inSeq is not None: | |
| 178 dpush('b_s4') | |
| 179 self.checkNew(i) | |
| 180 dpop('b_s4') | |
| 181 return changed | |
| 182 | |
| 183 def onepass(self,i0,iBound,scratch,stack): | |
| 184 """note that stack is not a simple run, but one with _negative_ numbers between | |
| 185 and possibly before the positive ones, indicating obligatory skips | |
| 186 """ | |
| 187 i=i0 # starting index into self/scratch/maybe | |
| 188 maybe=[0]*self.n | |
| 189 dprint('r: %s'%stack,i0,iBound) | |
| 190 req=sum((-r if r<0 else r) for r in stack) | |
| 191 while stack and i<iBound: | |
| 192 r=rr=stack.pop(0) | |
| 193 dprint('pop:',r) | |
| 194 if r<1: | |
| 195 # obligatory skip | |
| 196 # (Above init of self.allRuns is easier if we allow a 0 to be ignored | |
| 197 for k in range(i,i-r): | |
| 198 if self[k] is True: | |
| 199 # has to be False or None to skip | |
| 200 dprint('x0',k) | |
| 201 return False | |
| 202 i-=r | |
| 203 req+=r | |
| 204 r=rr=stack.pop(0) | |
| 205 # rr is run remaining -- how many we still need | |
| 206 # First, check if we can start here: 0 is OK, and n>0 iff n-1 is None or False | |
| 207 # if i>0 and self[i-1].val is True: | |
| 208 # dprint('x1',i) | |
| 209 # return | |
| 210 if (iBound-i)<req: | |
| 211 # Can't win, give up altogether | |
| 212 dprint('x2',i,iBound,req) | |
| 213 return False | |
| 214 j=i # start of run, if we find it | |
| 215 gapsFilled=0 | |
| 216 dprint('top:',j) | |
| 217 while rr>0: | |
| 218 # guaranteed by check above that we won't run over the far end | |
| 219 c=self[i].val | |
| 220 if c is False: | |
| 221 dprint('x3',i) | |
| 222 return False | |
| 223 if c is None: | |
| 224 # we could add a blob here | |
| 225 gapsFilled+=1 | |
| 226 # and a blob already present is OK | |
| 227 rr-=1 | |
| 228 i+=1 | |
| 229 # Maybe a win? | |
| 230 # look ahead, can we stop here? | |
| 231 if i<self.n and self[i].val is True: | |
| 232 dprint('x4',i) | |
| 233 return False | |
| 234 # elif gapsFilled==0: | |
| 235 # # We must have crossed at least one gap... | |
| 236 # print("Shouldn't happen: no gap! me:%s i:%s j:%s rr:%s"%(self,i, j, rr),file=sys.stderr) | |
| 237 # raise Exception | |
| 238 # Victory! | |
| 239 dprint('c6',r,j,i) | |
| 240 for k in range(j,i): | |
| 241 maybe[k]+=1 | |
| 242 req-=r | |
| 243 # on to the next run | |
| 244 # end of inner loop, did we win? | |
| 245 if (not stack) or i==iBound: | |
| 246 # yes | |
| 247 dprint('win:',maybe) | |
| 248 for k in range(iBound): | |
| 249 scratch[k]+=maybe[k] | |
| 250 return True | |
| 251 eprint("Shouldn't happen? - fell through",stack,i,iBound,err=102) | |
| 252 | |
| 253 def checkX(self,pos,crosspos): | |
| 254 # New x at pos, are there others that can be xed out now? | |
| 255 # Overkill as is? | |
| 256 dprint('cx',self.cLet+str(crosspos),pos) | |
| 257 if self.runs: | |
| 258 start0=self.margin0 # 0 if self.margin0==0 else self.margin0+1 | |
| 259 if self.marginal(range(start0,pos),self.runs[0]): | |
| 260 dprint('cx1a') | |
| 261 else: | |
| 262 dprint('cx1b') | |
| 263 # if len(self.runs)>1: | |
| 264 startN=self.marginN # self.marginN if (self.marginN==self.n-1) else self.marginN-1 | |
| 265 if self.marginal(range(startN,pos,-1),self.runs[-1]): | |
| 266 dprint('cx2a') | |
| 267 else: | |
| 268 dprint('cx2b') | |
| 269 | |
| 270 def checkNew(self,pos): | |
| 271 # New blob at pos, can we complete anything? | |
| 272 # Assuming @@ FTTB that it's never possible to definitively complete a | |
| 273 # non-marginal run, which is wrong if the length is unique and it's bounded... | |
| 274 start0=self.margin0 #0 if self.margin0==0 else self.margin0+1 | |
| 275 startN=self.marginN # self.marginN if (self.marginN==self.n-1) else self.marginN-1 | |
| 276 dprint('cn',start0,pos,startN) | |
| 277 changed=False | |
| 278 # First, find our boundaries: | |
| 279 s=pos # start index of our run | |
| 280 if s>start0: | |
| 281 while s>start0 and self[s-1].val is True: | |
| 282 s-=1 | |
| 283 f=pos # finish index of our run | |
| 284 if f<startN: | |
| 285 while f<startN and self[f+1].val is True: | |
| 286 f+=1 | |
| 287 l=(f-s)+1 # our length | |
| 288 dprint('%s:%s,%s,%s,%s:<%s>'%(self.cLet,s,f,l,self.runs,self)) | |
| 289 c=self.runs.count(l) | |
| 290 if c==0: | |
| 291 # not big enough yet | |
| 292 dprint('x0') | |
| 293 return | |
| 294 if self.runs[0]==l: | |
| 295 # is it safely left marginal, i.e. no blobs or big enough gaps before us? | |
| 296 if self.marginal(range(start0,s),l): | |
| 297 changed=True | |
| 298 dprint('n1') | |
| 299 # mark our margins | |
| 300 for i in range(start0,s): # not sure this is still needed since | |
| 301 # added gap-filling in self.marginal | |
| 302 if self[i].val is None: | |
| 303 self.newX(i,False) | |
| 304 if f<startN: | |
| 305 if self[f+1].val is None: | |
| 306 self.newX(f+1,True) | |
| 307 self.found0(f) # pull in the start margin at least to f+2 | |
| 308 else: | |
| 309 dprint('x1a') | |
| 310 if not changed: | |
| 311 if self.runs[-1]==l: | |
| 312 # is it safely _right_ marginal, i.e. no blobs or big enough gaps _after_ us? | |
| 313 if self.marginal(range(startN,f,-1),l): | |
| 314 changed=True | |
| 315 dprint('n2') | |
| 316 # mark our margins: still needed? see above | |
| 317 for i in range(startN,f,-1): | |
| 318 if self[i].val is None: | |
| 319 self.newX(i,False) | |
| 320 if s>start0: | |
| 321 if self[s-1].val is None: | |
| 322 self.newX(s-1,True) | |
| 323 self.foundN(s) # pull in the finish margin at least to s-2 | |
| 324 else: | |
| 325 dprint('x2a') | |
| 326 else: | |
| 327 dprint('x2b') | |
| 328 if changed: | |
| 329 self.resetAllRuns() | |
| 330 | |
| 331 def marginal(self,rng,l): | |
| 332 dprint('m%s'%self.cLet,rng.start,rng.stop,rng.step,l) | |
| 333 g=0 # length of a gap | |
| 334 for i in rng: | |
| 335 if self[i].val is True: | |
| 336 # Shouldn't be possible? | |
| 337 dprint('mx0') | |
| 338 return False | |
| 339 if self[i].val is False: | |
| 340 if g>0: | |
| 341 # Block a too-small gap | |
| 342 dprint('m1',i-g,i) | |
| 343 for j in range(i-g,i): | |
| 344 self.newX(j,True) | |
| 345 g=0 | |
| 346 else: | |
| 347 # None | |
| 348 g+=1 | |
| 349 if g==l: | |
| 350 # run could fit here, so no go | |
| 351 dprint('mx1') | |
| 352 return False | |
| 353 if g>0: | |
| 354 # Block a too-small gap | |
| 355 if rng.step==1: | |
| 356 # forward | |
| 357 dprint('m2f',i-g,i) | |
| 358 for j in range(i+1-g,i+1): | |
| 359 self.newX(j,True) | |
| 360 else: | |
| 361 # backward | |
| 362 dprint('m2b',i+g,i) | |
| 363 for j in range(i+g-1,i-1,-1): | |
| 364 self.newX(j,True) | |
| 365 return True | |
| 366 | |
| 367 def found0(self,i): | |
| 368 dprint('found0 called on %s at %s'%(self,i)) | |
| 369 i=self.margin0 | |
| 370 while self[i].val is False: | |
| 371 i+=1 | |
| 372 if self[i].val is True: | |
| 373 r=self.runs.pop(0) | |
| 374 self.initialComplete.append(r) | |
| 375 self.margin0+=r+1 | |
| 376 self.updateHeader(r=r,pre=True) | |
| 377 | |
| 378 def foundN(self,i): | |
| 379 dprint('foundN called on %s at %s'%(self,i)) | |
| 380 i=self.marginN | |
| 381 while self[i].val is False: | |
| 382 i-=1 | |
| 383 if self[i].val is True: | |
| 384 r=self.runs.pop() | |
| 385 self.finalComplete=[r]+self.finalComplete | |
| 386 self.marginN-=r+1 | |
| 387 self.updateHeader(r=r,pre=False) | |
| 388 | |
| 389 #=============new simple============ | |
| 390 def pass0(self): | 95 def pass0(self): |
| 391 # simple first pass down/across a row | 96 # simple first pass down/across a row |
| 392 for i in range(0,len(self.runs),2): | 97 for i in range(0,len(self.runs),2): |
| 393 run=self.runs[i] | 98 run=self.runs[i] |
| 394 for p in range(run.i+run.s,run.i+run.n): | 99 for p in range(run.i+run.s,run.i+run.n): |
| 486 self.prespace=' '*(maxWidth-self.width) | 191 self.prespace=' '*(maxWidth-self.width) |
| 487 self.fmt="%s%%s|"%self.prespace | 192 self.fmt="%s%%s|"%self.prespace |
| 488 self.infix="" | 193 self.infix="" |
| 489 self.suffix="" | 194 self.suffix="" |
| 490 | 195 |
| 491 def newBlob(self,x,crossCheck=False): | |
| 492 self[x].setVal(True) | |
| 493 if crossCheck: | |
| 494 dpush('b_cc') | |
| 495 self[x].column.checkNew(self.y) | |
| 496 dpop('b_cc') | |
| 497 | |
| 498 def newX(self,x,crossCheck=False): | |
| 499 dprint('nx %s%s@%s'%('R',self.y,x)) | |
| 500 self[x].setVal(False) | |
| 501 if crossCheck: | |
| 502 dpush('x_cc') | |
| 503 self[x].column.checkX(self.y,x) | |
| 504 dpop('x_cc') | |
| 505 | |
| 506 class Column(Vector): | 196 class Column(Vector): |
| 507 cLet='C' | 197 cLet='C' |
| 508 def __init__(self,n,m,pos): | 198 def __init__(self,n,m,pos): |
| 509 Vector.__init__(self,n,m) | 199 Vector.__init__(self,n,m) |
| 510 self.x=pos | 200 self.x=pos |
| 544 self.fmt="%s%%s"%(' '*self.prespace) | 234 self.fmt="%s%%s"%(' '*self.prespace) |
| 545 header=('-'.join(str(c) for c in self.runs if isinstance(c,Run))) | 235 header=('-'.join(str(c) for c in self.runs if isinstance(c,Run))) |
| 546 self.header=self.fmt%header | 236 self.header=self.fmt%header |
| 547 dprint(self.header) | 237 dprint(self.header) |
| 548 | 238 |
| 549 def newBlob(self,y,crossCheck=False): | |
| 550 self[y].setVal(True) | |
| 551 if crossCheck: | |
| 552 dpush('b_cc') | |
| 553 self[y].row.checkNew(self.x) | |
| 554 dpop('b_cc') | |
| 555 | |
| 556 def newX(self,y,crossCheck=False): | |
| 557 dprint('nx %s%s@%s'%('C',self.x,y)) | |
| 558 self[y].setVal(False) | |
| 559 if crossCheck: | |
| 560 dpush('x_cc') | |
| 561 self[y].row.checkX(self.x,y) | |
| 562 dpop('x_cc') | |
| 563 | |
| 564 class Cell: | 239 class Cell: |
| 565 def __init__(self,row,y,column,x): | 240 def __init__(self,row,y,column,x): |
| 566 # At the intersection of row and column Vectors | 241 # At the intersection of row and column Vectors |
| 567 self.row=row | 242 self.row=row |
| 568 self.column=column | 243 self.column=column |
| 588 self.val=v | 263 self.val=v |
| 589 return True | 264 return True |
| 590 | 265 |
| 591 class Nono(dict): | 266 class Nono(dict): |
| 592 # 0,0 is upper left, so increasing y goes _downwards_, to match the standard layout | 267 # 0,0 is upper left, so increasing y goes _downwards_, to match the standard layout |
| 593 def __init__(self,runsPerRow,runsPerCol): | 268 def __init__(self,runsPerRow,runsPerCol,debug): |
| 594 global SOLVER | 269 global SOLVER |
| 595 self.loop=0 | 270 self.loop=0 |
| 596 self.dp='' | 271 self.dp='' # old depth hack |
| 272 self.debug = debug | |
| 597 self.dstate=[] | 273 self.dstate=[] |
| 598 SOLVER=self | 274 SOLVER=self |
| 599 n=self.n=len(runsPerCol) | 275 n=self.n=len(runsPerCol) |
| 600 if n!=len(runsPerRow): | 276 if n!=len(runsPerRow): |
| 601 print("losing r:%s x c:%s"%(len(runsPerRow),n),sys.stderr) | 277 print("losing r:%s x c:%s"%(len(runsPerRow),n),sys.stderr) |
| 619 maxRRwidth=max(row.width for row in rr) | 295 maxRRwidth=max(row.width for row in rr) |
| 620 for r in rr: | 296 for r in rr: |
| 621 r.updateHeader(maxWidth=maxRRwidth) | 297 r.updateHeader(maxWidth=maxRRwidth) |
| 622 self.rowfmt="%s|%%s|"%(' '*maxRRwidth) | 298 self.rowfmt="%s|%%s|"%(' '*maxRRwidth) |
| 623 | 299 |
| 624 | |
| 625 def __str__(self): | 300 def __str__(self): |
| 626 lines=[self.rowfmt%('|'.join([(self.columns[i]).header[j] for i in range(self.n)])) # 'rotate' | 301 lines=[self.rowfmt%('|'.join([(self.columns[i]).header[j] for i in range(self.n)])) # 'rotate' |
| 627 for j in range(self.maxCRheight)] | 302 for j in range(self.maxCRheight)] |
| 628 lines+=[str(r) for r in self.rows] | 303 lines+=[str(r) for r in self.rows] |
| 629 return "\n".join(lines) | 304 return "\n".join(lines) |
| 630 | 305 |
| 631 #=============new simple============ | |
| 632 def pass0(self): # do columns of empty layout | 306 def pass0(self): # do columns of empty layout |
| 633 for c in self.columns: | 307 for c in self.columns: |
| 634 c.pass0() | 308 c.pass0() |
| 635 print(self) | 309 dprint('After Pass 0 for columns', self, sep = '\n') |
| 636 for r in self.rows: | 310 for r in self.rows: |
| 637 r.pass0() | 311 r.pass0() |
| 638 print(self) | 312 dprint('After Pass 0 for rows', self, sep = '\n') |
| 639 for c in self.columns: | 313 for c in self.columns: |
| 640 c.check0() | 314 c.check0() |
| 641 print(self) | 315 dprint('After Check 0 for columns', self, sep = '\n') |
| 316 dprint(self) | |
| 642 for r in self.rows: | 317 for r in self.rows: |
| 643 r.check0() | 318 r.check0() |
| 319 dprint('After Check 0 for rows', self, sep = '\n') | |
| 644 | 320 |
| 645 def solve(self): | 321 def solve(self): |
| 646 self.pass0() | 322 self.pass0() |
| 647 print(self) | 323 |
| 648 return | 324 def dprint(*args, **kwargs): |
| 649 someChanged=1 | 325 '''Debug print''' |
| 650 while someChanged>0: | 326 if SOLVER.debug: |
| 651 self.loop+=1 | 327 print(*args, **kwargs) |
| 652 someChanged=0 | 328 sys.stdout.flush() |
| 653 dprint("***Solve C %s***"%self.loop) | |
| 654 for c in self.columns: | |
| 655 someChanged+=c.step(c.x) | |
| 656 print(someChanged) | |
| 657 print(self) | |
| 658 dprint("***Solve R %s***"%self.loop) | |
| 659 for r in self.rows: | |
| 660 someChanged+=r.step(r.y) | |
| 661 print(someChanged) | |
| 662 print(self) | |
| 663 | |
| 664 def dpush(s): | |
| 665 SOLVER.dp+=' ' | |
| 666 SOLVER.dstate.append(s) | |
| 667 | |
| 668 def dpop(s): | |
| 669 assert(SOLVER.dstate.pop()==s) | |
| 670 SOLVER.dp=SOLVER.dp[1:] | |
| 671 | |
| 672 def dprint(*args): | |
| 673 print(SOLVER.dp,end='') | |
| 674 print(*args) | |
| 675 sys.stdout.flush() | |
| 676 | 329 |
| 677 def eprint(*args,**kw): | 330 def eprint(*args,**kw): |
| 331 '''error print''' | |
| 678 print(*args,file=sys.stderr) | 332 print(*args,file=sys.stderr) |
| 679 sys.stderr.flush() | 333 sys.stderr.flush() |
| 680 print(SOLVER) | 334 print(SOLVER) |
| 681 breakpoint() | 335 breakpoint() |
| 682 exit(kw['err']) | 336 exit(kw['err']) |
| 683 | 337 |
| 684 def wprint(*args): | |
| 685 print(*args,file=sys.stderr) | |
| 686 sys.stderr.flush() | |
| 687 | |
| 688 if __name__ == '__main__': | 338 if __name__ == '__main__': |
| 339 if sys.argv[1] == '-d': | |
| 340 sys.argv.pop(1) | |
| 341 debug = True | |
| 342 else: | |
| 343 debug = False | |
| 689 if len(sys.argv)>1: | 344 if len(sys.argv)>1: |
| 690 f=open(sys.argv[1]) | 345 f=open(sys.argv[1]) |
| 691 else: | 346 else: |
| 692 f=sys.stdin | 347 f=sys.stdin |
| 693 | 348 |
| 696 n=int(len(vv)/2) | 351 n=int(len(vv)/2) |
| 697 print('%sx%s puzzle'%(n,n),file=sys.stderr) | 352 print('%sx%s puzzle'%(n,n),file=sys.stderr) |
| 698 cols=[[int(i) for i in v.split('.')] for v in vv[:n]] | 353 cols=[[int(i) for i in v.split('.')] for v in vv[:n]] |
| 699 rows=[[int(i) for i in v.split('.')] for v in vv[n:]] | 354 rows=[[int(i) for i in v.split('.')] for v in vv[n:]] |
| 700 | 355 |
| 701 solver=Nono(rows,cols) | 356 solver=Nono(rows, cols, debug) |
| 357 print(solver) | |
| 358 print() | |
| 702 solver.solve() | 359 solver.solve() |
| 360 print('Done',solver, sep = '\n') | |
| 703 | 361 |
| 362 | |
| 363 |
