OBI 2024 Terceira fase P2

Esta página é dedicada aos comentários da OBI 2024. Para se preparar para a OBI, confira o nosso Roteiro de Estudos, nele você vai encontrar um direcionamento completo de como estudar!

Construtora

Comentário por Henrique Vianna

Conhecimento prévio necessário:

Perceba, primeiramente, que podemos considerar um segmento de prédios de mesma altura como um único prédio. Da mesma forma, perceba que a seguinte estratégia é válida e minimiza a quantidade de fases necessárias para a construtora completar a obra: selecionar o menor prédio e aumentar a sua altura até que ele atinja a altura do menor prédio entre seus vizinhos, removê-lo do vetor e refazer o processo até que reste um único prédio (o maior deles). Podemos remover o menor justamente porque ele passaria a formar um segmento de mesma altura com seu vizinho. Esse processo pode ser facilmente simulado com um vector ou até mesmo recursivamente.

Deve-se ressaltar que, no caso desse problema, soluções \mathcal{O}(N^3) também recebiam 100 pontos. Além disso, é possível resolvê-lo em \mathcal{O}(N).

Segue o código:


#include <bits/stdc++.h>
using namespace std;
const int INF = 110; // Valor maior que todos os outros
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n; cin >> n;
vector<int> a(n);
for(auto &x : a) cin >> x;
int resp = 0;
while(a.size() > 1)
{
int mn = *min_element(a.begin(), a.end()); // Valor minimo em a
int mnPos = 0;
for(int i = 0; i < a.size(); i++)
{
if(a[i] == mn)
{
mnPos = i;
int direita = (i + 1 < a.size() ? a[i + 1] - a[i] : INF);
int esquerda = (i - 1 >= 0 ? a[i - 1] - a[i] : INF);
// Igualo menor valor ao menor de seus vizinhos
resp += min(direita, esquerda);
break;
}
}
// Removo o minimo do vetor
a.erase(a.begin() + mnPos);
}
cout << resp << '\n';
}

view raw

Construtora.cpp

hosted with ❤ by GitHub

Retas

Comentário escrito por Fernando Gonçalves

Conhecimento prévio necessário:

Subtarefa 2: N = 2

Só precisamos checar se a intersecção das duas retas está no intervalo [X_1, X_2].

A equação da abscissa x da intersecção de duas retas é x = \frac{B_1 - B_2}{A_2 - A_1}, sendo as retas (A_1, B_1) e (A_2, B_2).

Obs: Quando A_1 = A_2, não existe intersecção e, assim, a fórmula dá erro por divisão por 0.

Subtarefa 3: N \leq 1000

A solução é a mesma que para N = 2, mas fazemos para cada par de retas.

Subtarefa 4: A_i = A_j para todo 1 \leq i, j \leq N - 1

As primeiras N-1 retas não tem intersecção entre si (elas têm todas a mesma inclinação).

Portanto, posso fazer a mesma ideia de N \leq 1000, mas só testo os pares com a última reta.

Subtarefa 5: X_1 = X_2

Só queremos ver quantas retas têm intersecção na abscissa X_1.

Para tanto, podemos manter um map de frequências, indicando quantas retas existem em um y na abscissa X_1.

Passo por todas as retas, quando estou na i-ésima, somo freq_{A_i * X_1 + B_i} na resposta e, em seguida, adiciono um na frequência desse y.

Subtarefa 6: X_2 = X_1 + 1 e A_i \leq 30 para todo 1 \leq i \leq N.

Podemos fixar quais os A das duas retas que vamos testar.

Com isso, sabemos que as retas devem ter X_1 * (A_j - A_i) \leq B_i - B_j \leq X_2 * (A_j - A_i) , o que é um intervalo fixo para cada par de valores de A. Obs: É preciso garantir que A_i < A_j.

Podemos, então, calcular essa quantidade por meio de uma line sweep e um two pointers, para cada par de valores de A.

Subtarefa 7: Sem restrições adicionais

Existem quatro casos da intersecção de duas retas: elas se encontram antes de X_1, elas se encontram depois de X_2, elas não se encontram, elas se encontram entre X_1 e X_2.

Os quatros tipos de intersecção.

Note que somente no caso de intersecção no meio a ordem relativa dos y dessas retas muda da abscissa X_1 para a abscissa X_2.

