Comentário por Murilo Maeda e Henrique Vianna
Conhecimentos Prévios
Esse é um problema relativamente complicado e descobrir a ideia final requer várias observações. Por isso, vamos comentar todas as parciais do problema, já que elas ajudam a resolver para 100 pontos.
Subtarefa 1:
Já que , haverá uma única reunião. Então, podemos simplesmente fazer uma a partir da anfitriã e ir marcando quem estiver dentro do intervalo estipulado.
Subtarefa 2: para todo – ou seja, a mãe da matriarca é a matriarca
O que essa subtarefa quer dizer é que a árvore vai ser uma linha, ou seja, se tomarmos a matriarca 1 como raíz, todos os vértices (fora a única folha) vão ter exatamente 1 filho. Com isso temos basicamente um vetor no qual os valores das fortunas vão decrescendo conforme o índice aumenta.
Para resolver esse problema, vamos manter um vetor auxiliar de tamanho que guarde no índice para quantas reuniões a matriarca foi convocada. Também vamos representar a árvore como um vetor que guarde na posição a fortuna da matriarca . Outra observação importante é que uma convocação agora se resume a encontrar a matriarca de menor índice cuja fortuna seja menor ou igual a e a matriarca de maior índice cuja fortuna seja maior do que .
Então, como o vetor está ordenado (do maior para o menor), basta fazermos duas buscas binárias: uma para encontrar a matriarca de maior fortuna que foi convocada e outra para encontrara a matriarca de menor fortuna que foi convocada e depois adicionar 1 no vetor de todo mundo entre essas duas (incluindo elas).
Para adicionar 1 em todos os elementos do vetor , vamos usar um truque de somas parciais. Para esse truque, se quisermos adicionar 1 nos elementos do intervalo , basta irmos no vetor , que guarda as somas parciais de prefixo do vetor , e somarmos 1 no índice e -1 no índice . dessa forma, somente os elementos no intervalo que queremos serão afetados quando calcularmos a soma daquele intervalo usando somas parciais.
Para a resposta, basta pegar a soma parcial de cada matriarca individualmente.
Subtarefa 3: e
Ao que essa tarefa se resume é que todo mundo na subárvore da anfitriã vai ser vai ser chamada. Ou seja, para cada das reuniões que forem feitas, basta somar 1 no vetor de resposta de todo mundo que está na subárvore da anfitriã. Para fazer isso, podemos linearizar a árvore e utilizar o mesmo truque de somas parciais que usamos na parcial anterior.
Para linearlizar a árvore, podemos utlizar a técnica "Euler Tour". Para melhor compreensão, analise o código a seguir:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int timer, tin[MAXN], tout[MAXN]; | |
void dfs(int u, int p) | |
{ | |
tin[u] = ++timer; | |
for(auto v : adj[u]) | |
{ | |
if(v == p) continue; | |
dfs(v, u); | |
} | |
tout[u] = timer; | |
} | |
Na implementação acima, temos: guarda o "tempo" atual da , guarda o tempo em que se "entra" no vértice , ou seja, o valor de quando foi chamada e , que representa o tempo em que se "sai" do vértice , ou seja, o maior valor de para algum na subárvore de . Sendo assim, a subárvore de qualquer vértice pode ser representada por um intervalo continuo , daí o nome "linearização".
Subtarefa 4: e
Note que nessa tarefa ainda mantemos a característica da subtarefa anterior de que quando uma reunião acontece, somente a subárvore da anfitriã é convidada. Mas, agora, a característica que não está presente é a de que todos vão ser chamados. Como isso acontece, não podemos mais utilizar o truque de somar 1 em todos os elementos do intervalo, já que podem haver "buracos" nos segmentos.
O que a outra condição que o enunciado nos dá, o limitante de fortuna, garante é que a altura máxima da árvore vai ser 200, já que há uma diferença de pelo menos 1 da fortuna de uma matriarca mais velha para outra mais nova. Então, podemos simplesmente marcar cada uma das anfitriãs e qual é o intervalo da reunião que elas fizeram. Para calcular a resposta pegamos um vértice por vez e subimos até a raiz, calculando para cada um dos vértices no caminho em quantas festas que ele foi anfitrião o vértice do qual nós partimos participou. Mas, note que, uma única matriarca pode ser anfitriã de várias festas com vários intervalos diferentes, então temos que encontrar uma maneira de guardar e checar a quais intervalos uma determinada fortuna pertence quando estamos subindo pela árvore.
Para fazer isso podemos simplesmente guardar um vetor de marcação para cada um dos vértices que indica para cada fortuna quantas festas ela participou com aquele vértice de anfitrião. Isto é, se um vértice é anfitrião de uma festa de intervalo e , basta somarmos 1 no intervalo do vetor de marcação do vértice . Quando formos calcular a resposta para um vértice de fortuna e estivermos passando pelo vértice , somamos o número guardado no vetor de marcação do vértice na resposta do vértice .
Subtarefa 5: Sem restrições adicionais
Para resolver o problema completamente deve-se perceber que é válido simplesmente “transferir” uma reunião de um vértice para o seu último ancestral que tem fortuna dentro do intervalo, ou seja, o ancestral de de menor profundidade na árvore que satisfaz . Para achar esse ancestral rapidamente, basta usarmos a técnica de binary lifting. Chamaremos esse vértice de . Tendo isso feito, para cada reunião , só precisamos considerar os descendentes de , não mais os ancestrais.
Tendo isso feito, podemos finalizar o problema de diversas maneiras. O jeito mais prático é fazer uma , mantendo alguma estrutura que permita somas e consultas (range sum, point query) em tempo logarítmico, como uma BIT ou uma Segment Tree. Pense nessas estruturas como um “vetor de frequência”, capaz de nos informar em quantas das reuniões que estão “ativas” (para um dado vértice , por conta da , só estarão “ativas” as reuniões que tem como um dos ancestrais de ou o próprio ) uma dada fortuna idade está contida. Quando “entrarmos” num vértice, somaremos, na nossa estrutura, no intervalo para toda reunião que tem como o nó atual. Para acharmos a resposta de um dado vertice , basta fazermos uma consulta na estrutura, obtendo a quantidade de reuniões em que está contida sua fortuna. Quando “sairmos” do nó, devemos desativar essas reuniões, subtraindo .
Veja o código abaixo para melhor compreensão:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <bits/stdc++.h> | |
using namespace std; | |
const int LOG = 21; | |
const int MAX = 2e5 + 10; | |
// Implementacao de Seg com Lazy | |
// Possivel fazer com Seg/BIT + soma de prefixos | |
int n, m, fort[MAX], dp[MAX][LOG], resp[MAX]; | |
int seg[4 * MAX], lzSum[4 * MAX]; | |
vector<int> adj[MAX]; | |
vector<pair<int, int>> reunioes[MAX]; | |
void refresh(int p, int l, int r) // Atualizar o valor do no p | |
{ | |
if(lzSum[p] == 0) return; | |
int add = lzSum[p]; lzSum[p] = 0; | |
seg[p] += (r - l + 1) * add; | |
if(l == r) return; | |
int e = 2 * p, d = e + 1; | |
lzSum[e] += add; | |
lzSum[d] += add; | |
} | |
int query(int a, int b, int p, int l, int r) // Query no intervalo [a, b], no atual p de intervalo [l, r] | |
{ | |
refresh(p, l, r); | |
if(a > r || b < l) return 0; | |
if(a <= l && r <= b) return seg[p]; | |
int m = (l + r) >> 1, e = 2 * p, d = e + 1; | |
return query(a, b, e, l, m) + query(a, b, d, m + 1, r); | |
} | |
void update(int a, int b, int x, int p, int l, int r) // Update de somar x no intervalo [a, b], no atual p e intervalo atual [l, r] | |
{ | |
refresh(p, l, r); | |
if(a > r || b < l) return; | |
if(a <= l && r <= b){ | |
lzSum[p] += x; | |
refresh(p, l, r); | |
return; | |
} | |
int m = (l + r) >> 1, e = 2 * p, d = e + 1; | |
update(a, b, x, e, l, m); update(a, b, x, d, m + 1, r); | |
seg[p] = seg[e] + seg[d]; | |
} | |
void dfs(int u, int p) | |
{ | |
// Adiciono as reunioes do no u (somo 1 nos intervalos de fortunas) | |
for(auto [e, d] : reunioes[u]) | |
update(e, d, 1, 1, 1, MAX); | |
// Resposta do no atual eh o numero de festas que contem a sua fortuna | |
resp[u] = query(fort[u], fort[u], 1, 1, MAX); | |
for(auto v : adj[u]) | |
if(v != p) dfs(v, u); | |
// Retiro as reunioes do no u (subtrario 1 nos intervalos de fortunas) | |
for(auto [e, d] : reunioes[u]) | |
update(e, d, -1, 1, 1, MAX); | |
} | |
int32_t main() | |
{ | |
ios_base::sync_with_stdio(false); | |
cin.tie(NULL); | |
cin >> n >> m; | |
for(int i = 1; i <= n; i++){ | |
cin >> fort[i] >> dp[i][0]; | |
adj[i].push_back(dp[i][0]); | |
adj[dp[i][0]].push_back(i); | |
} | |
// Calculo a DP do binary lifting | |
for(int j = 1; j < LOG; j++) | |
for(int i = 1; i <= n; i++) | |
dp[i][j] = dp[ dp[i][j - 1] ][j - 1]; | |
while(m--) | |
{ | |
int u, l, r; cin >> u >> l >> r; | |
// Calculo o ancestral de maior fortuna para qual posso "mandar" a reuniao, T_i | |
for(int j = LOG - 1; j >= 0; j--) | |
if(dp[u][j] && fort[ dp[u][j] ] <= r) u = dp[u][j]; | |
// Adiciono a reuniao ao no u | |
reunioes[u].emplace_back(l, r); | |
} | |
dfs(1, 1); | |
for(int i = 1; i <= n; i++) | |
cout << resp[i] << ' '; | |
cout << '\n'; | |
} |