SUPW
Editorial written by Ekansh Majumder.
We are given SUPW activities on different days, each taking a specific number of minutes. We need to select days to perform these duties such that we minimize the total time spent. The constraint is that no student can go three days in a row without participating.
There is only one subtask, and the number of days can be up to . A brute-force approach trying all valid combinations of days would take exponential time and will definitely not pass the time limit constraints.
Why greedy fails
There are two natural greedy solutions one might come up with for this problem. Let’s go over both.
The first approach is to strictly skip as many days as possible: skip day and day , which forces you to do day . Then skip day and day , which forces you to do day , and so on. The idea is to never perform the duty unless you are absolutely forced to. This fails because the problem asks us to minimize the number of minutes spent, not the number of days we work.
A test case where this fails is:
The optimal solution here is to pick day and day (or day and day ), which gives us a total of minutes. But our greedy solution forces us to pick day and day , giving us a massive cost of .
The second approach is to constantly pick the smallest element within a day range from your current position. For example, using the same test case:
If we look for the immediate minimum, we might pick day , day , and day . This gives us a cost of , which is still not the correct answer. This fails because the optimal solution sometimes requires picking a slightly larger number early on to buy enough “reach” to skip massive numbers later. The greedy approach snaps to the immediate smallest number, restricting our future choices.
Why DP?
A greedy algorithm chooses the locally optimal choice. It does not have the foresight to evaluate how a decision made on day changes the options available to us on day . A greedy approach simply cannot consider the long term consequences of a choice.
We use DP to solve this problem because it fixes the exact flaw that greedy approaches have. It evaluates all valid paths without actually simulating every single exponential combination. Instead of just looking for the best immediate choice, DP calculates the absolute cheapest way to arrive at the current state.
The rule stating a student “cannot go three days in a row without any SUPW duty” translates to a simple constraint: out of any consecutive days, we must pick at least one day to work.
Since the decision of whether to work on a particular day only depends on the choices we made in the immediate past (specifically, the last three days), this problem exhibits optimal substructure and overlapping subproblems, making it perfect for dynamic programming.
The DP state
Let’s define a state that helps us keep track of the minimum cost.
Let denote the minimum time spent on SUPW duties up to day , given that we choose to work on day . For the first three days, we have no previous days to rely on to skip a valid block. If we choose to work on day , day , or day , the minimum time spent up to that day is just the time required for that day itself.
vector<int> dp(n);
dp[0] = a[0];
dp[1] = a[1];
dp[2] = a[2];Notice that we define under the strict condition that we choose to work on day . We do this to avoid running into a wall while calculating .
For example, let’s say we define to be the minimum cost for the first days, regardless of whether we worked on day or not. With this state, we run into a wall when calculating . Let’s say we are on day and we know the best score for the first days is (). Now we ask if we can skip day . We have no memory or way to know what the last day we picked was, so we cannot enforce the three day rule using this DP state in a simple way.
Note: This trick of forcing the state to end on a specific element is also used to solve the Longest Increasing Subsequence (LIS) problem using DP (refer to page 70 here).
There is also an alternative way to solve this using a 2D DP, where represents the minimum cost from day to the end of the term, given that is the number of consecutive days we are still allowed to skip. We will also go over this solution later in the editorial.
Transition
If we perform a duty on day , when could we have performed our last duty?
According to the three day rule, the previous duty must have been on day , , or . If our last duty was on day , we would have skipped days , , and . This means we would have skipped consecutive days, which is invalid.
This gives us our transition:
for (int i = 3; i < n; i++) {
dp[i] = a[i] + min({dp[i-1], dp[i-2], dp[i-3]});
}The final duty we perform must be on one of the last three days of the term (day , , or ). If we finished our last duty on day , we would again break the rule by skipping the final three days. Therefore, the overall minimum cost is simply the minimum value among the last three elements of our DP array.
Implementation
#include <bits/stdc++.h>
using namespace std;
int main() {
cin.tie(NULL);
ios::sync_with_stdio(false);
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// dp[i] stores the minimum time
// given that we perform SUPW duty on day i
vector<int> dp(n);
dp[0] = a[0];
dp[1] = a[1];
dp[2] = a[2];
// Calculate the minimum cost
for (int i = 3; i < n; i++) {
dp[i] = a[i] + min({dp[i - 1], dp[i - 2], dp[i - 3]});
}
cout << min({dp[n - 1], dp[n - 2], dp[n - 3]}) << endl;
}The time complexity of this solution is as we iterate through the array of days exactly once.
Alternate solution
Thanks to Avighna for providing me with this solution.
Let denote the minimum time spent on SUPW duties from day to the end of the term, given that we are allowed to skip up to more consecutive days.
Because this approach calculates the cost from the current day to the end of the array, we will build our DP table backwards (from day down to day ).
The second dimension, , can take only values: , , or .
If , then we can safely skip the current day and the next day. If , then we have already skipped one day, so we are allowed to skip one more. If , then we are forced to work on this day.
At any day , the choices we can make depend entirely on our skip allowance .
Case #1: we have no skips left
If , we have already skipped the maximum allowed consecutive days. We must perform duty on day . The cost is the time for today () plus the optimal cost for the remaining days (). Because we worked today, our skip allowance for tomorrow completely resets to the maximum: .
if (j == 0) {
dp[i][j] = a[i] + dp[i + 1][2];
continue;
}Case #2: we have skips remaining
If we have an allowance of or , we have a choice. We can either perform the duty or skip it. We want the minimum of these two options:
- Skip today: we add a cost of for today, move to day , and our skip allowance decreases by (becoming ).
- Work today: we pay the cost for today (), move to day , and our skip allowance resets to full ().
This gives us the transition:
The base case occurs when . we have reached the end of the term, so the cost for any remaining days is .
using ll = int64_t;
ll n;
ll a[N];
ll dp[N][3];
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cin >> n;
for (ll i = 0; i < n; ++i) {
cin >> a[i];
}
for (ll i = n; i >= 0; --i) {
for (ll j = 0; j < 3; ++j) {
if (i == n) {
continue;
}
if (j == 0) {
dp[i][j] = a[i] + dp[i + 1][2];
continue;
}
dp[i][j] = min(dp[i + 1][j - 1], a[i] + dp[i + 1][2]);
}
}
cout << dp[0][2] << "\n";
}