Dessa forma, só precisamos ordenar as retas pelo y delas em X_1 e checar a quantidade de inversões na ordenação em X_2, esta será a nossa resposta.

Obs: existe corner quando a intersecção ocorre em X_1 ou em X_2, que pode ser corrigido por desempate na ordenação das retas.

Clique aqui para conferir o código.

Comentário por Murilo Maeda Kataoka

Conhecimento prévio necessário

  • Binary Lifting

OBS: perceba que pai[i] < i e que sempre podemos pegar o ancestral k níveis acima

Subtarefa 2: A estrutura forma uma linha

Para essa subtarefa podemos perceber que, em qualquer momento, se soubermos o pai de um certo vértice i, podemos facilmente calcular qual é o vértice que está k níveis acima dele. Isso porque se algum vértice v ainda possui filhos, com certeza, nenhum vértice acima dele sofreu reestruturação. Sabendo disso, podemos calcular facilmente o vértice que está x posições acima de v, já que o vértice com profundidade prof tem índice prof + 1, pois p[i + 1] = i. Então, o vértice que está x posições acima de v é o vértice v - x. Consequentemente, para um vértice i, quem está k níveis acima dele será o vértice pai[i] - (k - 1).

Agora, basta encontrar uma maneira de encontrar rapidamente o pai de um dado vértice i. Para fazer isso, vamos manter qual foi o menor índice que sofreu uma operação e um vetor que guarda o pai original dos vértices. Só precisamos saber do menor número porque as operações feitas em vértices de maior índice não importam, uma vez que o vértice menor é ancestral desses vértices maiores e, por isso, agora são todos filhos diretos do vértice menor. Seja u o menor número que sofreu reestruturação. Assim, em qualquer momento, para saber o pai de um determinado vértice i, caímos em dois casos:

  • i é menor que u ou ainda não houve reestruturação: nesse caso, a resposta é pai[i].
  • i é maior que u: nesse caso, a resposta é u.

A complexidade fica O(Q)

Subtarefa 3: N, Q \leq 500

Nessa parcial, para uma reestruturação no vértice v, podemos simplesmente passar na subárvore de v e setar que o pai desses vértices na subárvore é v. Precisamos também criar um vetor de marcação, onde marcamos todos os vértices que possuem algum ancestral que sofreu reestruturação. Fazemos isso porque se a operação de reestruturação for em algum vértice que está marcado no vetor de marcação, não fazemos nada, já que ele não possuirá nenhum filho.

Agora, para saber quem está k posições acima de um certo vértice i, vamos utilizar a mesma observação da parcial anterior, de que se um vértice ainda possui filhos, nenhum ancestral dele sofreu reestruturação. Então, podemos simplesmente subir k ancestrais fazendo pai[ pai[ ... pai[ i ] ... ] ], onde pegamos o pai k vezes.

A complexidade fica O(Q \times N ^ {2}).

Subtarefa 4: A estrutura forma uma árvore binária completa

Como a estrutura é uma árvore binária completa e N = 2 ^ {m}- 1, a sua principal característica é que a sua altura (ou profundidade) será no máximo m - 1, já que há 2 ^ i vértices que possuem profundidade i. E isso nos ajuda porque m \leq log_2 (10 ^5), ou m \leq 17.

Nesse caso, quando fizermos uma reestruturação no vértice v, podemos simplesmente utilizar um vetor de marcação para indicar que houve reestruturação nesse vértice. Vou mostrar a seguir o porquê disso bastar para essa parcial.

Quando quisermos pegar o ancestral k níveis acima de um vértice i, novamente queremos o ancestral de menor índice que sofreu reestruturação, pois temos a observação da subtask 1. Para achar esse ancestral, podemos simplesmente iterar por todos os ancestrais de i e pegar o ancestral de menor índice que está marcado no vetor de marcação. Então, a partir desse ancestral que achamos, basta subir k - 1 níveis e acharemos a resposta. Se não acharmos nenhum ancestral que esteja marcado, subimos k níveis a partir de i.

A complexidade fica O(1) para reestruturações e O(log_2 N) para achar o superior k níveis acima.

Subtarefa 5: Os nobres só têm que entregar relatórios para seus superiores diretos (pai)

Nesse subtask, basta saber o pai atual do vértice i para responder as perguntas de entrega de relatório.

