## Session 23

### Our Puzzle: Jealous Husbands

There is a village consisting of many pair-bonded couples that straddles a river. Occasionally, n ≥ 2 couples need to cross a river to conduct business. For example:

```    H1 W1 H2 W2   | |
```

They have a boat that holds at most 2 people at a time.

The husbands are a jealous lot. No husband will allow his wife to be on the same bank as another man unless he himself is there. ... even if there are lots of other people on the same bank. ... even if the other man's wife is there, too!

So, this is a legal configuration:

```    H1 W1 H2   | |   W2
```

But this is not:

```    W1 H2 W2   | |   H1
```

Clearly, these folks have some trust issues.

Your job is to engineer a solution that helps them survive in spite of their pathology. Let's attack the problem from bottom up. Your tasks are:

• Solve the problem for n= 2.
• Solve the problem for n= 3.
• Then design an algorithm that solves this problem for n ≥ 4.

The couples are jealous, but they are also cheap. They want to know:

How many river crossings are required for n-couple transfers?

Suggestion: Draw pictures. Doing so helped me.

### Debriefing the Jealous Husbands

(Perhaps that's what they're all worried about.)

We could attack this problem with brute force. This would involve trying all possible combinations of crossings.

• Going from left to right, we choose two passengers from the kleft people on the bank.
• Going from right to left, we choose one passenger from the kright people on that bank to take the boat back for the rest.

This, of course, leads to an explosion of possible paths combinations. This case is worse than usual, though: most of combinations travel through invalid states.

This problem benefits from a representation change. If we turn it into a graph problem, we will find that the problem is straightforward, and doesn't even require much search.

In our graph, each node represents a state of the world, with the arrow indicating which direction the boat is traveling. Each edge represents a legal transfer in the direction of the boat.

The edges are undirected, because each move is reversible.

Some states are dead ends and do not need to be considered further. In graph terminology, we say that they do not need to be "expanded". Consider the leftmost node in level two above. Neither husband can go back to the other side alone, because he would be on the same side with the other man's wife without the other man. So there are "new" moves out of this state, but none are legal.

Other states are similarly uninteresting, because the only possible move takes the couples back to the previous state. We don't need to consider these states, either. The rightmost node in level two is an example of this phenomenon.

The diagram above shows one example of each kind of uninteresting node but doesn't show other such states for level 2, or any such states lower in the graph.

By focusing only on interesting moves, the resulting graph is a nearly direct walk from the starting condition to the desired condition. There are four possible paths, because the graph has two binary decision points. Each requires five crossings. Here is one:

```    W1W2
W1
H1H2
H1
H1W1
```

At a high level, we might describe solving the two-couple case as:

```    choose a couple to move first
move both wives across
move the other wife back
move both husbands across
move the other husband back
move the other couple
```

Now, what if n = 3? We can simplify the three-couple problem into a two-couple problem! The idea is again to move one couple, but this time making sure the boat ends up on the original side of the river.

Here is the problem as a graph, showing only the interesting moves. Notice that it works its way down the left side and back up the right...

Moving one couple and leaving the boat on the original side of the river takes six moves. Here is an example:

```    H3W3
H3
W1W2
W1
H2H3
H2W2
```

At a high level, we can describe the transformation from three couples to two as:

```    choose a couple h-i:w-i to move
move w-i and another wife across
move the other wife back
move both remaining wives across
move one of the other wives back
move h-i and another husband across
move the other couple back
```

Now we face our original problem, where n = 2. We can use the five-crossing sequence above to bring the other two couples across. This results in an 11-crossing solution, for which there are 4x4=16 different paths.

Solving for n ≥ 4 couples can be accomplished in similar fashion: Use the six-crossing sequence to simplify an n-couple problem into an n-1-couple problem. As soon as we have a two-couple problem, we use our five-crossing sequence to finish the task.

We have fulfilled our contract, and marital discord is delayed for another day.

Our approach is good, old decrease-and-conquer. In terms of problem transformation, we call this instance simplification. We take a problem and transform it into a simpler problem of exactly the same form. (Not all instance simplifications involve solving the decreased element.)

