Comentário por Lúcio Cardoso e Willian Wang
Para conferir a prova na íntegra, clique aqui.
Detetive
Conhecimento prévio necessário:
Modelando o problema em um grafo
Vamos transformar a situação do problema em um grafo direcionado. Se , iremos dizer que o vértice terá uma aresta (direcionada) para o vértice .
Chamaremos de source todos os vértices cujo ingrau (quantidade de arestas que apontam para tal vértice) é igual a . Perceba que não existe caminho de nenhum vértice no grafo para alguma source. Além disso, se um vértice é definido como verdadeiro, então no mínimo uma das sources do grafo que tem um caminho para é, também, verdadeira. Ou seja, podemos fazer a seguinte afirmação:
Fato: Defina como o conjunto das sources do grafo que possuem um caminho para . Então, caso seja verdadeiro, existe um vértice que é verdadeiro.
Interseção dos conjuntos
Imagine que existe um vértice inicialmente verdadeiro no grafo. Vamos tomar um outro vértice qualquer, o qual não sabemos se é verdadeiro ou não. Perceba que se é um subconjunto de , então será certamente verdadeiro, pois alguma source que é verdadeira também possuirá um caminho para . Podemos generalizar esta observação com o seguinte lema:
Lema: Seja um vértice qualquer. O vértice será verdadeiro se, e somente se, existe algum vértice incialmente verdadeiro tal que é um subconjunto de .
Com isso, precisaremos encontrar, para cada vértice, o conjunto de sources que chegam nele. Podemos fazer isso usando uma DFS partindo de cada source: guardaremos um vetor de booleanos para cada vértice , indicando quais sources possuem um caminho para ele, e assim que a DFS de alguma source chegar em , indicaremos que . A complexidade total realizando cada uma dessas DFSs será .
Algoritmo em
Usando o lema acima, podemos utilizar de um algoritmo simples para resolver o problema em . Para cada vértice inicialmente verdadeiro, vamos checar se , para todo vértice do grafo. Se sim, marcaremos como verdadeiro. Usando o nosso vetor de booleanos , podemos encontrar a interseção em . Logo, como realizamos uma operação em com todos os vértices do grafo para cada vértice verdadeiro, a complexidade do algoritmo é . Essa solução era suficiente para um total de pontos no problema.
Otimização do algoritmo com bitsets
Um bitset é uma estrutura de dados do C++ utilizada para representar conjuntos. Com ela, podemos realizar interseção e união de conjuntos de tamanho em . Apesar da complexidade ser equivalente a , na prática, o bitset é muito mais rápido. Para mais informações, é recomendada a leitura da referência do C++ sobre bitsets.
Para otimizar o algoritmo acima, podemos representar o vetor booleano como um bitset. Assim, a interseção dos conjuntos e pode ser encontrada em , totalizando uma complexidade final de .
// Comentário Noic OBI 2019 - Fase 2 - Programação Nível 2 | |
// Detetive | |
#include <bits/stdc++.h> | |
using namespace std; | |
const int maxn = 1e3+10; | |
// bitset indicando quais sources do grafo chegam em cada vértice | |
bitset<maxn> path[maxn]; | |
// grafo das implicações | |
vector<int> grafo[maxn]; | |
// ingrau de cada vértice | |
int grau[maxn]; | |
// vetor de marcação da dfs | |
bool mark[maxn]; | |
bool verdadeiro[maxn]; | |
void dfs(int source, int u) | |
{ | |
// source tem um caminho para u | |
path[u][source] = 1; | |
mark[u] = true; | |
for (auto v: grafo[u]) | |
if (!mark[v]) | |
dfs(source, v); | |
} | |
int main(void) | |
{ | |
int E, I, V; | |
scanf("%d %d %d", &E, &I, &V); | |
for (int i = 1; i <= I; i++) | |
{ | |
int u, v; | |
scanf("%d %d", &u, &v); | |
// u -> v | |
grafo[u].push_back(v); | |
grau[v]++; | |
} | |
for (int i = 1; i <= E; i++) | |
{ | |
if (grau[i] == 0) | |
{ | |
memset(mark, 0, sizeof mark); | |
dfs(i, i); | |
} | |
} | |
for (int i = 1; i <= V; i++) | |
{ | |
int u; | |
scanf("%d", &u); | |
verdadeiro[u] = true; | |
for (int v = 1; v <= E; v++) | |
{ | |
// o comando & é usado para encontrar a interseção de bitsets | |
// count() calcular a quantidade de bits ligados no bitset | |
if ( (path[u]&path[v]).count() == path[u].count()) | |
verdadeiro[v] = true; | |
} | |
} | |
// imprime a resposta | |
for (int i = 1; i <= E; i++) | |
if (verdadeiro[i]) | |
printf("%d ", i); | |
printf("\n"); | |
} |
Matriz Super-Legal
Conhecimento prévio necessário:
Este mesmo problema estava presente na prova do Nível 1 e, provavelmente, foi um dos problemas mais interessantes dessa fase da OBI.
O seguinte lema é essencial na solução do problema:
Lema: Uma matriz é super-legal se, e somente se, todas as suas submatrizes x são legais.
Note que, pelo enunciado do problema, todas as submatrizes x de uma matriz super-legal devem ser legais. Queremos, então, provar que a condição é suficiente.
Fato 1: Toda matriz x () que possui todas as suas submatrizes x legais é super-legal.
Prova do Fato 1:
Vamos supor, por indução, que o fato vale para qualquer matriz x, onde . Se provarmos que a matriz x é legal, podemos concluir, como um resultado da hipótese de indução, que ela é super-legal.
Note que o caso base é o caso trivial consistindo apenas de uma matriz x. Temos que:
1. , pois a matriz de cantos e é legal.
2. , pois, por hipótese de indução, a matriz de cantos e é legal.
Somando as desigualdades, temos que:
.
Ou seja, a matriz x é legal. Assim, o Fato 1 está provado por indução.
Fato 2: Toda matriz x () que possui todas as suas submatrizes x legais é super-legal.
Prova do Fato 2: Análoga à prova do Fato 1.
Agora, vamos supor, novamente por indução, que uma matriz x () qualquer é super-legal. Para finalizar a prova, é suficiente provar que a matriz x será legal.
Perceba que os casos base da indução são exatamente o Fato 1 e o Fato 2. Temos que:
1. , pois pela hipótese de indução, a matriz de cantos e é legal.
2. , pois, pelo Fato 1, a matriz de cantos e é legal.
Somando as desigualdades, obtemos que:
.
Ou seja, a matriz x inteira é legal. Assim, a prova está concluída.
Redução do Problema
Utilizando dessa observação, vamos construir uma nova matriz de dimensões x, de valores ou . Ela será definida da seguinte maneira:
- Se a submatriz x de de cantos e for legal, então .
- Caso contrário, .
Suponha que a maior matriz super-legal de possui cantos e (). Então, teremos que todos os valores na submatriz em de cantos e serão iguais a . Ou seja, todas as submatrizes x em com canto superior esquerdo indo desde até devem ser legais.
Logo, a maior submatriz super-legal em representa, em , a submatriz de comprimentos e tal que o valor é máximo e que todos os seus elementos sejam iguais a . Resta agora encontrarmos esta matriz em .
Algoritmo em
Defina como o maior valor tal que são células de valor . Ou seja, é a "altura" da célula . Além disso, defina e como os comprimentos da matriz que desejamos encontrar em . Perceba que o valor de cada uma das células na última linha da matriz procurada é maior ou igual a .
Assim, para encontrar o valor máximo, ou seja, a área da maior matriz super-legal, podemos realizar o seguinte algoritmo:
Algoritmo 1:
- Fixe uma célula qualquer da matriz de tal forma que .
- Encontre o índice mais à direita tal que e .
- Encontre o índice mais à esquerda tal que e .
- Caso seja maior do que a resposta atual, atualize-a para este valor.
- Repita as etapas para as outras células não-nulas de .
Essencialmente, o algoritmo acima escolhe uma célula e encontra a matriz de maior comprimento possível tal que seu comprimento seja e que a célula seja uma das células na base da matriz.
Assim, note que é possível realizar um algoritmo de complexidade : Podemos encontrar os valores para cada célula em (mais detalhes no código abaixo) e realizar o Algoritmo 1 para cada uma das células, encontrando os valores e em . Essa solução era suficiente para conseguir pontos no problema.
Otimização do Algoritmo 1
Para uma solução mais eficiente, vamos encontrar os valores de de maneira rápida. Para isso, iremos utilizar a estrutura de dados stack (pilha) da STL do C++. O algoritmo será o seguinte:
Algoritmo 2:
- Inicie na célula da matriz , onde é a linha atual (iniciando por ) e declare uma pilha vazia de pares da forma .
- Se a célula atual for , remova o par no topo da pilha caso seu segundo elemento tenha valor maior ou igual a .
- Repita a etapa até que a pilha esteja vazia ou até que o valor do segundo elemento do par no seu topo seja menor do que .
- O índice será o valor do primeiro elemento do par no topo da pilha (ou será inexistente caso a pilha esteja vazia).
- Insira o par na pilha.
- Repita as etapas para os elementos restantes na linha atual. Após isso, recomece da etapa na linha seguinte.
O Algoritmo 2 é extremamente similar àquele usado no Problema da Semana #39 - Avançado. Para melhor entendimento, confira a sua solução.
Para encontrar o valor , o algoritmo será análogo: Basta iterar por cada linha de trás para frente e refazer o mesmo processo. Finalmente, perceba que a complexidade do Algoritmo 2 em cada uma das linhas de será , já que cada elemento na linha é inserido/removido da pilha no máximo uma vez.
Complexidade da solução: .
// Comentário Noic OBI 2019 - Fase 2 - Programação Nível 2 | |
// Matriz Super Legal | |
#include <bits/stdc++.h> | |
using namespace std; | |
typedef pair<int, int> pii; | |
const int maxn = 1e3+10; | |
int n, m; | |
// altura de cada célula | |
int h[maxn][maxn]; | |
// matrizes | |
int A[maxn][maxn], B[maxn][maxn]; | |
int j_0[maxn][maxn], j_1[maxn][maxn]; | |
void calc_h(void) | |
{ | |
for (int i = 1; i < n; i++) | |
{ | |
for (int j = 1; j < m; j++) | |
{ | |
if (!B[i][j]) continue; | |
// calculamos h[i][j] baseando-nos na célula acima | |
if (B[i-1][j] == 1) h[i][j] = 1+h[i-1][j]; | |
else h[i][j] = 1; | |
} | |
} | |
} | |
void calc_j0(void) | |
{ | |
for (int i = 1; i < n; i++) | |
{ | |
stack<pii> stk; | |
for (int j = 1; j < m; j++) | |
{ | |
// algoritmo 2 | |
while (!stk.empty() && stk.top().second >= h[i][j]) | |
stk.pop(); | |
// se j0 não existe, digamos que ele será uma posição a | |
// menos do limite (1) | |
if (stk.empty()) j_0[i][j] = 0; | |
else j_0[i][j] = stk.top().first; | |
stk.push({j, h[i][j]}); | |
} | |
} | |
} | |
void calc_j1(void) | |
{ | |
for (int i = 1; i < n; i++) | |
{ | |
stack<pii> stk; | |
for (int j = m-1; j >= 1; j--) | |
{ | |
// algoritmo 2 | |
while (!stk.empty() && stk.top().second >= h[i][j]) | |
stk.pop(); | |
// se j1 não existe, digamos que ele será uma posição a | |
// mais do que o limite (m-1) | |
if (stk.empty()) j_1[i][j] = m; | |
else j_1[i][j] = stk.top().first; | |
stk.push({j, h[i][j]}); | |
} | |
} | |
} | |
int main(void) | |
{ | |
scanf("%d %d", &n, &m); | |
for (int i = 1; i <= n; i++) | |
for (int j = 1; j <= m; j++) | |
scanf("%d", &A[i][j]); | |
// checando todas as matrizes 2x2 | |
for (int i = 1; i < n; i++) | |
for (int j = 1; j < m; j++) | |
if (A[i][j]+A[i+1][j+1] <= A[i+1][j]+A[i][j+1]) | |
B[i][j] = 1; | |
calc_h(); | |
calc_j0(); | |
calc_j1(); | |
// resposta do problema | |
int ans = 0; | |
for (int i = 1; i < n; i++) | |
for (int j = 1; j < m; j++) | |
if (h[i][j]) | |
ans = max(ans, (j_1[i][j]-j_0[i][j])*(h[i][j]+1)); // (x_m+1)*(y_m+1) | |
printf("%d\n", ans); | |
} |
Supermercado
Conhecimento prévio necessário:
- Entrada e Saída
- Algoritmos Gulosos
Perceba que, se gramas de um produto custam Bits, então grama custa Bits. Por consequência disso, é sempre uma escolha melhor comprar kilograma usando apenas do produto cujo custo por grama é o menor possível. Podemos, assim, ler cada um dos valores de um produto e imprimir aquele de menor valor multiplicado por gramas.
Complexidade: .
// Comentário Noic OBI 2019 - Fase 2 - Programação Nível 2 | |
// Supermercado | |
#include <bits/stdc++.h> | |
using namespace std; | |
int main(void) | |
{ | |
int n; | |
cin >> n; | |
// inicializamos a resposta com um valor muito grande, | |
// digamos 10^9 | |
double ans = 1e9; | |
for (int i = 1; i <= n; i++) | |
{ | |
double p, g; | |
cin >> p >> g; | |
// usamos a função min do C++ para escolher a resposta mínima | |
ans = min(ans, 1000.00*(p/g)); | |
} | |
// dizemos que a precisão do cout será de 2 dígitos após o ponto decimal | |
cout.precision(2); | |
cout.setf(ios::fixed); | |
cout << ans << "\n"; | |
} |