Christmas balls. Fuori tempo sull'ultimo testcase

Ciao a tutti, avevo bisogno di aiuto per risolvere christmas balls, perché riesco a risolvere tutti i testcase tranne l’ultimo, che va in exe timed out.
Questa era la soluzione originale che avevo pensato:

// NOTE: it is recommended to use this even if you don't understand the
// following code.

#include <bits/stdc++.h>

using namespace std;

// input data
int N;
vector<int> C;
vector<int> P;

// aggiunge gli elementi della seconda mappa alla prima
void add(unordered_map<int, int> &x, unordered_map<int, int> &y)
{
    for (auto i = y.begin(); i != y.end(); i++)
    {
        x[i->first] += y[i->first];
    }
}

unordered_map<int, vector<int>> tree;

unordered_map<int, int> post(int i);

// contiene il punteggio massimo globale
int ans = 1;

int main()
{
    cin >> N;
    C.resize(N);
    P.resize(N);
    for (int i = 0; i < N; i++)
    {
        // node i has color C[i]
        cin >> C[i];
    }

    for (int i = 1; i < N; i++)
    {
        // there is a link P[i] -> i
        // note: P[0] is undefined!
        cin >> P[i];
        tree[P[i]].push_back(i);
    }

    // insert your code here

    post(0);

    cout << ans << endl; // print the result
    return 0;
}

// il concetto base della mia soluzione è: 
// utilizzo una mappa per contare le occorrenze dei colori e di conseguenza poter 
// trovare la frequenza massima e la frequenza della frequenza massima scorrendo la mappa.
// per ogni padre prendo le mappe dei figli e le unisco in una mappa nuova
// dopodiché trovo il risultato nella mappa nuova, e se è meglio del risultato massimo, aggiorno
unordered_map<int, int> post(int i)
{
    unordered_map<int, int> m;
    
    if (tree[i].size() == 0)
    {
        // e' una foglia
        m[C[i]] = 1;
        return m;
    }

    // recupero i figli di questo nodo
    vector<int> neigh = tree[i];

    // visito ogni figlio e unisco insieme tutte le mappe
    for (int next : neigh)
    {
        unordered_map<int, int> figlio;
        
        figlio = post(next);
        add(m, figlio);
    }

    // aggiorno la mappa mettendo dentro il colore di questo nodo
    m[C[i]]++;
    
    // cerco la frequenza della frequenza massima
    int high = 0;
    int cont = 0;
    for(auto i = m.begin(); i!=m.end(); i++)
    {
        int freq = i->second;
        if(freq > high) 
        {
            // se trovo una nuova frequenza massima, devo ricominciare a contare da 0
            high = freq;
            cont = 0;
        }

        if(freq == high)
        {
            cont++;
        }
    }

    // utilizzo una variabile globale per semplicità
    // se il punteggio di questo nodo è maggiore del massimo precedente, aggiorno
    if(cont > ans) ans = cont;

    // ritorno la mappa così il padre di questo nodo potrà usarla per calcolare il suo punteggio
    return m;
}

Il problema di questa soluzione erano, ovviamente oltre all’ultimo testcase, tutti quelli con l’albero formato da una lista e con N abbastanza grande (in parole povere subtask 3).
Per risolvere ho semplicemente aggiunto un controllo sull’input che mi dice se l’albero è in realtà una lista, e nel caso lo fosse, utilizzo una funzione che più semplice che riesce a sfruttare le proprietà che ha una lista in questo problema.

int main()
{
    cin >> N;
    C.resize(N);
    P.resize(N);
    for (int i = 0; i < N; i++)
    {
        // node i has color C[i]
        cin >> C[i];
    }

    // se true, indica che l'albero e' in realtà una linked list
    bool notALine = false;

    for (int i = 1; i < N; i++)
    {
        // there is a link P[i] -> i
        // note: P[0] is undefined!
        cin >> P[i];

        if(P[i] != i-1) notALine = true;

        tree[P[i]].push_back(i);
    }

    // insert your code here

    if(notALine)
    {
        post(0);
    }
    else 
    {
        unordered_map<int, int > m;
        postLine(N-1, m, 0, 0);
    }

    cout << ans << endl; // print the result
    return 0;
}


