My solution is below. It is an almost anti-golf in its size, but it works very well. It supports n-way splits (although computational time becomes very long for any more than about 3 splits) and it supports removing duplicates.
class WordSplitChecker(object):
def __init__(self, word, splits=2):
if len(word) == 0:
raise ValueError, "word too short!"
if splits == 0:
raise ValueError, "splits must be > 1; it is impossible to split a word into zero groups"
self.word = word
self.splits = splits
def solve(self, uniq_solutions=False, progress_notifier=True):
"""To solve this problem, we first need to consider all the possible
rearrangements of a string into two (or more) groups.
It turns out that this reduces simply to a base-N counting algorithm,
each digit coding for which group the letter goes into. Obviously
the longer the word the more digits needed to count up to, so
computation time is very long for larger bases and longer words. It
could be sped up by using a precalculated array of numbers in the
required base, but this requires more memory. (Space-time tradeoff.)
A progress notifier may be set. If True, the default notifier is used,
if None, no notifier is used, and if it points to another callable,
that is used. The callable must take the arguments as (n, count,
solutions) where n is the number of iterations, count is the total
iteration count and solutions is the length of the solutions list. The
progress notifier is called at the beginning, on every 1000th iteration,
and at the end.
Returns a list of possible splits. If there are no solutions, returns
an empty list. Duplicate solutions are removed if the uniq_solutions
parameter is True."""
if progress_notifier == True:
progress_notifier = self.progress
solutions = []
bucket = [0] * len(self.word)
base_tuple = (self.splits,) * len(self.word)
# The number of counts we need to do is given by: S^N,
# where S = number of splits,
# N = length of word.
counts = pow(self.splits, len(self.word))
# xrange does not create a list in memory, so this will work with very
# little additional memory.
for i in xrange(counts):
groups = self.split_word(self.word, self.splits, bucket)
group_sums = map(self.score_string, groups)
if len(set(group_sums)) == 1:
solutions.append(tuple(groups))
if callable(progress_notifier) and i % 1000 == 0:
progress_notifier(i, counts, len(solutions))
# Increment bucket after doing each group; we want to include the
# null set (all zeroes.)
bucket = self.bucket_counter(bucket, base_tuple)
progress_notifier(i, counts, len(solutions))
# Now we have computed our results we need to remove the results that
# are symmetrical if uniq_solutions is True.
if uniq_solutions:
uniques = []
# Sort each of the solutions and turn them into tuples. Then we can
# remove duplicates because they will all be in the same order.
for sol in solutions:
uniques.append(tuple(sorted(sol)))
# Use sets to unique the solutions quickly instead of using our
# own algorithm.
uniques = list(set(uniques))
return sorted(uniques)
return sorted(solutions)
def split_word(self, word, splits, bucket):
"""Split the word into groups. The digits in the bucket code for the
groups in which each character goes in to. For example,
LIONHEAD with a base of 2 and bucket of 00110100 gives two groups,
"LIHAD" and "ONE"."""
groups = [""] * splits
for n in range(len(word)):
groups[bucket[n]] += word[n]
return groups
def score_string(self, st):
"""Score and sum the letters in the string, A = 1, B = 2, ... Z = 26."""
return sum(map(lambda x: ord(x) - 64, st.upper()))
def bucket_counter(self, bucket, carry):
"""Simple bucket counting. Ex.: When passed a tuple (512, 512, 512)
and a list [0, 0, 0] it increments each column in the list until
it overflows, carrying the result over to the next column. This could
be done with fancy bit shifting, but that wouldn't work with very
large numbers. This should be fine up to huge numbers. Returns a new
bucket and assigns the result to the passed list. Similar to most
counting systems the MSB is on the right, however this is an
implementation detail and may change in the future.
Effectively, for a carry tuple of identical values, this implements a
base-N numeral system, where N+1 is the value in the tuple."""
if len(bucket) != len(carry):
raise ValueError("bucket and carry lists must be the same size")
# Increase the last column.
bucket[-1] += 1
# Carry numbers. Carry must be propagated by at least the size of the
# carry list.
for i in range(len(carry)):
for coln, col in enumerate(bucket[:]):
if col >= carry[coln]:
# Reset this column, carry the result over to the next.
bucket[coln] = 0
bucket[coln - 1] += 1
return bucket
def progress(self, n, counts, solutions):
"""Display the progress of the solve operation."""
print "%d / %d (%.2f%%): %d solutions (non-unique)" % (n + 1, counts, (float(n + 1) / counts) * 100, solutions)
if __name__ == '__main__':
word = raw_input('Enter word: ')
groups = int(raw_input('Enter number of required groups: '))
unique = raw_input('Unique results only? (enter Y or N): ').upper()
if unique == 'Y':
unique = True
else:
unique = False
# Start solving.
print "Start solving"
ws = WordSplitChecker(word, groups)
solutions = ws.solve(unique)
if len(solutions) == 0:
print "No solutions could be found."
for solution in solutions:
for group in solution:
print group,
print
Sample output:
Enter word: wordsplit
Enter number of required groups: 2
Unique results only? (enter Y or N): y
Start solving
1 / 512 (0.20%): 0 solutions (non-unique)
512 / 512 (100.00%): 6 solutions (non-unique)
dspit worl
ordsl wpit
orsp wdlit
Must the program support mixed case input? And if so can it discard the case for the output? – Nemo157 – 2011-02-03T19:54:42.077
@Nemo157 It may ignore case and does not have to preserve it on the output. – Thomas O – 2011-02-03T19:55:39.180
Can the program output extra stuff, as long as what the requested part of the output is is clear to a human? – J B – 2011-02-03T22:49:22.843
@J B Yes it can. – Thomas O – 2011-02-03T23:22:02.277
ok, I'll improve that Perl then ;) Thanks – J B – 2011-02-04T07:51:57.010