Zadanie

Súhvezdia sú nudné. Existujú si na oblohe a veľa toho nenarozprávajú. Niektoré dvojice hviezd v súhvezdí sú spojené čiarou, vraj, aby ľuďom viac niečo pripomínali.

Sem-tam sa ale aj také súhvezdie potrebuje ponaťahovať. Hadonoš1 rád sníva. Rozmýšľa, ako rôzne by sa mohol ponaťahovať, keby bol nejakým iným súhvezdím. Vždy, keď sa mu takto prisní, že je iným súhvezdím, prudko sa zobudí a začne počítať, koľkými rôznymi spôsobmi by vedel svoje hviezdy usporiadať. Nemá ale príliš veľký mozog a tak sa mu snívajú iba samé spojité acyklické konfigurácie.

Nie je to ale iba tak. Nebyť súhvezdí, Kolumbus by nedoplával do Ameriky2, Cook by zablúdil už pri Dubline a Krtko by nevedel trafiť na správne sústredenie. Nemôžu sa teda naťahovať len tak, ako sa im zachce. Musia sa ľuďom javiť nezmenené.

Úloha

Hadonoš vám postupne popíše každý jeho sen. Každý sen je jedno súhvezdie, teda hviezdy a čiary, ktorými sú niektoré dvojice hviezd prepojené. Každé prisnené súhvezdie tvorí strom. Každá hviezda má svoje číslo, hviezdy sú teda rozoznateľné.

Hadonoš po zobudení potrebuje vyrátať, koľkými spôsobmi sa dajú v prisnenom súhvezdí preusporiadať hviezdy tak, aby platilo:

  • Hviezda číslo \(0\) nezmení svoju pozíciu
  • Množina pozícií na oblohe, ktoré boli obsadené hviezdami súhvezdia je pred usporiadaním rovnaká ako po ňom
  • Ak pred usporiadaním boli čiarou spojené hviezdy \(a\) a \(b\), tak sú spojené čiarou aj po usporiadaní

Každé súhvezdie teda tvorí strom zakorenený v \(0\).

Bez odpovede Hadonoš znovu nezaspí a na zajtrajšiu skúšku z astronómie príde unavený. Uspite ho!

Formát vstupu

Na prvom riadku vstupu sa nachádza číslo \(T\) – počet Hadonošovych snov. Platí \(1 \leq T \leq 10\). Nasleduje \(T\) popisov snov.

Popis sna začína riadkom s číslom \(n\) – počet hviezd v prisnenom súhvezdí. Platí \(3 \leq n \leq 10\,000\). Nasleduje \(n - 1\) riadkov. Na \(i\)-tom z nich sa nachádzajú čísla \(a_i\), \(b_i\) oddelené medzerou. Tie hovoria, že v prisnenom súhvezdí sú čiarou spojené hviezdy \(a_i\) a \(b_i\). Platí \(0 \leq a_i, b_i < n\).

Každé prisnené súhvezdie tvorí strom.

Formát výstupu

Pre \(i\)-ty sen vypíšte na \(i\)-ty riadok výstupu jedno číslo – počet rôznych usporiadaní hviezd \(i\)-teho prisneného súhvezdia spĺňajúcich vyššie popísané požiadavky. Keďže odpoveď môže byť veľmi veľká, vypisujte ju modulo \(10^9 + 7\).

Hodnotenie

Sú 4 sady vstupov. Platia v nich nasledovné obmedzenia:

Sada 1 2 3 4
\(3 \leq n \leq\) \(8\) \(20\) \(1\,000\) 10,000

Príklad

Input:

2
5
0 1
0 2
1 3
1 4
7
0 1
0 2
1 3
1 4
2 5
2 6

Output:

2
8

V prvom súhvezdí sú vyhovujúce dve možnosti. Prvou je nespraviť žiadnu zmenu, druhou je vymeniť vrcholy 3 a 4. V druhom súhvezdí je jednou z možností napríklad vymeniť koreňu ľavý a pravý podstrom a naviac vymeniť vrcholy 5 a 6.


  1. Nemýliť s hadonos.↩︎

  2. Možno…↩︎

Na vstupe sme mali strom popisujúci súhvezdie. Našou úlohou bolo spočítať, koľkými spôsobmi vieme preusporiadať hviezdy v súhvezdí, aby boli splnené isté podmienky. Konkrétnejšie, mali sme zrátať niečo ako koľko stromov je izomorfných so zadaným stromom tak, že koreňom je stále vrchol číslo 0. Presné podmienky si môžete pozrieť v zadaní.