// questa funzione è pensata unicamente per quando l'albero è in realtà una lista.
// il concetto di base è che ad ogni nodo, il conteggio dei colori e delle frequenze è uguale a quello
// dell'unico figlio + il colore del padre.
// ecco quindi che non serve più iterare sulla mappa delle frequenze ogni volta, ma basta semplicemente
// aggiornare il punteggio di nodo in nodo in tempo costante
void postLine(int i, unordered_map<int, int>& c, int highest, int cont)
{
    // aggiorno la mappa con il colore di questo nodo
    c[C[i]]++;
    
    // se utilizzando questo nodo raggiungo una nuova frequenza massima, devo ricominciare a contare
    if(c[C[i]] > highest ) highest = c[C[i]], cont = 0;
    // se utilizzando questo nodo raggiungo la frequenza massima con un altro colore, incremento il punteggio
    if(c[C[i]] == highest ) cont++;

    // aggiorno il punteggio massimo
    if(cont > ans) ans = cont; 
    
    // se non siamo alla radice, passo i miei risultati al padre, che può aggiornare il suo punteggio
    if(i > 0)
    {
        postLine(i-1, c, highest, cont);
    }
}

Adesso riesco a risolvere tutti i testcase, tranne l’ultimo. Ho fatto qualche esperimento e ho capito che l’albero nell’ultimo testcase è un binary tree con buona parte dei nodi con un figlio unico (non so se può essere utile questo dettaglio).
In ogni caso non ho idea di quale sia il problema. Ho ipotizzato che ci potesse essere un bottleneck in due parti principali: le chiamate ricorsive (molto probabile) o nell’iterare sulla mappa (non molto probabile, ho anche pensato a qualche soluzione per ottimizzare, ma non ho avuto successo). Nel caso delle chiamate ricorsive presumo sia necessario fare un po’ di pruning, ma non ho idea di come farlo. Qualche idea? Oppure magari sto andando completamente fuori strada?

Ciao, la tua soluzione ha complessità \mathcal{O}(N^2), per risolvere il problema devi trovare una soluzione con una complessità migliore.

I due problemi nel tuo codice sono:

  1. quando unisci due unordered_map nella funzione add;
  2. quando iteri su m alla fine della funzione post.

Queste due operazioni hanno complessità \mathcal{O}(N), per cui eseguirle su ogni nodo porta la complessità totale a \mathcal{O}(N^2).

Il primo punto si può risolvere facilmente con una tecnica chiamata small-to-large mentre per il secondo punto serve un po’ di ragionamento in più.

Ho guardato la mia soluzione ed effettivamente ho separato la gestione di un nodo con un solo figlio da quello dei nodi con più figli.
Inoltre per risparmiare tempo e spazio di memoria nel caso di nodi con più figli non ho usato oggetti ma puntatori a oggetti di tipo unordered_map<int, int>.
Creo i puntatori solo sulle foglie:
su un nodo padre con solo figlio assegno al puntatore di quel nodo quello del figlio e lo aggiorno
su un nodo padre con più figli assegno al puntatore del nodo quello del figlio più pesante e …
e alla fine deleto i puntatori degli altri figli.

Grazie per i consigli. Per quanto riguarda lo small-to-large avevo provato ad implementarlo e ho notato leggeri miglioramenti in alcuni test-case, ma senza speranza sull’ultimo.
Per il secondo punto, proverò a scervellarmi un po’ ma ora come ora non ho idee valide.
Avevo pensato che se potessi ordinare le mappe per valore e non per chiave sarebbe stato facile, ma non si può fare. Ogni altro metodo che conosco e che mi viene in mente per portare le frequenze massime in cima alla mappa richiede prima di scorrerla tutta.

Grazie per aver condiviso la tecnica che hai usato per risolvere il problema. Anche io avevo provato a gestire in modo i nodi con un figlio da quelli con più di uno, eliminando nei primi la necessità di scorrere su tutta la mappa per aggiornare il massimo, solo che avevo fatto ciò in modo simile a quello che ho scritto sopra.
Proverò a riflettere sulla questione puntatori e vedrò se mi si accende una lampadina