Agora, precisamos saber encontrar esse pai. Para fazer isso, basta saber para cada vértice qual é o ancestral de menor índice que sofreu alguma reestruturação (já que esse ancestral será seu pai atual). A fim de encontrar isso, podemos perceber que, quando fazemos reestruturação em um certo vértice v, que não possui nenhum ancestral que sofreu reestruturação, todos os vértices na sua subárvore irão ter ele como ancestral de menor índice que sofreu reestruturação e como pai.

Então, queremos uma estrutura que faça as seguintes operações:

  • Retorne para um vértice v qual é o ancestral de menor índice que sofre reestruturação (se nenhum ancestral tiver sofrido reestruturação, retorna o pai na árvore original).
  • Atualize para todos os vértices na subárvore de um vértice v que v é o ancestral de menor índice que sofreu reestruturação (se v já tiver um ancestral que sofreu reestruturação, ele não terá nenhum filho, então, nesse caso, essa atualização não deve fazer nada).

Para fazer essa estrutura, vamos primeiro linearizar a árvore utilizando Euler Tour, que está descrito na subtask 3  desse problema.

Código do euler tour:


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;
}

view raw

euler_tour.cc

hosted with ❤ by GitHub

Agora, temos uma ordenação dos vértices que permite encontrar todos os vértices na subárvore de v facilmente, já que todos os vértices j tais que tin[v] < tin[j] \leq tout[v] estão na subárvore de v.

Observação importante: Se todos os vértices de uma subárvore, representada pelo intervalo [i, j] de tin , possuem o mesmo pai v depois de uma reestruturação nesse vértice, para todas as operações de reestruturação subsequentes, eles terão o mesmo pai, isto é, pai[i] = pai[i + 1] = ... pai[j]. Note que, no futuro, pode ser que esse pai em comum não seja v, e sim um outro vértice u que era ancestral de v e sofreu reestruturação. Essa observação é verdadeira porque todas as reestruturações que mudam quem é o pai de algum vértice no intervalo em questão devem ter sido feitas em alguém que é ancestral de v e, como a subárvore de v está contida na subárvore de todos os seus ancestrais, todos os vértices na subárvore de v terão como novo pai o ancestral de v que foi reestruturado.

Com isso, podemos fazer uma segment tree no vetor da árvore linearizada (isto é, o índice i nesse vetor representa o vértice que possui tin igual a i e fazemos uma seg nesse vetor). Mas, como estamos querendo fazer updates em um intervalo e queries em um índice específico, essa seg será um pouco diferente.

Cada nó v da seg cai em dois casos:

  • Caso 1: Todos os vértices cujos tin estão no intervalo representado por v possuem o mesmo ancestral de menor índice i que sofreu reestruturação. Nesse caso, seg[v] = i.
  • Caso 2: Os vértices no intervalo de tin de v NÃO possuem o mesmo ancestral de menor índice i que sofreu reestruturação. Nesse caso, seg[v] = INF.

Utilizando a observação feita logo acima, fica fácil perceber que se todos os vértices no intervalo de tin de um nó v da seg tiverem o mesmo ancestral que sofreu reestruturação, eles sempre terão o mesmo ancestral para as operações seguintes. Por isso, podemos setar seg[v] = i. Por outro lado, se eles não possuem esse ancestral em comum, ainda não podemos afirmar nada sobre os seus pais atuais só com o vértice v. É necessário, então, descer para fazer a busca nos filhos de v.

Observe a situação inicial para maior clareza.

O update na seg será da seguinte forma:


//[ini,fim] é o intervalo representado pelo nó "pos" da seg
//queremos setar o intervalo [p,q] para ter pai igual a "val"
void update(int pos, int ini, int fim, int p, int q, int val)
{
//se o [ini,fim] e [p,q] são disjuntos, não faz nada
if(p > fim || q < ini) return;
//se [ini,fim] está inteiramente contido no intervalo [p,q]
if(p <= ini && fim <= q)
{
//com a observação, podemos só setar seg[pos] = val
//não é necessário descer para os filhos de pos
seg[pos] = min(seg[pos],val);
return;
}
int m = (ini + fim)/2;
int e = pos*2, d = pos*2 + 1;
//se não está inteiramente contido, desce até achar um que esteja inteiramente contido em [p,q].
update(e,ini,m,p,q,val);
update(d,m+1,fim,p,q,val);
}

