Introduction

A parliament has a whole number of seats. The population it represents does not divide evenly into them. So every democracy faces the same little arithmetic chore: turn each region's share of the people into a whole number of seats.

It sounds like rounding. Region A deserves 8.6 seats, region B deserves 4.4 — surely we just nudge each to the nearest integer and we're done? But the rounded numbers have to add up to exactly the house size, and that constraint quietly turns a trivial-looking task into a genuine trap.

The trap is not a bug in any one method. In 1980, Michel Balinski and Peyton Young proved that no apportionment rule can satisfy a few obviously fair conditions at once. Fair seat division isn't merely hard — it is mathematically impossible.

Watch a Paradox Appear

Three regions, fixed populations: A = 100, B = 51, C = 17 (168 people in all). We use the largest-remainder method: give each region its exact quota's whole part, then hand out the leftover seats to whoever has the biggest fractional remainder. It's the most intuitive rule there is.

Drag the slider to change the total number of seats and watch each region's allocation.

<p class="hint">Three regions with fixed populations share a parliament. The
<b>largest-remainder</b> rule gives each region the whole part of its exact quota,
then hands leftover seats to the biggest fractional remainders. Move the slider and
watch what happens to region <b>C</b> when the house grows from 14 to 15.</p>
<div class="seatrow">
  <label for="seats">Total seats:&nbsp;<b id="seatval">14</b></label>
  <input id="seats" type="range" min="3" max="24" value="14" />
</div>
<div class="btns">
  <button id="show14" type="button">Show 14 seats</button>
  <button id="show15" type="button">Add a seat → 15</button>
</div>
<table class="tbl">
  <thead><tr><th>Region</th><th>Population</th><th>Exact quota</th><th>Seats</th></tr></thead>
  <tbody id="rows"></tbody>
</table>
<div class="status" id="status">Slide to 14, then to 15, and watch region C.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.seatrow { display: flex; align-items: center; gap: .8rem; margin: .3rem 0 .6rem; }
.seatrow label { font-size: .95rem; }
input[type=range] { flex: 1; accent-color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.tbl { width: 100%; border-collapse: collapse; font-size: .92rem; }
.tbl th, .tbl td { text-align: center; padding: .45rem .3rem; border-bottom: 1px solid #e2e8ef; }
.tbl th { color: #1d3557; font-size: .82rem; text-transform: uppercase; letter-spacing: .03em; }
.tbl td.seats { font-weight: 700; font-size: 1.15rem; color: #1d3557; }
.tbl tr.drop td.seats { color: #c92f3c; }
.tbl tr.gain td.seats { color: #0a7d33; }
.status { font-size: 1rem; font-weight: 600; margin: .7rem 0 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Apportionment by the largest-remainder (Hamilton) method.
// Each region gets floor(quota); leftover seats go to the largest fractional
// remainders. This stays "within quota" but suffers the ALABAMA PARADOX: growing the house can take a seat away from a region. (Balinski-Young, 1980.)
const REGIONS = [
  { name: 'A', pop: 100 },
  { name: 'B', pop: 51 },
  { name: 'C', pop: 17 },
];
const TOTAL = REGIONS.reduce((s, r) => s + r.pop, 0); // 168

function apportion(seats) {
  const quotas = REGIONS.map(r => (r.pop * seats) / TOTAL);
  const floors = quotas.map(Math.floor);
  let used = floors.reduce((a, b) => a + b, 0);
  let leftover = seats - used;
  const order = quotas
    .map((q, i) => ({ i, frac: q - Math.floor(q) }))
    .sort((a, b) => b.frac - a.frac);
  const seatsArr = floors.slice();
  for (let k = 0; k < leftover; k++) seatsArr[order[k].i]++;
  return { quotas, seatsArr };
}

let prev = null; // previous seat allocation to detect a drop/gain

function render(seats) {
  document.getElementById('seatval').textContent = seats;
  const { quotas, seatsArr } = apportion(seats);
  const tbody = document.getElementById('rows');
  tbody.innerHTML = '';
  REGIONS.forEach((r, i) => {
    const tr = document.createElement('tr');
    let cls = '';
    if (prev) {
      if (seatsArr[i] < prev[i]) cls = 'drop';
      else if (seatsArr[i] > prev[i]) cls = 'gain';
    }
    tr.className = cls;
    tr.innerHTML =
      '<td>' + r.name + '</td>' +
      '<td>' + r.pop + '</td>' +
      '<td>' + quotas[i].toFixed(2) + '</td>' +
      '<td class="seats">' + seatsArr[i] + '</td>';
    tbody.appendChild(tr);
  });
  return seatsArr;
}

function setStatus(t, cls) {
  const s = document.getElementById('status');
  s.textContent = t; s.className = 'status ' + cls;
}

function update(seats, compareTo) {
  prev = compareTo;
  const now = render(seats);
  if (compareTo) {
    const dropped = REGIONS.findIndex((r, i) => now[i] < compareTo[i]);
    if (dropped >= 0)
      setStatus(fmt('Paradox! The house grew to %seats% seats, yet region %name% LOST a seat.', { seats: seats, name: REGIONS[dropped].name }), 'bad');
    else setStatus(fmt('House size %seats%: no region lost a seat here.', { seats: seats }), 'ok');
  } else {
    setStatus(fmt('House size %seats%. Now add a seat and watch region C.', { seats: seats }), '');
  }
  return now;
}

const slider = document.getElementById('seats');
let lastSeats = 14;
let lastAlloc = update(14, null);
slider.addEventListener('input', () => {
  const seats = +slider.value;
  const compareTo = seats === lastSeats + 1 ? lastAlloc : null;
  const now = update(seats, compareTo);
  lastSeats = seats; lastAlloc = now;
});
document.getElementById('show14').addEventListener('click', () => {
  slider.value = 14; lastSeats = 14; lastAlloc = update(14, null);
});
document.getElementById('show15').addEventListener('click', () => {
  // ensure baseline is 14, then step to 15 to expose the paradox
  const base = apportion(14).seatsArr;
  slider.value = 15; lastSeats = 15; lastAlloc = update(15, base);
});
Result
Console

Now do the cruel experiment. Set the house to 14 seats — region C gets 2. Then add a single seat to make 15. Region C drops to 1, even though the parliament just got bigger. This is the Alabama paradox, named for the 1880 US census where Alabama would have lost a seat as the House grew from 299 to 300. No region misbehaved; the arithmetic did.

The Real Complexity

What exactly goes wrong? It is not that the problem is slow to compute — apportioning seats is fast. The difficulty is that fairness itself is contradictory.

  • Quota. Each region should get either the floor or the ceiling of its exact share — never wildly more or fewer seats than it deserves.
  • No Alabama paradox. Growing the total number of seats should never take a seat away from a region.
  • No population paradox. If region A grows faster than region B, A should never lose a seat to B.

Each condition is obviously fair on its own. Balinski and Young (1980) proved you cannot have all of them. Every method makes a trade-off: the largest-remainder (Hamilton) method stays within quota but suffers the Alabama paradox; divisor methods (Jefferson, Webster, Huntington-Hill) dodge the paradoxes but can break quota, handing a region more or fewer seats than its rounded share.

This is an impossibility theorem, the same shape as Arrow's result on voting or the halting problem: not a puzzle waiting for a cleverer algorithm, but a proof that the perfectly fair rule does not exist.

Where It Matters

Apportionment is not an academic curiosity — it allocates real power and resources:

  • Legislatures. The US House of Representatives reassigns its 435 seats among the states after every census; the method has been changed by law several times precisely because of these paradoxes.
  • Proportional representation. Most parliamentary democracies convert vote percentages into seats using exactly these rules (D'Hondt, Sainte-Laguë), and party seat counts hinge on the choice.
  • Rounding percentages. Even a humble pie chart that must show whole-number percents summing to 100 faces a miniature apportionment problem.
  • Resource allocation. Splitting indivisible goods — staff across offices, machines across factories — in proportion to demand is the same puzzle.

Wherever whole units must mirror continuous shares, the Balinski-Young trade-off is waiting. It is a cousin of the discrete-vs-continuous gap behind integer programming.

Conclusion

Apportionment looks like a rounding chore and turns out to be an impossibility theorem. You can stay within quota, or you can avoid the Alabama paradox, but Balinski and Young proved (1980) you cannot do both — there is no perfectly fair way to turn shares into seats.

So every democracy makes a quiet choice about which unfairness to accept, and writes it into law. The next time you see a parliament's seat chart, remember: behind those tidy integers is a proven impossibility, the same flavor of "no clever rule will save you" that haunts P vs NP and the halting problem. The fairness we crave simply isn't on the menu.

Share this article

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

Comments

Loading comments...

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