Guião da 1ª Aula Teórica

Sumário

  1. Noção de computador como máquina programável.
  2. Programação como arte de resolver problemas.
  3. Conceito de algoritmo.
  4. Resolução do problema do cálculo do máximo divisor comum de dois inteiros positivos: escrita de um algoritmo em pseudo-código (informal), e demonstração da sua correcção.
  5. Conceitos de linguagens naturais, de programação e máquina.  Diferenças.
  6. Conceito de compilador como tradutor duma linguagem de programação para linguagem máquina.
  7. Implementação do algoritmo desenvolvido em C++.
  8. Programa como concretização dum algoritmo.
  9. Fases da resolução dum problema usando um computador.

Apresentação

Escrever no quadro:

Apresentar o docente, dizer contactos.

Pedir contactos do delegado de turma.

Explicar razão de ser das aulas teóricas a turmas separadas: interacção.  Explicar liberdade de frequência: se não estão interessados ou atentos, saiam ou nem cheguem a entrar.  Para os convencer de que responder erradamente é normal, pedir-lhes para fazerem em coro uma qualquer palhaçada no início e depois dizer que pior que a figura de parvos que acabaram de fazer é impossível!  Referir pavor do ridículo dos portugueses.

Dizer que a apresentação formal é na sala marcada pelo GARE ou na aula prática do turno A.  Dizer qual é.

Aula

Começar por perguntar se sabem o que é um computador.  Escrever as respostas no quadro.  Discuti-las.

Um computador é uma máquina (ou utensílio ou ferramenta).  Mas que um martelo ou uma máquina de lavar também são máquinas.  Que distingue o computador?  Ser programável.  De acordo, mas a máquina de lavar também tem programas...  É uma máquina programável genérica.  Só com muita imaginação se pode usar uma máquina de lavar para gerir as contas num banco...

O que é programar então?  É ensinar um computador a resolver determinado problema ou determinados tipos de problemas.

Escrever a frase:

Diz-se que só se compreende realmente um assunto depois de o ter ensinado a alguém.  Na realidade só se compreende realmente um assunto depois de o ter ensinado a um computador.

Knuth

Donde, se o programador ensinou o computador a resolver o problema, então o programador sabe resolvê-lo.  Daí que não seja exagero classificar a programação como a Arte de Resolver Problemas.

Um conceito importante em programação é o de algoritmo.  Um algoritmo está (quase) para a programação como uma receita para o culinária.  

Fazer o quadro abaixo (escrever só a coluna da direita).  Eles que adivinhem a da esquerda.  Também se pode fazer ao contrário.  Ou alternado.

Um computador é mais do que um simples executor de ordens.  Um computador tem:

  1. Um processador.
  2. Memória rápida: RAM e ROM.
  3. Memória lenta: o disco rígido.
Qual é a analogia?

Explorar respostas.

 

Programação Culinária
Algoritmo Receita (abstracta)
Programa Receita (numa linguagem)
Entradas Número de pratos
Saídas Pratos
Valores Ingredientes
Variáveis Recipientes
Memória volátil (RAM) Bancada
Memória permanente (discos, etc.) Frigorífico, despensa
Computador Cozinha
Processador Cozinheiro
Comentários Apartes para o cozinheiro

Os algoritmos têm cinco características importantes:

  1. Finitude:  Tem de terminar (e as receitas também).
  2. Definitude:  Cada passo está precisamente definido.  Nas receitas "deixar alourar um pouco" não é lá muito preciso, pois não?  Sendo um humano (inteligente) a seguir a receita a coisa acaba por funcionar.  Mas o algoritmo é seguido pelo computador (estúpido).
  3. Entradas:  No caso duma receita será por exemplo o número de pessoas para que se deseja cozinhar.  Por vezes há algoritmos sem entradas.
  4. Saídas:  No caso duma receita presume-se que sejam os pitéus.  Um algoritmo, como uma receita, tem de ter sempre pelo menos uma saída.
  5. Eficácia:  Todas as operações indicadas têm de ser executáveis na prática.
Tudo é muito bonito, mas como se desenvolve um algoritmo?  Bom, é preciso começar por pensar num problema para resolver.  Proponho o cálculo do máximo divisor comum de dois inteiros positivos.

Entradas: m e n (exemplo 12 e 8)
Saída: k tal que k = mdc(m, n)

Desenhar variáveis usando notação UML!  Usar ? para o valor de k.

Relembrar divisão inteira.  Introduzir ÷ para o resto.

m, n e k são variáveis inteiras.  O que é isso?  Para já podemos imaginar que são caixas, cada uma das quais contém um e um só número inteiro.  Cada variável tem um nome (por exemplo, m), um tipo (por exemplo, inteiro) e um valor (por exemplo, 12).  