Perceba que temos que fazer seg[pos] = min(seg[pos],val) para evitar que ocorram reestruturações que, por exemplo, reestruturem um vértice v e depois um filho de v. Como pai[i] < i, fazer seg[pos] = min(seg[pos],val) faz com que essa segunda atualização não faça nada, que é o esperado.

A query será dessa forma:


//o nó "pos" representa o intervalo de tin [ini,fim]
//queremos saber o pai do vértice i tal que tin[i] = id
int query(int pos, int ini, int fim, int id)
{
//se id não está em [ini,fim], retorna que é inválido (é bom ter, mesmo que isso nunca aconteça)
if(id > fim || id < ini) return -1;
//se for diferente de INF, todos os filhos tem o mesmo pai (seg[pos])
//como id já está em [ini,fim], só retorna seg[pos]
if(ini == fim || seg[pos] != INF)
return seg[pos];
int m = (ini + fim)/2;
int e = pos*2, d = pos*2 + 1;
//se o id que estamos buscando está na esquerda, retorna o da esquerda
//caso contrário retorna o da direita
if(id <= m) return query(e,ini,m,id);
else return query(d,m+1,fim,id);
}

Para ficar mais claro, segue um exemplo de reestruturação:

Após reestruturação no vértice 5 (o update na seg fará com que os vértices com tin no intervalo [tin[5] + 1, tout[5]] = [5,7] tenham como ancestral de menor índice que sofreu reestruturação o vértice 5).

As queries e os updates ficam O(log N).

Subtask 6: sem restrições adicionais

Agora que já sabemos como fazer reestruturações rapidamente, basta descobrir como responder quem é o superior k posições acima de um certo vértice v rapidamente.

Para fazer isso utilizamos binary lifting! Para fazer o binary lifting, computamos a dp[i][x] normal, que guarda, para um i, qual é o vértice que está 2 ^{x} posições acima dele.  Agora que temos isso, para saber quem está k níveis acima de i, basta fazer i = dp[i][x], para todo x tal que 2 ^ {x} e k tenham um bit ativo em comum na notação binária desses números. Note que é o mesmo processo feito para igualar a profundidade de u e v no algoritmo de lca.

Observação importante: Se um certo vértice v possui filhos, nenhum ancestral seu sofreu reestruturação e são todos iguais aos da árvore original. Isso é porque, se algum deles tivesse sofrido reestruturação, todos os filhos de v seriam agora desse ancestral de v que foi reestruturado.

Como sabemos o pai atual de um certo vértice v (já que temos a seg descrita na parcial anterior), basta subir k - 1 posições a partir do seu pai utilizando binary lifting. Como k - 1 possui no máximo log_2 N bits, a complexidade de uma operação desse tipo é O(log N).

Segue o código para ficar mais claro:

 


