Problema TALCA de CodeChef

¡Buenos días, tardes o noches!
Acabo de resolver este problema y quiero saber si se puede mejorar la complejidad a NlogN o bien si alguien tiene ideas más lindas para la complejidad que me quedó a mí.
Mi solución corre en O(N logN) para la construcción y O(Q(log^2N)) para responder las queries.

Mi idea fue la siguiente
  1. Armar la estructura para obtener el LCA con una raíz arbitraria, la versión que usa Binary Lifting.
  2. Por cada query (r, u, v):
    1. Obtener el LCA c de (u, v).
    2. Hacer una Búsqueda Binaria en el camino (u, c) y (v, c) que encuentre el primer nodo x tal que la distancia del siguiente nodo del camino a r sea mayor que la distancia de x a r.
    3. Cada rama va a tener un candidato y me quedo con el que esté más cerca de r.
  3. Rogar que no de TLE. Pero bueno, dio AC gracias a los 3 segundos de tiempo.
Y el código
#include<bits/stdc++.h>
#define sp system("pause")
#define forn(i, n) for(int i = 0; i < n; i++)
using namespace std;
const int MAXN = 2E5 + 5, LOG = 19, RAIZ = 0;
struct lcaTable{
    int level[MAXN], sparse[MAXN][LOG];
    lcaTable(){
        forn(i, MAXN) forn(j, LOG) sparse[i][j] = RAIZ;
        level[RAIZ] = -1;
    }
    void insert(int u, int p){
        level[u] = level[p] + 1;
        sparse[u][0] = p;
        for(int i = 1; i < LOG; i++) sparse[u][i] = sparse[sparse[u][i - 1]][i - 1];
    }
    int lca(int u, int v){
        if(level[u] < level[v]) swap(u, v);
        int i;
        for(i = 0; level[u] - (1<<i) > level[v]; i++);
        for(; level[u] > level[v]; i--){
            if(level[u] - (1<<i) < level[v]) continue;
            u = sparse[u][i];
        }
        if(u == v) return u;
        for(i = 0; sparse[u][i] != sparse[v][i]; i++);
        for(; i >= 0; i--){
            if(sparse[u][i] == sparse[v][i]) continue;
            u = sparse[u][i];
            v = sparse[v][i];
        }
        return sparse[u][0];
    }
    int dist(int u, int v){
        int c = lca(u, v);
        return level[u] + level[v] - level[c] * 2;
    }
    int climb(int u, int k){
        while(k){
            int i = log2(k);
            u = sparse[u][i];
            k -= (1<<i);
        }
        return u;
    }
    int nearestNodeInPath(int u, int v, int r){ //Returns the nearest node to r in the path from u to v
        //Is mandatory that 'v' is ancestor of 'u'
        int d = level[u] - level[v];
        int bl = -1, br = d, bh;
        while(br - bl > 1){
            bh = (bl + br) / 2;
            int a = climb(u, bh), b = climb(u, bh + 1);
            a = dist(a, r), b = dist(b, r);
            if(a > b) bl = bh;
            else br = bh;
        }
        int ret = climb(u, br);
        return ret;
    }
    int query(int r, int u, int v){
        int c = lca(u, v);
        int a = nearestNodeInPath(u, c, r);
        int b = nearestNodeInPath(v, c, r);
        if(dist(a, r) < dist(b, r)) return a;
        return b;
    }
} lca;
vector<int> adj[MAXN];
void dfs(int u, int p){
    lca.insert(u, p);
    for(int v : adj[u]){
        if(v == p) continue;
        dfs(v, u);
    }
}
int main(){
    int n;
    cin>>n;
    forn(i, n - 1){
        int u, v;
        cin>>u>>v;
        u--, v--;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    dfs(0, 0);
    int q;
    cin>>q;
    while(q--){
        int r, u, v;
        cin>>r>>u>>v;
        r--, u--, v--;
        cout<<(lca.query(r, u, v) + 1)<<"\n";
    }
    return 0;
}

Leo sus ideas, gracias :3

3 Me gusta

Hola!

Está buena la idea. Se puede hacer algo parecido analizando un poco más el dibujo, para evitar el log^2 y tener un solo log en las queries:

Idea

Primero efectivamente tomamos el LCA c de u y v. Ahora analizamos un par de casos:

  1. Si r no está en el subárbol con raíz en c, entonces c es la respuesta.
  2. Si r=c entonces la respuesta también es c=r
  3. Si r está en el subárbol con raíz en un hijo h de c, tal que h está en el camino desde c hasta u o hasta v (o sea que hay a lo sumo dos posibles h en estas condiciones), la respuesta es directamente LCA(r, v) si h está en el camino a v, y análogamente es LCA(r, u) si h está en el camino hacia u.
  4. Si no se dieron las anteriores, la respuesta también es c.

Todo esto se puede verificar si uno tiene cuidado con O(1) queries de LCA normales, así que usando un LCA con sparse table se tiene precómputo O(N \lg N) y queries en O(1).

La más delicada de chequear es 3, pero para eso se puede verificar la profundidad de los nodos LCA(r, u) y LCA(r, v) a ver si son más profundos que c o no.

1 me gusta

Gracias por la explicación detallada :3
Pero yendo más a la implementación, habría solo dos casos ¿no es así?

  1. Cuando r no está en el subárbol con raíz en c, entonces la respuesta es c.
  2. Caso contrario, se toma el LCA(r, u) y LCA(r, v), si alguno es distinto de c, entonces esa es la respuesta, caso contrario, la respuesta es c.
    Digo esto porque r podría estar en cualquier lugar del subárbol con raíz en c y si subimos, eventualmente toca el camino entre c y u o c y v.
1 me gusta

Lo pensé un poco y obtuve el AC con 0.37s. Luego leyendo vi que caí en la solución de 3 LCAs por query, usando Binary Lifting, que describía al final @santo al final de su idea.

Lo que planteas igual es más sencillo.

Más o menos lo que hice

Yo saqué LCA(u,v), LCA(r,u) y LCA(r,v).
Para facilitarme los checks, digamos que dep(v) > dep(u), donde dep(x) es la profundidad del nodo en el árbol enraízado en un nodo arbitrario que construí.

Así puedo ir chequeando:

  • Si LCA(r,v) = v, en dicho caso la respuesta es v.
  • De lo contrario, si LCA(u,v) \neq u, verifico si LCA(r,u) = u, en dicho caso la respuesta es u.
  • De lo contrario, si tanto dep(LCA(r,u)) como dep(LCA(r,v)) son menores o iguales a dep(LCA(u,v)), en dicho caso la respuesta es LCA(u,v), porque cubre el caso de estar afuera del subárbol que decís.
  • Si nada de lo anterior se cumple, la respuesta es LCA(r,u) o LCA(r,v), sea aquel el cual su dep% es mayor a dep(LCA(u,v)).

Medio complicado igual lo mío. Igual dejo el código:

Código 1
#include <bits/stdc++.h>
    
//#pragma GCC optimize("Ofast,unroll-loops")
//#pragma GCC target("avx,avx2,fma")
    
#define forn(i,n) for(int i = 0; i < int(n); i++)
#define forsn(i,s,n) for(int i = int(s); i < int(n); i++)
#define dforn(i,n) for (int i = int(n)-1; i >= 0; i--)
#define dforsn(i,s,n) for(int i = int(n)-1; i >= int(s); i--)
#define dbg(x) cerr << #x << " = " << x << endl;
#define all(c) (c).begin(),(c).end()
#define pb push_back
#define fst first
#define snd second
#define FAST_IO ios::sync_with_stdio(false);cin.tie(nullptr);
    
using namespace std;
typedef vector<int> vi;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> ii;

const int MAXN = 2e5+5;
const int MAXK = 25;

vi G[MAXN];
int P[MAXK][MAXN], dep[MAXN];
bitset<MAXN> done;

void dfs (int st, int lvl) {
    done[st] = true;
    dep[st] = lvl;

    for (auto &i : G[st]) 
        if (!done[i]) 
            P[0][i] = st, dfs(i,lvl+1);
}

int lca (int a, int b) {
    if (dep[a] > dep[b]) swap(a,b);

    int dif = dep[b]-dep[a];
    forn(i,MAXK) if (dif&(1<<i)) b = P[i][b];

    if (a == b) return a;
    dforn(i,MAXK)
        if (P[i][a] != P[i][b])
            a = P[i][a], b = P[i][b];

    return P[0][a];
}

int main() {
    int n; scanf("%d",&n);

    forn(i,n-1) {
        int u,v; scanf("%d %d",&u,&v); u--, v--;
        G[u].pb(v), G[v].pb(u);
    }

    P[0][0] = 0;
    dfs(0,0);

    forsn(k,1,MAXK) forn(i,n) P[k][i] = P[k-1][P[k-1][i]];

    int q; scanf("%d",&q);
    forn(i,q) {
        int r,u,v; scanf("%d %d %d",&r,&u,&v); r--, u--, v--;

        if (dep[u] > dep[v]) swap(u,v);

        int uv = lca(u,v), ru = lca(r,u), rv = lca(r,v);
        
        int rta = -1;

        if (rv == v) rta = v; // si es su hijo. Pruebo 1º con v (el más profundo, por el caso que lca(u,v) == u || lca(u,v) == v)
        else if (uv != u && ru == u) rta = u; // el otro caso que es hijo, si están en ramas distintas
        else if (dep[ru] <= dep[uv] && dep[rv] <= dep[uv]) rta = uv; // para esté punto descarté lo de abajo. pruebo ahora si está arriba: en dicho caso es lca(u,v)
        else rta = (dep[ru] > dep[uv] ? ru : rv);

        printf("%d\n",rta+1);
    }

    return 0;
}

Aunque fácilmente lo pude editar para probar lo que decís. Cambio sólo la parte de los ifs al recibir las queries por:

Código ifs a cambiar
if (dep[ru] <= dep[uv] && dep[rv] <= dep[uv]) rta = uv; // si está fuera del subárbol
else rta = (dep[ru] > dep[uv] ? ru : rv);

Y también da AC, más o menos en el mismo tiempo :smiley: (pues entiendo que lo que más cuesta son los LCAs).

Agradezco la propuesta de problema! Está bueno para pensarlo.

1 me gusta