next_inactive up previous





Curso de Pós-Graduação em Ciência da Computação



Projeto e Análise de Algoritmos



Trabalho Prático #1

Departamento de Ciência da Computação
Universidade Federal de Minas Gerais
Belo Horizonte -- Brasil























Aluno: Adriano César Machado Pereira
E-mail: adrianoc@dcc.ufmg.br
Data: 16/Maio/2003

Sumário

Questão 1 - Avaliação de Somatórios

Avaliar as somas:

a) ${\sum_{i=1}^n a^{i}}$


Considere $S_{n}$ a soma da série (o que queremos avaliar), então temos:

\begin{displaymath}S_{n}={\sum_{i=1}^n a^{i}}\end{displaymath}

Expandindo essa série, temos:

\begin{displaymath}S_{n}= a + a^{2}+ a^{3}+ \ldots +a^{n-1}+ a^{n}\end{displaymath}

Multiplicando cada lado da equação por $a$, temos:

\begin{displaymath}aS_{n}=a^{2}+ a^{3}+ \ldots +a^{n}+ a^{n+1}\end{displaymath}

Realizando a subtração de $aS_{n}$ por $S_{n}$, temos:

\begin{displaymath}aS_{n}-S_{n}=a^{n+1}-a\end{displaymath}

Colocando $S_{n}$ em evidência, temos:

\begin{displaymath}S_{n}(a-1)=a(a^{n}-1)\end{displaymath}

Isolando o termo $S_{n}$, que é a soma buscada, obtemos:

\begin{displaymath}S_{n}=a\frac{a^{n}-1}{a-1}\end{displaymath}

para o caso em que $a \neq 1$.

Já para $a = 1$ temos a solução trivial $S_{n}=n$, obtida a partir da soma do valor $1$ que ocorre $n$ vezes.
Observação: vale ressaltar que para valor de $n$ tendendo a infinito, a série converge somente quando $-1 < a < 1$, fazendo com que a soma vá converter para $S_{n}=\frac{a}{1-a}$.







b) ${\sum_{i=1}^n \frac{1}{i}}$

De acordo com Cormen [5], para inteiros positivos $n$, o $n$-ésimo número harmônico é:


\begin{displaymath}H_{n}= 1+ \frac{1}{2}+\frac{1}{3}+\frac{1}{4}+\ldots+\frac{1}{n}\end{displaymath}

Essa soma é exatamente o somatório ${\sum_{i=1}^n \frac{1}{i}}$, que gera como resultado:

\begin{displaymath}= ln (n) + O(1)\end{displaymath}

Podemos demonstrar esse resultado utilizando a técnica de aproximação por integral:
$\displaystyle \int_{m}^{n+1} f(x) dx \leq \sum_{i=m}^n f(i) \leq \int_{m-1}^n
f(x) dx$     (1)
$\displaystyle {\sum_{i=1}^n \frac{1}{i}} \geq \int_{m}^{n+1} f(x) dx = ln (n+1)$     (2)

Então devemos resolver (1) e (2). A aproximação por integral fornece uma estimativa restrita para o n-ésimo número harmônico. Para o limite inferior, obtemos:
$\displaystyle {\sum_{i=1}^n \frac{1}{i}} \geq ln (n+1)$     (3)

Para o limite superior, obtemos:
$\displaystyle ln(n) + 1 \leq {\sum_{i=1}^n \frac{1}{i}}$     (4)

Portanto, de (3) e (4), temos que:

\begin{displaymath}{\sum_{i=1}^n \frac{1}{i}}=ln (n) + O(1)\end{displaymath}


c) ${\sum_{i=1}^n log (i)}$ Considere $S_{n}$ a soma da série (o que queremos avaliar), então temos:

\begin{displaymath}S_{n}={\sum_{i=1}^n log (i)}\end{displaymath}


\begin{displaymath}S_{n}= log(1)+log(2)+log(3)+\ldots+log(n)\end{displaymath}

Utilizando a propriedade de logaritmo $log(a) + log(b) = log(a*b)$, temos:

\begin{displaymath}S_{n}= log (1*2*3*\ldots*n)\end{displaymath}

Portanto temos que:

\begin{displaymath}S_{n}= log (n!)\end{displaymath}

Utilizando aproximação de Stirling [5], podemos escrever que:
$S_{n}= log (n!)$ como $ \Theta (n log n)$.

Questão 2 - Complexidade de Tempo

Apresente a complexidade de tempo:

1)

        Pesquisa(n);
        if  n <= 1
          then termine
          else begin
              obtenha o maior elemento dentre os n elementos;
             {de alguma forma isto permite descartar 2/5 dos}
             {elementos e fazer uma chamada recursiva no resto}
             Pesquisa(3n/5);
          end;
Podemos escrever a fórmula de recorrência analisando o caso base, quando $n=1$ então $T(1)= 0$ e no caso para $n> 1$ então $T(n)=
(n-1) + T(3n/5)$. Dessa forma, temos que:


\begin{displaymath}\left\{\begin{array}{lr}
T(1)=0&,~\forall~n = 1\\
T(n)=(n-1)+T(3n/5)&,~\forall~n > 1
\end{array}\right.
\end{displaymath}

\begin{eqnarray*}
T(n)&=&(n-1)+T\left(\frac{3}{5}n\right)\\
T\left(\frac{3}{5}n...
...^{k}\frac{3}{5}^{i}n-1\right)+T\left((\frac{3}{5})^{k+1}n\right)
\end{eqnarray*}



A cada iteração, são descartados $2/5$ de $n$. No caso de $k$ iterações temos que a condição de parada seja

\begin{displaymath}\left((\frac{3}{5})^{k}n\right)\leq 1\end{displaymath}

Logo

\begin{eqnarray*}\left((\frac{3}{5})^{k}\right)&\leq& 1/n\\
k &\leq& \log_{3/5}1/n\\
k &\leq& \log_{3/5}1-\log_{3/5}n\\
k &\leq& -\log_{3/5}n\\
\end{eqnarray*}



Somando temos
$\displaystyle T(n)$ $\textstyle =$ $\displaystyle \sum_{i=0}^{k}\left((\frac{3}{5})^{i}\right)(n-1)$ (5)
$\displaystyle T(n)$ $\textstyle =$ $\displaystyle n\sum_{i=0}^{k}\left((\frac{3}{5})^{i}\right)-k$  


Utilizando a soma resolvida na questão 1 (letra a), temos que: $S_{n}={\sum_{i=1}^n a^{i}}$ $=a\frac{a^{n}-1}{a-1}$ para o caso em que $a \neq 1$.

Fazendo um ajuste no somatório para que $i$ inicie com valor 1 e resolvendo esse somatório encontramos o seguinte resultado:

$\displaystyle T(n)$ $\textstyle =$ $\displaystyle \frac{5n}{2} + \log_{3/5}n + 1$  

2)

       FUNCTION Fib(n: integer): integer;
         BEGIN
           IF n = 0
             THEN Fib := 0
             ELSE IF n = 1
               THEN Fib := 1
               ELSE Fib := Fib(n-1) + Fib(n-2)
         END;

