A Question of Ingestion

Once I get LateX working on these pages, I'll make this write-up better!

This is an optimization problem which can be efficiently solved using dynamic programming (DP).

Given an n-course meal, with each couse providing some number of calories, we are tasked with optimizing the total number of calories consumed (our "score") by making a series of decisions whether to eat a course or skip it. Whether we ate or skipped a course affects the amount we can eat during the next course. At each course, we effectively have three options.

  1. Eat this course, adding however many calories we can eat to our score and reducing our future hunger by one third.
  2. Skip this course, replenishing our hunger to what it was the previous round.
  3. Skip two courses, fully replenishing our hunger.

The key observation that enables a DP strategy is this:

With n be the number of courses in the meal,
x be the maximum number of consecutive courses you can eat before your hunger becomes 0,
course_value[c] be the calorie content of course c,
and hunger(m) be the total number of calories we can eat after m consecutive meals. For c in [1, n] and k in [1, x], the optimal "score" is given by the following recurrence:


_4
score(n, x) = 0
_4
score(c, k) = max(score(c+1, k+1) + course_value[c], # eat course `c`
_4
score(c+1, k-1), # skip course `c`
_4
score(c+2, 0)) # skip both course `c` and course `c+1`

Once you have an expression of your optimal strategy as a recurrence, the step that makes it a DP solution is to memoize previously computed states of said recurrence.

The state space of our solution is two dimensional, with the extents being n, the number of courses, and x the maximum number of consecutive courses we can eat before having no appetite. This means we need to allocate an n*x matrix in which to memoize the states of our recurrence.

Finally, we have a choice as to whether we want to implement our recurrence iteratively or recursively. The C++ code below shows the recursive implementation while the Python shows the iterative implementation.

In the C++ solution, I expressed the recurrence in terms of the current course and the current hunger; in the Python, I expressed the recurrence more like the explanation above, in terms of the current course and current consecutive meals eaten. They are equivalent, and to store the current hunger in the dp matrix, I mapped each unique hunger to an index in [0, x]

ingestion.cpp
ingestion.py

_42
#include <iostream>
_42
#include <vector>
_42
#include <functional>
_42
#include <unordered_map>
_42
_42
using ll = long long;
_42
_42
// dp[c][k] = the highest total calories we can consume with a capacity
_42
ll dp[101][101];
_42
_42
int main () {
_42
ll n, m;
_42
std::cin>>n>>m;
_42
std::vector<ll> courses(n);
_42
for (auto& c : courses)
_42
std::cin >> c;
_42
_42
std::unordered_map<ll, ll> cap_idx;
_42
_42
for (ll amt=m, ate=0; amt > 0; ++ate, amt=2*amt/3)
_42
cap_idx[amt]=ate;
_42
_42
for (int r = 0; r < cap_idx.size(); ++r)
_42
for (int c = 0; c < n; ++c)
_42
dp[r][c] = -1;
_42
_42
std::function<ll(ll, ll, int)> select = [&](ll cap, ll prev_cap, int course) {
_42
if (cap==0 || course >= n) return 0LL;
_42
ll& best_option = dp[cap_idx[cap]][course];
_42
if (best_option != -1) return best_option;
_42
ll skip1 = select(prev_cap, cap, course+1);
_42
ll skip2 = select(m, cap, course+2);
_42
ll take = std::min(courses[course], cap) + select(2*cap/3, cap, course+1);
_42
best_option = std::max({skip1, skip2, take});
_42
return best_option;
_42
};
_42
_42
ll ans = select(m, m, 0);
_42
_42
std::cout << ans << '\n';
_42
return 0;
_42
}

If you found this solution helpful, consider leaving a star!