# | Time | Username | Problem | Language | Result | Execution time | Memory |
---|---|---|---|---|---|---|---|
336545 | aanastasov | Counting Mushrooms (IOI20_mushrooms) | C++17 | 10 ms | 468 KiB |
This submission is migrated from previous version of oj.uz, which used different machine for grading. This submission may have different result if resubmitted.
#include "mushrooms.h"
#include <array>
#include <algorithm>
#include <cassert>
#include <vector>
#include <iostream>
#define DEBUG(x) std::cerr << #x << ": " << x << std::endl
/**
* Let's say that we want to identify K zeroes or ones in the first phase,
* which are subsequently to be used in the second phase,
* and let's say that we have B bits of efficiency per small query.
*
* Then we are going to need (2K - 1) / B + (N - 2K) / K queries in total.
* Note that we have N = 20000, so the expression is: (2K - 1) / B + (20000 - 2K) / K.
* As per wolframalpha.com, the optimal choice for K is 100 * sqrt(B),
* which shows that the optimal K is a function of B,
* which does make sense: as we can identify more bits per small query,
* it makes sense to identify a larger number of them
* so that we need fewer queries in the second phase.
*
* Finally sqrt(2) = 1.41, sqrt(2.5) = 1.58, and sqrt(3) = 1.73.
* With a particular efficiency, we would need
* (200 * sqrt(B) - 1) / B + (20000 - 200 * sqrt(B)) / (100 * sqrt(B)).
* With 2 bits of effiency, this evaluates to
* 281 / 2 + (20000 - 282) / 141 = 280.
*
* Moreover, if we can get to 3 bits of efficiency per small query,
* then we would need (2 * 173) / 3 + (20000 - 2 * 173) / 173 queries = 229 queries,
* which is pretty close to what we want.
*/
const int PHASE_ONE_COUNT = 142;
template<typename T> void debugVector(std::vector<T> v) {
std::cerr << "{";
for (int i = 0; i < v.size(); ++i) {
if (i > 0) std::cerr << ", ";
std::cerr << v[i];
}
std::cerr << "}" << std::endl;
}
inline int random(int a, int b) {
return a + rand() % (b - a + 1);
}
struct Machine {
int n;
std::vector<int> order;
Machine(int n) {
this->n = n;
order = std::vector<int>(n - 1);
for (int i = 0; i < order.size(); ++i) {
order[i] = i + 1;
if (i > 0) {
int j = random(0, i - 1);
std::swap(order[i], order[j]);
}
}
}
int use(std::vector<int> seq) {
auto modifiedSeq = std::vector<int>(seq.size(), -1);
for (int i = 0; i < seq.size(); ++i) {
if (seq[i] == 0) {
modifiedSeq[i] = 0;
} else {
modifiedSeq[i] = order[seq[i] - 1];
}
}
return use_machine(modifiedSeq);
}
};
/*
int use_my_machine(std::vector<int> seq) {
// debugVector(seq);
return use_machine(seq);
}*/
std::pair<std::vector<int>, std::vector<int>> runPhaseOneTwoAndHalf(int n, int phaseOneCount,
Machine &machine) {
assert(n >= 1000);
// TODO(aanastasov): For this particular algorithm for phase one,
// we might need to permute the input to reduce the chances of adversarial input.
// We should be able to do this inside use_my_machine,
// which will be completely transparent to this implementation, which is pretty nice.
auto zeroes = std::vector<int>();
auto ones = std::vector<int>();
zeroes.push_back(0);
// Step 1: Identify sufficiently many items to have 3 zeroes or 3 ones.
// We spend too many queries here, it's better to utilize the 2-bit efficiency query here.
while (zeroes.size() < 3 && ones.size() < 3 && zeroes.size() + ones.size() < n) {
int next = zeroes.size() + ones.size();
auto seq = std::vector<int>{0, next};
int count = machine.use(seq);
assert(count >= 0 && count <= 1);
if (count == 0)
zeroes.push_back(next);
else
ones.push_back(next);
}
assert(n <= 4 || zeroes.size() == 3 || ones.size() == 3);
// Step 2: Utilize a 2.5-bit efficiency query from now on.
// The structure is the following:
// count = use_machine(known[0], unknown[0], known[1], unknown[1], known[2], unknown[2]),
// where known[0] = known[1] = known[2] are all 0s or all 1s, as identified from above.
// Now, unknown[2] is completely determined by the parity of count.
//
// However, there are two cases for unknown[0] and unknown[1]:
// Case 1: if unknown[0] = unknown[1], then we know them entirely, so we've identified 3 bits.
// Case 2: if unknown[0] /= unknown[1], then we know the are different, but we don't know
// which one is which; thus, we attempt to identify unknown[0] with a subsequent query by
// designating it as unknown[2], after which we would be able to deduce both.
// Therefore, with 50% we identify 3 bits, and with 50% we identify 1 bit + whether
// unknown[0] and unknown[1] are same or different, which is 1 more bit, for a total of 2.
// In expectation, we have 50% * 3 bits + 50% * 2 bits = 2.5 bits on average.
auto prevUnknown = std::vector<int>();
while (zeroes.size() < phaseOneCount
&& ones.size() < phaseOneCount
&& zeroes.size() + ones.size() < n) {
assert(prevUnknown.empty()
|| (prevUnknown.size() == 2 && std::min(prevUnknown[0], prevUnknown[1]) > 0));
int next = zeroes.size() + ones.size() + prevUnknown.size();
auto unknown = std::vector<int>{next, next + 1, next + 2};
if (!prevUnknown.empty()) {
unknown[2] = prevUnknown[0];
}
for (auto x : unknown) if (x >= n) {
assert(false); // This shouldn't happen because n >= 1000.
}
auto known = std::vector<int>();
assert(zeroes.size() >= 3 || ones.size() >= 3);
const bool usingZeroes = (zeroes.size() >= 3);
if (usingZeroes) {
known = std::vector<int>{zeroes[0], zeroes[1], zeroes[2]};
} else {
known = std::vector<int>{ones[0], ones[1], ones[2]};
}
auto seq = std::vector<int>{known[0], unknown[0], known[1], unknown[1],
known[2], unknown[2]};
const int count = machine.use(seq);
// Step 2.1: Need to handle the case where prevUnknown.empty() is false,
// so that we can identify those two old bits completely;
// as of now, we just know that they are not the same.
// Step 2.2: If prevUnknown.empty() is true, then we can identify one more item: unknown[2].
if (!prevUnknown.empty()) {
if (usingZeroes) {
if (count % 2 == 0) {
zeroes.push_back(prevUnknown[0]); ones.push_back(prevUnknown[1]);
} else {
zeroes.push_back(prevUnknown[1]); ones.push_back(prevUnknown[0]);
}
} else {
if (count % 2 == 0) {
zeroes.push_back(prevUnknown[1]); ones.push_back(prevUnknown[0]);
} else {
zeroes.push_back(prevUnknown[0]); ones.push_back(prevUnknown[1]);
}
}
prevUnknown.clear();
} else {
if (usingZeroes) {
if (count % 2 == 0) zeroes.push_back(unknown[2]); else ones.push_back(unknown[2]);
} else {
if (count % 2 == 0) ones.push_back(unknown[2]); else zeroes.push_back(unknown[2]);
}
}
assert(prevUnknown.empty());
// Step 2.3: Need to check if unknown[0] and unknown[1] are the same,
// in which we case we know them entirely,
// or if they are different, in which case, we need to push them to prevUnknown.
const int count2 = count % 2 != 0 ? count - 1 : count;
assert(count2 == 0 || count2 == 2 || count2 == 4);
if (count2 == 0 || count2 == 4) { // same
if (usingZeroes) {
if (count2 == 0) { zeroes.push_back(unknown[0]); zeroes.push_back(unknown[1]); }
else { ones.push_back(unknown[0]); ones.push_back(unknown[1]); }
} else {
if (count2 == 4) { zeroes.push_back(unknown[0]); zeroes.push_back(unknown[1]); }
else { ones.push_back(unknown[0]); ones.push_back(unknown[1]); }
}
} else { // different
prevUnknown.push_back(unknown[0]);
prevUnknown.push_back(unknown[1]);
}
}
if (!prevUnknown.empty()) {
// Need to handle this case correctly.
auto seq = std::vector<int>{0, prevUnknown[0]};
int count = machine.use(seq);
if (count == 0) {
zeroes.push_back(prevUnknown[0]);
ones.push_back(prevUnknown[1]);
} else if (count == 1) {
zeroes.push_back(prevUnknown[1]);
ones.push_back(prevUnknown[0]);
} else {
assert(false);
}
}
return {zeroes, ones};
}
std::pair<std::vector<int>, std::vector<int>> runPhaseOne(int n, int phaseOneCount,
Machine &machine) {
auto zeroes = std::vector<int>();
auto ones = std::vector<int>();
zeroes.push_back(0);
while (zeroes.size() < 2 && ones.size() < 2 && zeroes.size() + ones.size() < n) {
int next = zeroes.size() + ones.size();
auto seq = std::vector<int>{0, next};
int count = machine.use(seq);
assert(count >= 0 && count <= 1);
if (count == 0)
zeroes.push_back(next);
else
ones.push_back(next);
}
assert(n <= 2 || zeroes.size() == 2 || ones.size() == 2);
while (zeroes.size() < phaseOneCount && ones.size() < phaseOneCount && zeroes.size() + ones.size() < n) {
int next0 = zeroes.size() + ones.size();
int next1 = next0 + 1;
if (next1 >= n) break;
bool useKnownZeroes = zeroes.size() >= 2;
auto seq = std::vector<int>();
if (useKnownZeroes)
seq = std::vector<int>{zeroes[0], next0, zeroes[1], next1};
else
seq = std::vector<int>{ones[0], next0, ones[1], next1};
int count = machine.use(seq);
assert(count >= 0 && count <= 3);
if (useKnownZeroes) {
if (count >= 2) ones.push_back(next0); else zeroes.push_back(next0);
if (count % 2 != 0) ones.push_back(next1); else zeroes.push_back(next1);
} else {
if (count < 2) ones.push_back(next0); else zeroes.push_back(next0);
if (count % 2 != 1) ones.push_back(next1); else zeroes.push_back(next1);
}
}
return {zeroes, ones};
}
int count_mushrooms(int n) {
int phaseOneCount = std::min(n / 2, PHASE_ONE_COUNT);
// NOTE(aanastasov): This is purely to reduce the number of edge cases
// that needs to be consider with the more efficient phase one algorithm.
Machine machine(n);
auto response = (n <= 1000)
? runPhaseOne(n, phaseOneCount, machine)
: runPhaseOneTwoAndHalf(n, phaseOneCount, machine);
auto zeroesLocs = response.first;
auto onesLocs = response.second;
int zeroesCount = 0;
int onesCount = 0;
int next = zeroesLocs.size() + onesLocs.size();
while (next < n) {
int len = std::min(n - next, (int)std::max(zeroesLocs.size(), onesLocs.size()));
auto seq = std::vector<int>(2 * len);
auto known = zeroesLocs.size() > onesLocs.size() ? zeroesLocs : onesLocs;
for (int i = 0; i < len; ++i) seq[i * 2] = known[i];
for (int i = 0; i < len; ++i) seq[i * 2 + 1] = next++;
int count = (machine.use(seq) + 1) / 2;
if (known == onesLocs) {
zeroesCount += count;
onesCount += len - count;
} else {
onesCount += count;
zeroesCount += len - count;
}
}
return zeroesCount + zeroesLocs.size();
}
Compilation message (stderr)
# | Verdict | Execution time | Memory | Grader output |
---|---|---|---|---|
Fetching results... |