Estruturas de Dados 2019/2020 (CC1007) - DCC/FCUP

Aula Prática #09 - Complexidade Algorítmica
(semana de 20/04 a 24/04)


Conteúdos das teóricas

Nesta aula iremos abordar conceitos de complexidade algorítmica. Será por isso conveniente ver o que foi falado nas teóricas:


Exercício 1) Notação assintótica

[ver slides 25 a 34]

Para os pares de funções seguintes indique se é verdadeira ou falsa cada uma das seguintes afirmações: f(n) = O(g(n)), f(n) = \Omega(g(n)) e f(n) = \Theta(g(n)). Explique sucintamente as suas opções.

  1. f(n) = 5n^2 - 100n + 34$ ; $g(n) = 2n^3 - 97
  2. f(n) = 54n + 100$ ; $g(n) = 6n - 24
  3. f(n) = 2^n$ ; $g(n) = 6n^2
  4. f(n) = 30$ ; $g(n) = \log 30
  5. f(n) = n \log n + n$ ; $g(n) = n^2
  6. f(n) = \sqrt(n)$ ; $g(n) = \log n

Exercício 2) Complexidade do TAD Conjunto

  1. Recorde o TAD Conjunto (IntSet) dado nas aulas.

    Para a implementação ArrayListIntSet (que usava um array como lista dos números do conjunto), qual a complexidade temporal de cada um dos seguintes métodos? (use notação \Theta, para ser mais preciso - espreite o slide 40 para um exemplo nos métodos de listas):

    1. contains(int x)
    2. add(int x)
    3. remove(int x)
    4. size()
    5. clear()

  2. Na aula #05 foi sugerida uma implementação usando um array de booleanos (ao invés de manter uma lista de números). Recordando o que foi pedido na sua essência:

    - Manter um array isElem[] de valores booleanos (verdadeiro ou falso). isElem[i] diz-nos se o número i está ou não no conjunto. O tamanho do array determina o tamanho do número máximo. Manter numa outra variável size o número de elementos. Por exemplo, o conjunto {1,5,7} seria representado por isElem[]={F,T,F,F,F,T,F,T,F,F,F} e size=3

    0 1 2 3 4 5 6 7 8 9
    F T F F F T F T F F

    Para esta implementação com o array de booleanos, qual é a complexidade temporal de cada um dos 5 mesmos métodos do TAD IntSet? O que melhorou em relação ao ArrayListIntSet? Qual é o tradeoff? (ou seja, para conseguir melhor complexidade temporal, o que passamos a precisar em termos de memória? quais as limitações desta nova implementação?)

Exercício 3) Previsão do tempo de execução

[ver slides 35 a 37]

Imagine que tem um programa P implementado, que recebe como input n números. Ao experimentar executá-lo com testes aleatorizados, obteve os seguintes tempos de execução:

  1. Qual será a complexidade temporal mais provável do programa P? Justifique.
  2. Indique uma estimativa de quanto tempo demoraria o programa para um caso com 5000 números. Justifique.

Exercício 4) Problema do subarray contíguo de soma máxima

O objectivo deste exercício é ter algoritmos de diferentes complexidades para um mesmo problema e ir verificando a sua eficiência temporal. Começe por ler o enunciado do problema, disponível em [ED198] Um Jogo com Sequências (Volume 5).

  1. Uma primeira solução com força bruta com tempo Θ(n3)

    Quais são todas as subsequências contíguas possíveis? Seja v[] um array contendo a sequência e começado na posição 0. As sequências possíveis são então todos os subarrays v[i..j] tal que 0≤i≤j<n.

    Uma solução exaustiva seria passar por todas estas subsequências e para cada uma delas calcular o valor da respectiva soma, escolhendo a melhor possível. Supondo que já temos a leitura feita para o array v[], uma maneira de fazer isto seria a indicada pelo código seguinte:

          int maxSoFar = v[0]; // porque é esta uma boa escolha para a melhor soma inicial?
          for (int i=0; i<n; i++) // Todas as posicoes iniciais possiveis
             for (int j=i; j<n; j++) { // Todas as posicoes finais possiveis
                int sum = 0;
                for (int k=i; k<=j; k++) // Calcular soma entre posicoes i e j
                   sum += v[k];
                System.out.println("Soma entre " + i + " e " + j + " = " + sum);
                if (sum > maxSoFar) maxSoFar = sum;
             }
          System.out.println(maxSoFar);
    

    Perceba o que faz o código de cima, implemente a parte restante (leitura) e submeta-o no Mooshak. Veja quantos pontos obtém e porque motivo o programa falha nos testes maiores (basta clicar no resultado da submissão para ter acesso a uma tabela detalha com os resultados).

  2. Melhorando a solução para Θ(n2)

    Intuitivamente, olhando para o código anterior podemos notar que em cada soma estamos a repetir muitos cálculos. Quando passamos do cálculo de soma(v[i..j]) para soma(v[i..j+1]) não precisamos de voltar a recalcular tudo (começando novamente em i, e basta-nos adicionar v[j+1] à soma anterior!

    Dito de outro modo, soma(v[i..j+1]) = soma(v[i..j]) + v[j+1]. Podemos utilizar isto para remover o terceiro ciclo com k que tínhamos na solução anterior.

    Implemente esta nova solução e submeta-a. Quantos pontos passa a obter? Porque é que passa em mais testes?

  3. Melhorando ainda mais para Θ(n)

    Uma solução quadrática ainda não é suficiente e temos de a melhorar para tempo linear. Para isso vamos usar o algoritmo de Kadane.

    Considere que best(i) representa o melhor subarray que termina na posição i. Sabemos como "caso base" que best(0) = v[0] (é o único subarray possível que termina na primeira posição).

    Se soubermos o valor de best(i), como calcular o valor de best(i+1)?

    No final de tudo, o melhor subarray é o melhor valor de best(i) para um qualquer i (o melhor pode terminar em qualquer posição).

    Para calcular isto basta-nos percorrer uma única vez o array v[] e em cada iteração calcular em tempo constante o valor da melhor soma a terminar na posição actual usando a melhor soma a terminar na posição anterior. A complexidade temporal fica portanto linear.

    Implemente esta solução e submeta-a, verificando que já consegue passar todos os testes sem exceder o tempo limite.


Exercícios extra para consolidação de conhecimentos

  1. Tem disponível no Volume 5 mais dois pequenos problemas onde a solução mais "bruta" em termos de complexidade temporal não passa e tem de fazer algo um pouco mais eficiente. Estes dois problema foram o problema mais fácil da qualificação (do respectivo ano) das Olimpíadas Nacionais de Informática um concurso para alunos do secundário:

Exercício de Desafio

O exercicio de desafio desta semana é um problema de uma fase de qualificação das Olimpíadas Nacionais de Informática, um concurso para alunos do secundário, e tem precisamente a ver com eficiência algorítmica. Deve tentar resolver o seguinte problema, que está disponível para submissão no Mooshak, no volume de Desafios:

O problema não é muito complicado (era da fase de qualificação), mas uma solução "bruta" não passa no tempo. Ao pensarem numa solução procurem sempre perceber qual a complexidade do que estão a pensar para conseguirem "prever" se vai ou não passar.

Como é um mini-desafio, não vou dar mais dicas, mas se precisarem de alguma ajuda avisem. Fico à espera de ver os vossos programas :)