Sunday, December 9, 2012

BIT - Binary Indexed Tree e Interval Product (Latin America 2012)

Eu decidi criar esse post para falar sobre as famosas Binary Indexed Trees (BITs) e explicar sua aplicação em um problema que apareceu na final brasileira da Maratona de Programação de 2012. A BIT é uma estrutura bem interessante que armazena a soma acumulada em um vetor de maneira eficiente. Seu tamanho é estático (isto é, definido ao criar a estrutura) e tem 2 operações: update(idx, val) e read(idx). update é uma função que soma val na posição idx e read é uma operação que lê a soma acumulada até a posição idx. Ambas operações são executadas em O(lgN).

Sabendo isso eu recomendo a leitura do artigo do Topcoder sobre BITs (em inglês). Não considero que seja necessário saber com detalhes por que ela funciona, o importante é saber como usá-la e quando usá-la, por isso eu não vou entrar nesse mérito. Mas se você tiver interesse em entender exatamente o funcionamento é importante saber como inteiros são representados em binário (complemento de 2).

Quem costuma usar árvores de intervalos completas deve se perguntar qual a vantagem de usar uma BIT, já que uma árvore pode fazer muito mais operações e que não dependem de começar do início. A resposta é basicamente uma: tamanho do código, o código inteiro da BIT tem umas 10 linhas, enquanto o código de uma árvore de intervalos tem bem mais do que isso. O código inteiro dela é (copiado do artigo do Topcoder):

int read(int idx){
 int sum = 0;
 while (idx > 0){
  sum += tree[idx];
  idx -= (idx & -idx);
 }
 return sum;
}
 
void update(int idx ,int val){
 while (idx <= MaxVal){
  tree[idx] += val;
  idx += (idx & -idx);
 }
}
 

Começou a achar interessante a BIT agora? Acho que a maneira mais fácil de se pensar em uma BIT quando você está trabalhando com soma acumulada é imaginar realmente que ela é um vetor de soma acumulada. Ou seja, suponha que você tenha o seguinte vetor v={0, -1, 4, 2, 1, 2}, o vetor de soma acumulada seria sum={0, -1, 3, 5, 6, 8}.

Para inicializar a BIT é preciso zerar todas posições do vetor tree. Importante: Você não pode usar a posição 0 da BIT, pois ela não é capaz de lidar com esse valor (0 & -0 = 0, ou seja, ela entra em loop infinito).

Depois, é preciso adicionar cada elemento no vetor, chamando update(i, v[i]) para assim criar o vetor de soma acumulada. Depois disso, para saber a soma acumulada até um determinado ponto basta chamar read(i), ou então, se você quiser saber a soma de um intervalo[i,j] basta fazer read(j)-read(i-1).

Se você quiser trocar o valor de uma dada posição i de a para b basta chamar update(i, -a) e update(i, b), assim o valor anterior que estava naquela posição é removido e o novo valor é inserido.

Acredito que esse é o essencial. Vamos ver a aplicação dela no problema Interval Product da final brasileira da Maratona de 2012 então? O enunciado está aqui. Recomendo a leitura com calma para entender exatamente o que o problema pede.

A solução do problema começa com a identificação de que propriedade é interessante de ser usada: não é preciso saber o valor dos números para saber se o produto é positivo ou negativo, basta saber o sinal deles, se houver um número par de números ímpares no produto então o resultado vai ser positivo, caso contrário ele vai ser negativo. Além de perceber que o produto vai ser zero se algum dos valores multiplicados for zero.

Sendo assim a ideia é usar 2 BITs, uma para guardar o número acumulado de números ímpares e outra para guardar o número acumulado de zeros.

Para cada consulta deve-se então pegar o número de ímpares no intervalo e o número de zeros, usando a ideia explicada anteriormente, e fazer a verificação se o número é positivo, negativo ou zero.

Para atualizar também é bem simples, basta manter armazenado o valor de cada posição e quando uma posição é atualizada seguir a ideia explicada para mudar o valor de um elemento na BIT. Ou seja, se o valor anterior naquela posição tinha o mesmo sinal do novo então nada muda, mas se o valor mudou de negativo pra positivo é necessário subtrair 1 na BIT de negativos, se o valor mudar de positivo pra negativo então deve ser adicionado 1 na mesma BIT. Para atualizar a BIT de zeros uma ideia similar deve ser adotada.

Bom, espero que essa breve explicação da funcionalidade da BIT possa ajudar alguém, não ficou tão bom quanto eu queria que tivesse ficado, mas serve como mais um texto a respeito.

Wednesday, July 11, 2012

Lista de exercícios para a OBI

Quem já está acompanhando aqui no blog os comentários sobre os problemas da lista de exercícios para a OBI já está sabendo, mas como a ideia é que todos possam treinar para a OBI com esses problemas, explicarei do que ela se trata.

A lista que está disponível aqui já está com os problemas da primeira semana de treinamento disponível, ela tem 3 níveis, P1, para aqueles que fizeram esse ano PJ e ano que vem farão a prova de P1; P2, para quem fez P1 esse ano; e P2++, problemas mais difíceis e voltados para aqueles interessados em se preparar para a seletiva para a IOI do ano que vem.