There is one small matter left to be solved. How many river crossings are required in our solution for n-couple transfers? To solve this, we can set up a recurrence relation:

```    C(n) = 6 + C(n-1) for n > 2
C(2) = 5
```

As always, we solve the recurrence by working backwards:

```    C(n) =  6 + C(n-1)
=  6 + [ 6 + C(n-2) ] = 2(6) + C(n-2)
= 12 + [ 6 + C(n-3) ] = 3(6) + C(n-3)
= 18 + [ 6 + C(n-4) ] = 4(6) + C(n-4)
...
= 6i + C(n-i)              now, let i = n-2
...
= 6(n-2) + C(n-(n-2))
= 6(n-2) + C(2)
= 6(n-2) + 5
```

This approach results in a well-behaved θ(n) algorithm, with reasonable constants.

### Problem Transformations

Sometimes, solving a problem as stated can be quite daunting. There may be no obvious way to generate an efficient algorithm. But if we translate the problem into a different form, we may be able more easily to generate an efficient algorithm.

Our solution to the Jealous Husbands problem used two powerful transform-and-conquer techniques:

• representation change, which changes the form of the data being used
• instance simplification, which is often a form of decrease-and-conquer

A third transform-and-conquer technique, problem reduction, makes a bigger transformation. It changes the nature of the problem altogether.

Reducing one problem into another can help us to generate an algorithm that looks much different than we might expect. In some situations, this may be the only way we have to generate an algorithm. Even when we do have alternatives, problem reduction is a great way to stretch your mind, getting practice in using analogies to make creative leaps from one problem space to another.

### An Example of Problem Reduction

Consider the problem of computing least common multiple of two integers, m and n. lcm(m, n) is the smallest number evenly divisible by both m and n. For simplicity, we will assume that m is the larger of the two.

We can attack this problem with brute force by searching the sequence m+, m+1, m+2, ..., until we find a number that is divisible by both m and n. This requires doing a lot of divisions: O(m²) for naive algorithms, or approximately O(m1.585) for our divide-and-conquer approach. And remember, the latter algorithm improves on the former only for very large numbers!

Divisions are at least as costly as multiplications, so this is not an attractive solution. Can we do better?

We could use brute force in a different way:

1. Factor m down to factorsm = {m1, m2, ...}.
2. Factor n down to factorsn = {n1, n2, ...}.
3. Find the union of
• the intersection factorsm ∩ and factorsn
• the difference factorsm - factorsn
• the difference factorsn - factorsm
4. Return the product of all integers in the union.

For example, suppose m = 16 and n = 24.

• factorsm = {2, 2, 2, 2}
• factorsn = {2, 2, 2, 3}

• factorsm ∩ factorsn = {2, 2, 2}
• factorsn - factorsm = {3}
• factorsm - factorsn = {2}

• the union (factorsm ∩ factorsn) ∪ (factorsn - factorsm) ∪ (factorsm - factorsn) = {2, 2, 2, 2, 3}
• the product of these is 2 x 2 x 2 x 2 x 3 = 48

This algorithm operates in O(n²) time and, worse, requires a potentially long list of prime numbers.

Do these two algorithms and their weaknesses sound familiar? They might, because we saw a similar problem back in Session 4: gcd(m, n). Remembering that problem can help us now.

Let's use a little Arithmetic Truth:

```    gcd(m,n) * lcm(m,n) = m * n
```

Which means that:

```                m * n
lcm(m,n) = --------
gcd(m,n)
```

This truth converts our problem into a different problem: find gcd(m, n), then solve for lcm(m, n) using one more multiplication and one more division. We already have an efficient algorithm, for gcd(m, n), courtesy of Session 4: Euclid's algorithm.

Nice. We will use problem reduction again when solving some tougher problems later in the course.

### An Exercise

You are given an array of n numbers and an integer s.

Your job: Write an algorithm that returns true if and only if the array contains two numbers whose sum is s.

### An Algorithm

Again, the brute force approach is straightforward: try all combinations of ni + nj. This will require ½n(n-1) comparisons, which is θ(n²). Surely we can do better...

We can using transform-and-conquer. First, sort the list, which is an O(n log n) operation. Starting with a sorted array, we have an O(n) approach for finding a solution.

