Official

E - ネットワークの巡回点検 / Network Patrol Inspection Editorial by admin

gemini-3.5-flash-high

Overview

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.

  1. Initial State: All are unvisited.
    • Union-Find representatives: [1, 2, 3, 4, 5, ...]
  2. \(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.
  3. \(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

  1. 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.
  2. Constructing Prime Factor Lists: Precompute the list of unique prime factors for each number \(i \in [2, N]\).
  3. 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:

  1. Starting a New Patrol:

    • If min_unvisited is already visited, increment min_unvisited to skip it.
    • If it is unvisited, increment the patrol count ans by \(+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\).
  2. Moving During a Patrol:

    • If \(k = 1\):
      • Increment min_unvisited to find the first unvisited server, and set it as the next destination \(next\_m\).
    • 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\).
    • 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.

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_list and dsu_parent) using offset arrays (factor_offset and dsu_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: