Chef Showdown
A group of n chefs participate in a turn-based strategic cooking competition. On each chef's turn, he/she cooks a dish. Chef k has skill level S(k) = F_k / F_(n+1), where F_k is the k -th Fibonacci...
Problem Statement
This archive keeps the full statement, math, and original media on the page.
A group of chefs (numbered \(\#1\), \(\#2\), etc) participate in a turn-based strategic cooking competition. On each chef’s turn, he/she cooks up a dish to the best of his/her ability and gives it to a separate panel of judges for taste-testing. Let \(S(k)\) represent chef \(\#k\)’s skill level (which is publicly known). More specifically, \(S(k)\) is the probability that chef \(\#k\)’s dish will be assessed favorably by the judges (on any/all turns). If the dish receives a favorable rating, then the chef must choose one other chef to be eliminated from the competition. The last chef remaining in the competition is the winner.
The game always begins with chef \(\#1\), with the turn order iterating sequentially over the rest of the chefs still in play. Then the cycle repeats from the lowest-numbered chef. All chefs aim to optimize their chances of winning within the rules as stated, assuming that the other chefs behave in the same manner. In the event that a chef has more than one equally-optimal elimination choice, assume that the chosen chef is always the one with the next-closest turn.
Define \(W_n(k)\) as the probability that chef \(\#k\) wins in a competition with \(n\) chefs. If we have \(S(1) = 0.25\), \(S(2) = 0.5\), and \(S(3) = 1\), then \(W_3(1) = 0.29375\).
Going forward, we assign \(S(k) = F_k/F_{n+1}\) over all \(1 \le k \le n\), where \(F_k\) is a Fibonacci number: \(F_k = F_{k-1} + F_{k-2}\) with base cases \(F_1 = F_2 = 1\). Then, for example, when considering a competition with \(n = 7\) chefs, we have \(W_7(1) = 0.08965042\), \(W_7(2) = 0.20775702\), \(W_7(3) = 0.15291406\), \(W_7(4) = 0.14554098\), \(W_7(5) = 0.15905291\), \(W_7(6) = 0.10261412\), and \(W_7(7) = 0.14247050\), rounded to \(8\) decimal places each.
Let \(E(n)\) represent the expected number of dishes cooked in a competition with \(n\) chefs. For instance, \(E(7) = 42.28176050\).
Find \(E(14)\) rounded to \(8\) decimal places.
Problem 481: Chef Showdown
Mathematical Foundation
Definition 1 (Game state). A state is a pair where is the set of remaining chefs with , and is the index within the ordered set indicating whose turn begins the next round.
Theorem 1 (Geometric round structure). Let the turn order within state be , and define for each . A round consists of all chefs attempting a dish in sequence. Then:
(i) The probability that no chef succeeds in a full round is
(ii) The probability that chef is the first to succeed in a given round is
(iii) These probabilities satisfy .
(iv) The number of completely failed rounds before the first success is geometrically distributed with parameter .
Proof. Each chef’s success is an independent Bernoulli trial with parameter .
For (i): a complete failure occurs if and only if every chef fails, which has probability by independence.
For (ii): chef is the first to succeed in a round if and only if all fail and succeeds. By independence, this has probability .
For (iii): we verify
This is proved by induction on . For : . Assuming it holds for chefs, adding the -th chef contributes , and the sum becomes .
For (iv): rounds are independent (each round’s outcome depends only on fresh Bernoulli trials), so the count of completely failed rounds before the first success follows .
Corollary 1. Conditioned on the first success occurring in some round, the probability that chef is the one to succeed is
Proof. Immediate from the law of total probability and the memoryless property of the geometric distribution.
Theorem 2 (Optimal elimination strategy). For each state and each chef , define as the probability that ultimately wins. When succeeds and must choose whom to eliminate, the optimal target is
with ties broken by choosing the chef whose turn comes soonest after in the cyclic order, where denotes the appropriate starting index in .
Proof. By backward induction on .
Base case: . The remaining chef wins with probability 1; no elimination decision is required.
Inductive step: Suppose the winning probabilities are uniquely determined for all states with . When chef succeeds in state , eliminating target transitions the game to state , where is determined by the cyclic turn order (the next chef after who is not ). A rational agent maximizes over all valid targets . This is well-defined by the inductive hypothesis. The tie-breaking rule is deterministic and consistent.
Theorem 3 (Expected dish count). The expected number of dishes cooked from state satisfies the recurrence
where is the expected number of dishes cooked within the successful round, is the optimal elimination target for chef , and is the resulting starting index.
Equivalently, combining the first two terms:
with the base case for any single chef .
Proof. Condition on the geometric number of failed rounds . In each of the failed rounds (expected count ), exactly dishes are cooked, contributing in expectation. In the successful round, chef succeeds at position (i.e., after dishes in that round), with conditional probability . This gives the partial-round expected dish count .
After the elimination, the game transitions to state and the expected remaining dishes are , weighted by the conditional probability .
The base case holds because no more dishes are cooked once a single chef remains.
Editorial
Chef k has skill S(k) = F_k / F_{n+1} (Fibonacci ratio). Chefs eliminate each other optimally. Compute E(14), the expected number of dishes cooked. Method: bitmask DP over game states (remaining chefs, start index). For each state, compute winning probabilities via Theorem 2 (backward induction on optimal elimination) and expected dishes via Theorem 3 (geometric round structure + recursive decomposition).
Pseudocode
Compute Fibonacci numbers F[1..n+1]
S[k] = F[k] / F[n+1] for k = 1..n
States are (bitmask of remaining chefs, start index within the ordered set)
For each each state (mask, start_idx) in order of increasing popcount(mask):
If popcount(mask) == 1 then
W[mask][start_idx][lone_chef] = 1.0
E[mask][start_idx] = 0.0
continue
chefs = sorted list of set bits in mask
m = len(chefs)
Reorder chefs cyclically starting at start_idx
Compute Q, P[j], and reach probabilities p_reach[j]
For each chef c_j, find optimal target t_j* via argmax of W
(with tie-breaking by cyclic distance)
Compute W[mask][start_idx][k] for all k via weighted sum
Compute E[mask][start_idx] via Theorem 3
Return E[full_mask][0]
Complexity Analysis
- Time: . There are states (mask, start); for each state with chefs, evaluating all elimination choices costs per chef, yielding per state. Summing over all masks: . For , this is approximately operations.
- Space: for storing winning probabilities and expected dishes .
Answer
Code
Each problem page includes the exact C++ and Python source files from the local archive.
/*
* Project Euler Problem 481: Chef Showdown
*
* Chef k has skill S(k) = F_k / F_{n+1}. Chefs eliminate each other optimally.
* Compute E(14) via bitmask DP over game states (remaining chefs, start index).
*
* Theorems applied:
* - Geometric round structure (Theorem 1): first-success probabilities P_j
* - Optimal elimination (Theorem 2): backward induction on win probabilities
* - Expected dishes (Theorem 3): geometric + recursive decomposition
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 14;
double fib[MAXN + 2];
double skill[MAXN];
void init_fib(int n) {
fib[1] = fib[2] = 1;
for (int i = 3; i <= n + 1; i++)
fib[i] = fib[i - 1] + fib[i - 2];
}
vector<int> get_chefs(int mask, int n) {
vector<int> v;
for (int i = 0; i < n; i++)
if (mask & (1 << i)) v.push_back(i);
return v;
}
int find_next_turn(const vector<int>& order, int j, int elim_chef) {
int m = order.size();
for (int k = j + 1; k < m; k++)
if (order[k] != elim_chef) return order[k];
for (int k = 0; k < j; k++)
if (order[k] != elim_chef) return order[k];
return -1;
}
map<pair<int,int>, vector<double>> memo_win;
map<pair<int,int>, double> memo_exp;
vector<double> compute_win(int mask, int start_idx, int n);
double compute_exp(int mask, int start_idx, int n);
vector<double> compute_win(int mask, int start_idx, int n) {
auto key = make_pair(mask, start_idx);
auto it = memo_win.find(key);
if (it != memo_win.end()) return it->second;
vector<int> chefs = get_chefs(mask, n);
int m = chefs.size();
if (m == 1) {
vector<double> w(n, 0);
w[chefs[0]] = 1.0;
return memo_win[key] = w;
}
vector<int> order;
for (int i = start_idx; i < m; i++) order.push_back(chefs[i]);
for (int i = 0; i < start_idx; i++) order.push_back(chefs[i]);
vector<double> p_reach(m);
p_reach[0] = 1.0;
for (int j = 1; j < m; j++)
p_reach[j] = p_reach[j - 1] * (1.0 - skill[order[j - 1]]);
double p_none = p_reach[m - 1] * (1.0 - skill[order[m - 1]]);
vector<double> result(n, 0);
for (int j = 0; j < m; j++) {
double p_j = p_reach[j] * skill[order[j]] / (1.0 - p_none);
double best_wp = -1;
int best_e = -1;
vector<double> best_w;
for (int e = 0; e < m; e++) {
if (e == j) continue;
int elim = order[e];
int new_mask = mask ^ (1 << elim);
vector<int> nc = get_chefs(new_mask, n);
int nxt = find_next_turn(order, j, elim);
int ns = 0;
if (nxt >= 0)
for (int k = 0; k < (int)nc.size(); k++)
if (nc[k] == nxt) { ns = k; break; }
vector<double> w = compute_win(new_mask, ns, n);
if (w[order[j]] > best_wp + 1e-15) {
best_wp = w[order[j]];
best_e = e;
best_w = w;
} else if (fabs(w[order[j]] - best_wp) < 1e-15) {
if ((e - j + m) % m < (best_e - j + m) % m) {
best_e = e;
best_w = w;
}
}
}
for (int k = 0; k < n; k++)
result[k] += p_j * best_w[k];
}
return memo_win[key] = result;
}
double compute_exp(int mask, int start_idx, int n) {
auto key = make_pair(mask, start_idx);
auto it = memo_exp.find(key);
if (it != memo_exp.end()) return it->second;
vector<int> chefs = get_chefs(mask, n);
int m = chefs.size();
if (m == 1) return memo_exp[key] = 0;
vector<int> order;
for (int i = start_idx; i < m; i++) order.push_back(chefs[i]);
for (int i = 0; i < start_idx; i++) order.push_back(chefs[i]);
vector<double> p_reach(m);
p_reach[0] = 1.0;
for (int j = 1; j < m; j++)
p_reach[j] = p_reach[j - 1] * (1.0 - skill[order[j - 1]]);
double p_none = p_reach[m - 1] * (1.0 - skill[order[m - 1]]);
double exp_partial = 0;
for (int j = 0; j < m; j++)
exp_partial += (j + 1) * p_reach[j] * skill[order[j]];
exp_partial /= (1.0 - p_none);
double exp_dishes = p_none / (1.0 - p_none) * m + exp_partial;
double exp_after = 0;
for (int j = 0; j < m; j++) {
double p_j = p_reach[j] * skill[order[j]] / (1.0 - p_none);
double best_wp = -1;
int best_e = -1, best_ns = 0, best_nm = 0;
for (int e = 0; e < m; e++) {
if (e == j) continue;
int elim = order[e];
int new_mask = mask ^ (1 << elim);
vector<int> nc = get_chefs(new_mask, n);
int nxt = find_next_turn(order, j, elim);
int ns = 0;
if (nxt >= 0)
for (int k = 0; k < (int)nc.size(); k++)
if (nc[k] == nxt) { ns = k; break; }
vector<double> w = compute_win(new_mask, ns, n);
if (w[order[j]] > best_wp + 1e-15) {
best_wp = w[order[j]];
best_e = e;
best_ns = ns;
best_nm = new_mask;
} else if (fabs(w[order[j]] - best_wp) < 1e-15) {
if ((e - j + m) % m < (best_e - j + m) % m) {
best_e = e;
best_ns = ns;
best_nm = new_mask;
}
}
}
exp_after += p_j * compute_exp(best_nm, best_ns, n);
}
return memo_exp[key] = exp_dishes + exp_after;
}
int main() {
int n = 7;
init_fib(n);
for (int k = 0; k < n; k++)
skill[k] = fib[k + 1] / fib[n + 1];
printf("E(7) = %.8f\n", compute_exp((1 << n) - 1, 0, n));
memo_win.clear();
memo_exp.clear();
n = 14;
init_fib(n);
for (int k = 0; k < n; k++)
skill[k] = fib[k + 1] / fib[n + 1];
printf("E(14) = %.8f\n", compute_exp((1 << n) - 1, 0, n));
return 0;
}
"""
Project Euler Problem 481: Chef Showdown
Chef k has skill S(k) = F_k / F_{n+1} (Fibonacci ratio). Chefs eliminate
each other optimally. Compute E(14), the expected number of dishes cooked.
Method: bitmask DP over game states (remaining chefs, start index).
For each state, compute winning probabilities via Theorem 2 (backward
induction on optimal elimination) and expected dishes via Theorem 3
(geometric round structure + recursive decomposition).
"""
def solve(n):
fib = [0] * (n + 2)
fib[1] = fib[2] = 1
for i in range(3, n + 2):
fib[i] = fib[i - 1] + fib[i - 2]
skills = [fib[k + 1] / fib[n + 1] for k in range(n)]
def get_chefs(mask):
return [i for i in range(n) if mask & (1 << i)]
cache_win = {}
cache_exp = {}
def find_next_turn(order, j, elim_chef):
"""Find the next chef in cyclic order after position j, excluding elim_chef."""
m = len(order)
for k in range(j + 1, m):
if order[k] != elim_chef:
return order[k]
for k in range(j):
if order[k] != elim_chef:
return order[k]
return None
def compute_win(mask, start_idx):
key = (mask, start_idx)
if key in cache_win:
return cache_win[key]
chefs = get_chefs(mask)
m = len(chefs)
if m == 1:
result = [0.0] * n
result[chefs[0]] = 1.0
cache_win[key] = result
return result
order = chefs[start_idx:] + chefs[:start_idx]
p_reach = [0.0] * m
p_reach[0] = 1.0
for j in range(1, m):
p_reach[j] = p_reach[j - 1] * (1.0 - skills[order[j - 1]])
p_none = p_reach[-1] * (1.0 - skills[order[-1]])
result = [0.0] * n
for j in range(m):
p_j = p_reach[j] * skills[order[j]] / (1.0 - p_none)
best_win_prob = -1.0
best_elim = -1
best_wins = None
for e in range(m):
if e == j:
continue
elim_chef = order[e]
new_mask = mask ^ (1 << elim_chef)
new_chefs = get_chefs(new_mask)
next_in_cycle = find_next_turn(order, j, elim_chef)
new_start = new_chefs.index(next_in_cycle) if next_in_cycle is not None else 0
w = compute_win(new_mask, new_start)
if w[order[j]] > best_win_prob + 1e-15:
best_win_prob = w[order[j]]
best_elim = e
best_wins = w
elif abs(w[order[j]] - best_win_prob) < 1e-15:
if (e - j) % m < (best_elim - j) % m:
best_elim = e
best_wins = w
for k in range(n):
result[k] += p_j * best_wins[k]
cache_win[key] = result
return result
def compute_exp(mask, start_idx):
key = (mask, start_idx)
if key in cache_exp:
return cache_exp[key]
chefs = get_chefs(mask)
m = len(chefs)
if m == 1:
cache_exp[key] = 0.0
return 0.0
order = chefs[start_idx:] + chefs[:start_idx]
p_reach = [0.0] * m
p_reach[0] = 1.0
for j in range(1, m):
p_reach[j] = p_reach[j - 1] * (1.0 - skills[order[j - 1]])
p_none = p_reach[-1] * (1.0 - skills[order[-1]])
exp_success = sum(
(j + 1) * p_reach[j] * skills[order[j]] for j in range(m)
)
exp_success /= 1.0 - p_none
exp_failed = p_none / (1.0 - p_none) * m
exp_dishes = exp_failed + exp_success
exp_after = 0.0
for j in range(m):
p_j = p_reach[j] * skills[order[j]] / (1.0 - p_none)
best_win_prob = -1.0
best_elim = -1
best_new_mask = 0
best_new_start = 0
for e in range(m):
if e == j:
continue
elim_chef = order[e]
new_mask = mask ^ (1 << elim_chef)
new_chefs = get_chefs(new_mask)
next_in_cycle = find_next_turn(order, j, elim_chef)
new_start = new_chefs.index(next_in_cycle) if next_in_cycle is not None else 0
w = compute_win(new_mask, new_start)
if w[order[j]] > best_win_prob + 1e-15:
best_win_prob = w[order[j]]
best_elim = e
best_new_mask = new_mask
best_new_start = new_start
elif abs(w[order[j]] - best_win_prob) < 1e-15:
if (e - j) % m < (best_elim - j) % m:
best_elim = e
best_new_mask = new_mask
best_new_start = new_start
exp_after += p_j * compute_exp(best_new_mask, best_new_start)
result = exp_dishes + exp_after
cache_exp[key] = result
return result
full_mask = (1 << n) - 1
return compute_exp(full_mask, 0)
if __name__ == "__main__":
E7 = solve(7)
print(f"E(7) = {E7:.8f}")
E14 = solve(14)
print(f"E(14) = {E14:.8f}")