#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
const int MAXN = 1e5 + 10;
const int LOG = 22;
int seg[MAXN*4],marc[MAXN*4], dp[MAXN][LOG], tin[MAXN], tout[MAXN];
int N,temp = 0;
vector<int> grafo[MAXN];
//Lineariza a árvore
void setup(int v, int p)
{
tin[v] = ++temp;
for(int viz : grafo[v])
{
if(viz == p)
continue;
setup(viz,v);
}
tout[v] = temp;
}
//[ini,fim] é o intervalo representado pelo nó "pos" da seg
//queremos setar o intervalo [p,q] para ter pai igual a "val"
void update(int pos, int ini, int fim, int p, int q, int val)
{
//se o [ini,fim] e [p,q] são disjuntos, não faz nada
if(p > fim || q < ini) return;
//se [ini,fim] está inteiramente contido no intervalo [p,q]
if(p <= ini && fim <= q)
{
//com a observação, podemos só setar seg[pos] = val
//não é necessário descer para os filhos de pos
seg[pos] = min(seg[pos],val);
return;
}
int m = (ini + fim)/2;
int e = pos*2, d = pos*2 + 1;
//se não está inteiramente contido, desce até achar um que esteja inteiramente contido em [p,q].
update(e,ini,m,p,q,val);
update(d,m+1,fim,p,q,val);
}
//o nó "pos" representa o intervalo de tin [ini,fim]
//queremos saber o pai do vértice i tal que tin[i] = id
int query(int pos, int ini, int fim, int id)
{
//se id não está em [ini,fim], retorna que é inválido (é bom ter, mesmo que isso nunca aconteça)
if(id > fim || id < ini) return -1;
//se for diferente de INF, todos os filhos tem o mesmo pai (seg[pos])
//como id já está em [ini,fim], só retorna seg[pos]
if(ini == fim || seg[pos] != INF)
return seg[pos];
int m = (ini + fim)/2;
int e = pos*2, d = pos*2 + 1;
//se o id que estamos buscando está na esquerda, retorna o da esquerda
//caso contrário retorna o da direita
if(id <= m) return query(e,ini,m,id);
else return query(d,m+1,fim,id);
}
//Responde quem está k posições acima do vértice v
int responde(int v, int k)
{
//acha o pai de v
int paiCur = query(1,1,N,tin[v]);
//Faz o binary lifting para ver quem está k - 1 posições acima do pai de v
k--;
for(int i = 0; i < LOG; i++)
{
if(k & (1 << i))
{
paiCur = dp[paiCur][i];
}
}
return paiCur;
}
int main()
{
cin.tie(0)->sync_with_stdio(0);
cin >> N;
//Lê o grafo e inicializa a dp do binary lifting
dp[1][0] = 1;
for(int i = 2; i <= N; i++)
{
cin >> dp[i][0];
grafo[dp[i][0]].push_back(i);
}
setup(1,1);
//Calcula a dp do binary lifting
for(int i = 1; i < LOG; i++)
{
for(int j = 1; j <= N; j++)
{
dp[j][i] = dp[dp[j][i-1]][i-1];
}
}
//Inicializa todo nó da seg como INF
for(int i = 1; i <= 4*N; i++)
{
seg[i] = INF;
}
//Inicializa as folhas da seg como os os pais dos vértices na árvore inicial
for(int i = 1; i <= N; i++)
{
update(1,1,N,tin[i],tin[i],dp[i][0]);
}
int Q; cin >> Q;
while(Q--)
{
int type; cin >> type;
if(type == 1)
{
int v,k; cin >> v >> k;
cout << responde(v,k) << "\n";
}
else
{
int v; cin >> v;
//Veja que o update é de tin[v] + 1 até tout[v] e não de tin[v] até tout[v]
//Isso porque não podemos setar que o pai de v é o próprio v
update(1,1,N,tin[v] + 1,tout[v],v);
}
}
}

view raw

burocracia.cpp

hosted with ❤ by GitHub

Brigadeiros

Comentário escrito por João Pedro Castro

Conhecimento prévio necessário:

Subtarefa 2: N \leq 50, K \leq 3, T \leq 1000

Vamos brutar tudo (testar todas as possibilidades de algo). Primeiro, vamos brutar as K posições finais, isso é O({N \choose K}). Agora, para cada uma dessas possibilidades vamos brutar a ordem que cada um vai (se o primeiro menor das posições iniciais vai com a segunda menor das posições finais, etc), isso é O(K!). Por fim, para cada um desses casos conseguimos calcular em O(K) (mas até O(N) passaria) o t mínimo para chegar nessa posição, e se t \leq T falamos que a resposta é o máximo entre a resposta atual e a soma da quantidade de brigadeiros nas posições finais. A complexidade final é O({N \choose K} \cdot K! \cdot K).

Dica de implementação: para brutar todas as possibilidades, você pode criar um vetor de N posições que comece com N - K 0s e depois tenha K 1s. E então, usar next_permutation para ver todas as possibilidades. Em uma dada permutação desse vetor original você pode dizer que se e somente se a i-ésima posição for igual a 1 tem alguém do grupo.

Subtarefa 3: N \leq 16, T \leq 1000

Podemos fazer a mesma estratégia da parcial anterior mas com uma observação a mais: sejam as posições iniciais do grupo s_1 < s_2 < ... < s_k, e as finais f_1 < f_2 < ... < f_k: para conseguir o tempo mínimo de chegar nessa posição é ideal dizer que o cara na posição s_i vai para a posição f_i para todo 1 \leq i \leq k; ou seja, o menor vai com o menor, o segundo menor vai com o segundo menor, etc. A intuição disso é bem simples, e não é difícil "provar" (sem muita formalidade) fazendo umas contas.

