Advertisement

Turning "I know why this works" into a proof

Started by September 16, 2009 02:27 PM
6 comments, last by Maze Master 15Β years, 1Β month ago
Hi, I'm currently taking a course on algorithms, and a major part of said course is to write proofs about why a given algorithm is correct/has a certain complexity/has some other property. Unfortunately, I'm not very good with proofs. Most times, I can explain why a given property holds, but when the time comes to turn it into a proof, I simply hit a dead end. I'm currently working on an assignment, where the objective is to device a brute force search algorithm for solving the knapsack problem, prove it correct, give a complexity estimate and prove that estimate. The algorithm I've come up with looks like this:

    Function knap
    Returns:
        A pair of an integer and a list; the list is the most valuable list of
                items that fits in the knapsack, and the integer is the total
                value of said list.
    Inputs:
        Let xs be a list of roads that may be taken (items that may be chosen)
        Let p be the path we took to get here (items we've previously chosen)
        Let c be the maximum capacity of the knapsack
        Let bestPath be the best "path" (set of items), and set it initially
                to the empty list.
        Let val be the value of bestPath, and set it initially to 0

    If size(p) > c, then return (val, bestPath)

    If value(p) > val
        Let bestPath = p
        Let val = value(p)
    End If

    For Each x in xs
        (val, bestPath) = knap(xs - x, p + x, c, bestPath, value)
    End For
    Return (val, bestPath)
Looks like this implemented in haskell:

-- xs is the list of itms to choose from, p is the candidate solution,
-- c is the knapsack capacity, bv is the value of the current best solution,
-- and bl is the current best solution
knap xs p c bv bl
    | sackSize p > c = (bv, bl)
    | otherwise      = foldl forEachItem (bv', bl') xs where
        (bv', bl') -- if value(p) > val .. end if
            = case sackValue p of
                v | v > bv -> (v, p)
                _          -> (bv, bl)
        forEachItem (bv, bl) x
            = knap (delete x xs) (x:p) c bv bl

        sackSize = foldl (\acc (x, _) -> acc + x) 0
        sackValue = foldl (\acc (_, x) -> acc + x) 0
The problem is, as you may have guessed, that I'm having trouble proving the correctness of it. More specifically, I'm trying to prove that it actually examines all valid candidate solutions. Explaining why it doesn't miss any possible solution in plain English is easy: For every way to pick an item x from a set, we add x to our candidate solution and recursively examine all ways to pick another item when x is already taken and thus no longer available until we either run out of items to choose from, or the weight of the candidate solution exceeds the knapsack capacity. With every item added to the candidate solution, we check if the candidate is better than the current best solution, and update that best solution accordingly. ...but I can't seem to turn that into a proof. I'm not asking for you to write out a complete proof for me, but perhaps someone could give me a nudge in the right direction? I've tried creating a proof by induction for it, but I'm having a hard time defining the base cases and induction steps, especially since it's an accumulating recursive function.
It sounds like a proof by contradiction would be appropriate in this case:

Assume that there there is a result that is better than the one that your algorithm would have returned. Then, show that this is impossible.
Advertisement
It might be easier if you get rid of the loop, because then the algorithm will be "purely" recursive and so it will be easier to prove this using induction.

My thinking here is that for each item, it will either appear in the best solution or it will not, so you try both and take the better one.

Something like:

x = first_element(xs)try1 = knap(xs - x, p, c, bestPath, value)try2 = knap(xs - x, p + x, c, bestPath, value)return max(try1, try2)



EDIT: As a side note, why do you need val? Isn't it always equal to value(bestPath)?

[Edited by - Gage64 on September 17, 2009 2:07:35 AM]
I would first start with a simpler version of the algorithm, and then, if necessary, optimize with with tail calls and accumulator arguments.
best :: [Integer] -> [Integer] -> [Integer]-------------------------------------------best xs ys    | sum xs >= sum ys   = xs    | otherwise          = ysknap :: [Integer] -> Integer -> [Integer]-----------------------------------------knap [] _       = []knap (x:xs) c    | x <= c    = best (knap xs c) (x : knap xs (c - x))    | otherwise = knap xs c


So, I'd say three things need to be proven:
With rs = knap xs c
a) rs βŠ† xs
b) βˆ‘ rs ≀ c
c) βˆ€ ys βŠ† xs, βˆ‘ ys ≀ c β†’ βˆ‘ ys ≀ βˆ‘ rs