A função acima, denominada sequência de Fibonacci, pode ser expressa através de uma relação de recorrência de segunda ordem, de acordo com Grossman [6]:

$T(n) = T(n-1) + T(n-2)$

A fim de solucionar essa equação, podemos supor uma solução exponencial do tipo $r^n$ e substituímos na relação para encontrar a equação característica. Assim, temos:

$r^n = r^{n-1} + r^{n-2}$

Dividindo a equação por $r^{n-2}$ e passando os termos para o lado esquerdo, encontramos a equação característica $r^2 - r - 1 = 0$. Agora basta resolver essa equação do segundo grau e teremos como raízes:

$r1 = \frac{1 + \sqrt{5}}{2}$ e
$r2 = \frac{1 - \sqrt{5}}{2}$

De acordo com o teorema da combinação linear das soluções dessa equação [6], a solução geral obtida é da forma:

$T(n) = Ar_1^n + Br_2^n$.

Ou seja, em nosso caso seria:

$T(n) = A(\frac{1+\sqrt{5}}{2})^n + B(\frac{1-\sqrt{5}}{2})^n$

Agora basta utilizar os valores da condições iniciais fornecidas para se determinar os valores de $A$ e $B$:

$T(0) = 0 = A(\frac{1+\sqrt{5}}{2})^0 + B(\frac{1-\sqrt{5}}{2})^0$
$T(0) = A + B$
$T(1) = 1 = A(\frac{1+\sqrt{5}}{2})^1 + B(\frac{1-\sqrt{5}}{2})^1$
$T(1) = A(\frac{1+\sqrt{5}}{2}) + B(\frac{1-\sqrt{5}}{2})$

Portanto a solução desse sistema é:

$A = \frac{1}{\sqrt{5}}$ e
$B = -\frac{1}{\sqrt{5}}$

Conclui-se então que a solução para os termos da sequência Fibonacci é a seguinte:

$f_n = \frac{1}{\sqrt{5}}(\frac{1+\sqrt{5}}{2})^n - \frac{1}{\sqrt{5}}(\frac{1-\sqrt{5}}{2})^n $

3)

       PROCEDURE Sort1 ( VAR A: ARRAY[1..n] OF integer;
                 i, j: integer ); { n uma potencia de 2 }
         BEGIN
           IF i < j
             THEN BEGIN
               m := (i + j - 1)/2;
               Sort1(A,i,m);
               Sort1(A,m+1,j);
               Merge(A,i,m,j);
              {Merge intercala A[i..m] e A[m+1..j] em A[i..j] }
             END;
         END;

Esse algoritmo realiza os seguintes passos de maneira recursiva:

Sabendo-se que o custo do 1º Sort = $T(n/2)$, o custo do 2º Sort = $T(n/2)$ e o custo do Merge = $n-1$ no pior caso, podemos obter a seguinte relação de recorrência:


