Below is my solution, formatted to use Jannis' testing classes.

The solution basically recursively calls clean? on the full array of
words, then on the first half and on the second half, then on the
first quarter, the second quarter, the third quarter, etc.  Whenever
clean? returns true, it stops dividing that section of the array.
(This description is breadth-first, but the algorithm actually runs
depth-first.  Breadth-first is easier to explain, and the final result
is the same.)

I then noticed that we don't really need to make all these calls to
clean?, because we can sometimes deduce what the results will be
without calling clean?.  For example,  if we call clean? on [0...8]
and the result is false, then we call clean? on [0...4] and the result
is true, then we know that clean? on [4...8] must return false, so we
don't need to make this call.

The ordinary recursion is done by the method findBanned(aWords).  When
the above deduction tells us that clean? will return false, we instead
call the method findBannedThereIsOne(aWords).  In the above example we
would call findBannedThereIsOne(aWords[4...8]), and it will skip
calling clean? on aWords[4...8].

(Yes, findBannedThereIsOne is an awkward name, but at least it's
better than findBannedAndWeKnowThereIsAtLeastOne)

When findBannedThereIsOne is called with an array with only one
element, it doesn't need to call clean? at all, because we know this
must be a banned word.

So that findBanned and findBannedThereIsOne only need to deal with
arrays whose size is a power of two, the main routine,
runYourAlgorithm, divides the input array into chunks of these sizes.
As I'm typing this, it occurs to me that this code might actually run
faster if runYourAlgorithm didn't do this.  I'll give this a try, but
for now I'll post the version that does the chunking.

runYourAlgorithm also handles the case of an empty array, so the other
two routines don't need to handle it.

Here are the results with Jannis' testing classes.  For the times in
seconds, keep in mind that these tests were run on an ancient 500MHz
Pentium III with other work going on at the same time.

Runs:          10
Words:         121430
Banned Words:  3
Average
Seconds:     0.5561
Mails:       76
Verified:      10/10
Rebuilt Banned Words

Runs:          10
Words:         121430
Banned Words:  126
Average
Seconds:     254.4113
Mails:       1948
Verified:      10/10
Rebuilt Banned Words

Runs:          10
Words:         3930
Banned Words:  29
Average
Seconds:     12.5187
Mails:       327
Verified:      10/10
Rebuilt Banned Words

Runs:          10
Words:         125282
Banned Words:  80
Average
Seconds:     139.9058
Mails:       1319
Verified:      10/10
Rebuilt Banned Words

Runs:          10
Words:         127367
Banned Words:  10
Average
Seconds:     1.5328
Mails:       210
Verified:      10/10
Rebuilt Banned Words

Wayne Vucenic
No Bugs Software
"Ruby and C++ Contract Programming in Silicon Valley"

======================================

class YourAlgorithm < RQ9Algorithm
def run()
runYourAlgorithm(@words)
end

# Returns an array containing all banned words from aWords
def runYourAlgorithm(aWords)
return [] if aWords.empty?

# Compute the largest power of two <= aWords.size
powerOfTwo = 1
powerOfTwo *= 2 while powerOfTwo * 2 <= aWords.size

# To simplify the logic in findBanned, we always call it with an
# array whose size is a power of two.
aBanned = findBanned(aWords[0...powerOfTwo])

# If we didn't process all of aWords, recursively do the rest
if powerOfTwo < aWords.size
aBanned += runYourAlgorithm(aWords[powerOfTwo..-1])
end

aBanned
end

# Returns an array containing all banned words from aWords
# aWords.size is a power of 2, and is > 0
def findBanned(aWords)
if aWords.size == 1
@filter.clean?(aWords[0]) ? [] : aWords
elsif @filter.clean?(aWords.join(' '))
[]
else
iSplit = aWords.size / 2
if @filter.clean?(aWords[0...iSplit].join(' '))
# There is at least one banned word in 0..-1, but not in 0...iSplit,
# so there must be one in iSplit..-1
findBannedThereIsOne(aWords[iSplit..-1])
else
# From the test above we know there is a banned word in 0...iSplit
findBannedThereIsOne(aWords[0...iSplit]) +
findBanned(aWords[iSplit..-1])
end
end
end

# Returns an array containing all banned words from aWords
# aWords.size is a power of 2, and is > 0
# Our caller has determined there is at least one banned word in aWords
def findBannedThereIsOne(aWords)
if aWords.size == 1
# Since we know there is at least one banned word, and since there is
# only one word in the array, we know this word is banned without
# having to call clean?
aWords
else
iSplit = aWords.size / 2
if @filter.clean?(aWords[0...iSplit].join(' '))
# There is at least one banned word in 0..-1, but not in 0...iSplit,
# so there must be one in iSplit..-1
findBannedThereIsOne(aWords[iSplit..-1])
else
# From the test above we know there is a banned word in 0...iSplit
findBannedThereIsOne(aWords[0...iSplit]) +
findBanned(aWords[iSplit..-1])
end
end
end
end