After getting a perfect score in the first round, I crashed and burned in round 2, and only managed to get 1 problem out of 4 correct. For what it's worth, my solution on the 2nd problem got over 80% of the test cases right. For the 3rd problem, I managed to finish a correct solution 10 minutes after the contest was over. As punishment for such a dismal performance, I'm forcing myself to write up the solutions to the problems of this round.
Boomerang Decoration
This problem was the easiest as it was the only one that I got full credit for. Here's a link to the problem statement, Boomerang Decoration.
I employed a pretty common strategy. Basically, there are $N$ spots to paint. Let us index them $0,1,\ldots,N-1.$ Now, consider a pivot point $p.$ We will paint everything $i < p$ from the left using a prefix. We will paint everything $i \geq p$ from the right using a suffix. Let $L(p)$ be the number of prefix paintings we need if we pivot at point $p.$ Let $R(p)$ be the number of suffix paintings we need if we pivot at point $p.$ Then, we have that our solution is $$\min_{p \in \{0,1,\ldots,N\}} \max\left(L(p), R(p)\right),$$ so we just need to compute $L(p)$ and $R(p)$, which we can do with a recurrence relation and dynamic programming.
Let $x_0,x_1,\ldots,x_{N-1}$ be the initial left half of the boomerang. Let $y_0,y_1,\ldots,y_{N-1}$ be the right half of the boomerang that we're trying transform the left side into. Let $L^*(p)$ be the number of blocks we've seen so far, where a block is defined as a contiguous sequence of letters. Clearly, $L(0) = L^*(p) = 0$ since we're not painting anything in that case. Then, for $p = 1,2,\ldots,N$, \begin{equation} L^*(p) = \begin{cases} 1 &\text{if}~p=1 \\ L^*(p - 1) &\text{if}~x_{p-1} = x_{p-2} \\ L^*(p - 1) + 1 &\text{if}~x_{p-1} \neq x_{p-2}, \end{cases} ~\text{and}~ L(p) = \begin{cases} L(p-1) &\text{if}~x_{p-1} = y_{p-1} \\ L^*(p) &\text{if}~x_{p-1} \neq y_{p-1}. \end{cases} \end{equation} since if the letters match, there is no need to paint, and if they don't we only need to paint once for each block.
Similarly, we define $R^*(p)$ as the number of blocks seen from the right. $R(N) = R^*(N) = 0$ since the $N$th index doesn't actually exist. Then, for $p = N-1,N-2,\ldots,0$, \begin{equation} R^*(p) = \begin{cases} 1 &\text{if}~p=N-1 \\ R^*(p + 1) &\text{if}~x_{p} = x_{p+1} \\ R^*(p + 1) + 1 &\text{if}~x_{p} \neq x_{p+1}, \end{cases} ~\text{and}~ R(p) = \begin{cases} R(p+1) &\text{if}~x_{p} = y_{p} \\ R^*(p) &\text{if}~x_{p} \neq y_{p}. \end{cases} \end{equation}
Thus, our run time is $O(N)$. Here's the code that implements this idea.
#include <algorithm>
#include <climits>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int countSteps(const string &left, const string &right) {
int N = left.length();
vector<int> leftSteps; leftSteps.reserve(N + 1);
leftSteps.push_back(0);
int leftBlocks = 0;
for (int i = 0; i < N; ++i) {
if (i == 0 || right[i] != right[i - 1]) ++leftBlocks;
if (left[i] == right[i]) {
leftSteps.push_back(leftSteps.back());
} else {
leftSteps.push_back(leftBlocks);
}
}
vector<int> rightSteps(N + 1, 0);
int rightBlocks = 0;
for (int i = N - 1; i >= 0; --i) {
if (i == N - 1 || right[i] != right[i + 1]) ++rightBlocks;
if (left[i] == right[i]) {
rightSteps[i] = rightSteps[i + 1];
} else {
rightSteps[i] = rightBlocks;
}
}
int minSteps = INT_MAX;
for (int i = 0; i <= N; ++i) {
// paint everything strictly to the left, paint everything to right including i
minSteps = min(minSteps, max(leftSteps[i], rightSteps[i]));
}
return minSteps;
}
int main(int argc, char *argv[]) {
ios::sync_with_stdio(false); cin.tie(NULL);
int T; cin >> T;
for (int t = 1; t <= T; ++t) {
int N; cin >> N;
string left, right;
cin >> left >> right;
cout << "Case #" << t << ": "
<< countSteps(left, right)
<< '\n';
}
cout << flush;
return 0;
}
Carnival Coins
This problem is a probability problem that also makes use of dynamic programming and a recurrence relation. Here's the problem statement, Carnival Coins. I probably spent too long worrying about precision and trying to find a closed-form solution.
In any case, for this problem, given $N$ coins, we need to calculate the binomial distribution for all $n = 0,1,2,\ldots,N$ with probability $p$. Fix $p \in [0,1]$. Let $X_{n,k}$ be the probability $\mathbb{P}(X_n = k),$ where $X_n \sim \operatorname{Binomial}(n,p)$, that is, it is the number of heads if we flip $n$ coins. We use a similar idea to counting an unordered set of $k$ objects from $n$ objects without replacement in Counting Various Things.
Clearly, $\mathbb{P}(X_{n,k}) = 0$ if $k > n$. Also, $\mathbb{P}(X_{0,0}) = 1$. Now let $n \geq 1.$ Consider the $n$th coin. It's heads with probability $p$ and tails with probability $p - 1$, so for $k = 0,1,\ldots,n$, we have that \begin{equation} X_{n,k} = \begin{cases} (1-p)X_{n-1,0} &\text{if}~k=0 \\ (1-p)X_{n-1,k} + pX_{n-1,k-1} &\text{if}~k=1,2,\ldots,n-1 \\ pX_{n-1,n-1} &\text{if}~k=n \end{cases} \end{equation} since if we flip tails, we must have $k$ heads in the first $n-1$ coins, and if we flip heads, we must have $k - 1$ heads in the first $n$ coins.
Now, the problem states that we win if we get more that $K$ coins, too, so we really need the tail distribution. Define $Y_{n,k} = \mathbb{P}(X_n \geq k)$. Then, $Y_{n,n} = X_{n,n}$ since we can't have more than $n$ heads, and for $k = 0,1,\ldots,n-1$, \begin{equation} Y_{n,k} = X_{n,k} + Y_{n,k+1}. \end{equation}
We can compute this all in $O(N^2)$ time. I was hesistant to do this calculate since $p$ is a double
, and I was afraid of the loss of precision, but it turns out using a long double
table works.
Now, suppose we want to maximize expected value with $N$ coins. We can play all $N$ coins at once. Then, our probability of winning is $Y_{N,K}.$ Our second option is to break up our coins into two groups of size say $m$ and $N-m$. These two groups may further be broken up into more groups. Suppose we know the optimal strategy for $n = 1,2,\ldots,N-1$ coins. Let $E[n]$ be the maximum expected value when playing with $n$ coins. The maximum expected value of playing with the two groups, $m$ coins and $N-m$ coins, is $E[m] + E[N-m]$ by linearity of expectation.
This strategy only makes sense if both of the groups are of size at least $K$. Clearly, $E[K] = Y_{K,K} = X_{K,K}.$ Then, for all $n = 0,1,2, \ldots, N,$ we have \begin{equation} E[n] = \begin{cases} 0, &\text{if}~n < K \\ Y_{K,K}, &\text{if}~n = K \\ \max\left(Y_{n,K}, \sup\left\{E[m] + E[n-m] : m = K,K+1,\ldots,\lfloor n/2\rfloor\right\}\right), &\text{if}~n = K + 1,\ldots,N. \end{cases} \end{equation}
Our solution is $E[N]$. Since $N \geq K$, running time is $O(N^2)$. Here's the code.
#include <iostream>
#include <iomanip>
#include <vector>
using namespace std;
long double P[3001][3001]; // pre allocate memory for probability
double computeExpectedPrizes(int N, int K, double p) {
// P[n][k] = P(X_n >= k), where X_n ~ Binomial(n, p)
// first calculate P(X_n = k)
P[0][0] = 1;
P[1][0] = 1 - p; P[1][1] = p;
for (int n = 2; n <= N; ++n) {
P[n][0] = P[n-1][0]*(1-p);
for (int k = 1; k < N; ++k) {
// probability of hitting k when nth coin is heads and nth coin is tails
P[n][k] = p*P[n-1][k-1] + (1-p)*P[n-1][k];
}
P[n][n] = P[n-1][n-1]*p;
}
// make cumulative
for (int n = 1; n <= N; ++n) {
for (int k = n - 1; k >= 0; --k) P[n][k] += P[n][k+1];
}
vector<long double> maxExpectedValue(N + 1, 0); // maxExpectedValue[n] is max expected value for n coins
// two cases: all coins in 1 group or coins in more than 1 group
for (int n = 0; n <= N; ++n) maxExpectedValue[n] = P[n][K]; // put all the coins in 1 group
for (int n = 1; n <= N; ++n) {
// loop invariant is that we know maxExpectedValue for 0,...,n - 1
for (int m = K; m <= n/2; ++m) { // just do half by symmetry
// split coins into two parts, play separately with each part
maxExpectedValue[n] = max(maxExpectedValue[n],
maxExpectedValue[m] + maxExpectedValue[n - m]);
}
}
return maxExpectedValue.back();
}
int main(int argc, char *argv[]) {
ios::sync_with_stdio(false); cin.tie(NULL);
int T; cin >> T;
cout << fixed;
cout << setprecision(9);
for (int t = 1; t <= T; ++t) {
int N, K; // coins and goal
cin >> N >> K;
double p; cin >> p; // probability of coin landing heads
cout << "Case #" << t << ": "
<< computeExpectedPrizes(N, K, p)
<< '\n';
}
cout << flush;
return 0;
}
Snakes and Ladders
This problem was the one I finished 10 minutes after the contest ended. I had everything right, but for some reason, I got stuck on deriving a fairly simple recurrence relation in the last 10 minutes. Here's the problem statement, Snakes and Ladders.
A couple of key insights must be made here.
- Since a snake occurs between ladders of the same height, so group them by height.
- Taller ladders obstruct snakes, so process the ladders by descending height, and store obstructions as we go.
- Deal with the ladders in unobstructed blocks, so sort them by position to put ladders in contiguous blocks
Now, the cost of feeding a snake is the square of its length, which makes processing each unobstructed block a little bit tricky. This is where I got stuck during the contest. The naive way is to compute all the pairwise distances and square them. This isn't fast enough. Here's a better method.
Let $x_1,x_2,\ldots,x_N$ be the position of our ladders, such that $x_1 \leq x_2 \leq \cdots \leq x_N$. Now, for $n \geq 2,$ let $$ C_n = \sum_{k=1}^{n-1} (x_n - x_k)^2 ~\text{and}~ S_n = \sum_{k=1}^{n-1} (x_n - x_k) ,$$ so the total cost of feeding this blocks is $C = \sum_{n=2}^N C_n$. We have that \begin{align*} C_n &= \sum_{k=1}^{n-1} (x_n - x_k)^2 = \sum_{k=1}^{n-1}\left((x_n - x_{n-1}) + (x_{n-1} - x_k)\right)^2 \\ &= \sum_{k=1}^{n-1}\left[(x_n - x_{n-1})^2 + 2(x_n-x_{n-1})(x_{n-1} - x_k) + (x_{n-1}-x_k)^2\right]\\ &= C_{n-1} + (n-1)(x_n - x_{n-1})^2 + 2(x_n-x_{n-1})\sum_{k=1}^{n-1}(x_{n-1} - x_k)\\ &= C_{n-1} + (n-1)(x_n - x_{n-1})^2 + 2(x_n-x_{n-1})S_{n-1} \end{align*} since the last term drops out the summation when $k = n - 1$. Then, we can update $S_n = S_{n-1} + (n-1)(x_n - x_{n-1}).$ We let $C_1 = S_1 = 0.$ Thus, we can compute $C_n$ in $O(1)$ time if we already know $C_{n-1}.$
Since we only look at each ladder once, the biggest cost is sorting, so the running time is $O(N\log N)$, where $N$ is the number of ladders. Here's the code.
#include <algorithm>
#include <iostream>
#include <map>
#include <set>
#include <vector>
using namespace std;
const int MOD = 1000000007;
int computeFeedingCost(const map<int, vector<int>> &ladders) {
set<int> blockEnds; blockEnds.insert(1000000000); // block delimiter
long long cost = 0;
// go through heights in decreasing order
for (map<int, vector<int>>::const_reverse_iterator hIt = ladders.crbegin(); hIt != ladders.crend(); ++hIt) {
int currentLadder = 0;
int N = (hIt -> second).size(); // number of ladders at this height
for (int blockEnd : blockEnds) { // go block by block, where blocks are delimited by blockEnds vector
int blockStart = currentLadder; // remember the beginning of the block
long long xSum = 0;
long long xSquaredSum = 0;
while (currentLadder < N && (hIt -> second)[currentLadder] <= blockEnd) {
if (currentLadder > blockStart) {
// difference in position from this ladder to previous ladder
long long xDiff = (hIt -> second)[currentLadder] - (hIt -> second)[currentLadder-1];
xSquaredSum += (currentLadder - blockStart)*(xDiff*xDiff) + 2*xDiff*xSum; xSquaredSum %= MOD;
xSum += (currentLadder - blockStart)*xDiff; xSum %= MOD;
cost += xSquaredSum; cost %= MOD;
}
if ((hIt -> second)[currentLadder] == blockEnd) {
break; // start next block from this ladder
} else {
++currentLadder;
}
}
}
for (int newBlockEnd : hIt -> second) blockEnds.insert(newBlockEnd);
}
return cost;
}
int main(int argc, char *argv[]) {
ios::sync_with_stdio(false); cin.tie(NULL);
int T; cin >> T;
for (int t = 1; t <= T; ++t) {
int N; // number of ladders
cin >> N;
map<int, vector<int>> ladders; // ladders by height
for (int n = 0; n < N; ++n) {
int x, h; cin >> x >> h; // ladder position and height
ladders[h].push_back(x);
}
for (map<int, vector<int>>::iterator it = ladders.begin(); it != ladders.end(); ++it) {
// within each height sort by position
sort((it -> second).begin(), (it -> second).end());
}
cout << "Case #" << t << ": "
<< computeFeedingCost(ladders)
<< '\n';
}
cout << flush;
return 0;
}
Costly Labels
This problem is much more involved. I'll write about it in a separate post, Assignment Problem and the Hungarian Method.
New Comment
Comments
No comments have been posted yet. You can be the first!