Pozorovania

Zamyslime sa najskôr nad tým, kedy sa budú dať hviezdy preusporiadavať. Pozrime sa na koreň. Aby sme dostali iný strom spĺňajúci podmienky, môžeme zmeniť poradie tých podstromov koreňa, ktoré sú v nejakom zmysle rovnaké. Aby sme teda zachovali správne prepojenie vrcholov hranami, budeme môcť koreňu iba vymieňať rovnaké podstromy. Avšak, v rámci každého podstromu môžeme spraviť tiež nejaké preusporiadanie, ktoré štruktúru podstromu nezmení. Rekurzívne teda môžeme popísať vyhovujúce preusporiadania pre podstromy koreňa, a tak ďalej, až ku listom.

Ako ale povedať, či sú dva podstromy rovnaké?

Vzorové riešenie

Je množstvo možností, ktoré nám na riešenie tejto otázky napadnú. Či už je to porovnávanie počtu vrcholov, listov, alebo hĺbok, žiaden z týchto spôsobov nebude fungovať. Nájsť príklady dvoch rôznych stromov, pre ktoré tieto metriky budú tvrdiť, že sú rovnaké, nie je problém.

Riešenie je prekvapivo jednoduché. Dva podstromy sú zameniteľné práve vtedy, keď ich korene majú ako synov rovnaké podstromy. Nezáleží nám na poradí synov. Zameniteľné budú, pretože synov vieme vhodne preusporiadať a neporušíme pri tom žiadnu podmienku zo zadania.

Budeme teda podstromom priraďovať id. Dva podstomy budeme považovať za zameniteľné, ak majú rovnaké id. Tieto čísla budeme priraďovať rekurzívne od listov. Stačí nám na to jedno DFS. Listy budú mať id = 1. Keď sa rekurzívne zavoláme do všetkých synov, budú už mať priradené id. Tieto ich id si zaradom dáme do poľa. Dostaneme tak napríklad pole [1, 2, 4, 1, 1, 6]. Z toho chceme určiť id aktuálneho vrchola. Zistili sme už ale, že na poradí synov nezáleží. Aby naše pole teda určovalo id vrchola, usporiadame si ho. Následne sa pozrieme, či sme už niekedy boli vo vrchole s týmto usporiadaným poľom id-čiek synov. Ak áno, pridelíme aktuálnemu vrcholu rovnaké id ako danému vrcholu. Ak sme takého pole ešte nevideli, pridelíme nášmu vrcholu napríklad najmenšie ešte nepridelené id.

Ostáva nám ešte vyriešiť kombinatorickú časť úlohy. Nech ways[ID] hovorí, koľkými spôsobmi vieme preusporiadať podstrom s id = ID. Už vieme, že môžeme vymieňať synov s rovnakým id. Každého syna zároveň vieme preusporiadať. Konkrétne, syna s id = x vieme usporiadať ways[x] spôsobmi. Jednotlivé typy synov sú na sebe nezávislé, preto tieto počty medzi sebou vynásobíme. Označme cnt[ID] počet synov aktuálneho vrchola s id = ID. Ďalej označme \(S\) množinu id-čiek synov aktuálneho vrchola (teda je bez duplikátov). Dostaneme nasledový vzťah:

\[\text{ways}[\text{ID}] = \prod_{x \in S} \text{cnt}[x]! \cdot \text{ways}[x]^{\text{cnt}[x]}\]

Samozrejme, všetko modulujeme. Celkovým výsledkom potom bude \(\text{ways}[\text{id}_{\text{koreň}}]\).

Časová zložitosť bude kvôli usporiadavaniu v DFS \(O(n \log n)\) na test. Pamäť bude \(O(n)\).

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const ll MXN = 10111;
const ll MOD = 1000000007;
ll TC, n, a, b;
vector<vector<ll>> G;
map<vector<ll>, ll> ids_by_children; // Tymto namapujeme usporiadane pole id-ciek deti na id aktualneho vrchola
vector<ll> vertex_ids, ways, fact;

void calc_fact() {
    fact.clear();
    fact.push_back(1);
    for(ll i = 1; i < MXN; i++)
        fact.push_back((fact[i - 1] * i) % MOD);
}