Assim, conseguimos só brutar as O({N \choose K}) possibilidades de escolhas, e calcular o tempo para chegar lá em O(K). A complexidade final é O({N \choose K} \cdot K).

Dica: o caso onde {a \choose b} é maior é quando b = \frac{a}{2}, então nesse caso o pior é {16 \choose 8} \approx 10^{4.1}, que ainda passa tranquilamente.

Subtarefa 4 e 5: N \leq 50, T \leq 10^5

Vamos usar programação dinâmica! Aqui a definição de cada parte:

  • Estado: nosso estado vai ser dp_{i, j, t} = s, onde:
    • i significa que usamos as posições de 1 até i para as posições finais dos membros do grupo que colocamos até agora.
    • j significa que já colocamos os j primeiros membros do grupo.
    • t significa o tempo máximo que podemos usar para chegar na melhor soma possível.
    • s significa a melhor soma possível que conseguimos dado i, j e k.
  • Caso base: o único caso base necessário é dp_{0, 0, 0} = 0. O resto, se não calcularmos, pode ser visto só como -\infty.
  • Transição: vou colocar várias possibilidades, e vai ser só o máximo entre elas.
    • Podemos só não botar nada em i: dp_{i - 1, j, t}.
    • Podemos ficar parados sem fazer nada: dp_{i, j, t - 1}.
    • Se j > 0 , podemos botar o j-ésimo (do menor para o maior em posição) membro do grupo na posição i: dp_{i - 1, j - 1, t - |i - posicao_j|} + p_i (perceba que essa transição só funciona por conta da nossa observação que é sempre melhor colocar o menor com menor, segundo menor com segundo menor, etc.)
  • A resposta, dado o estado, é dp_{N, K, T}.

A complexidade é O(N^2 \cdot T).

Subtarefa 6: N \leq 100

Vamos fazer a mesma DP, só que com uma observação a mais. Responda essa pergunta: se posso fazer trocas de elementos adjacentes, qual o número máximo de swaps para ordenar o vetor?

Se você já viu a técnica de bubble sort, essa pergunta tem uma resposta bem direta: é O(N^2), e na verdade sempre vai ser menor que N^2 (não convém mostrar a conta aqui). Logo, posso chegar de qualquer ordenação do vetor para qualquer outra em menos de N^2 operações, ou seja, logo após receber o T podemos dizer que T = min(T, N^2) e a resposta ainda estaria certa, pois se existe uma resposta válida fazendo mais que N^2 trocas, também existirá uma fazendo menos que N^2 trocas.

Assim, tendo que T \leq N^2, temos uma complexidade de O(N^4).

Subtarefa 7: Sem restrições adicionais.

Quando você leu o enunciado, idealmente se atentou de um detalhe específico que não seria colocado sem motivo: 0 \leq P_i \ leq 9. Ou seja, todo prato tem entre 0 e 9 brigadeiros, mas o que isso faz a gente pensar? Se você já está acostumado com problemas de DP, é direto: a soma máxima é O(N).

E uma técnica legal de problemas de DP, é que em diversos problemas se você tem algo como dp_{a, b, c} = d (ou em um caso geral com quantas variáveis você quiser), você pode dizer que dp_{a, b, d} = c, ou qualquer outra troca. Ou seja, se você tem X variáveis que indicam seu estado, e mais 1 que indica sua resposta, você pode trocar qualquer uma das que indicam o estado com a da resposta que o estado ainda vai ser perfeitamente expressado. Claro que varia de problema a problema, e vai ter trocas melhores que outras, mas em problemas de DP sempre pense em qual a maneira com menos possibilidades de guardar esse estado.

No nosso caso, ao invés de fazer a dp_{i, j, t} = s, vamos fazer a dp_{i, j, s} = t. Já que o máximo de t é O(N^2) e o máximo de s é O(N). Agora a DP significa: colocando os j primeiros do grupo em algumas das i primeiras posições, qual o menor tempo t para atingir a soma s? Se for impossível podemos só dizer que dp_{i, j, s} = \infty.

O estado foi explicado acima, o caso base é o mesmo, e o resto fica assim:

  • Transição: de novo, colocarei várias coisas aqui, e agora só pegue o mínimo entre elas.
    • Não colocamos nada em i: dp_{i - 1, j, s}.
    • Colocamos o j-ésimo do grupo em i: dp_{i - 1, j - 1, s - P_i} + |i - posicao_j|.
  • A resposta vai ser o maior S tal que existam quaisuer x e y com dp_{x, y, S} \leq T.

