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 ![S(U)](https://i0.wp.com/noic.com.br/wp-content/plugins/latex/cache/tex_d9f214d848af7d8b18cce93120f59780.gif?w=640&ssl=1)
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 ![O(E^3)](https://i0.wp.com/noic.com.br/wp-content/plugins/latex/cache/tex_d5325179241baed0bd6c102b90267f49.gif?w=640&ssl=1)
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 ![O(NM^2)](https://i0.wp.com/noic.com.br/wp-content/plugins/latex/cache/tex_2426424c17273961e448ad09ef789f04.gif?w=640&ssl=1)
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"; | |
} |