SPC2025Q6 - Editorial

PROBLEM LINK:

Practice
Contest: Division 1
Contest: Division 2
Contest: Division 3
Contest: Division 4

Author:
Tester: apoorv_me
Editorialist: iceknight1093

DIFFICULTY:

Easy

PREREQUISITES:

Sorting

PROBLEM:

There are N wells, the i-th at (X_i, Y_i).
Every second, channels extend outward in the four cardinal directions from each well.
Two wells are considered connected if their channels meet.
Find the first time at which all the wells are connected, either directly or indirectly.

EXPLANATION:

Suppose we had just two wells, at (X_1, Y_1) and (X_2, Y_2). Then, they’d have to be directly connected to each other in the end.
To decide when this will happen, we need to analyze some cases:

  1. If X_1 = X_2, then this will happen at time \text{ceil}\left(\frac{|Y_1 - Y_2|} 2\right), since their channels will essentially grow towards each other, and meet at the midpoint.
  2. Similarly, if Y_1 = Y_2, the time required is \text{ceil}\left(\frac{|X_1 - X_2|} 2\right).
  3. Finally, if X_1\neq X_2 and Y_1 \neq Y_2, their channels will meet at time \max(|X_1 - X_2|, |Y_1 - Y_2|), since the horizontal channel of one must meet the vertical channel of another.

When there are more wells, we can still apply the logic above to each pair of wells to figure out when they’ll be directly connected to each other.

Let t_{i, j} be the time at which i and j become directly connected.

We now have the following straightforward algorithm:

  1. Sort all the t_{i, j} values in ascending order, and process them in this order.
  2. Start with all the wells being their own components.
  3. When looking at t_{i, j}, merge the components of i and j if they’re different, since i and j are now directly connected.
  4. After a merge, if all wells are in the same component, this t_{i, j} is our answer.
    This can be tracked by, for example, checking if the number of merges done equals N-1.

Generating the t_{i, j} values takes quadratic time, and sorting them takes \mathcal{O}(N^2 \log N) time.
Thereafter, the merging can really be done in any way: you can use a data structure like a DSU to do it quickly, or even do it naively in linear time since merges can only happen N-1 times in total anyway.


Alternately, observe that the described process is exactly what we get when running Kruskal’s algorithm on the weighted graph whose weights are the t_{i, j} values; and the result is the MST of this graph.
We’re technically looking for the minimum bottleneck spanning tree (MBST) of the graph, but an MST is also an MBST so any MST algorithm will work: in particular, Prim’s algorithm can be implemented to run in \mathcal{O}(N^2) time by performing each step in linear time.
This saves a \log N factor from the solution, if really necessary.

TIME COMPLEXITY:

\mathcal{O}(N^2 \log N) per testcase.

CODE:

Author's code (C++)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF (int)1e18

mt19937_64 RNG(chrono::steady_clock::now().time_since_epoch().count());

struct ufds{
    vector <int> root, sz;
    int n;
 
    void init(int nn){
        n = nn;
        root.resize(n + 1);
        sz.resize(n + 1, 1);
        for (int i = 1; i <= n; i++) root[i] = i;
    }
 
    int find(int x){
        if (root[x] == x) return x;
        return root[x] = find(root[x]);
    }
 
    bool unite(int x, int y){
        x = find(x); y = find(y);
        if (x == y) return false;
 
        if (sz[y] > sz[x]) swap(x, y);
        sz[x] += sz[y];
        root[y] = x;
        return true;
    }
};

void Solve() 
{
    int n; cin >> n;
    
    vector <int> x(n), y(n);
    for (int i = 0; i < n; i++){
        cin >> x[i] >> y[i];
    }
    
    vector <array<int, 3>> a;
    for (int i = 0; i < n; i++){
        for (int j = i + 1; j < n; j++){
            int t;
            if (x[i] == x[j] || y[i] == y[j]){
                t = (abs(x[i] - x[j] + y[i] - y[j]) + 1) / 2;
            } else {
                t = max(abs(x[i] - x[j]), abs(y[i] - y[j]));
            }
            
            a.push_back({t, i, j});
        }
    }
    
    sort(a.begin(), a.end());
    
    ufds uf; uf.init(n);
    int comps = n;
    
    if (n == 1){
        cout << 0 << "\n";
        return;
    }
    
    for (auto pi : a){
        int t = pi[0];
        int x = pi[1];
        int y = pi[2];
        if (uf.unite(x + 1, y + 1)){
            comps--;
        }
        
        if (comps == 1){
            cout << t << "\n";
            return;
        }
    }
}