Numa variável há duas características fixas e uma variável.  O nome e o tipo são fixos.  O valor é variável.

Como m e n são entradas já lá têm valores (12 e 8, neste caso):

A variável k contém o quê?  Lixo!  Lixo?  O que é isso?  É um inteiro qualquer, sobre o qual não temos qualquer controlo:

Uma analogia com a culinária diria que os recipientes estão para a culinária como as variáveis para a programação.  Mas a analogia não é perfeita: as variáveis contêm sempre qualquer coisa!

É uma cozinha algo estranha, onde os recipientes têm sempre alguma coisa dentro, mesmo quando arrumados...  E ainda por cima, muitas vezes é lixo...

Regras do jogo: o nosso algoritmo é uma sequência de instruções que agem sobre as variáveis existentes.

Como se resolve o problema?

Pedir aos alunos propriedades interessantes do máximo divisor comum.  Anotá-las e discuti-las.  Frisar:

Pedir sugestões para receitas de resolução e anotá-las.  Depois descrever intuitivamente o algoritmo mais simples: Podemos escrever isto duma forma mais "limpa":

se m < n então
    k <- m
senão
    k <- n

enquanto m ÷ k <> 0 ou n ÷ k <> 0 faça-se:
    k <- k - 1

Explicar <-.  Explicar ordem de execução.  Explicar enquanto: enquanto o açúcar não estiver no ponto ir mexendo.

Será que isto é mesmo um algoritmo?  I.e., é finito, bem definido, tem entradas e saídas e cada operação é eficaz?

Que tem entradas e saídas já sabemos.  Que é bem definido, não há dúvidas, pois cada passo está bem especificado.  As operações são todas eficazes.  Falta saber que termina sempre e com o valor correcto!

Vamos testar com os valores de entrada 12 e 8.

Fazer traçado alterando valor em k.  Chamar-lhe traçado.

Funcionou!  Será que funciona sempre?  Podíamos testar agora com 12 e 45, por exemplo.  E depois?  Não se pode demonstrar que um algoritmo funciona através de testes!  Quando muito demonstra-se que não funciona.

Para demonstrar que funciona teríamos de testar todos os pares de inteiros positivos, que infelizmente são infinitos.  Porquê testar com todos?  Porque nada nos garante que se o algoritmo produzir resultados correctos para os primeiros 1000 pares, os produzirá também para todos os restantes pares.

Suponham que só nos interessam inteiros até um milhão.  Se demorarmos 1 milionésimo de segundo a testar cada par, ainda demoramos 1000000 de segundos, ou seja, 12 dias...

Como demonstrá-lo, então? 

O ideal é fazer esta explicação num diagrama de actividade, colocando as asserções como comentários.  Ver folhas teóricas.  Usar a nomenclatura indicada na primeira figura:

Primeiro, observem que admitimos que, logo no início, 0 < m e 0 < n.

Como esta é uma condição que se admite ser válida no início, chamamos-lhe pré-condição:

{PC: m e n são inteiros positivos.}
se m < n então
    k <- m
senão
    k <- n

enquanto m ÷ k <> 0 ou n ÷ k <> 0 faça-se:
    k <- k - 1

Dizer o que são comentários: notas que não afectam o cozinhado, mas que são justificações para o cozinheiro interessado.

O que é que sabemos depois do SE?  Que k contém o menor dos dois valores.  Ou seja

{PC: m e n são inteiros positivos.}
se m < n então
    k <- m
senão
    k <- n
{k = min(m, n)}

enquanto m ÷ k <> 0 ou n ÷ k <> 0 faça-se:
    k <- k - 1

Explicar colocação de comentários com asserções entre as instruções do algoritmo.

Mas isso significa que mdc(m, n) <= k.  É óbvio?  Não?  Imaginemos que não.  Então k = min(m, n) < mdc(m, n).  Ou seja, o máximo divisor comum é maior que o menor dos valores!  Absurdo.  Donde só pode ser mdc(m, n) <= k.

Suponham agora que a condição do enquanto é falsa.  Isso significa que m ÷ k = 0 e n ÷ k = 0.  Ou seja, que k é divisor de m e n.  Mas como começámos com mdc(m, n) <= k, então k tem de ser k = mdc(m, n).  Porquê?  Se não fosse igual então k seria maior que o mdc(m, n) e k dividiria m e n.  Ou seja, haveria um divisor k de m e n maior que o máximo divisor comum de m e n.  Absurdo!

Ou seja, se a condição do enquanto for falsa, então k é o máximo divisor comum procurado.