Non puoi ordinare le mappe sia per chiave che per valore ma quello che puoi fare è usare due mappe, una ordinata per chiave e una ordinata per valore

Alla fine sono riuscito a risolvere il problema.
Utilizzando la tecnica small-to-large, suggerita da bortoz, ho risparmiato un sacco di tempo evitando di iterare su mappe grandissime per aggiungere elementi a mappe con 1 solo elemento.
Per quanto riguarda come trovare il punteggio massimo dopo aver fatto il merge delle mappe dei figli, beh, direttamente nella funzione add faccio un controllo semplicissimo per verificare se ho trovato una nuova frequenza massima o se devo aggiornare il punteggio.
Questo però dava problemi se implementato insieme allo small-to-large, e quindi ho poi utilizzato delle variabili che tenevano conto per ogni nodo il punteggio massimo, e poi, passando queste variabili al nodo padre, potevo ottenere il risultato ottimale del figlio e vedere se durante il merge potevo ottenere un risultato migliore.

#include <bits/stdc++.h>

using namespace std;

int N;
vector<int> C;
vector<int> P;

unordered_map<int, vector<int>> tree;
int ans = 1;

// aggiunge gli elementi della seconda mappa alla prima
// e allo stesso tempo controlla le frequenze massime
void add(unordered_map<int, int> &x, unordered_map<int, int> &y, int &mx, int &cont)
{
    for (auto i = y.begin(); i != y.end(); i++)
    {
        x[i->first] += y[i->first];

        // mx tiene conto della frequenza massima
        // cont invece conta le occorrenze della frequenza massima
        if (x[i->first] > mx)
        {
            // nuova frequenza massima trovata: riparto a contare
            mx = x[i->first];
            cont = 1;
        }
        else if (x[i->first] == mx)
        {
            cont++;
        }
    }
}

unordered_map<int, int> post(int i, pair<int, int> &pr)
{
    // mappa delle occorrenze dei colori se decido di prendere questo nodo
    unordered_map<int, int> m;

    if (tree[i].size() == 0)
    {
        // foglia
        m[C[i]] = 1;
        pr.first = 1;
        pr.second = 1;
        return m;
    }

    m[C[i]]++; // aggiungo il colore di questo nodo alla mappa

    int mx = 1;   // frequenza massima di questo nodo
    int cont = 1; // quante volte occorre la frequenza massima a questo nodo

    // figli di questo nodo
    vector<int> neigh = tree[i];

    for (int next : neigh)
    {
        unordered_map<int, int> figlio;
        // questo pair verrà modificato dentro la chiamata ricorsiva
        // e conterrà la frequenza massima e le occorrenze relative
        // al figlio che stiamo visitando

        pair<int, int> p;

        figlio = post(next, p);

        // se la mappa dei colori del figlio è più grande della mappa di questo nodo,
        // conviene semplicemente invertirle, così da non dover iterare inutilmente su
        // frequenze che non devono essere aggiornate

        if (figlio.size() > m.size())
        {
            swap(m, figlio);
            // dato che mx e cont sono variabili relative alla mappa di questo nodo,
            // se devo fare lo swap delle mappe, devo cambiare anche queste due variabili,
            // che assumono il punteggio calcolato dalla chiamata ricorsiva del figlio
            mx = p.first;
            cont = p.second;
        }

        add(m, figlio, mx, cont);
    }

    // aggiorno il risultato. Utilizzo una variabile globale per comodità
    if (cont > ans)
        ans = cont;


    // modifico il parametro alla fine, così il padre di questo nodo,
    // che ha chiamato ricorsivamente questa funzione, può utilizzare
    // i risultati calcolati qui dentro per semplificare la ricerca

    pr.first = mx;       // frequenza massima a questo nodo
    pr.second = cont; // occorrenze della frequenza massima

    return m;
}

int main()
{
    cin >> N;
    C.resize(N);
    P.resize(N);
    for (int i = 0; i < N; i++)
    {
        cin >> C[i];
    }

    for (int i = 1; i < N; i++)
    {
        cin >> P[i];
        tree[P[i]].push_back(i);
    }

    pair<int, int> pr;
    post(0, pr);

    cout << ans << endl;
    return 0;
}