O objetivo não é competir para ver quem resolve mais problemas da lista, o intuito desse placar é apenas permitir que exista uma referência de comparação para saber como está seu andamento. Por favor, levem a lista a sério e não fiquem fazendo brincadeiras com os nomes dos colegas, e sejam honestos, marquem apenas os problemas que vocês realmente fizeram.

Mesmo que você não tenha participado da OBI esse ano ou então nem mesmo participe da OBI, você é bem vindo a resolver os problemas propostos, o foco principal é OBI, mas os problemas mais difíceis devem ser interessantes até para quem participa da Maratona de Programação.

A ideia é soltar também sempre comentários sobre os problemas propostos, com códigos da solução depois de um tempo, de forma a auxiliar aqueles que estão com mais dificuldades na resolução ou que querem apenas descobrir se existe outra maneira de resolver o problema. Contribuições de comentários e códigos são, é claro, sempre muito bem vindas.

Ainda não resolvi os problemas Garota Hiperativa (na verdade eu resolvi esse durante a própria prova da Maratona em 2010, mas preciso relembrar e pensar novamente) e Metrô Engenhoso, então, se alguém quiser contribuir com comentários ou soluções, por favor, é só me falar.

Passando a limpo os problemas propostos para a primeira semana:


P1:
Burning Midnight Oil - Comentários
Binary and octal - Comentários
Summation - Comentários
Aeroporto - Comentários
Plug-in - Comentários (outro)
P2:
Regular Bracket Sequence - Comentários
Investindo no mercado de ações 1 - Comentários
Matrizes - Comentários
Bitmap - Comentários
P2++:
Os doces da Candy - Comentários e solução
Cerca do jardim - Comentários e solução
Garota hiperativa
Metrô engenhoso - Resolução do Marcos

Espero colocar os códigos de todos problemas que já têm comentários em breve, mas sintam-se livres pra contribuir com códigos também. Os comentários e solução dos problemas Os doces da Candy e Cerca do Jardim estão em inglês pois eu tinha feito eles no começo do ano e a ideia era atingir o maior público possível com a solução.

Update: O Marcos Kawakami postou comentários da solução para o problema Metrô engenhoso, muito bem feitos por sinal, o link foi adicionado

Update2: Todos problemas das listas da P1 e P2 estão com códigos da solução já disponíveis, só falta agora o problema Garota Hiperativa, que eu espero postar em breve

Matrizes - OBI 2010 e Bitmap - Spoj

E para finalizar os comentários sobre os problemas selecionados para a primeira lista de treinos para OBI, vou falar em um mesmo post do problema Matrizes, da OBI 2010 e disponível no SpojBR (link) e do problema Bitmap, do Spoj (link).

Começando pelo problema Matrizes, o enunciado dá uma fórmula para gerar os valores das matrizes quadradas A e B e pede o valor de uma célula da matriz C = A x B. O tamanho máximo das matrizes é 105 e o valor máximo de cada célula é 104. Ou seja, cada matriz tem 105 x 105 elementos, o que já mostra que é inviável gerar as matrizes para calcular a matriz C resultante.

Pensando com calma em como é feita uma multiplicação de matrizes você consegue perceber que para calcular o valor de uma determinada célula apenas uma linha de A e uma coluna de B importam. Dessa forma basta fazer a soma das multiplicações da linha pela coluna e o valor da posição pedida na matriz C é obtido. É importante lembrar que a soma pode estourar um inteiro comum, já que 105 x 104 x 104 é maior do que 231.

Agora sobre o problema Bitmap, do Spoj (link). O enunciado começa definindo a distância entre dois pontos de uma imagem preto e branco como a distância de Manhattan delas (ou seja, a soma das diferenças absolutas entre x e y) e a partir daí ele pede a distância de cada ponto da imagem para o ponto branco mais próximo.

Observando como é feita a distância entre dois pontos (cada ponto fica a distância 1 de seus vizinhos que dividem um lado com eles) é possível pensar em uma BFS, já que ela é capaz de encontrar o menor caminho entre dois pontos em um grafo no qual as arestas têm todas o mesmo custo.

Mas essa BFS tem uma sacada, você não tem apenas um ponto de partida, pois você quer saber qual a distância mínima de um dos pontos brancos para cada um dos pontos pretos, dessa forma, é como se você estivesse começando sua busca em cada um dos pontos brancos existentes e a partir daí você vai marcando as células encontradas e ainda não visitadas com a distância mínima.

Códigos do Andrei para os problemas Matrizes: http://ideone.com/O5nVk e Bitmap: http://ideone.com/vdliv

Tuesday, July 10, 2012

Aeroporto - OBI 2002

Olá a todos, o post da vez será com comentários sobre o problema Aeroporto, que é da OBI 2002 e está disponível no SpojBR (link).