The first two shouldn't be too hard, the third depends on how rigorous you want it. You could define subsets (or actually, sublists) as
βˆ… βŠ† xs
xs βŠ† ys β†’ (x:xs) βŠ† (x:ys)
xs βŠ† ys β†’ xs βŠ† (y:ys)
and perform induction using these three cases, something like
βˆ‘ βˆ… ≀ c β†’ βˆ‘ βˆ… ≀ βˆ‘ rs (trivial)
(βˆ‘ ys ≀ c β†’ βˆ‘ ys ≀ βˆ‘ rs) β†’ βˆ‘ (y:ys) ≀ c β†’ βˆ‘ (y:ys) ≀ βˆ‘ knap (y:xs) c
(βˆ‘ ys ≀ c β†’ βˆ‘ ys ≀ βˆ‘ rs) β†’ βˆ‘ ys ≀ c β†’ βˆ‘ ys ≀ βˆ‘ knap (x:xs) c
(I didn't check carefully, but it ought to be something along these lines)
To prove this I would take the correct solution, say n1 to ni. Then prove that your algorithm will find it. This is trivial as the first call will find n1, the next recursive call will find n2, etc.

Edit: For full marks you'll also need to prove the case where there are multiple correct solutions. There you can just take one of them, show that your algorithm will examine it and therefore the solution it returns will be as good.

[Edited by - thooot on September 17, 2009 10:29:27 AM]
Here's a full machine-checked correctness proof, assuming my specifications are correct of course... It uses the same principle I mentioned previously, so it definitely is a feasible approach. Induction on the list for the two first theorems, then induction on sublists for the third.
Inductive sublist (A : Set) : list A -> list A -> Prop :=| sublist_empty : forall xs, sublist A nil xs| sublist_add   : forall x xs ys, sublist A xs ys -> sublist A (x::xs) (x::ys)| sublist_skip  : forall xs y ys, sublist A xs ys -> sublist A xs (y::ys).Implicit Arguments sublist [ A ].Fixpoint sum (xs : list nat) : nat :=  match xs with    | nil   => O    | x::xs => x + sum xs  end.Fixpoint knap (xs : list nat) (capacity : nat) {struct xs} : list nat :=  match xs with    | nil   => nil    | x::xs => if le_lt_dec x capacity               then let a := knap xs capacity in                    let b := x :: knap xs (capacity - x) in                      if le_lt_dec (sum a) (sum b) then b else a               else knap xs capacity  end.Theorem knap_upper_bound : forall xs c, sum (knap xs c) <= c.Proof.  induction xs; intros.  simpl; auto with arith.  simpl.  destruct (le_lt_dec a c).  destruct (le_lt_dec (sum (knap xs c)) (a + sum (knap xs (c - a)))).  simpl.  specify IHxs (c - a).  omega.  specify IHxs c.  trivial.  specify IHxs c.  trivial.Qed.Theorem knap_sublist : forall xs c, sublist (knap xs c) xs.Proof.  induction xs; intros.  simpl.  constructor.  simpl.  destruct (le_lt_dec a c).  destruct (le_lt_dec (sum (knap xs c)) (a + sum (knap xs (c - a)))).  apply sublist_add.  apply IHxs.  apply sublist_skip.  apply IHxs.  apply sublist_skip.  apply IHxs.Qed.  Theorem knap_max : forall xs ys c, sublist ys xs -> sum ys <= c -> sum ys <= sum (knap xs c).Proof.  intros.  revert c H0.  induction H; intros.  simpl.  auto with arith.  simpl in * |- *.  destruct (le_lt_dec x c).  destruct (le_lt_dec (sum (knap ys c)) (x + sum (knap ys (c - x)))).  simpl.  specify IHsublist (c - x).  assert (sum xs <= c - x).  omega.  specify IHsublist H1; clear H1.  omega.  assert (x + sum (knap ys (c - x)) <= sum (knap ys c)).  omega.  clear l0.  cut (x + sum xs <= x + sum (knap ys (c - x))).  intros; omega.  clear H1.  apply plus_le_compat_l.  specify IHsublist (c - x).  assert (sum xs <= c - x).  omega.  specify IHsublist H1; clear H1.  trivial.  assert False.  omega.  contradiction.  simpl.  destruct (le_lt_dec y c).  destruct (le_lt_dec (sum (knap ys c)) (y + sum (knap ys (c - y)))).  simpl.  specify IHsublist c H0.  omega.  apply (IHsublist c H0).  apply (IHsublist c H0).Qed.
Advertisement
Thanks for all the replies, you guys are a better source of knowledge for this than the book we're using.

In the end, I decided to simplify the algorithm quite a bit. The deadline was coming up, and since I'm a bit overworked this semester I decided to play it safe rather than risk failing the course.

The algorithm I turned in basically generated all permutations of the input list, then examined every prefix of every permutation starting from the shortest and breaking when it reached one with weight > knapsack capacity. Finally, among the best per-permutation prefix, I selected the most valuable one to return. Total complexity of this horrible algorithm: n*n!.
Fortunately, the point of the excercise was to show that this kind of algorithm is slow, and then contrast it with the really simple and elegant dynamic programming pseudo-polynomial variant, so the horrible complexity will hopefully not be counted against me. On the upside, it was quite easy to create a proof for it.

Are there any fairly easy-to-prove algorithms you could recommend for building up my proof writing skills? I fear the worst for the October exam if I can't improve this area, and I believe I need to start simple before moving on to the nastier ones.
So, it seems to me that you could do induction on the number of items to be examined.

Suppose your algorithm will always find the best knapsack of any specified capacity from k objects (inductive hypothesis). Given an item list of length k+1, then there are 2 cases. Either the k+1'th item is in the best knapsack or it's not.

If it's _not_ in the best knapsack, then applying your algorithm to the other k objects will find the best knapsack by the inductive hypothesis.

On the other hand, suppose the k+1'th item _is_ in the best knapsack. The best knapsack that definitely contains the k+1'th item is equivalent to adding the k+1'th item to the best sub-knapsack from the other k objects, except with sub-knapsack capacity reduced by the weight of the k+1'th object. Why? If there were a "better" knapsack containing the k+1'th object, then you could just remove the k+1'th object and what's left would be a "better than the best" reduced-weight-sub-knapsack.

Your algorithm finds the best reduced-weight sub-knapsack by the inductive hypothesis, so it will find the best knapsack containing the k+1'th object. Since you have found the best object in all cases, your algorithm must find the best object, end proof.

Does that make sense?

This topic is closed to new replies.

Advertisement