Mercurial > hg > python
view nono.py @ 7:13f600be3a1b
implemented newBlob and found0, refactoring display
author | Henry S. Thompson <ht@inf.ed.ac.uk> |
---|---|
date | Wed, 11 Mar 2020 17:13:05 +0000 |
parents | a56d5285575b |
children | 3276cf6ee6df |
line wrap: on
line source
#!/usr/bin/python3 # Expects e.g. ^A copy from Nonograms dprint preview cols, then blank line, then rows # rows are space-separated # cols are one-digit-after-another, unless some 2-digit, in which case x is separator # E.g. # 13x1x2 # 19 # maps to # 13 # 1 1 # 2 9 import sys Red='[31m' eRed='[39m' RedFmt=Red+'%s'+eRed def interleave(*args): for vals in zip(*args): yield from vals class Vector(list): # reads top-to-bottom or left-to-right def __init__(self,n,m,runs): list.__init__(self,list(range(n))) self.n=n self.margin0=0 # a run can start here, so if >0 then self[m0-1].val must be False self.marginN=n-1 # a run can end here, so if <n-1 then self[mN+1].val ditto self.runs=self.origRuns=runs self.initialComplete="" self.finalComplete="" self.resetAllRuns() def __repr__(self): return "V@%s%s:%s"%(self.x,self.origRuns,list.__repr__(self)) def __str__(self): return '%s|'%('|'.join(str(c) for c in self)) def myPrintSize(self): return sum(len(str(run)) for run in self.runs)+len(self.runs)-1 def resetAllRuns(self): # compute the set of all possible layouts for runs self.rn=len(self.runs) rtot=sum(self.runs) self.allRuns=list(self.seedList(0,0,0, sum(1+self.runs[k] for k in range(self.rn))-1)) self.nar=len(self.allRuns) def seedList(self,i,j,pos,runLen): """ :param i: starting skip before next run :type i: 0 if pos==0 else 1 :param j: next run number :type j: int :param pos: left margin :type pos: int """ bound=self.n-(pos+runLen)+1 #dprint('s',i,j,pos,runLen,bound) if j==self.rn: yield [] return r=self.runs[j] for v in range(i,bound): for sub in self.seedList(1,j+1,pos+v+r,runLen-(r+1)): yield [-v,r]+sub def step(self): scratch=[0 if c.val is None else c.val for c in self] for k,runs in enumerate(self.allRuns): dprint('=====pass %s======'%k) self.onepass(0,self.n,scratch,runs.copy()) dprint(scratch) newSeq=False inSeq=False for i in range(self.n): if scratch[i]==self.nar: # If blobby in _every_ pass, then must be a blob if inSeq: newSeq=False else: inSeq=True newSeq=True if self[i].val is None: self.newBlob(i,newSeq) elif self[i].val is True: # already there pass else: print("Shouldn't happen: attempt to blob where x already present! %s at %s"%(self,i),file=sys.stderr) exit(101) else: inSeq=newSeq=False def onepass(self,i0,iBound,scratch,stack): """note that stack is not a simple run, but one with _negative_ numbers between and possibly before the positive ones, indicating obligatory skips """ i=i0 # starting index into self/scratch/maybe j=-1 # index into run maybe=[0]*iBound dprint('r: %s'%stack) req=sum((-r if r<0 else r) for r in stack) while stack and i<iBound: r=rr=stack.pop(0) dprint('pop:',r) if r<1: # obligatory skip # (Above init of self.allRuns is easier if we allow a 0 to be ignored i-=r req+=r r=rr=stack.pop(0) # rr is run remaining -- how many we still need j+=1 # index of current run in self.runs, we'll need to decorate that eventually inOne=-1 # if non-neg, records the start point of a possible run gapsFilled=0 # First, check if we can start here: 0 is OK, and n>0 iff n-1 is None or False if i>0 and i<iBound: while self[i-1].val: i+=1 if (iBound-i)<req: # Can't win, give up altogether dprint('c0',i,iBound,req) return while i<iBound: c=self[i].val dprint('top',i,c,inOne,rr) if c is None: # we could add a blob here dprint('c1') gapsFilled+=1 rr-=1 if inOne<0: dprint('c1a',i) # starts here inOne=i # fall through to check for completion else: dprint('c2') # c is a bool if inOne<0: dprint('c2a') if c: dprint('c2a1') # a *, we can possible start something here inOne=i rr-=1 # fall through to check for completion else: dprint('c2a2') # an x, can't start here, just move along i+=1 continue else: dprint('c2b') if c: dprint('c2b1') # a blob, extend or complete a partial rr-=1 # fall through to check for completion else: # abandon a partial dprint('c2b2') inOne=-1 rr=r i+=1 continue if rr>0: dprint('c3') # we're not done, carry on i+=1 continue # Maybe a win? # look ahead, can we stop here? # NB _self_.n if i+1<self.n and self[i+1].val: dprint('c4') # Nope inOne=-1 rr=r gapsFilled=0 i+=1 continue elif gapsFilled==0: dprint('c5') # We must have crossed at least on gap... print("Shouldn't happen: no gap! me:%s i:%s j:%s rr:%s inOne:%s"%(self,i, j, rr, inOne),file=sys.stderr) exit(100) # Victory! dprint('c6',r,inOne,i) for k in range(inOne,i+1): maybe[k]+=1 i+=1 req-=r break # on to the next run # end of inner loop, did we win? if (not stack) or i==iBound: # yes dprint('win:',maybe) for k in range(iBound): scratch[k]+=maybe[k] def checkNew(self,pos): # New blob at pos, can we complete anything? # Assuming @@ FTTB that it's never possible to definitively complete a # non-marginal run, which is wrong if the length is unique and it's bounded... changed=False # First, find our boundaries: s=pos # start index of our run while s>self.margin0 and self[s-1].val is True: s-=1 f=pos # finish index of our run while f<self.marginN and self[f+1].val is True: f+=1 l=(f-s)+1 # our length c=self.runs.count(l) if c==0: # not big enough yet dprint('n0') return j=self.runs.index(l) if j==0: # is it safely left marginal, i.e. no blobs or big enough gaps before us? if self.marginal(range(self.margin0,s),l): changed=True # mark our margins for i in range(self.margin0+1,s): if self[i].val is None: self[i].setVal(False) if f<self.marginN-1: if self[f+1].val is None: self[f+1].setVal(False) if f<self.marginN-2 and self[f+2].val is True: dprint('n1') self.checkNew(f+2) # I think@@ self.found0(f) # pull in the start margin at least to f+2 elif j==self.rn-1: # is it safely _right_ marginal, i.e. no blobs or big enough gaps _after_ us? if self.marginal(range(self.marginN-1,r,-1),l): changed=True # mark our margins for i in range(self.marginN-1,f,-1): if self[i].val is None: self[i].setVal(False) if s>self.margin0+1: if self[s-1].val is None: self[s-1].setVal(False) if s>self.margin0+2 and self[s-2].val is True: dprint('n2') self.checkNew(s-2) # I think@@ self.foundN(s) # pull in the finish margin at least to s-2 if changed: self.resetAllRuns() def marginal(self,rng,l): g=0 # length of a gap for i in rng: if self[i].val is True: return False if self[i].val is False: g=0 else: # None g+=1 if g==l: return False return True def found0(self,i): dprint('found0 called on %s at %s'%(self,i)) i=self.margin0 while self[i].val is False: i+=1 if self[i].val is True: r=self.runs.pop(0) self.initialComplete+=(RedFmt%r) self.margin0+=r+1 def foundN(self,i): print('foundN called on %s at %s'%(self,i),file=sys.stderr) class Row(Vector): def __init__(self,n,m,runs,pos): Vector.__init__(self,n,m,runs) self.y=pos self.width=self.myPrintSize() def __str__(self): return ((self.fmt%(' '.join(str(r) for r in self.runs)))+ Vector.__str__(self)) def updateHeader(self,maxWidth): self.maxWidth=maxWidth self.fmt="%s%%s|"%(' '*(maxWidth-self.width)) def newBlob(self,x,newSeq): self[x].setVal(True) self[x].column.checkNew(self.y) if newSeq: # We don't always check ourself, to avoid unnecessary duplication... self.checkNew(x) class Column(Vector): def __init__(self,n,m,runs,pos): Vector.__init__(self,n,m,runs) self.x=pos self.height=self.myPrintSize() def updateHeader(self,maxHeight): self.maxHeight=maxHeight self.fmt="%s%%s"%(' '*(maxHeight - self.height)) header=('-'.join(str(c) for c in self.runs)) self.header=self.fmt%header # pad to same 'height' def newBlob(self,y,newSeq): self[y].setVal(True) self[y].row.checkNew(self.x) if newSeq: # We don't always check ourself, to avoid unnecessary duplication... self.checkNew(y) class Cell: def __init__(self,row,y,column,x): # At the intersection of row and column Vectors self.row=row self.column=column self.x=x self.y=y self.val=None # three valued: None(unknown), True(filled), False(empty) self.row[x]=self self.column[y]=self def __repr__(self): return "C@(%s,%s):%s"%(self.x,self.y,self.val) def __str__(self): return ' ' if self.val is None else ('\u25A0' if self.val else 'x') def setVal(self,v): if v is True: if self.val is False: print("Warning: x -> * at %s,%s"%(self.x,self.y),file=sys.stderr) elif self.val is True: # No-op return self.val=v else: if self.val is not None: print("Warning: %s -> %s at %s,%s"%(self.val,v,self.x,self.y),file=sys.stderr) self.val=v class Nono(dict): # 0,0 is upper left, so increasing y goes _downwards_, to match the standard layout def __init__(self,runsPerRow,runsPerCol): n=self.n=len(runsPerCol) if n!=len(runsPerRow): print("losing r:%s x c:%s"%(len(runsPerRow),n),sys.stderr) exit(1) self.rc=runsPerRow self.cc=runsPerCol # print col nums>9 vertically :-( self.columns=cc=[Column(n,self,runsPerCol[i],i) for i in range(20)] self.maxCRheight=maxCRheight=max(col.height for col in cc) for c in cc: c.updateHeader(maxCRheight) self.rows=rr=[Row(n,self,runsPerRow[i],i) for i in range(20)] maxRRwidth=max(row.width for row in rr) for r in rr: r.updateHeader(maxRRwidth) self.rowfmt="%s|%%s|"%(' '*maxRRwidth) for x in range(20): for y in range(20): self[(x,y)]=Cell(rr[y],y,cc[x],x) def __str__(self): lines=[self.rowfmt%('|'.join([(self.columns[i]).header[j] for i in range(self.n)])) # 'rotate' for j in range(self.maxCRheight)] lines+=[str(r) for r in self.rows] return "\n".join(lines) def dprint(*args): pass if __name__ == '__main__': if len(sys.argv)>1: f=open(sys.argv[1]) else: f=sys.stdin cols=[] for l in f: l=l.rstrip() if l=='': break if 'x' in l: vv=[int(s) for s in l.split('x')] else: vv=[int(c) for c in l] cols.append(vv) rows=[[int(s) for s in l.split()] for l in f] solver=Nono(rows,cols) print(solver) for c in solver.columns: c.step() print() print(solver) for r in solver.rows: r.step() print() print(solver)