Add subdirectories
Add /ipynb/ and /py/ subdirectories to keep the home page neater.
This commit is contained in:
154
py/pal.py
Normal file
154
py/pal.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import string, random, os, re, bisect
|
||||
|
||||
"""Produce Panama-ish Palindromes. Copyright (C) 2002, Peter Norvig.
|
||||
See http://www.norvig.com/license.html and http://www.norvig.com/pal-alg.html"""
|
||||
|
||||
def is_panama(p):
|
||||
"Test if p is a Panama-ish palindrome."
|
||||
def is_unique(seq): return len(seq) == len(dict(zip(seq, seq)))
|
||||
return (p.endswith('Panama') and is_palindrome(p)
|
||||
and is_unique([s.strip() for s in p.split(',')]))
|
||||
|
||||
def is_palindrome(phrase):
|
||||
"Test if a phrase is a palindrome."
|
||||
cphrase = canonical(phrase)
|
||||
return cphrase == reverse(cphrase)
|
||||
|
||||
def canonical(word, sub=re.compile('[^A-Za-z0-9]').sub):
|
||||
"The canonical form for comparing: lowercase alphanumerics."
|
||||
return sub('', word).lower()
|
||||
|
||||
def read_dict(filename='npdict.txt'):
|
||||
"Read the file into global variables _fw and _bw and _truename."
|
||||
global _fw, _bw, _truename
|
||||
_fw, _bw, _truename = [], [], {'': ''}
|
||||
for word in open(filename).read().splitlines():
|
||||
w = canonical(word)
|
||||
_fw.append(w)
|
||||
_bw.append(reverse(w))
|
||||
_truename[w] = word
|
||||
_fw.sort(); _bw.sort()
|
||||
return len(_fw), len(_bw), len(_truename)
|
||||
|
||||
def update(obj, **entries): obj.__dict__.update(entries); return obj
|
||||
|
||||
class PalDict:
|
||||
"""A dictionary from which you can find canonical words that start or end
|
||||
with a given canonical substring, and find the true name of a
|
||||
canonical word."""
|
||||
def __init__(self, fw=None, bw=None, truename=None):
|
||||
update(self, fw=fw or _fw, bw=bw or _bw, truename=truename or _truename)
|
||||
|
||||
def startswith(self, prefix, k=100):
|
||||
"""Return up to k canonical words that start with prefix.
|
||||
If there are more than k, choose from them at random."""
|
||||
return k_startingwith(k, self.fw, prefix)
|
||||
|
||||
def endswith(self, suffix, k=100):
|
||||
"""Return up to k canonical words that end with suffix.
|
||||
If there are more than k, choose from them at random.
|
||||
Both the suffix and the word returned are reversed."""
|
||||
return k_startingwith(k, self.bw, suffix)
|
||||
|
||||
def k_startingwith(k, words, prefix):
|
||||
"""Choose up to k words that match the prefix (choose randomly if > k)."""
|
||||
start = bisect.bisect(words, prefix)
|
||||
end = bisect.bisect(words, prefix + 'zzzz')
|
||||
n = end - start
|
||||
if k >= n:
|
||||
results = words[start:end]
|
||||
random.shuffle(results)
|
||||
else: # Should really try to avoid duplicates
|
||||
results = [words[random.randrange(start, end)] for i in range(k)]
|
||||
return results
|
||||
|
||||
class Panama:
|
||||
def __init__(self, L='A man, a plan', R='a canal, Panama', dict=None):
|
||||
left = [canonical(w) for w in L.split(', ')]
|
||||
right = [canonical(reverse(w)) for w in reverse(R.split(', '))]
|
||||
update(self, left=left, right=right, dict=dict or PalDict(), best=0,
|
||||
seen={}, diff=len(''.join(left)) - len(''.join(right)))
|
||||
for word in left + map(reverse, right):
|
||||
self.seen[word] = 1
|
||||
|
||||
def missing(self, k=20):
|
||||
"""Return the substring that is missing, and candidate words."""
|
||||
if self.diff >= 0: # Left is longer, missing on right
|
||||
substr = self.left[-1][-self.diff:]
|
||||
return substr, self.dict.endswith(substr, k)
|
||||
else: # Right is longer, missing on left
|
||||
substr = self.right[-1][self.diff:]
|
||||
return substr, self.dict.startswith(substr, k)
|
||||
|
||||
def search(self, k=200):
|
||||
"Search for palindromes; consider at most k words at each level."
|
||||
self.stack = [self.missing(k)]
|
||||
while self.stack:
|
||||
substr, words = self.stack[-1]
|
||||
if is_palindrome(substr):
|
||||
self.report()
|
||||
if words:
|
||||
self.extend(words.pop(), k)
|
||||
elif not self.backtrack():
|
||||
return
|
||||
|
||||
def extend(self, word, k):
|
||||
"Add a new word (unless we've already seen it)."
|
||||
if self.diff >= 0: # Left is longer, add to right
|
||||
fword = reverse(word)
|
||||
if fword in self.seen: return
|
||||
self.diff -= len(fword)
|
||||
self.seen[fword] = 1
|
||||
self.right.append(word)
|
||||
self.stack.append(self.missing(k))
|
||||
else: # Right is longer, add to left
|
||||
if word in self.seen: return
|
||||
self.diff += len(word)
|
||||
self.seen[word] = 1
|
||||
self.left.append(word)
|
||||
self.stack.append(self.missing(k))
|
||||
|
||||
def backtrack(self):
|
||||
"Remove the last word added; return 0 if can't backtrack"
|
||||
if self.diff >= 0: # Left is longer, pop from left
|
||||
if not self.left: return 0
|
||||
word = self.left.pop()
|
||||
self.diff -= len(word)
|
||||
del self.seen[word]
|
||||
else: # Right is longer, pop from right
|
||||
if not self.right: return 0
|
||||
word = self.right.pop()
|
||||
self.diff += len(word)
|
||||
del self.seen[reverse(word)]
|
||||
self.stack.pop()
|
||||
return 1
|
||||
|
||||
def report(self):
|
||||
"Write current state to log file."
|
||||
if len(self) > self.best + 200:
|
||||
self.best = len(self)
|
||||
print self.best
|
||||
self.bestphrase = str(self)
|
||||
assert is_panama(self.bestphrase)
|
||||
f = open('pallog%d.txt' % os.getpid(), 'w')
|
||||
f.write(self.bestphrase + '\n')
|
||||
f.close()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.left) + len(self.right)
|
||||
|
||||
def __str__(self):
|
||||
truename = self.dict.truename
|
||||
lefts = [truename[w] for w in self.left]
|
||||
rights = [truename[reverse(w)] for w in reverse(self.right[:])]
|
||||
return ', '.join(lefts + ['*****'] + rights)
|
||||
|
||||
def reverse(x):
|
||||
"Reverse a list or string."
|
||||
if type(x) == type(''):
|
||||
return ''.join(reverse(list(x)))
|
||||
else:
|
||||
x.reverse()
|
||||
return x
|
||||
|
||||
if __name__ == '__main__': read_dict(); p = Panama(); p.search()
|
||||
Reference in New Issue
Block a user