Temos O(9 \cdot N^3) estados, cada transição é O(1), logo a complexidade final é O(9 \cdot N^3) = O(N ^3).

OBS: A complexidade acaba ficando apertada, então para ficar tranquilo você tem que fazer pequenas "otimizações" (basicamente não passar por estados impossíveis), veja o código abaixo para ver as que fiz.

Clique aqui para conferir o código.

Jogo de Pratos

Comentário escrito por Fernando Gonçalves

Conhecimento prévio necessário:

Esse problema têm duas partes que trabalharemos separadamente: a parte dos feitiços e a das refeições. Neste comentário, será discutida a ideia geral do problema e, ao final, haverá discussão das parciais.

Parte dos feitiços

Temos N equações de reta. Podemos usar cada uma delas quantas vezes quisermos e em qualquer ordem, contanto que sejam usadas exatamente K delas no total. Queremos achar, dado um X inicial, qual o máximo valor que podemos atingir.

Para tanto, usaremos, a todo momento, a equação que mais aumenta o nosso X, só precisamos agora achar qual essa reta para toda operação.

Prova

Todas as retas são estritamente crescentes (é garantido pelo enunciado).

Assim, se eu tiver um X maior, consigo um resultado maior em todas as retas.

Portanto, uma estratégia que não maximiza o X após a operação nunca é ótima.

Utilizando o Convex Hull Trick, conseguimos achar a reta ótima para cada intervalo de X. Dessa forma, conseguimos, por meio de uma busca binária nesse convex hull de retas,  achar em O(log_2N) a reta ótima para um X qualquer. Isso nos dá uma complexidade de O(K * log_2N) por query.

Estamos, contudo, fazendo algumas operações desnecessárias.

