Aula Prática #08 - Complexidade Algorítmica


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

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

  f(n) g(n)
(a) \(5n^2 - 100n + 34\) \(2n^3 - 97\)
(b) \(54n + 100\) \(6n - 24\)
(c) \(2^n\) \(6n^2\)
(d) \(30\) \(\log 30\)
(e) \(n \log n + n\) \(n^2\)
(f) \(\sqrt{n}\) \(\log n\)


Exercício 2) Complexidade do TAD Conjunto

  1. 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\))

    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

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. Comece por ler o enunciado do problema, disponível em [ED198] Um Jogo com Sequências.
  1. Uma primeira solução com força bruta com tempo \(\Theta(n^3)\)

    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 \dots j]\) tal que \(0 \leq i \leq j \lt 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 detalhada com os resultados).

  2. Melhorando a solução para \(\Theta(n^2)\)

    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 \ldots j])\) para \(soma(v[i \ldots 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 \ldots j+1]) = soma(v[i \ldots 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 \(\Theta(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.