int32_t main() 
{
    auto begin = std::chrono::high_resolution_clock::now();
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int t = 1;
    // freopen("in",  "r", stdin);
    // freopen("out", "w", stdout);
    
    cin >> t;
    for(int i = 1; i <= t; i++) 
    {
        //cout << "Case #" << i << ": ";
        Solve();
    }
    auto end = std::chrono::high_resolution_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
    cerr << "Time measured: " << elapsed.count() * 1e-9 << " seconds.\n"; 
    return 0;
}
Tester's code (C++)
#include<bits/stdc++.h>
using namespace std;

#ifdef LOCAL
#include "../debug.h"
#else
#define dbg(...)
#endif

#ifndef LOCAL
struct input_checker {
    string buffer;
    int pos;

    const string all = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    const string number = "0123456789";
    const string upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const string lower = "abcdefghijklmnopqrstuvwxyz";

    input_checker() {
        pos = 0;
        while (true) {
            int c = cin.get();
            if (c == -1) {
                break;
            }
            buffer.push_back((char) c);
        }
    }

    int nextDelimiter() {
        int now = pos;
        while (now < (int) buffer.size() && buffer[now] != ' ' && buffer[now] != '\n') {
            now++;
        }
        return now;
    }

    string readOne() {
        assert(pos < (int) buffer.size());
        int nxt = nextDelimiter();
        string res;
        while (pos < nxt) {
            res += buffer[pos];
            pos++;
        }
        return res;
    }

    string readString(int minl, int maxl, const string &pattern = "") {
        assert(minl <= maxl);
        string res = readOne();
        assert(minl <= (int) res.size());
        assert((int) res.size() <= maxl);
        for (int i = 0; i < (int) res.size(); i++) {
            assert(pattern.empty() || pattern.find(res[i]) != string::npos);
        }
        return res;
    }

    int readInt(int minv, int maxv) {
        assert(minv <= maxv);
        int res = stoi(readOne());
        assert(minv <= res);
        assert(res <= maxv);
        return res;
    }

    long long readLong(long long minv, long long maxv) {
        assert(minv <= maxv);
        long long res = stoll(readOne());
        assert(minv <= res);
        assert(res <= maxv);
        return res;
    }

    auto readInts(int n, int minv, int maxv) {
        assert(n >= 0);
        vector<int> v(n);
        for (int i = 0; i < n; ++i) {
            v[i] = readInt(minv, maxv);
            if (i+1 < n) readSpace();
        }
        return v;
    }

    auto readLongs(int n, long long minv, long long maxv) {
        assert(n >= 0);
        vector<long long> v(n);
        for (int i = 0; i < n; ++i) {
            v[i] = readLong(minv, maxv);
            if (i+1 < n) readSpace();
        }
        return v;
    }

    void readSpace() {
        assert((int) buffer.size() > pos);
        assert(buffer[pos] == ' ');
        pos++;
    }

    void readEoln() {
        assert((int) buffer.size() > pos);
        assert(buffer[pos] == '\n');
        pos++;
    }

    void readEof() {
        assert((int) buffer.size() == pos);
    }
};
#else

struct input_checker {
    string buffer;
    int pos;

    const string all = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    const string number = "0123456789";
    const string upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const string lower = "abcdefghijklmnopqrstuvwxyz";

    input_checker() {
    }

    int nextDelimiter() {
        int now = pos;
        while (now < (int) buffer.size() && buffer[now] != ' ' && buffer[now] != '\n') {
            now++;
        }
        return now;
    }

    string readOne() {
        assert(pos < (int) buffer.size());
        int nxt = nextDelimiter();
        string res;
        while (pos < nxt) {
            res += buffer[pos];
            pos++;
        }
        return res;
    }

    string readString(int minl, int maxl, const string &pattern = "") {
      string X; cin >> X;
      return X;
    }

    int readInt(int minv, int maxv) {
        assert(minv <= maxv);
        int res;  cin >> res;
        assert(minv <= res);
        assert(res <= maxv);
        return res;
    }

    long long readLong(long long minv, long long maxv) {
        assert(minv <= maxv);
        long long res;  cin >> res;
        assert(minv <= res);
        assert(res <= maxv);
        return res;
    }

    auto readInts(int n, int minv, int maxv) {
        assert(n >= 0);
        vector<int> v(n);
        for (int i = 0; i < n; ++i) {
            v[i] = readInt(minv, maxv);
            if (i+1 < n) readSpace();
        }
        return v;
    }

    auto readLongs(int n, long long minv, long long maxv) {
        assert(n >= 0);
        vector<long long> v(n);
        for (int i = 0; i < n; ++i) {
            v[i] = readLong(minv, maxv);
            if (i+1 < n) readSpace();
        }
        return v;
    }

    void readSpace() {
    }

