E - ネットワークの巡回点検 / Network Patrol Inspection Editorial by admin
gemini-3.5-flash-highOverview
This problem asks us to simulate patrolling servers from \(1\) to \(N\) according to given rules, and find the total number of times we start a new patrol.
If we perform a naive simulation, finding the “smallest unvisited server with a common prime factor to move to next” will take too much time. To speed up this search, we need to design an efficient skip algorithm utilizing a Union-Find (DSU).
Analysis
Limitations of Naive Simulation
Let the current position be \(k\). The next destination is “the smallest unvisited server \(m\) greater than \(k\) that shares a common prime factor \(p\) with \(k\).”
To find this, for each prime factor \(p\) of \(k\), we could naively scan multiples of \(p\) greater than \(k\) (\(k+p, k+2p, \dots\)) to find an unvisited one. However, if many servers have already been visited, we would end up skipping a large number of “already visited servers” before reaching an unvisited one. In the worst-case scenario, this scan could take \(O(N)\) time, making the overall time complexity \(O(N^2)\), which results in a Time Limit Exceeded (TLE).
Speed-up Idea: Skipping with Union-Find
To quickly skip “already visited servers”, we utilize Union-Find.
For each prime \(p\), we prepare an independent Union-Find that manages only “multiples of \(p\)”. When a multiple of \(p\), \(x \cdot p\), is visited, we merge (union) index \(x\) and \(x+1\) in the Union-Find.
Concrete Example: \(p = 3\)
We manage the multiples of \(3\), which are \(3, 6, 9, 12, 15, \dots\), with indices \(x = 1, 2, 3, 4, 5, \dots\) respectively.
- Initial State: All are unvisited.
- Union-Find representatives:
[1, 2, 3, 4, 5, ...]
- Union-Find representatives:
- \(6\) (\(x=2\)) is visited:
- Merge \(x=2\) and \(x=3\).
- Union-Find representatives:
[1, 3, 3, 4, 5, ...](the parent of \(2\) becomes \(3\)) - At this point, to find the next multiple of \(3\) greater than \(6\), we compute
find(2 + 1) = find(3) = 3, which immediately tells us that \(9\) (\(x=3\)) is the next candidate.
- \(9\) (\(x=3\)) is also visited:
- Merge \(x=3\) and \(x=4\).
- Union-Find representatives:
[1, 4, 4, 4, 5, ...] - In this state, if we search for the “smallest multiple of \(3\) greater than \(6\)” again, we get
find(2 + 1) = find(3) = 4, allowing us to skip the already visited \(9\) and directly find \(12\) (\(x=4\)).
In this way, by using the find operation of Union-Find (with path compression), we can skip visited elements in almost constant time \(O(\alpha(N))\).
Algorithm
1. Preparation
- Computing SPF (Smallest Prime Factor): Using a method similar to the Sieve of Eratosthenes, we compute the smallest prime factor of each number \(i\). This allows us to perform prime factorization of any number \(i\) quickly in \(O(\log i)\) time.
- Constructing Prime Factor Lists: Precompute the list of unique prime factors for each number \(i \in [2, N]\).
- Enumerating Primes and Initializing Union-Find: Enumerate primes \(p\) up to \(N\) and assign an ID to each. For each prime \(p\), construct a Union-Find of size \(\lfloor N/p \rfloor + 2\).
2. Simulation
Let min_unvisited be a variable pointing to the smallest unvisited server index, initialized to \(1\).
As long as min_unvisited <= N, repeat the following steps:
Starting a New Patrol:
- If
min_unvisitedis already visited, incrementmin_unvisitedto skip it. - If it is unvisited, increment the patrol count
ansby \(+1\), and set the current position \(k = min\_unvisited\). - Mark \(k\) as visited. For each prime factor \(p\) of \(k\), merge \(k/p\) and \(k/p + 1\) in the Union-Find for \(p\).
- If
Moving During a Patrol:
- If \(k = 1\):
- Increment
min_unvisitedto find the first unvisited server, and set it as the next destination \(next\_m\).
- Increment
- If \(k \ge 2\):
- For each prime factor \(p\) of \(k\), compute
find(k/p + 1)in the Union-Find for \(p\) to obtain the next unvisited candidate index \(next\_idx\). - Among the candidate values \(next\_idx \cdot p\), let the smallest one that is less than or equal to \(N\) be the next destination \(next\_m\).
- For each prime factor \(p\) of \(k\), compute
- If \(next\_m\) does not exist (exceeds \(N\)), end the current patrol and return to Step 1.
- If it exists, move to \(k = next\_m\), mark \(k\) as visited, merge \(k/p\) and \(k/p + 1\) for each prime factor \(p\) of \(k\), and repeat the movement.
- If \(k = 1\):
Complexity
Time Complexity: \(O(N \log \log N)\)
- SPF Construction: \(O(N \log \log N)\).
- Prime Factor List Construction: The number of prime factors for each number \(i\) is at most \(7\) when \(N \le 5 \times 10^5\) (since \(2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17 > 5 \times 10^5\)). Therefore, the overall complexity is around \(O(N)\).
- Union-Find Initialization: The total number of elements across all primes \(p\) is \(\sum_{p \le N} \frac{N}{p} = O(N \log \log N)\).
- Simulation: Each server is visited at most once. The number of Union-Find operations performed upon visiting a server is proportional to the number of its prime factors (at most \(7\)). Since Union-Find operations take almost constant time \(O(\alpha(N))\), the overall simulation takes \(O(N \alpha(N))\).
Consequently, the total time complexity is \(O(N \log \log N)\), which easily runs within the time limit.
Space Complexity: \(O(N \log \log N)\)
- The total size of the prime factor lists of all numbers and the total number of nodes in all Union-Find structures are both \(O(N \log \log N)\) (or practically \(O(N)\)), which fits within several tens of megabytes, well below typical memory limits (e.g., 1024MB).
Key Implementation Points
Memory Flattening (Optimization): Using
vector<vector<int>>heavily in C++ can cause overhead from dynamic memory allocation and cache misses. In the provided code, two-dimensional data (such as the prime factor list of each number and the Union-Find arrays for each prime) are flattened into one-dimensional arrays (factor_listanddsu_parent) using offset arrays (factor_offsetanddsu_offset) pointing to their respective start positions. This maximizes execution speed and memory efficiency.Source Code
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <span>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N;
if (!(cin >> N)) return 0;
if (N == 1) {
cout << 1 << "\n";
return 0;
}
// SPF (Smallest Prime Factor) calculation
vector<int> spf(N + 1);
iota(spf.begin(), spf.end(), 0);
for (int i = 2; i * i <= N; ++i) {
if (spf[i] == i) {
for (int j = i * i; j <= N; j += i) {
if (spf[j] == j) spf[j] = i;
}
}
}
// Prime factorization list flattening
vector<int> factor_offset(N + 2, 0);
for (int i = 2; i <= N; ++i) {
int temp = i;
while (temp > 1) {
int p = spf[temp];
factor_offset[i + 1]++;
while (temp % p == 0) temp /= p;
}
}
for (int i = 2; i <= N + 1; ++i) {
factor_offset[i] += factor_offset[i - 1];
}
vector<int> factor_list(factor_offset[N + 1]);
vector<int> factor_cur = factor_offset;
for (int i = 2; i <= N; ++i) {
int temp = i;
while (temp > 1) {
int p = spf[temp];
factor_list[factor_cur[i]++] = p;
while (temp % p == 0) temp /= p;
}
}
auto get_factors = [&](int i) {
return std::span<const int>(factor_list.data() + factor_offset[i], factor_list.data() + factor_offset[i + 1]);
};
// Primes list and mapping
vector<int> primes;
vector<int> prime_to_id(N + 1, -1);
vector<bool> is_prime(N + 1, true);
is_prime[0] = is_prime[1] = false;
int p_id = 0;
for (int p = 2; p <= N; ++p) {
if (is_prime[p]) {
primes.push_back(p);
prime_to_id[p] = p_id++;
for (int j = 2 * p; j <= N; j += p) {
is_prime[j] = false;
}
}
}
int num_primes = primes.size();
vector<int> dsu_offset(num_primes + 1, 0);
for (int i = 0; i < num_primes; ++i) {
int p = primes[i];
int size = N / p + 2;
dsu_offset[i + 1] = dsu_offset[i] + size;
}
int total_dsu_size = dsu_offset[num_primes];
vector<int> dsu_parent(total_dsu_size);
for (int i = 0; i < num_primes; ++i) {
int offset = dsu_offset[i];
int size = N / primes[i] + 2;
for (int j = 0; j < size; ++j) {
dsu_parent[offset + j] = j;
}
}
auto find = [&](int p_id, int i) -> int {
int offset = dsu_offset[p_id];
int curr = i;
while (dsu_parent[offset + curr] != curr) {
curr = dsu_parent[offset + curr];
}
int temp = i;
while (temp != curr) {
int next = dsu_parent[offset + temp];
dsu_parent[offset + temp] = curr;
temp = next;
}
return curr;
};
auto merge = [&](int p_id, int i, int j) {
int root_i = find(p_id, i);
int root_j = find(p_id, j);
if (root_i != root_j) {
dsu_parent[dsu_offset[p_id] + root_i] = root_j;
}
};
vector<bool> visited(N + 1, false);
int ans = 0;
int min_unvisited = 1;
while (min_unvisited <= N) {
if (visited[min_unvisited]) {
min_unvisited++;
continue;
}
ans++;
int k = min_unvisited;
visited[k] = true;
for (int p : get_factors(k)) {
int pid = prime_to_id[p];
merge(pid, k / p, k / p + 1);
}
while (true) {
int next_m = N + 1;
if (k == 1) {
while (min_unvisited <= N && visited[min_unvisited]) {
min_unvisited++;
}
if (min_unvisited <= N) {
next_m = min_unvisited;
}
} else {
for (int p : get_factors(k)) {
int pid = prime_to_id[p];
int start_idx = k / p + 1;
int next_idx = find(pid, start_idx);
long long val = (long long)next_idx * p;
if (val <= N) {
next_m = min(next_m, (int)val);
}
}
}
if (next_m > N) {
break;
}
k = next_m;
visited[k] = true;
for (int p : get_factors(k)) {
int pid = prime_to_id[p];
merge(pid, k / p, k / p + 1);
}
}
}
cout << ans << "\n";
return 0;
}
This editorial was generated by gemini-3.5-flash-high.
posted:
last update: