Introduction

A linear program can have more variables than atoms in the observable universe. Listing them all — let alone handing them to a solver — is hopeless. Yet some of the most important optimization problems in the world (airline crew schedules, truck routes, cutting patterns) have exactly this shape.

Column generation is the trick that makes them tractable. The idea is brutally simple: instead of writing down every variable, you start with a tiny handful, solve the small problem optimally, and then ask a clever question — is there any variable I left out that could still improve the solution? If yes, add it and re-solve. If no, you are done; the small solution is already globally optimal.

The "clever question" is itself an optimization problem called the pricing subproblem. Its output is a single new column (variable) that, if it exists, has the best chance of reducing cost. The loop — solve the restricted LP, price a new column, add it, repeat — is the column generation algorithm, introduced by Gilmore and Gomory in 1961 for cutting-stock problems and later unified by Dantzig and Wolfe's decomposition principle.

The payoff is dramatic: problems with trillions of variables are solved with a few hundred columns in practice.

Watch It Work

Below is a small set-cover LP: we have 6 items (1–6) and a universe of possible subsets, each with a cost. We want to cover every item at minimum total cost. The LP has dozens of possible columns (subsets), but column generation starts with just a few and adds the most profitable one at each step.

<div class="cg-wrap">
  <div class="cg-top">
    <div class="cg-left">
      <div class="legend"><strong>{{items_to_cover}}</strong><span id="items-display"></span></div>
      <div class="col-list-wrap">
        <div class="col-list-label">{{active_cols_label}}</div>
        <div id="active-cols" class="col-list"></div>
      </div>
      <div class="col-list-wrap">
        <div class="col-list-label">{{pool_label}}</div>
        <div id="pool-cols" class="col-list pool"></div>
      </div>
    </div>
    <div class="cg-right">
      <div class="result-box">
        <div class="rb-title">{{lp_solution}}</div>
        <div id="rb-body">{{press_next}}</div>
      </div>
      <div class="pricing-box" id="pricing-box" style="display:none">
        <div class="pb-title">{{pricing_step}}</div>
        <div id="pb-body"></div>
      </div>
    </div>
  </div>
  <div class="btns">
    <button id="btn-next" type="button">{{btn_next}}</button>
    <button id="btn-reset" type="button" class="ghost">{{btn_reset}}</button>
  </div>
  <div id="log" class="log"></div>