E se essa condição, chamada guarda do enquanto, for verdadeira?  Então sabemos que mdc(m, n) <= k e que k não é divisor de m e n simultaneamente.  E então?  Que se tira daqui?  Se não é divisor, também não é máximo divisor comum!  Então k só pode ser maior que o mdc(m, n).  Isso significa que k é grande demais e portanto deve ser diminuído.  Retiremos uma unidade a k.  Depois dessa operação já não podemos afirmar que mdc(m, n) < k.  Mas podemos dizer que mdc(m, n) <= k!

Aqui talvez seja útil dizer que dizer x < k é o mesmo que dizer que x <= k - 1, desde que se esteja a tratar com inteiros!

Notem que recuperámos a condição que sabíamos válida antes do enquanto: mdc(m, n) <= k.  Ou seja, o ciclo está construído de tal forma que essa condição se mantém.....  Daí que a essa condição se chame condição invariante do enquanto.

Isto chega para demonstrar que, se o algoritmo alguma vez terminar, termina com o valor correcto.  Mas não demonstra que termina sempre.  Saber que termina para o caso 12 8 nada nos diz acerca do caso 1000001 20304996!  Como demonstrá-lo?

Neste caso é simples.  k começa sempre com o menor dos valores m e n, que é um inteiro positivo.  Em cada passo da repetição o valor de k diminui de um.  É fatal, portanto, que k atinja 1.  Acontece que 1 é divisor de todos os inteiros positivos, e portanto também de m e n.  Nessa altura a guarda do enquanto é falsa e o algoritmo termina.

Mas esquecemo-nos de uma coisa.  Já temos um algoritmo em português, mas será que podemos ensinar o computador a resolver este problema em português?  Como quem diz: "olha, para achares o máximo divisor comum de dois inteiros positivos m e n, pegas no menor deles e pões em k, depois, enquanto k não dividir algum desses inteiros, vais-lhe subtraindo um.  Quando terminares, k será o máximo divisor comum de m e n".  Infelizmente não.  Os computadores não falam português.

O português é uma linguagem natural.  Os computadores ainda não são fluentes em linguagem natural.  Que "falam" eles, então?  linguagem máquina.  É uma linguagem muito básica.  Tão básica, que é muito desagradável exprimirmo-nos nela.  Vocês vão falar de linguagem máquina em Arquitectura de Computadores.

Então, se não podemos usar linguagem natural, nem queremos usar linguagem máquina, o que é que usamos?  Uma linguagem da programação de alto nível, i.e., algo que, não sendo uma linguagem natural, com as suas imprecisões e ambiguidades, apesar disso não seja penoso para um humano "falar".

A linguagem de programação que vamos usar é o C++.  O computador não percebe directamente C++.  Há um programa, chamado compilador, que traduz de C++ para linguagem máquina.

Nota importante:

O nosso objectivo é ensinar-vos a programar.  Usamos o C++ como poderíamos usar muitas outras linguagens.  Este não é um curso de C++!

Um programa é a concretização de um algoritmo numa linguagem de programação.

Então é preciso concretizarmos o nosso algoritmo em C++.  Ou seja, temos de escrever um programa correspondente ao algoritmo.  O programa é assim:

#include <iostream>

using namespace std;

int main()
{
    cout << "Introduza dois inteiros positivos: ";
    int m, n;
    cin >> m >> n;

    int k;
    if(m < n)
        k = m;
    else
        k = n;

    while(m % k != 0 or n % k != 0)
        --k;

    cout << "O máximo divisor comum de " << m << " e " << n << " é "
         << k << endl;
}

Fazer explicação breve do programa.

Quais são as fases de resolução de um problema, então?

  1. Especificação do problema [humano].
  2. Desenvolvimento do algoritmo que o resolve [humano].  É neste passo que se faz mais uso da inteligência, da criatividade, do engenho, da iniciativa, da experiência, etc.
  3. Concretização do algoritmo na linguagem de programação: escrita do programa [humano].  Este passo é mecânico e relativamente pouco interessante.
  4. Tradução do programa para linguagem máquina [computador].
  5. Execução do programa para resolver um problema particular (e.g., mdc(131, 47)) [computador].
Confusos?  Se sim, não se preocupem.  A ideia desta aula é dar um cheirinho do que há-de vir.  Tudo isto será visto com mais cuidado nas próximas aulas.

O que é importante reter desta aula é:

  1. Programar é resolver problemas.
  2. Que um algoritmo é uma "receita" com características especiais: finitude, definitude, entradas, saídas, e eficácia.
  3. Que não se demonstra a correcção de um algoritmo por testes.
  4. Que existem vários tipos de linguagens: naturais, de programação de alto nível, e máquina (por exemplo).
  5. Que um programa é a concretização de um algoritmo numa linguagem de programação.
  6. Que um compilador traduz um programa escrito numa linguagem de programação para linguagem máquina.