This is by far the longest series of blog articles about one topic. It's interesting to write while thinking, learning and programming just one step before I write it down for you here. I think there isn't enough easy to read information on the internet about the Simplex method.
What can I do with this algorithm? What types of problems can I solve? How to make it fast? How to make it accurate?
I tried to explain in the last couple of blog posts: How does it work and how can I implement it?
Again this post will give you a new part of answering that question. In the next article I want to face the question: How to make it accurate and will give you a hint why the current approach isn't the best in the end of this article.
My goal is to understand the simplex method, linear programming and mixed integer programming by programming it on my own. I want to help you guys to understand this method and learn what's going on in the background if you use a real solver. It's hard to build a solver which is at least in some parts as good and fast as a solver where several of hundreds of people are working on for years. Anyway it's still a goal to come as close as I can get. It should still be simple to follow me on my journey and using Python here I think is a reasonable approach even if C++ would be faster.
This article is about adding constraints to a solved tableau.
Why should we do it?
Let's assume we have our diet problem again and we solved it for several hundreds or thousands of ingredients and some nutrition values and then there is a new constraint because a new study came out: We should eat at least an apple a day. Well we forgot to add that constraint in the first place. At the moment we would need to add this constraint and solve everything again. This might be time consuming.
Another use case is the one I used for the diet problem and will use here: We have a list of ingredients and therfore constraints and we have some constraints telling us that we shouldn't eat more than a specified amount of one ingredient. Using this approaches we have to add a lot of constraints.
If we have 100 ingredients and only three minimal requirements we would normally have three constraints but because of this specified maximum amount we would add another 100 constraints. This sounds a lot of overhead. Why overhead? We have to do it right? Well we remember that we have a basic feasible solution where we use only three variables out of the 100 if we have only three constraints.
Remember: We can't set more variables to a value greater than zero than we have constraints.
If we solve our problem without those extra 100 constraints we can only have three ingredients which might not fulfill the maxmimum constraint. It might be reasonable to add those at maximum three constraints afterwards if necessary.
This meant that we should eat 6.47 portions of oats, drinking 2.60 portions of milk and eating a bit mor than two portions of cake. We have an objective of ~541.
We dissatisfy the oats constraints and the cake constraint, but not the milk constraint. In the best solution without this constraint drinking milk isn't even close to the maximum constraint. Therefore it was probably a good strategy to not use it up-front.
Let's start with the oats constraint. How would the tableau look like if we add that one:
We just added a new row and a new column for the new slack variable. It looks weird to just add a constraint there, right? I mean we don't have a basic feasible solution anymore but we could simply use gaussian elimination to get a zero in the oats column on the last row.
Now we have a problem. We have kind of a basic feasible solution but there is a negative value in our b column. This is an absolutely no go. Anyway it makes absolutely sense. This means that we set oats still to 6.47 as we can see in the first row but we have to set a slack variable to -2.47 to reduce it by 2.47 oat portions to fulfill our constraint. This obviously doesn't solve our problem.
We can use the so called dual simplex pivot here. Which is basically the same thing as our normal pivot we just do it somehow on the transposed matrix. Instead of looking for a negative value in our objective row we look for a negative value on our right hand side. Using this as our leaving row and the entering column is the minimal absolute quotient of the objective row with the negative values on our leaving row. Here the negative values would be -9.9 in row x2 and -3.3 in row x6. The absolute quotients would be:
9.977.83.329.2≈≈7.868.85
So we try to get a 1 where -9.9 is and have x2 as in our basis. We get this tableau:
We have a basic feasible solution and as we can see on our objective row... It's optimal. We only have 4 portions of oats so it looks like we achieved our goal.
Unfortunately we have to eat more cake now and we aren't allowed to eat more than one portion of it. Therefore we have to add a new line and a new slack column to our tableau.
Whatever I want to think about something else now. Last time we achieved some huge speed ups with the dual problem. Do we have the option to add a new constraint there as well?
I used the column description c1,c2,c3 for our three constraints here. If we want to add a constraint to this one we normally add a new column and not a new row. After solving this tableau it would look like this:
As we can see all lines changed and it doesn't really make sense to just normally add a new column here because the new added column would have to change in the same way the other columns changed during each gauss step. We could keep track of every step we did and apply it to the new column.
This is the start of a maximum problem, where z would be 0 in the beginning. If we write it as short as possible it would look like this:
xA−cbbz
Here A is a matrix as described in the last tableau, b and c are vectors and z is just a number.
Let's assume A includes the slack variables already and therefore we have a basic feasible solution. We still write it as the matrix above. Anyway we can also write it in a different way:
xBB−cBxNN−cNbbz
Here B is the matrix for the slack variables or later on for the basic variables. xN are the variables we are interested in (Non basic yet). xB are the basic variables (at the moment the slack variables). I'll use B on the most left part as they do it here. It's a good course about the simplex algorithm and you'll find a lot of stuff which I'll not mention here. Anyway I mention some stuff here which I find interesting which aren't mention there and my version is more compact in some way. I recommend to read both versions ;)
Now during the simplex method we do some gaussian elimination stuff and therefore we get another basic feasible solution and so on but we never really change the tableau. We only use elementary row operations.
If we look at our tableau in a different stage it would still look like this:
xBI0xNN~−cN~bb~z~
x~ means that this is the new value of the corresponding variable. We want to describe this new values in form of the values before to have a step from one BFS to another.
We start with Ax=b. This is the most basic form we can describe the tableau normally. Then we added B and N. Therfore it looks like this now:
Ax=BxB+NxN=b
In the other form:
xB+N~xN=b~
To get to this form we can multiply the first equation by B−1.
B−1Ax=xB+B−1NxN=B−1b
Therefore:
N~=B−1Nb~=B−1b
What about −cN~ and z~ ?
To compute z we have to remember that we always start our last row (the objective row) using −c and z=0. Why are we doing this or how is this really working. Actually the tableau doesn't look like this:
xA−cbbzz01xA−cbbz
it more looks like this. That's the reason why z is 0 at the beginning to have the "constraint":
1−c=0
where c is the objective function. Therefore it holds that:
z=cBxB+cNxN
Now we had xB+N~xN=b~ so xB=−N~xN+b~. Consequently:
Furthermore A=IN therfore A=N so that it's simply:
z01xB−1A−c+cBB−1AbB−1bcBB−1b
We use the matrix B−1 quite often as well as cBB−1. Therefore we set:
y~=cBB−1S~=B−1
to get
z01xS~A−c+y~AbS~by~b
We can also split it into the form of our real variables and the slack variables to get:
z01xS~A−c+y~AsS~y~bS~by~b
Maybe while you were totally suck into the mathematical part... You forgot why we actually did this. We still want to be able to add a column to our solved tableau.
We can describe our mathematical part using these equations:
We just added the column c4 here. Now we have to update our tableau it a way that it still fulfills our constraints of the diet problem. By adding a new column we kind of destroyed everything we achieved before.
We changed A and we changed c therefore we have to apply the equations (2) and (3).
We can do the same stuff for our cake constraint. I hope you enjoyed the mathematical part or at least think that you got a better understanding of the whole structure. I'm currently working on making the Simplex algorithm (ok my implementation of it :D) more accurate.
You might think: What? Why is it not accurate? Well computers aren't accurate.
Open your console and type python to get into the python console. Now type round(2.675,2). This should round 2.675 to two decimal places.
Give it a go! I'll talk about it next time.
You can have a look at how I implemented everything at my github repository. Yes a lot of stuff have changed since last time. I'm sorry about that.
Unfortunately this series came to an end as I weren't able to solve some of the problems maybe I should give it a go another time.
If you're interested in another series check out my posts on constraint programming: Solving Sudokus again but also much more, faster and in depth: Constraint Programming Solver from Scratch.
Thanks for reading.
If you've enjoyed this and some other posts on my blog I would appreciate a small donation either via PayPal or Patreon whereas the latter is a monthly donation you can choose PayPal for a one time thing. For the monthly donation you'll be able to read a new post earlier than everyone else and it's easier to stay in contact.
Want to be updated? Consider subscribing on Patreon for free