</div>
*{box-sizing:border-box;}
body{font-family:system-ui,sans-serif;color:#1a2233;margin:0;font-size:14px;}
.cg-wrap{padding:4px 0;}
.cg-top{display:flex;gap:14px;flex-wrap:wrap;}
.cg-left,.cg-right{flex:1;min-width:190px;}
.legend{margin-bottom:8px;font-size:13px;}
#items-display{display:inline-flex;gap:5px;flex-wrap:wrap;margin-left:6px;}
.item-chip{width:26px;height:26px;border-radius:50%;display:inline-flex;align-items:center;
           justify-content:center;font:700 12px monospace;border:2px solid #1d3557;color:#1d3557;}
.item-chip.covered{background:#1d3557;color:#fff;}
.col-list-wrap{margin-bottom:9px;}
.col-list-label{font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.04em;margin-bottom:5px;}
.col-list{display:flex;flex-wrap:wrap;gap:5px;}
.col-chip{border-radius:6px;padding:4px 9px;font:600 12px monospace;cursor:default;
          background:#e8eef3;border:1.5px solid #b0bec5;color:#1a2233;display:flex;align-items:center;gap:5px;}
.col-chip .cc-sub{font-size:10px;color:#546e7a;}
.col-chip.entering{background:#fff3cd;border-color:#f59e0b;animation:pulse .5s ease 2;}
.col-chip.in-basis{background:#d1fae5;border-color:#10b981;color:#064e3b;}
.col-chip.dropped{background:#fee2e2;border-color:#ef4444;color:#7f1d1d;text-decoration:line-through;}
@keyframes pulse{0%,100%{transform:scale(1);}50%{transform:scale(1.1);}}
.pool .col-chip{background:#f5f5f5;border-color:#d1d5db;color:#9ca3af;}
.result-box,.pricing-box{border-radius:8px;padding:10px 13px;margin-bottom:10px;font-size:13px;line-height:1.7;}
.result-box{background:#eff6ff;border:1.5px solid #93c5fd;}
.pricing-box{background:#fffbeb;border:1.5px solid #fcd34d;}
.rb-title,.pb-title{font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px;}
.rb-title{color:#1e40af;}.pb-title{color:#b45309;}
.row{margin:1px 0;}
.hi{font-weight:700;color:#1e40af;}
.hi-neg{font-weight:700;color:#b91c1c;}
.hi-ok{font-weight:700;color:#065f46;}
.btns{display:flex;gap:8px;margin:10px 0;}
button{font:600 13px system-ui;padding:.4rem 1rem;border:1.5px solid #1d3557;
       background:#1d3557;color:#fff;border-radius:7px;cursor:pointer;}
button.ghost{background:#fff;color:#1d3557;}
button:disabled{opacity:.35;cursor:not-allowed;}
.log{font-size:11.5px;color:#4b5563;border-top:1px solid #e5e7eb;padding-top:6px;
     max-height:86px;overflow-y:auto;line-height:1.8;}
.log-entry{padding:0;}
.done-banner{background:#d1fae5;border:1.5px solid #10b981;border-radius:8px;
             padding:10px 14px;font-weight:700;color:#064e3b;margin-bottom:8px;font-size:13px;}
// Code not found

Click Next iteration to run one pricing step: the algorithm checks all remaining columns for the one with the most negative reduced cost, adds it to the restricted LP, and re-optimizes. When no column has negative reduced cost, the current solution is provably optimal — even though most columns were never considered.

The Real Complexity

Column generation is elegant, but its complexity depends on two separate pieces.

The master LP (the restricted problem with the current column set) is an ordinary LP. Each re-solve is polynomial — efficient simplex or interior-point methods apply.

The pricing subproblem asks: among all columns not yet in the model, is there one with negative reduced cost? If the pricing subproblem is easy (e.g., a shortest-path in a graph, as in vehicle routing), the whole algorithm runs in polynomial time. But if pricing is itself NP-hard — as it is for some cutting-stock variants — then the column generation loop is not guaranteed to be efficient.

In practice, three phenomena keep it fast:

  • Sparse optimal bases: a linear program with m constraints has an optimal basis of at most m columns, regardless of how many variables exist. Column generation exploits this — only those m columns ever need to enter.
  • Warm starting: each LP re-solve starts from the previous optimal basis, so successive solves are nearly free.
  • Early termination for integer programs: when combined with branch-and-bound (giving branch-and-price), column generation provides the LP relaxation at each node, and good columns found early propagate throughout the tree.

The status: column generation is a proven polynomial method for LPs when pricing is tractable. For integer programs it becomes a heuristic framework — powerful in practice, but without worst-case guarantees. The cutting-stock problem itself remains open regarding exact complexity for integer solutions, with connections to bin packing.

Where It Matters

Column generation is not a theoretical curiosity — it is the engine behind some of the largest optimization systems in the world:

  • Airline crew scheduling: each column is a crew pairing (a sequence of flights assigned to one crew). There are astronomically many pairings; column generation adds only the cheapest ones that can still improve the schedule. Airlines with thousands of flights use this daily.
  • Vehicle routing: each column is a route for one vehicle. The pricing subproblem finds the shortest feasible route given current dual prices — a constrained shortest-path problem solvable in polynomial time. This is how logistics companies optimize fleets in real time.
  • Cutting stock: a roll of material can be cut in countless patterns. Column generation adds only the patterns worth cutting. Gilmore and Gomory's 1961 paper on this problem launched the field.
  • Network design and telecommunications: channel assignment, wavelength routing, and bandwidth allocation all involve exponentially many path variables managed by column generation.
  • Integer programming in general: branch-and-price extends column generation to integer problems and is the state of the art for many hard combinatorial problems, including set cover and bin packing.

The common thread: whenever a problem's variables correspond to combinatorial objects (routes, patterns, schedules) and the number of objects is exponential, column generation is the natural tool.

Conclusion

Column generation embodies a powerful idea: the optimal solution to a huge problem often uses only a tiny fraction of all possible variables. By starting small and asking — at each step — whether any excluded variable could help, the algorithm zeros in on that tiny fraction without ever enumerating the rest.

The pricing subproblem is the heart of the method. When it is tractable, column generation turns seemingly impossible problems into routine computation. When it is hard, the framework still provides a principled way to attack problems that no other method can handle at scale.

The next time you board a flight or receive a package on time, there is a good chance that a column generation solver — pricing routes and schedules one column at a time — made that possible. For deeper connections, explore linear programming and bin packing, two of the pillars column generation rests on.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/column-generation/Content licensed under CC BY-NC 4.0.