Suppose s = 0. We just need to find two numbers with same absolute value but different signs: ni + (-ni). If we sort the array in non-decreasing order of absolute value, we can look for consecutive values that satisfy this condition!

For example, we might start with this array:

```    [26, 42, -17, 47, -26, 1006]
```

We sort the array in ascending order of absolute value:

```    [-17, 26, -26, 42, 47, 1006]
```

Now, we walk down list until we see 26 and -26 together and choose them as our answer.

Of course, s doesn't have to be 0. Let's transform and conquer one more time. If ni + nj = s, then (ni-k) + (nj-k) = (s-2k). If we let k = s/2), this becomes (ni-s/2) + (nj-2/2) = 0. Our simple sort-and-scan algorithm now solves the problem!

So, our final algorithm is

```    INPUT array A[1..n] of integers
integer s

sort array in non-decreasing order of absolute value
for every A[i]
A[i] = A[i] - s/2
for every pair A[i], A[i+1]
if A[i], A[i+1] = 0
return A[i], A[i+1]
fail
```

So using two representation changes:

• presorting
• normalizing to s to 0
we convert a seemingly hard problem into a straightforward O(n log n) problem.

(Even better, we may be able to do the search at the same time as we sort, because an insertion sort would make the same sequence of comparisons that our pair search needs to make. Implemented this way, the algorithm could short-circuit as soon as it finds a successful pair.)

Students in a previous semester proposed an intriguing approach. I call it the RNB algorithm, in honor of its creators Ryan, Noyce, and Berns:

```    // Input: array of numbers a[0..n-1], number s
// Output: whether or not s can be made by summing 2 numbers

sort A using a sort that is O(n log n)

i = 0
j = n-1
while i < j
if a[i] + a[j] > s
j = j - 1
else if a[i] + a[j] < s
i = i + 1
else
return a[i], a[j]

fail
```

The idea is to walk two "pointers" from the ends of the array toward the middle, looking at sums along the way. We move the pointers based on the direction of the "error" between the sum and the target sum.

Does this always work? I think so, but I haven't proven it yet... Can you prove it, or create a counterexample?

Representation change and instance simplification can do almost magical things to a hard problem. (The AI scientist in me wonders if they are the basis of most creative problem solving.) The trick: knowing lots of different representations, and developing some intuition about when to use them!

I love one of Patrick Henry Winston's principles from the study of artificial intelligence:

Choose the right representation, and the problem is nearly solved.

It is oh, so true -- and oh, so like a siren. If only we can choose the right representation, everything is a search problem -- or logic problem, or a means-ends problem, or.... In general, though, it turns out that choosing the "right representation" is often as hard as, or harder than, the original problem!

### Presorting as Representation Change

Presorting is an especially useful form of representation change when dealing with lists.

We saw an example last time where presorting can help: determining if all items in a list of values are unique.

• We can create a simple brute-force algorithm that operates in O(n²) time by comparing all pairs of values.

• We can convert the problem using a "sort and scan" strategy into a simple linear search, which operates in O(nlogn) + O(n) = O(nlogn) time.

• In Session 19, we saw another approach, using hashing, to make the problem mostly an insertion problem! This O(n) in the best case, O(n logn)in almost all other cases, and O(n²) in the worst case.

We can use a similar approach to find the median of a list: presort, then go to position n/2 in the array. The Quickselect algorithm in our first session optimizes on this approach. It sorts only until it finds the value its needs.

Perhaps more surprising is that we can use this approach to find the mode of the list, the item that occurs most frequently in the list. After we presort the list, we can make a single pass to find the item that has the longest "run" in the sorted list!

An implicit assumption in presorting-based transformations is that the values in the list are comparable. That's true by definition for the median problem, but not for the mode. (Can you think of some common data that are incomparable?)

### Wrap Up

• Reading -- Read these lecture notes. Try to write out in more detail some of the algorithms that are given only in sketch form. Count the number of basic operations performed, so that you can firm their complexity. Work the bonus exercise. Ask any questions you have.

• Homework -- No new homework.

Eugene Wallingford ..... wallingf@cs.uni.edu ..... April 15, 2014