    void readEoln() {
    }

    void readEof() {
    }
};
#endif


struct DSU{
  int N, cmp;
  vector<int> par, sz;
  DSU(int N_) : N(N_ + 1), cmp(N_), par(N_ + 1), sz(N_ + 1, 1) {
    iota(par.begin(), par.end(), 0);
  }

  int find(int node) {
    if(par[node] == node) {
      return node;
    }
    return par[node] = find(par[node]);
  }

  bool join(int u, int v) {
    u = find(u);
    v = find(v);
    if(u == v) {
      return false;
    }
    if(sz[u] > sz[v]) {
      swap(u, v);
    }

    sz[v] += sz[u];
    par[u] = v;
    --cmp;
    return true;
  }
};

int32_t main() {
  ios_base::sync_with_stdio(0);
  cin.tie(0);

  input_checker inp;
  int T = inp.readInt(1, (int)1e3), NN = 0; inp.readEoln();
  while(T-- > 0) {
    int N = inp.readInt(1, (int)2e3); inp.readEoln();
    vector<pair<int, int>> A(N);
    for(auto &[x, y]: A) {
      x = inp.readInt(1, (int)1e9); inp.readSpace();
      y = inp.readInt(1, (int)1e9); inp.readEoln();
    }

    vector<pair<int, int>> ed;  ed.reserve(N * (N - 1) / 2);
    for(int i = 0 ; i < N ; ++i) {
      for(int j = i + 1 ; j < N ; ++j) {
        ed.emplace_back(i, j);
      }
    }

    auto get_dist = [&](int i, int j) {
      if(A[i].second == A[j].second) {
        return (abs(A[i].first - A[j].first) + 1) / 2;
      }
      if(A[i].first == A[j].first) {
        return (abs(A[i].second - A[j].second) + 1) / 2;
      }
      return max(abs(A[i].first - A[j].first), abs(A[i].second - A[j].second));
    };

    sort(ed.begin(), ed.end(), [&](auto &x, auto &y) {
      return get_dist(x.first, x.second) < get_dist(y.first, y.second);
    });

    DSU ds(N);
    int ans = 0;
    for(auto &[x, y]: ed) {
      if(ds.join(x, y)) {
        ans = max(ans, get_dist(x, y));
      }
    }
    cout << ans << '\n';
  }
  assert(NN <= (int)5e5);
  inp.readEof();
  
  return 0;
}

Editorialist's code (Python3)
for _ in range(int(input())):
    n = int(input())
    wells = []
    for i in range(n):
        x, y = map(int, input().split())
        wells.append((x, y))
    
    times = []
    for i in range(n):
        for j in range(i+1, n):
            x1, y1 = wells[i]
            x2, y2 = wells[j]
            
            if x1 == x2 or y1 == y2: times.append(((abs(x1 - x2) + abs(y1 - y2) + 1) // 2, i, j))
            else: times.append((max(abs(x1 - x2), abs(y1 - y2)), i, j))
    times.sort()
    comp = list(range(n))
    merges = 0
    for t, i, j in times:
        if comp[i] == comp[j]: continue
        x, y = comp[i], comp[j]
        merges += 1
        for ii in range(n):
            if comp[ii] == x: comp[ii] = y
        if merges == n-1:
            print(t)
            break
    else:
        print(0)

why does this solution give TLE: 1114224956
It is also O(n^2log(n))

It’s really hard to understand what “touch” means in the problem description.
p.s. My O(n^2logn + n^2) solution also got TLE.

1 Like

i was also getting tle , so i just check for the 30 closest pair for any point , after this i ignored the rest of the points and this gave me AC.

https://www.codechef.com/viewsolution/1114312968

avoid sorting vector of vector. Maintaining order is a costly operation. I have used pair in your code and got AC

The directly or indirectly is missing in the original problem statement.

I now feel i should have broken the loop if the all vertex gets connected. That makes my solution almost the same as the tester’s .
But still just having an extra o(n^2*inv. ack) (at max) with that solution and getting a tle
says that maybe a 2sec time limit would have been better :sweat_smile:

Thanks. This is something new i learnt

1 Like

can some explain me why the answer for this input
2
1 1
3 7
is 6 not 4
reason → we can move 1st point as ( (1,1) → (2,1) → (3,1) → 3,2) → (3,3) ),
and 2nd point as ( (3,7) → (3,6) → (3,5) → 3,4) → (3,3) ) as mentioned in the question each second it expands in all four direction

This can be solved in O(N2) using Prim’s algorithm which results in significantly simpler code without needing to sort. Check this .

I used BS and it worked

What a horrible choice of words in the problem statement. It took me five tries to understand what you meant @iceknight1093