// Umocnovanie q^x v O(log x) s modulovanim
ll expo(ll q, ll x) {
    if(x == 0)
        return 1LL;
    if(x == 1)
        return q;
    ll base = expo((q * q) % MOD, x / 2);
    if(x & 1LL)
        base = (base * q) % MOD;
    return base % MOD;
}

void dfs(ll u, ll p) {
    vector<ll> child_ids;
    map<ll, ll> cnt;
    for(int i = 0; i < G[u].size(); i++) {
        ll v = G[u][i];
        if(v != p) {
            dfs(v, u);
            child_ids.push_back(vertex_ids[v]);
            cnt[vertex_ids[v]]++;
        }
    }
    sort(child_ids.begin(), child_ids.end());
    bool done = true; // Ci sme uz pridelili id aktualnemu child_ids
    if(ids_by_children.find(child_ids) == ids_by_children.end()) {
        ids_by_children[child_ids] = ids_by_children.size(); // Pridelime sekvencne dalsie id
        done = false; // Ak sme uz davnejsie nepridelili id, tak este nemame zratane ways, tak si to poznacime
    }
    vertex_ids[u] = ids_by_children[child_ids];
    if(!done) {
        // Pouzijeme sucin zo vzoraku
        for (auto &x: cnt) {
            ways[vertex_ids[u]] *= (fact[x.second] * expo(ways[x.first], x.second)) % MOD;
            ways[vertex_ids[u]] %= MOD;
        }
    }
}

int main() {
    calc_fact(); // Predpocitame si faktorialy % MOD
    cin >> TC;
    while(TC--) {
        cin >> n;
        G.clear();
        G.resize(n);
        for(int i = 0; i < n - 1; i++) {
            cin >> a >> b;
            G[a].push_back(b);
            G[b].push_back(a);
        }
        ids_by_children.clear();
        ways.assign(n + 1, 1);
        vertex_ids.assign(n, -1);
        dfs(0, -1);
        cout << ways[vertex_ids[0]] << '\n'; // Koren je 0
    }

    return 0;
}
import sys

sys.setrecursionlimit(1000000)

MXN = 10111
MOD = 10**9 + 7
fact = [1]


# Logaritmicke umocnovanie q^x s modulovanim
# Da sa namiesto toho pouzit funkcia pow
def expo(q, x):
    if x == 0:
        return 1
    if x == 1:
        return q
    base = expo((q * q) % MOD, x // 2)
    if x % 2 == 1:
        base = (base * q) % MOD
    return base % MOD


def dfs(u, p, G, ids_by_children, vertex_ids, ways):
    child_ids = []
    cnt = {}
    for i in range(len(G[u])):
        v = G[u][i]
        if v != p:
            dfs(v, u, G, ids_by_children, vertex_ids, ways)
            child_ids.append(vertex_ids[v])
            if vertex_ids[v] not in cnt:
                cnt[vertex_ids[v]] = 0
            cnt[vertex_ids[v]] += 1
    child_ids = tuple(sorted(child_ids))  # Usporiadanie pole id-ciek deti
    done = True  # Ci sme uz aktualnemu child_ids pridelili id niekedy davnejsie
    if child_ids not in ids_by_children:
        ids_by_children[child_ids] = len(ids_by_children)  # Sekvencne pridelime dalsie id
        done = False  # Este sme nepridelili, musime to spravit v dalsich par riadkoch
    vertex_ids[u] = ids_by_children[child_ids]
    if not done:
        # Pouzijeme sucin zo vzoraku
        for k, v in cnt.items():
            ways[vertex_ids[u]] *= (fact[v] * expo(ways[k], v)) % MOD
            ways[vertex_ids[u]] %= MOD


# Predpocitame si faktorialy % MOD
for i in range(MXN):
    fact.append((fact[i] * (i + 1)) % MOD)

TC = int(input())

for tc in range(TC):
    ids_by_children = {}
    n = int(input())
    G = [[] for _ in range(n)]
    for i in range(n - 1):
        a, b = [int(x) for x in input().split()]
        G[a].append(b)
        G[b].append(a)
    ways = [1] * n
    vertex_ids = [-1] * n
    dfs(0, -1, G, ids_by_children, vertex_ids, ways)
    print(ways[vertex_ids[0]])  # Koren je 0

Diskusia

Tu môžte voľne diskutovať o riešení, deliť sa o svoje kusy kódu a podobne.

Pre pridávanie komentárov sa musíš prihlásiť.