\begin{displaymath}\left\{\begin{array}{lr}
T(1)=0&,~\forall~n=1\\
T(n)=2T(n/2)+ (n-1)&,~\forall~n>1
\end{array}\right.
\end{displaymath}

\begin{eqnarray*}
T(n)&=&2T(n/2)+ (n-1)\\
2T(n/2)&=&2^{2}T(n/2^{2})+(n-2)\\
2^...
...
\vdots
\\
2^{x-1}T(n/2^{x-1})&=&2^{x}T(n/2^{x}+ (n-2^{x-1})\\
\end{eqnarray*}



Portanto foi realizada a expansão do problema até que a condição de parada seja atingida. Adicionando lado a lado e realizando as simplificações, obteremos $T(n)$:

\begin{eqnarray*}
T(n)&=&(\sum_{k=0}^{x-1}n-2^{k})+2^{x}T(n/2^{x})\\
T(n)&=&(\s...
...^{x})\\
T(n)&=&n(x) - (\frac{2^{x}-1}{2-1}) +2^{x}T(n/2^{x})\\
\end{eqnarray*}



Considerando $n=2^{x}$ temos que:

\begin{displaymath}T(n)=n log_2(n) - (n-1)\end{displaymath}


\begin{displaymath}T(n)=n log_2(n) - n+ 1 = O(nlog_2(n))\end{displaymath}

4)

PROCEDURE Sort2 ( VAR A: ARRAY[1..n]OF integer;
                  i, j: integer );
{ n uma potencia de 3 }
BEGIN
  IF i < j
  THEN BEGIN
       m := ( (j - i) + 1 )/3;
       Sort2(A,i,i+m-1);
       Sort2(A,i+m,i+ 2m -1);
       Sort2(A,i+2m,j);
       Merge(A,i,i+m,i+2m, j);
       { Merge intercala A[i..(i+m-1)], A[(i+m)..(i+2m-1) e
         A[i+2m..j] em A[i..j] a um custo ( (5n/3) -2 ) }
       END;
END;

Esse algoritmo realiza os seguintes passos de maneira recursiva:

Sabendo-se que o custo do 1º Sort = $T(n/3)$, o custo do 2º Sort = $T(n/3)$, o custo do 3º Sort = $T(n/3)$, e o custo do Merge = $(5n/3)-2$ no pior caso, podemos obter a seguinte relação de recorrência:


\begin{displaymath}\left\{\begin{array}{lr}
T(1)=0&,~\forall~n=1\\
T(n)=3T(n/3)+ (5n/3) -2&,~\forall~n>1
\end{array}\right.
\end{displaymath}

\begin{eqnarray*}
T(n)&=&3T(n/3)+ ((5n/3)-2)\\
3T(n/3)&=&3^{2}T(n/3^{2})+(3^{1}...
...(n/3^{x-1})&=&3^{x}T(n/3^{x})+ (3^{x-1}*(5n/3^{x})-2*3^{x-1})\\
\end{eqnarray*}



Portanto foi realizada a expansão do problema até que a condição de parada seja atingida. Adicionando lado a lado e realizando as simplificações, obteremos $T(n)$:

\begin{eqnarray*}
T(n)&=&(\sum_{k=0}^{x-1}(5n/3^{k+1}*3^{k})-2*3^{k})+3^{x}T(n/3...
...x}T(n/3^{x})\\
T(n)&=&(5n/3)*(x) - (3^{x}-1)+3^{x}T(n/3^{x})\\
\end{eqnarray*}



Considerando $n=3^{x}$ temos que: $x=\log_3n$


\begin{displaymath}T(n)=(5n/3)*(x) - (n-1)+ n*T(1)\end{displaymath}


\begin{displaymath}T(n)=(5n/3)*(\log_3n) - (n-1)\end{displaymath}


\begin{displaymath}T(n)=(5n/3)*(\log_3n) - n +1\end{displaymath}

Portanto a complexidade desta relação de recorrência é da forma: $O(n) = n\log_3(n)$.

Questão 3 - Cálculo de Limite Inferior

Algoritmos em que a sequência ordenada que determinam se baseia apenas em comparações entre os elementos de entrada são denominados algoritmos de ordenação por comparação. Os algoritmos Hepsort, Quicksort e Ordenação por Intercalação são exemplos desse tipo de algoritmo.

A análise da complexidade envolve o problema de se estabelecer um limite inferior para essa classe de algoritmos que se baseiam em comparação entre chaves. Ao obter esse limite torna-se mais intuitivo a análise de desempenho desses algoritmos.

A demonstração formal exige inicialmente definirmos um modelo denominado Árvore de Decisão. Uma Árvore de Decisão é uma árvore binária completa que representa as comparações executadas por um algoritmo de ordenação quando ele opera sobre uma entrada de um determinado tamanho conhecido. A execução do algoritmo de ordenação corresponde a determinar um caminho desde a raiz da árvore de decisão até uma folha. Em cada nó interno é feita uma comparação, sendo que a subárvore determina as comparações subsequentes. Quando se chegaa uma folha, o algoritmo de ordenação estabelece a ordem $a_{1}\leq a_{2}\leq \ldots \leq a_{n}$. Como qualquer algoritmo de ordenação deve ser capaz de produzir cada permutação de sua entrada, então para que uma ordenação por comparação seja correta é necessário que cada uma das $n!$ permutações sobre $n$ elementos apareçam como uma das folhas da árvore de decisão, e que cada uma dessas folhas sejam acessíveis a partir da raiz por um caminho correspondente a um execução real da ordenação por comparação [5].

O problema inicial consiste em achar um árvore de decisão que minimize o número máximo de comparações feitas, ou seja, o comprimento do caminho mais longo da raiz de uma árvore de decisão até qualquer de suas folhas acessíveis, que representa o número de comparações para o pior caso que o algorimo de ordenação correspondente executa. Logo devemos determinar a altura da árvore, conforme descrito em [5,8].

A fim de obter a altura de uma árvore de decisão em que cada permutação aparece como uma folha acessível basta considerarmos $h$ como a altura e $l$ como folhas acessíveis e teremos que cada $n!$ permutações da entrada aparece como uma folha, logo obtemos $n!\leq l$. Comouma árvore binária de altura $h$ não possui mais que $2^{h}$ folhas, logo temos que

\begin{displaymath}n!\leq l \leq 2^{h}\end{displaymath}

Dessa forma, podemos escrever a inequação anterior utilizando as propriedades de logaritmos da seguinte forma:

\begin{displaymath}h \geq log (n!)\end{displaymath}

E utilizando a aproximação de Stirling [5], temos que:

\begin{displaymath}log (n!) = n log n - n/(ln 2) + 1/2 log n + O(1)\end{displaymath}


Nesse caso, estamos interessados somente na complexidade, portanto temos que o limite inferior pode ser obtido da seguinte forma:

Teorema do Limite Inferior 3.1   Qualquer algoritmo de ordenação por comparação exige $n \log n$ comparações no pior caso.

Questão 4 - Estudo Comparativo de Algoritmos de Ordenação Parcial

problema da ordenação parcial ocorre quando se deseja obter os $k$ primeiros elementos de um arranjo contendo $n$ elementos, em uma ordem ascendente ou descendente. Quando $k=1$ o problema se reduz a encontrar o mínimo (ou o máximo) de um conjunto de elementos. Quando $k=n$ caimos no problema clássico de ordenação.

O problema da ordenação parcial ocorre em muitas situações práticas [4]. Por exemplo, para facilitar a busca de informação na Web existem as máquinas de busca, que são sistemas para recuperação de informação na Web [3,9] é comum uma consulta na Web retornar centenas de milhares de documentos relacionados com a consulta. Entretanto, o usuário está interessado apenas nos $k$ documentos mais relevantes, onde $k$, em geral, é menor do que 200 documentos (na realidade o usuário consulta apenas os 10 primeiros documentos mostrados na primeira página de resposta na maioria das vezes). Consequentemente, a comunidade de recuperação de informação necessita de algoritmos eficientes de ordenação parcial.

O objetivo deste trabalho foi realizar um estudo comparativo dos principais algoritmos de ordenação interna que possam ser adaptados para realizar eficientemente ordenação parcial. Aprincípio foi solicitado considerar os seguintes algoritmos de ordenação interna: Inserção, Shellsort, Heapsort, Quicksort [10].

Hoje em dia há várias situações que demandam a ordenação de apenas um subconjunto de elementos dentre seu conjunto total. Nesses casos, ordenar um vetor e listar apenas os $k$ primeiros termos requer trabalho extra, já que os algoritmos estudados de ordenação ordenam totalmente o vetor. A solução trivial para esse tipo de problema é bem simples, sendo que consiste em aplicar um algoritmo de busca por comparação entre chaves (Quicksort, por exemplo) e, após ordenado o vetor, listar apenas os primeiros $k$ elementos. Nesse trabalho prático, implementou-se novas versões desses algoritmos para o problema da Ordenação Parcial e as discussões e resultados obtidos são apresentados nas questões 1 a 4 que se seguem. Decidiu-se por não alterar o Shellsort, uma vez que ele é uma variação do algoritmo de inserção e sua análise de complexidade não é conhecida.

Parte 1

Determine experimentalmente o número esperado de (i) comparações e (ii) movimento-de-registros para cada um dos métodos de ordenação que utilizar nesse trabalho.

Observação: Utilize arquivos de diferentes tamanhos com chaves geradas randomicamente. Use o programa Permut.c apresentado abaixo para obter uma permutação randômica de $n$ valores. Repita cada experimento algumas vezes e obtenha a média para cada medida de complexidade. Dê a sua interpretação dos dados obtidos, comparando-os com resultados analíticos.

Inicialmente gostaria de ressaltar que vou apresentar os algoritmos que criei baseados nos algoritmos de ordenação por inserção, seleção, Quicksort, Heapsort e outra versão do Quicksort que encontrei para o problema da ordenação parcial. Mas gostaria de fazer algumas observações sobre o algoritmo Shellsort a seguir.

Algoritmo Shellsort

Criado por Donald L. Shell em 1959, o Shellsort é apenas uma extensão do algoritmo da inser ção direta. Esta extensão resume-se a dar um valor de salto para o teste de inserção. O Shellsort apresenta ótimos resultados para arquivos de tamanho moderado.

Para o problema de Ordenação Parcial, o Shellsort também seria adequado para arquivos de tamanho moderado, e a sua solução seria a trivialmente obtida. Neste trabalho o objetivo é obter eficiência na ordenação parcial, portanto optei por não utilizá-lo. Além disso, existe a impossibilidade de se analisar a complexidade de tempo desse algoritmo, o que tornaria inviável a minha análise, uma vez que é desconhecida a razão pela qual o Shellsort é eficiente[10].

Não foi possível alterar esse algoritmo de forma a otimizá-lo para o problema em questão, sem que sua essência fosse modificada.

Algoritmo Inserção Parcial

O algoritmo de ordenação por inserção é bastante simples e eficiente para uma quantidade pequena de números. A idéia de funcionamento é semelhante ao ordenamento de cartas de baralho na mão de um jogador. A mão esquerda começa vazia e a mão direita insere uma carta de cada vez na posição correta. Ao final, quando todas as cartas foram inseridas, elas já estão ordenadas. Para encontrar, durante a inserção, a posição correta para um valor, compara-se este valor um-a-um com as cartas já na mão esquerda, até encontrarmos a posição correta.

Neste algoritmo não há necessidade de utilização de nenhuma estrutura de dados extra, além do próprio vetor inicial com os valores. Considere que os números a serem ordenados estão armazenados num vetor. O algoritmo de ordenação por inserção tem complexidade proporcional a $n^2$ no pior caso, onde n é o número de elementos a serem ordenados. O código do algoritmo para ordenação por inserção pode ser obtido em [10,5].

A fim de atender ao problema de ordenação parcial alterei o algoritmo para que fossem desconsiderados os elementos maiores que o k-ésimo elemento já ordenado. Assim, o algoritmo de ordenação parcial ficou da seguinte maneira:

void InsercaoParcial(Vetor A, Indice k)
{
    Indice i, j;
    Item x;
    for (i = 2; i <= n; i++) {
        x = A[i];
        if (i > k) { // Alteração realizada para evitar comparação com 
            j = k;   // elementos maiores que o k-ésimo elemento.
        }
        else {
            j = i - 1;
        }

        A[0] = x;  /* sentinela */
        numComparacoes++;
        while (x.Chave < A[j].Chave) {
            A[j + 1] = A[j];
            numMovimentacoes++;
            j--;
        	numComparacoes++;
        }
        if ( i <= k){
            A[j + 1] = x;
            numMovimentacoes++;
        }else if (j != k){ // Alteração realizada para ordenação dos
            A[j + 1] = x;  // k-ésimos ser feita corretamente.
            numMovimentacoes++;
        } // if

    } // for
}  /* Insercao Parcial */

A tabela 1 ilustra o número esperado de comparações do algoritmo de inserção parcial implementado. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de comparações entre chaves.


Tabela: Número Esperado de Comparações do Algoritmo de Inserção Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 1009 1024 1275 250929
10000 10010 10031 36816 25032488
100000 100010 100031 2644434 2496463806
1000000 1000011 1000033 255649150 -


A tabela 2 ilustra o número esperado de movimento de registros do algoritmo de inserção parcial implementado. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de movimentações.


Tabela: Número Esperado de Movimento de Registros do Algoritmo de Inserção Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 20 41 325 250929
10000 22 54 27402 25032488
100000 22 54 2550044 2496463806
1000000 24 57 254705102 -


Algoritmo Seleção Parcial

O método de ordenação por Seleção Direta é levemente mais eficiente que o método Bubblesort, ainda que se trate de um algoritmo apenas para ordenação de pequenos arranjos.

A lógica consiste em se varrer o arranjo comparando todos os seus elementos com o primeiro. Caso o primeiro elemento esteja desordenado em relação ao elemento que está sendo comparado com ele no momento, então é feita a troca. Ao se chegar ao final do arranjo, teremos o menor valor na primeira posição do arranjo.

Este primeiro passo nos garante que o menor elemento fique na primeira posição. Continuamos, assim, a varrer os demais elementos, comparando-os com a segunda posição do arranjo (já desconsiderando a primeira posição, que foi anteriormente ordenada em relação ao arranjo como um todo).

O algoritmo original da Seleção tem ordem de complexidade igual a $O(n^2)$, pois para cada um dos $n$ elementos a serem ordenados, necessita-se de uma busca nos $n$ elementos restantes, a menos daqueles que já estão ordenados. A alteração realizada, reduz a função assintótica para $O(k*n)$ para valores de k menores do que n, onde normalmente $k<<n$.

O algoritmo de seleção parcial que implementei é bem parecido com o original. Nessa versão modificada apenas modifiquei de $n$ por $k$ no anel mais externo para iterar somente sobre os primeiros $k$ elementos. O código do algoritmo de ordenação por seleção parcial é apresentado a seguir:

void SelecaoParcial (Vetor A, Indice k)
{
    Indice i, j, Min;
    Item x;

    for (i = 1; i < (k+1); i++) {        // Alteracao realizada para 
        Min = i;                         // pegar só k elementos
        for (j = i + 1; j <= n; j++) {
            numComparacoes++;
            if (A[j].Chave < A[Min].Chave) {
                Min = j;
            }
        }

        x = A[Min];
        A[Min] = A[i];
        numMovimentacoes++;
        A[i] = x;
        numMovimentacoes++;
}
}  /* Selecao Parcial */

A tabela 3 ilustra o número esperado de comparações do algoritmo de seleção parcial implementado. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de comparações entre chaves.


Tabela: Número Esperado de Comparações do Algoritmo de Seleção Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 999 1997 9945 499500
10000 9999 19997 99450 4995000
100000 99999 199997 994500 49950000
1000000 999999 1999997 9945000 -


A tabela 4 ilustra o número esperado de movimento de registros do algoritmo de seleção parcial implementado. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de movimentações.


Tabela: Número Esperado de Movimento de Registros do Algoritmo de Seleção Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 2 4 20 2000
10000 2 4 200 20000
100000 2 4 2000 200000
1000000 2 4 20000 -



Algoritmo QuickSort Parcial

O algoritmo Quicksort parte da idéia de dividir o problema de ordenação de $n$ elementos em dois problemas menores e idênticos. Para isso é escolhido um elemento, que chamamos de pivô. Esse pivô particiona o conjunto em dois: um com os elementos menores do que ele e outro com os elementos maiores. A partir desse ponto, o algoritmo é executado recursivamente nas duas partições geradas.

A otimização que foi proposta para o problema da ordenação parcial consiste em podar o conjunto, desprezando todos os elementos maiores do que o $k$-ésimo elemento. Sendo assim, há três possíveis situações, que podem ocorrer de acordo com a escolha do pivô:

O código do algoritmo Quicksort parcial implementado é apresentado a seguir:

void Particao(Vetor A, Indice Esq, Indice Dir, Indice *i, Indice *j)
{
    Item x, w;

    *i = Esq;
    *j = Dir;
    x = A[(*i + *j) / 2];   /* obtem o pivo x */
    do {
		numComparacoes++;
		while (A[*i].Chave < x.Chave){
			(*i)++;
			numComparacoes++;
		}
		numComparacoes++;
		while (A[*j].Chave > x.Chave){
			(*j)--;
			numComparacoes++;
		}
		if (*i <= *j) {
			w = A[*i];
			A[*i] = A[*j]; numMovimentacoes++;
			A[*j] = w; 	   numMovimentacoes++;
			(*i)++;
			(*j)--;
		}
    } while (*i <= *j);   /* Particao */
}


void Ordena(Vetor A, Indice Esq, Indice Dir, Indice k)
{
    Indice i, j;

    Particao(A, Esq, Dir, &i, &j);
    /* Otimizacao */
    if ( (j - Esq) >= (k-1) ){
	if (Esq < j)
	    Ordena(A, Esq, j, k);
    }else{
	if (Esq < j)
	    Ordena(A, Esq, j, k);
	if (i < Dir)
	    Ordena(A, i, Dir, k);
    }
}  /* Ordena */


void QuicksortParcial(Vetor A, Indice k)
{
    Ordena(A, 1, n, k);
}  /* Quicksort */

A tabela 5 ilustra o número esperado de comparações do algoritmo Quicksort parcial implementado. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de comparações entre chaves.


Tabela: Número Esperado de Comparações do Algoritmo de Quicksort Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 754 755 794 5804
10000 6874 6874 7853 72663
100000 39010 39010 59664 882264
1000000 616116 616116 764774 10338608


A tabela 6 ilustra o número esperado de movimento de registros do algoritmo Quicksort parcial implementado. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de movimentações.


Tabela: Número Esperado de Movimento de Registros do Algoritmo de Quicksort Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 744 745 778 5230
10000 6857 6857 7751 66915
100000 38998 38998 58951 825053
1000000 616091 616091 753421 9766686


Algoritmo HeapSort Parcial

Heapsort é um método de ordenação cujo princípio de funcionamento é o mesmo utilizado para a ordenação por seleção, a saber:

O custo para encontrar o menor (ou o maior) item entre n itens custa n-1 comparações. Esse custo pode ser reduzido através da utilização de uma estrutura de dados denominada fila de prioridades [10].

O algoritmo Heapsort parcial implementado foi elaborado invertendo inicialmente a construção do Heap. Para isso alterei a função Refaz do algoritmo fornecido pelo livro [10]. Dessa forma os elementos de menor valor foram sendo colocados no topo da pilha, possibilitando refazer a pilha somente até o k-ésimo elemento.

O código do algoritmo Heapsort parcial implementado é apresentado a seguir:

void Refaz(Indice Esq, Indice Dir, Vetor A)
{
    Indice i;
    int j;
    Item x;

    i = Esq;
    j = i * 2;
    x = A[i];
    while (j <= Dir) {
	numComparacoes++;
	if (j < Dir) {
	    if (A[j].Chave >= A[j + 1].Chave){ // Alteracao realizada para construir
			                               // o heap de maneira invertida
			j++;
		}
	}
	numComparacoes++;      
	if (x.Chave < A[j].Chave){ // Alteracao realizada para construit
		                       // o heap de maneira invertida
	    goto LABEL;
	}
	A[i] = A[j];
	numMovimentacoes++;
	i = j;
	j = i * 2;
    }
LABEL:
    A[i] = x;
	numMovimentacoes++;
}  /* Refaz */


void HeapsortParcial(Vetor A, Indice k)
{
    Indice Esq, Dir;
    Item x;
	int aux = 0;       // Alteracao 3 - variável que controla a posição
	                   //               no heap invertido

    /* constr  o i o heap */
    Esq = n / 2 + 1;
    Dir = n;
    while (Esq > 1) {
	Esq--;
	Refaz(Esq, Dir, A);
    }
    /* ordena o vetor */
    while (aux < k) { // Alteracao 4 - mantêm os menores elementos 
		              // ordenados nas posições (n - k)
	x = A[1];
	A[1] = A[n - aux];
	A[n - aux] = x;
	Dir--;
	aux++;
	Refaz(Esq, Dir, A);
    }
}  /* HeapsortParcial */

A tabela 7 ilustra o número esperado de comparações do algoritmo Heapsort parcial implementado. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de comparações entre chaves.


Tabela: Número Esperado de Comparações do Algoritmo de Heapsort Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 663 668 703 4418
10000 6717 6722 7311 60535
100000 66692 66701 74526 771174
1000000 666483 666492 762259 9399931


A tabela 8 ilustra o número esperado de movimento de registros do algoritmo Heapsort parcial implementado. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de movimentações.


Tabela: Número Esperado de Movimento de Registros do Algoritmo de Heapsort Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 1727.8 1738 1824 11058.8
10000 17412.4 17426.4 18817.2 144207
100000 174362.2 174379.8 191746.8 1774789.8
1000000 1744042 1744062.6 1950859.2 21048350.4


Algoritmo Partial QuickSort

O algoritmo desta subseção foi obtido em [2] e utilizado como comparação para minhas implementações realizadas. O código desse algoritmo é listado a seguir. Realizou-se as devidas instrumentações para efeitos experimentais.

void PartialQuickSort(T v[], int a, int b, int first, int last)
{
        if ( ! overlap(a, b, first, last) )
                return;
        // caso básico (tamanho <= 1)
        if (a >= b)
                return;
        // inicializações
        T x = v[(a + b) / 2];
        int i = a;
        int j = b;
        // passo de partição
        do
 {
                numComparacoes++;
                while (v[i] < x){
                    i++;
                    numComparacoes++;
                }
                numComparacoes++;
                while (v[j] > x){
                    j--;
                    numComparacoes++;
                }
                if (i > j){
                        break;
                }
                T tmp = v[i];
                v[i++] = v[j];
                numMovimentacoes++;
                v[j--] = tmp;
                numMovimentacoes++;
        } while (i <= j);
        PartialQuickSort(v, a, j, first, last);
        PartialQuickSort(v, i, b, first, last);
}

A tabela 9 ilustra o número esperado de comparações do algoritmo Partial Quicksort, implementação obtida em C++. Esses valores foram coletados experimentalmente, executando vários testes com esse algoritmo, que foi instrumentado,obtendo posteriormente a média de comparações entre chaves.


Tabela: Número Esperado de Comparações do Algoritmo Partial Quicksort
k
n 1 2 $<< n$ (k=1%n) n
1000 1678.6 1680.4 1817.8 13256.8
10000 24817 24819.2 25747.2 179772.2
100000 217590.8 217606.6 233991 2241206
1000000 1920362.2 1920362.2 2094874.8 27387556.6


A tabela 10 ilustra o número esperado de movimento de registros do algoritmo Partial Quicksort, implementação obtida em C++. Esses valores foram coletados experimentalmente, executando vários testes com o algoritmo, que foi instrumentado,obtendo posteriormente a média de movimentações.


Tabela: Número Esperado de Movimento de Registros do Algoritmo de Partial Quicksort
k
n 1 2 $<< n$ (k=1%n) n
1000 610 610 642 5205
10000 4288 4288 4676 67015
100000 39928 39928 59341 823670
1000000 665958 665958 731330 9773111


Parte 2

Uma opção alternativa é considerar os mesmos algoritmos propostos e determinar experimentalmente o tempo de execução de cada um dos métodos de ordenação indicados acima.

Observação: Use o programa Permut.c para gerar arquivos de tamanhos 1.000, 10.000, 100.000 e 1.000.000 de registros. Para cada tamanho de arquivo dê a sua interpretação dos dados obtidos.

Algoritmo Inserção Parcial

A tabela 11 ilustra o tempo de execução do algoritmo de inserção parcial implementado. Esses valores foram obtidos experimentalmente, executando vários testes com o algoritmo, que foi instrumentado, obtendo posteriormente a média de tempo de execução.


Tabela: Tempo (em microsegundos) de Execução do Algoritmo de Inserção Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 31.2 38.2 38.2 5205
10000 291 283.2 814.4 1093676.4
100000 3989.4 12020.2 149291 121256842.6
1000000 37825 108237 10747130 -


Algoritmo Seleção Parcial

A tabela 12 ilustra o tempo de execução do algoritmo de seleção parcial implementado. Esses valores foram obtidos experimentalmente, executando vários testes com o algoritmo, que foi instrumentado, obtendo posteriormente a média de tempo de execução.


Tabela: Tempo (em microsegundos) de Execução do Algoritmo de Seleção Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 16.2 28.4 116.8 5706.2
10000 150.2 254.6 25460 1144377
100000 2432.2 32867.4 4400936.6 154876392
1000000 21707 163693 434079561 -


Algoritmo QuickSort Parcial

A tabela 13 ilustra o tempo de execução do algoritmo Quicksort parcial implementado. Esses valores foram obtidos experimentalmente, executando vários testes com o algoritmo, que foi instrumentado, obtendo posteriormente a média de tempo de execução.


Tabela: Tempo (em microsegundos) de Execução do Algoritmo de Quicksort Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 55 55.2 59.2 409
10000 568.4 571.2 658.4 5177.2
100000 4430.6 4584.2 30128.4 159373.8
1000000 131474.2 176242.8 178860.4 1689749.6


Algoritmo HeapSort Parcial

A tabela 14 ilustra o tempo de execução do algoritmo Heapsort parcial implementado. Esses valores foram obtidos experimentalmente, executando vários testes com o algoritmo, que foi instrumentado, obtendo posteriormente a média de tempo de execução.


Tabela: Tempo (em microsegundos) de Execução do Algoritmo de Heapsort Parcial
k
n 1 2 $<< n$ (k=1%n) n
1000 65.2 65.6 69.4 479.6
10000 626.4 617 678.8 6302.8
100000 11116 20646.8 31541 200801.8
1000000 268991.8 259120.2 339932.2 3939563.6


Algoritmo Partial QuickSort

A tabela 15 ilustra o tempo de execução do algoritmo Partial Quicksort, implementação obtida em C++. Esses valores foram obtidos experimentalmente, executando vários testes com o algoritmo, que foi instrumentado, obtendo posteriormente a média de tempo de execução.


Tabela: Tempo (em microsegundos) de Execução do Algoritmo Partial Quicksort
k
n 1 2 $<< n$ (k=1%n) n
1000 44.4 44.6 47.2 398.8
10000 431.4 419.2 455.2 5083.4
100000 5205.4 5739.6 6655.8 102162.8
1000000 132136 137435.6 156575.6 1673920.2


Parte 3

Apresente resultados analíticos para os algoritmos estudados. Considere diferentes valores de $k$, tais como $k=1$, $k=2$, $k<<n$, e $k=n$, onde o caso $k<<n$ é o mais interessante.

Algoritmo Inserção Parcial

A análise de complexidade do algoritmo inserção parcial apresentado é a seguinte:

\begin{displaymath}\sum_{i=2}^{k-1}i + \sum_{i=k}^{n}k\end{displaymath}


\begin{displaymath}(kn-k^{2}+k)+(\frac{k^{2}-k}{2})-1\end{displaymath}

Dessa forma, a partir do momento que o k-ésimo elemento é encontrado, então a soma fica da forma $(2+3+4+5+\ldots + k + k + k \ldots)$. Substituindo $k$ por $n$ obteríamos a análise de complexidade apresentada em [10], ou seja:
$C(n)=(n^{2} + n -2)/2$.

Algoritmo Seleção Parcial

A análise de complexidade do algoritmo de seleção parcial implementado é a seguinte:


\begin{displaymath}\sum_{i=1}^{k}\sum_{j=i+1}^{n}1\end{displaymath}


\begin{displaymath}\sum_{i=1}^{k}(n-i) = (n-1)+(n-2)+ \dots + (n-k)\end{displaymath}


\begin{displaymath}\sum_{i=1}^{k}(n-i) = (n-1)k/2\end{displaymath}


\begin{displaymath}O(n)= (nk - k)/2\end{displaymath}

Para o caso de $n$ ser $k$ encontraremos a complexidade de tempo apresentada em [10].

Algoritmo QuickSort Parcial

A análise de complexidade do algoritmo Quicksort parcial implementado é mais complicada que os demais e requer analisar os três casos previamente descritos na subseção 4.1.4.

Já é sabido que se ao gerar o pivô se uma das partições fica vazia, então temos o pior caso possível. Nesse caso a ordem de complexidade seria da ordem de $O(n)=n^{2}$. Essa demonstração não é intuitiva, mas sua demonstração pode ser vista em [5]. A outra possibilidade seria o melhor caso do algoritmo implementado, onde o pivô escolhido para particionar o vetor, seria o nosso $k$. Sendo assim a recursividade seria aplicada somente na partição onde se encontra somente os valores menores que $k$. Essa complexidade seria da ordem de $O(n)=(k-1)\log (k-1)$. Mas essa situação é bem difícil de ocorrer.

Mas a situação mais provável de ocorrer é quando a partição com elementos menores que o pivô possua maior número de elementos que $k$. E nesse caso a complexidade conhecida do QuickSort [10] é aplicada, ou seja: $O(n)=n \log n$. Porém essa complexidade é aplicada somente ao conjunto de elementos menores que o pivô e as demais partições são descartadas. Sendo assim a complexidade do Quicksort parcial para esse terceiro caso é variável no intervalo que vai de $O(n)=(k-1)\log (k-1)$ até $O(n)=(n-1) \log (n-1)$.

Algoritmo HeapSort Parcial

A análise de complexidade do HeapsortParcial é trivial. É sabido que a construção do Heap possui custo igual a $\log n$. Com a alteração realizada no código, a busca para os $k$ primeiros termos é linear, logo a função Refaz é executada $k$ vezes, logo a complexidade do Heapsort Parcial é $k\log n$. Observe que no caso em que $k$ for igual a $n$, a complexidade obtida será $O(n)=n \log n$, já conhecida na literatura.

Conclusão

A fim de se concluir os aspectos mais interessantes deste trabalho, nesta seção são apresentados alguns resultados comparativos e observações dos mesmos. Vale ressaltar que todos os experimentos foram realizados em uma máquina com processador AMD ATHLON 1 GHz, com 1 GHz de memória, rodando sistema operacional Linux Debian Linux - versão 2.4.20.

As tabelas 171618 fazem um resumo dos resultados obtidos para vários valores de k aplicando as análises de complexidade obtidas para cada um dos algoritmos implementados, levando-se em conta o tamanho do conjunto igual a 100.000 elementos.


Tabela: Número de Movimentações de Chaves dos Algoritmos para n = 100.000
k
Algoritmos 1 2 $<< n$ (k=1%n) n
Inserção parcial 22 54 2550044 2496463806
Seleção parcial 2 4 2000 200000
Quicksort parcial 38998 38998 58951 825053
Heapsort parcial 174362.2 174379.8 191746.8 1774789.8
Partial Quicksort 39928 39928 59341 823670


A análise da tabela 16 possibilitou concluir que, em termos movimentação de de chaves, o melhor algoritmo foi o Quicksort parcial implementado no trabalho. Para isso foi considerado o valor de n mais interessante, ou seja, $n << k$.


Tabela: Número de Comparações dos Algoritmos para n = 100.000
k
Algoritmos 1 2 $<< n$ (k=1%n) n
Inserção Parcial 100010 100031 2644434 2496463806
Seleção parcial 99999 199997 994500 49950000
Quicksort parcial 39010 39010 59664 882264
Heapsort parcial 66692 66701 74526 771174
Partial Quicksort 217590.8 217606.6 233991 2241206


A análise da tabela 17 possibilitou concluir que, em termos de comparação entre chaves, o melhor algoritmo foi o Quicksort parcial implementado no trabalho. Para isso foi considerado o valor de n mais interessante, ou seja, $n << k$.


Tabela 18: Tempo (em microsegundos) dos Algoritmos para n = 100.000
k
Algoritmos 1 2 $<< n$ (k=1%n) n
Inserção parcial 3989.4 12020.2 149291 121256842.6
Seleção parcial 2432.2 32867.4 4400936.6 154876392
Quicksort parcial 4430.6 4584.2 30128.4 159373.8
Heapsort parcial 11116 20646.8 31541 200801.8
Partial Quicksort 5205.4 5739.6 6655.8 102162.8


Porém para o problema proposto (ordenação parcial), a métrica mais importante é eficiência em termos de tempo computacional. A análise da tabela 18 demonstra que, sendo o tempo mínimo de execução o objetivo principal, o algoritmo Partial Quicksort é o que proporcionou o melhor resultado. Vale ressaltar que isso foi possível, mesmo realizando mais movimentações de chaves e comparações entre elas, devido a um passo inicial que ele executa, lhe possibilitando obter melhor elemento pivô.

Considerando diferentes valores de $k$, tais como $k=1$, $k=2$, $k<<n$ e $k=n$, onde o caso é o mais interessante, podemos descrever a complexidade, como pode ser visto na tabela 19.


Tabela: Análise de Complexidade para Ordenação Parcial
Algoritmo $k=1$ $k=2$ $k<<n$ $k=n$
Inserção $O(n)$ $O(2*n) $ $O(k*n)$ $O(n^2)$
Seleção $O(n)$ $O(2*n) $ $O(k*n)$ $O(n^2)$
Heapsort $O(nlog(n))$ $O(nlog(n))$ $O(nlog(n))$ $O(nlog(n))$
Quicksort $O(nlog(n))$ $O(nlog(n))$ $O(nlog(n))$ $O(nlog(n))$


Vale ressaltar que o algoritmo Quicksort possui complexidade da ordem $O(n^2)$ no pior caso, quando a pivotação gera uma partição com $n-1$ elementos e a outra vazia.

Parte 4

Procure trabalhos relacionados na literatura (se houver) que tratem do assunto e descreva suscintamente esses trabalhos.

Durante o estudo e implementação deste trabalho, alguns trabalhos relacionados foram relevantes e podem ser úteis para os interessados no assunto. Esses trabalhos são suscintamente descritos a seguir.

Algoritmo para encontrar k-ésimo elemento

No livro The Design and Analysis of Computer Algorithms [1] é apresentado um algoritmo para encontrar o $k$-ésimo elemento, cuja complexidade é linear. A conjugação desse algoritmo, com o algoritmo Quicksort Parcial implementado nesse trabalho seria a melhor opção encontrada para o problema da ordenação parcial. A implementação desse algoritmo para encontrar o pivô teria complexidade linear. O k-ésimo menor elemento seria escolhido para ser o pivô do algoritmo de ordenação parcial Quicksort Parcial, o que teria complexidade $O((k-1)log(k-1))$. Portanto esse algoritmo conjugado com a solução apresentada teria complexidade $O((k-1)log(k-1)) + O(n)$.

partial_sort - funcao do C++

Existe uma função em C++ que realiza ordenação parcial. Essa função, denominada partial_sort é baseada historicamente no Heapsort, sendo que sempre ordena os $k$ primeiros elementos de um conjunto com $n$ elementos em ordem de complexidade $O(nlog(n))$. Mais referências sobre essa função podem ser obtidas em [7].

Agradecimentos

Os estudos em conjunto foram muito proveitosos e contribuiram muito para meu aprendizado, assim como as discussões relacionadas ao tema deste trabalho prático. Por isso agradeço aos colegas e amigos Bruno Dias Abrahão, Erikson Freitas de Morais, Jacques Fux, Leonardo Barbosa e Oliveira, entre outros.

Referências Bibliográficas

1
AHO, HOPCROFT, U.
The Design and Analysis of Computer Algorithms.
Addison-Wesley Publishing Company, 1974.

2
ALGORITHM, P. S.
Licenciatura em engenharia electrotécnica e de computadores - disciplina de algoritmos e estruturas de dados, 2001 - http://www.fe.up.pt/ jlopes/teach/2000-01/AED/probs/res-folha-3.pdf.

3
BAEZA-YATES, R., AND RIBEIRO-NETO, B.
Modern Information Retrieval.
Addison-Wesley, 1999.

4
BARBOSA, R. A.
Problema de ordenação parcial - comunicação pessoal, akwan information technologies, 2003.

5
CORMEN, T. H., LEISERSON, C. E., AND RIVEST, R. L.
Introduction to Algorithms.
MIT Press/McGraw-Hill, Cambridge, Massachusetts, 1990.

6
GROSSMAN, J. W.
Discrete Mathematics - An Introduction to Concepts, Methods, and Applications.
Macmillan Publishing Company, New York, NY, 1990.

7
JOSUTTIS, N. M.
The C++ standard library: a tutorial and reference.
Addison-Wesley Longman Publishing Company, Boston, MA, 1999.

8
KNUTH, D. E.
Fundamental Algorithms, second ed., vol. 1 of The Art of Computer Programming.
Addison-Wesley, Reading, Massachusetts, 1973.

9
WITTEN, I. H., MOFFAT, A., AND BELL, T.
Managing Gigabytes Compressing and Indexing Documents and Images.
Morgan Kaufmann Publishers (second edition), 1999.

10
ZIVIANI, N.
Projeto de Algoritmos com Implementações em Pascal e C.
Editora Pioneira, São Paulo, Brasil, 1993.

About this document ...




Curso de Pós-Graduação em Ciência da Computação



Projeto e Análise de Algoritmos



Trabalho Prático #1

This document was generated using the LaTeX2HTML translator Version 2K.1beta (1.48)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -split 0 trabalho1

The translation was initiated by Adriano Cesar Machado Pereira on 2003-05-19


next_inactive up previous
Adriano Cesar Machado Pereira 2003-05-19