Note que, quando a reta ótima for aquela com maior A do convex hull, todas as operações restantes serão feitas nesta reta. Se sobraram K' operações, o X final (X') será X' = X + K' * B  (caso A == 1) ou X' = A(A(A(...A(A*X + B)+B)+B)...)+B = A^{K'}*X + B*\sum_0^{K'-1}A^i = A^{K'}*X + B*\frac{A^{K'} - 1}{A - 1} (caso A > 1).

Além disso, no máximo uma operação será usada em qualquer outra reta.

Prova

Sendo as retas do convex hull (A_1, B_1), (A_2, B_2), ..., (A_n, B_n), com A_i < A_{i+1} para todo 1 \leq i < n, temos que B_i > B_{i+1} (uma reta "dominada" por outra nunca é ótima).

Quando usamos uma reta i, nenhuma outra reta menor que i será usada daqui para frente. Portanto, suponha sem perda de generalidade que usamos a reta 1.

Após essa operação, temos que X' = A_1 * X + B_1 => X' > B_1 => X' > B_i para todo 1 \leq i \leq n.

Suponha, por contradição, que agora usaremos a reta i  (1 \leq i < n).

Sabemos, assim, que A_i * X' + B_i > A_n * X' + B_n (pois i é ótima) <=> (A_n - A_i) * X' < B_i - B_n.

Entretanto, X' > B_i e A_n > A_i e B_n \geq 0. Assim, (A_n - A_1) * X' > X' > B_i > B_i - B_n => (A_n - A_1) * X' > B_i - B_n, o que é uma contradição.

Dessarte, depois de uma operação, somente a última reta será usada.

Desta forma, só precisamos achar a reta ótima para o X inicial, aplicar uma operação com esta, e aplicar todas as outras com a última reta. A complexidade disso é O(log_2N) por query, ou seja, O(Qlog_2N) no total.

Parte das refeições

Temos M equações de reta. Precisamos usar cada uma delas exatamente uma vez, mas podemos escolher a ordem. Essa ordem, portanto, será alguma permutação p de tamanho M.

Note que, para um dado X, o resultado de todas as operações será X' = a'_{p_M} * (a'_{p_{M-1}} * (... a'_{p_2} * (a'_{p_1} * X + b'_{p_1}) + b'_{p_2}) ... ) + b'_{p_{M-1}} ) + b'_{p_M}. Assim, temos que X' = X * \prod_1^M a'_{p_i} + \sum_1^M ( b'_{p_i} * \prod_{i+1}^M a'_{p_j}), ou seja, a ordem ótima independe do X.

Dessa forma, só precisamos calcular o maior somatório possível.

Intuitivamente, uma solução gulosa deveria funcionar (afinal, achar o maior valor de uma soma de produtos é um problema classicamente guloso), contudo a ordenação não é óbvia. Usaremos, então, um argumento da troca para achar essa ordenação ótima.

Suponha que temos uma ordem qualquer p tal que, quando trocamos a ordem de p_i e p_{i+1}, o somatório aumenta. Como o prefixo e o sufixo das duas ordens é igual, temos que a'_{p_{i+1}} * b'_{p_i} + b'_{p_{i+1}} < a'_{p_i} * b'_{p_{i+1}} + b'_{p_i} <=> (a'_{p_{i+1}} - 1) * b'_{p_i} < (a'_{p_i} - 1) * b'_{p_{i+1}}. Dessa forma, excetuando-se o caso em que um dos a' é 1 (estes sempre estão no prefixo da ordem), a troca acontece quando \frac{b'_{p_i}}{a'_{p_i} - 1} < \frac{b'_{p_{i+1}}}{a'_{p_{i+1}} - 1} e, portanto, nossa ordem gulosa é a ordenação por maior \frac{b'}{a'-1}.

Tanto esse somatório quanto o produto dos a' podem ser calculados no começo do código, e sua conta calculada ao final de cada query. Resolvemos, então, essa parte em O(Mlog_2M)

Resumo da solução

Começamos o código ordenando as operações do segundo por maior \frac{b'}{a'-1} (Cuidado: os a' = 1 devem ser colocados no prefixo).

Então, calculamos tanto o produto dos a' dessas M operações quanto o somatório \sum_1^M (b'_i * \prod_{i+1}^M a'_j) provenientes desse ordenamento. Salvaremos esses números para calcular a resposta final em cada query.

Em seguida, ordenamos as operações do primeiro tipo por a crescente, e calculamos o convex hull das retas. Podemos, também, calcular os intervalos de X ótimo para cada reta, para facilitar a implementação futura, mas isso não é estritamente necessário.

Agora, começamos a lidar com as queries. Para cada uma, vamos usar uma busca binária (ou ternária, caso os intervalos de X não tenham sido calculados) para encontrar a primeira reta usada. Depois disso, usamos todas as K-1 operações restantes na última reta do convex hull (que é aquela com maior a). Finalmente, aplicamos a operação do segundo tipo, calculando, com isso, a resposta.

A complexidade total desse código é O(Qlog_2N + Nlog_2N + Mlog_2M).

Clique aqui para conferir o código.

Subtaks

Nem todas as subtaks do problema precisavam dessas observações e/ou desses algoritmos. Apresentaremos ideias gerais de cada uma delas abaixo.

  • Subtarefa 2 (N = M = K = 2): Nessa parcial, somente a independência entre o X e a ordem da segunda operação precisa ser notada: podemos só testar toda combinação de cada operação. Nota: É necessário fazer as partes separadamente para evitar overflow.
  • Subtarefa 3 (M = 1, Q = 1, é garantido que a quantidade final não excede 10^9 + 7): Aqui, não é preciso resolver a segunda parte do problema. Ademais, a primeira parte pode ser resolvida com a mesma ideia, mas sem o uso do convex hull. Além disso, soluções que contém erro no cálculo do módulo ou que tentam maximizar o resto da resposta ao invés da resposta em si passam aqui.
  • Subtarefa 4 (N, Q, K \leq 500, M = 1): Ainda não é necessário resolver a segunda parte; a primeira parte pode ser resolvida apenas testando qual a melhor reta para cada operação.
  • Subtarefa 5 (N, Q, K, M \leq 500): A segunda parte precisa ser resolvida como descrito; a primeira parte pode ser resolvida apenas testando qual a melhor reta para cada operação.
  • Subtarefa 6 (N, Q \leq 3000): A segunda parte precisa ser resolvida como descrito; todas as observações da primeira parte são necessárias, mas pode-se só testar a reta ótima para o primeiro X, ao invés de implementar convex hull.
  • Subtarefa 7 (Sem restrições adicionais): A solução descrita no comentário.