O problema pede o aeroporto mais congestionado dentre todos os A existentes. O congestionamento de um aeroporto é definido como a soma do número de vôos que chegam mais o número de vôos que saem dele.

Para a resolução do exercício então basta manter um vetor para cada aeroporto com o número de vôos que ele tem. Para cada vôo os contadores do aeroporto de partida e de destino são incrementados. Ao final basta ver qual o maior número de vôos que um aeroporto tem em uma passada pelo vetor e posteriormente percorrer o vetor imprimindo todos aeroportos com o mesmo número de vôos que o máximo.

Código do Andrei para o problema: http://codepad.org/8cPuTEnU

Sunday, March 25, 2012

Candy's Candy - ICPC Latin America 2011

The problem I've just solved is Candy's Candy from ICPC Latin America 2011, the problem statement is available at ICPC Live Archive.

This problem is solved with just a little of mathematical insight. Once you start thinking about it the solution comes easily.

First of all, we will define C as the number of candies each package has and P as the number of packages, also S is the sum of all the candies. So it is not hard to see that P*C=S, so the number of candies C in each package must be a divisor of T.

It can also be noted that C must be a multiple of F (the number of flavors) because each variety package must have at least one candy of each type and at least one variety package must exist in each valid configuration. So we write C = K*F

Now, for a given C, each flavor can be written as: v[i] = C*ni + Ri, Ri < C, this means Ri is the remainder of the division of v[i] (the number of candies of the i-th flavor) by C. This remainder makes for the initial amount of candies available and so if any Ri is different from the others no valid configuration can be done with C. If all Ri's are equal we must then test if R is a multiple of K. This necessity comes from the fact that the amount of candies which don't belong to a one-flavor package must be a multiple of C, so F*R is the amount remaining candies. We reach then: F*R % C = F*R % K*F = R % K = 0.

Ok, so now all restrictions have been verified and we just need to calculate the number of valid configurations. To do so we take m = min{v[i]} and add the floor of (m - R - 1) / C to our answer.

My code is available at: http://codepad.org/MErwgxGa

Monday, March 19, 2012

Ball Stacking - ICPC Latin America 2011

This time I solved the problem Ball Stacking from ICPC Latin America 2011, the statement for this problem is available here.

This problem is solved with dynamic programming. The first thing that should be done to make things easier is changing the organization of the balls, so that each ball depends on the balls to its left and above it. So, this way one ball can only be taken if all the balls from the rectangle with it and the first ball as extremes have also been taken.

Following this, each cell is initialized with the sum of all the balls that must be taken for the current ball to also be taken. Later, the maximum sum is the maximum of the total sum of the current cell plus the value of the cell on its top and to its left, which has already been processed.

The last part is to update the value of the cell. It is the maximum of: {the column going from the top till the current ball, this column plus the value to its top and right, this column plus the value to its right, the value right on top of the current ball}. The column is taken because these balls are required to take the current ball and the balls to the left have still to be processed, they shall take care of taking the balls on top of them. Then the column must be taken together with both the value to the right and to the right and top because these balls to the right require that the ones to the left also be taken. There is still the possibility of taking no extra balls and using the value of the ball above.

I know the explanation was not very good, so any questions you may ask.


My code is available at http://codepad.org/NlZWcutk

Saturday, March 10, 2012

Garden Fence - ICPC Latin America 2011

The problem statement for Garden Fence is available here.

This currently is the problem with the second lowest number of total users who got it accepted and the one with least submissions of all among those from ICPC Latin America 2011 available at the Live Archive.

One important thing to notice before we start trying to place the fence is that we can make its line get as close to a tree (point) as we can without really crossing it, so there is no need to worry about cutting trees which get in the fence's way.

Now, the problem asks us to draw a line to separate the points into two disjoint sets in such a way to minimize a given sum. So, testing every line created by each pair of distinct points should give us the answer, there are N^2 such lines and for each of them we need to see to which side each point belongs, this takes O(N) with the naïve approach, what gives an overall complexity of O(N^3), which is too slow for this problem.

Still thinking about the naïve approach, what could be improved to reduce its complexity? Calculating the whole sum for each pair is making it become too slow, could that be changed so that the current sum could be used to calculate the sum for the next pair of points? Yes, that is possible and is exactly what I did to solve this problem. I just needed a good ordering for the points.

So the idea for my solution is:

For each point:
  • Sort other points according to their angles with current point
  • Calculate for the first point in the ordered set the sum of each side of the line comprised by it and the current point
  • For each point in the ordered set:
    • Rotate the line, in such a way that the current point of the ordered set changes sides
    • Update the sums and check to see if there is a new minimum 
Special care must be taken with collinear points which line cross the current point, they must all be processed at once. Four different sums exist that need to be kept track of, the sums on each side of the line of each kind of point, the composition of them gives the final answer.

The complexity of sorting the points is O(NlgN) and the complexity of rotating the line is O(N) for each point, as this is done once for each point, that gives us O(N^2 + N^2lgN) = O(N^2lgN), and this is fast enough for this problem.

My code is available at codepad