Resolução da 8ª Aula Prática

Resoluções

1.a) a 1.c)  Ver ex1abc.C.

1.d)  Há muitas outras formas!  Ver folhas teóricas!  Há métodos recentes ainda mais eficientes e usados em criptografia...

2.  Ver ex2.C.

O primeiro passo é escrever o esqueleto do programa incluindo a pré-condição e a condição objectivo.  Para simplificar a escrita da condição objectivo, convencionou-se que se representavam os valores lidos to teclado através da notação cinj, com j = 0, ..., n - 1.  Por exemplo, cin2 significa o terceiro inteiro lido.

#include <iostream>

using namespace std;

int main()
{
    cout << "Quantos números vai introduzir? ";
    int n;
    cin >> n;

    if(n < 1) {
        cout << "Tem de introduzir pelo menos um inteiro!" << endl;
        return 1;
    }

    cout << "Introduza " << n << " números." << endl;

    // PC: 1 <= n e o canal de entrada cin está em bom estado e contém n números inteiros.

    int maximo = ?;

    ...

    // CO: (E j : 0 <= j < n : cinj = maximo) e (Q j : 0 <= j < n : cinj <= maximo).

    cout << "O máximo é " << maximo << '.' << endl;
}

Em seguida enfraquece-se a condição objectivo de modo a obter a condição invariante.  Neste caso pode-se simplesmente substituir o limite superior do quantificador (constante n) por uma nova variável C++ i introduzida para o efeito e deixá-la variar numa gama apropriada:

CI: (E j : 0 <= j < i : cinj = maximo) e (Q j : 0 <= j < i : cinj <= maximo) e 0 <= i <= n.
(Na realidade existe uma outra condição que se assume ser sempre verdadeira: o número de valores já lidos de cin é exactamente igual a i.  Não se inclui essa condição na condição invariante para aliviar um pouco a notação.)

Esta condição invariante afirma simplesmente que maximo é o maior dos valores lidos até ao momento.

Como guarda, escolhe-se simplesmente i <> n, de modo que CI e ¬G implica CO.

O passo seguinte é escolher a inicialização do ciclo.  Como não existe máximo de um conjunto vazio (porque o quantificador "existe um" é falso se o número de termos for zero), a forma mais simples de garantir a veracidade da condição invariante é fazendo:

int maximo;
cin >> maximo; //
depois desta instrução maximo = cin0.
int i = 1;
Analisando os termos da condição invariante um a um, verifica-se que:
  1. 0 <= i <= n, é uma afirmação verdadeira, dada a pré-condição;
  2. (Q j : 0 <= j < 1 : cinj <= maximo) é o mesmo que cin0 <= maximo, e portanto verdadeira, dado que maximo = cin0; e
  3. (E j : 0 <= j < i : cinj = maximo) é o mesmo que cin0 = maximo, e portanto verdadeira, dado que maximo = cin0.

Em seguida há que escolher o progresso.  Neste caso pode-se optar pela simples incrementação de i:

++i;
pois garante a terminação do ciclo.

Falta escolher uma acção que garanta a invariância da condição invariante apesar do progresso.  O passo pode-se escrever:

// Aqui assume-se que CI e G, ou seja,
// (E j : 0 <= j < i : cinj = maximo) e (Q j : 0 <= j < i : cinj <= maximo) e 0 <= i <= n e i <> n, ou ainda,
// (E j : 0 <= j < i : cinj = maximo) e (Q j : 0 <= j < i : cinj <= maximo) e 0 <= i < n.
acção
++i;
// Aqui pretende-se que
// CI: (E j : 0 <= j < i : cinj = maximo) e (Q j : 0 <= j < i : cinj <= maximo) e 0 <= i <= n.
Partindo dos objectivos, podemos obter a condição mais fraca a impor antes do progresso para que depois dele a condição invariante seja verdadeira.  Para isso substitui-se i por i + 1, pois ++i é equivalente a i = i + 1:
// (E j : 0 <= j < i + 1 : cinj = maximo) e (Q j : 0 <= j < i + 1 : cinj <= maximo) e 0 <= i + 1 <= n, ou seja,
//
(E j : 0 <= j < i + 1 : cinj = maximo) e (Q j : 0 <= j < i + 1 : cinj <= maximo) e -1 <= i < n.
++i;

// (E j : 0 <= j < i : cinj = maximo) e (Q j : 0 <= j < i : cinj <= maximo) e 0 <= i <= n.
É fácil verificar que

((E j : 0 <= j < i : cinj = maximo) ou cini = maximo) e 
(Q j : 0 <= j < i : cinj <= maximo) e cini <= maximo e 0 <= i < n
implica
(E j : 0 <= j < i + 1 : cinj = maximo) e (Q j : 0 <= j < i + 1 : cinj <= maximo) e -1 <= i < n.

Agora há que escolher a acção de modo a que

// (E j : 0 <= j < i : cinj = maximo) e (Q j : 0 <= j < i : cinj <= maximo) e 0 <= i < n.
acção
// implica ((E j : 0 <= j < i : cinj = maximo) ou cini = maximo) e 
// (Q j : 0 <= j < i : cinj <= maximo) e cini <= maximo e 0 <= i < n.
É claro que o valor de i não necessita de ser afectado pela acção.  Mas o de maximo sim.  Como o fazer?  Haverá alguma circunstância em que a variável maximo não necessite de ser alterada, isto é, em que a acção possa ser nula?  Comparando termo a termo as asserções antes e depois da acção, conclui-se que isso só acontece se cini <= maximo.  Então a acção deverá consistir em duas instruções, uma que nos permita obter o valor de cini (e guardá-lo numa variável auxiliar) e numa instrução de selecção:

int v;
cin >> v; //
A partir de aqui v = cini.
if(v <= maximo)
    ;
else
    instrução2

Resta saber que instrução deverá ser usada para o caso em que cini > maximo, ou seja, v > maximo.  Essa instrução tem de levar a:

// (E j : 0 <= j < i : cinj = maximo) e (Q j : 0 <= j < i : cinj <= maximo) e 0 <= i < n e cini > maximo.
instrução2
// implica ((E j : 0 <= j < i : cinj = maximo) ou cini = maximo) e 
// (Q j : 0 <= j < i : cinj <= maximo) e cini <= maximo e 0 <= i < n.

Antes da instrução, maximo contém o maior dos valores que estava no canal de entrada.  No entanto, o valor acabado de ler (cini) contém um valor superior ao que está em maximo.  Assim, maximo deverá tomar o valor de cini de modo a continuar a ter o maior dos valores lidos do canal de entrada até ao momento.  A instrução a usar é portanto:

maximo = v;

Tem de se verificar se esta acção é correcta. Para isso calcula-se a pré-condição mais fraca que conduz à asserção final pretendida:

// ((E j : 0 <= j < i : cinj = cini) ou cini = cini) e 
// (Q j : 0 <= j < i : cinj <= cini) e cini <= cini e 0 <= i < n, ou seja,
// ((E j : 0 <= j < i : cinj = cini) ou V) e (Q j : 0 <= j < i : cinj <= cini) e V e 0 <= i < n, ou seja,
// (Q j : 0 <= j < i : cinj <= cini) e 0 <= i < n.
maximo = v; // e v = cini.
// implica ( (E j : 0 <= j < i : cinj = maximo) ou cini = maximo) e 
// (Q j : 0 <= j < i : cinj <= maximo) e cini <= maximo e 0 <= i < n.

Como

(E j : 0 <= j < i : cinj = maximo) e (Q j : 0 <= j < i : cinj <= maximo) e 0 <= i < n e cini > maximo
implica 
(Q j : 0 <= j < i : cinj <= maximo) e 0 <= i < n e cini > maximo
implica 
(Q j : 0 <= j < i : cinj < cini) e 0 <= i < n
implica 
(Q j : 0 <= j < i : cinj <= cini) e 0 <= i < n,

a invariância fica demonstrada.

Finalmente a demonstração de que o ciclo termina, i.e., que o ciclo está não só parcialmente correcto mas totalmente correcto, pode ser feita notando que i começa em 1 e é incrementado a cada passo, e portanto atingirá n ao fim de um número finito de passos (exactamente n - 1, recorde-se que a pré-condição garante que 1 <= n).

O programa completo fica então:

#include <iostream>

using namespace std;

int main()

{
    cout << "Quantos números vai introduzir? ";
    int n;
    cin >> n;

    if(n < 1) {
        cout << "Tem de introduzir pelo menos um inteiro!" << endl;
        return 1;
    }

    cout << "Introduza " << n << " números." << endl;


    int maximo;
    cin >> maximo;

    int i = 1;
    while(i != n) {
        int v;
        cin >> v;
        if(v > maximo)
            maximo = v;
        ++i;
    }

    cout << "O maximo é " << maximo << '.' << endl;
}

4.a)  Ver ex4a.C.

O primeiro passo é escrever o esqueleto da função incluindo a PC e a CO.

/** Indica se existe um múltiplo de k entre m e n inclusive.
    @pre 0 < k e 0 <= m <= n.
    @post haMultiploEntre = (E j : m <= j <= n : j ÷ k = 0). */
bool haMultiploEntre(int const k, int const m, int const n)
{
    assert(0 < k and 0 <= m and m <= n);

    bool ha = ?;

    ...

    // CO: ha = (E j : m <= j <= n : j ÷ k = 0).
    return ha;
}

Nota:  Usa-se um nome incompleto (ha) para simplificar as expressões abaixo.

O passo seguinte passa por identificar a necessidade de um ciclo.  Neste caso assume-se que um ciclo é necessário (mas ver próxima resposta).

Em seguida enfraquece-se a condição objectivo do ciclo de modo a obter a condição invariante.  Neste caso pode-se simplesmente substituir o limite superior do quantificador (constante n) por uma nova variável C++ i introduzida para o efeito:

CI: ha = (E j : m <= j <= i : j ÷ k = 0) e m <= i <= n.
Como guarda escolhe-se simplesmente i <> n, de modo a que CI e ¬G implica CO.

O passo seguinte é escolher a inicialização do ciclo.  A forma mais simples de tornar verdadeira a condição invariante é fazendo:

ha = false;
i = m - 1;

pois dessa forma o quantificador tem zero termos e portanto valor F.

Em seguida há que escolher o progresso.  Neste caso pode-se optar pela simples incrementação de i:

++i;
Falta escolher uma acção que garanta a invariância da condição invariante apesar do progresso.  O passo pode-se escrever:
// Aqui assume-se que CI e G, ou seja,
// ha = (E j : m <= j <= i : j ÷ k = 0) e m <= i <= n ei <> n ou ainda,
// ha = (E j : m <= j <= i : j ÷ k = 0) e m <= i < n.
acção
++i;
// Aqui pretende-se que CI seja verdadeira, ou seja,
// ha = (E j : m <= j <= i : j ÷ k = 0) e m <= i <= n.
Partindo dos objectivos, pode-se obter a condição mais fraca a impor antes do progresso para que depois dele a condição invariante seja verdadeira:
// ha = (E j : m <= j <= i + 1 : j ÷ k = 0) e m <= i + 1 <= n, ou seja,
// ha = (E j : m <= j <= i + 1 : j ÷ k = 0) e m - 1 <= i < n.
++i;
// ha = (Ej : m <= j <= i : j ÷ k = 0) e m <= i <= n.

Agora há que escolher a acção de modo a que

// ha = (E j : m <= j <= i : j ÷ k = 0) e m <= i < n
acção
// implica ha = (E j : m <= j <= i + 1 : j ÷ k = 0) e m - 1 <= i < n.
É claro que o valor de i não necessita de ser afectado pela acção.  Mas o de ha precisa.  Em particular a variável ha tem de passar a conter o valor que tinha antes da acção disjunto com (i + 1) ÷ k = 0.  Ou seja, a acção será:
ha = ha or (i + 1) % k == 0;
Pode-se verificar que é uma boa escolha obtendo a condição mais fraca a impor antes da acção:
// (ha ou (i + 1) ÷ k = 0) = (E j : m <= j <= i + 1 : j ÷ k = 0) e m - 1 <= i < n.
ha = ha or (i + 1) % k == 0;
// implica ha = (E j : m <= j <= i + 1 : j ÷ k = 0) e m - 1 <= i < n.
Como
ha = (E j : m <= j <= i : j ÷ k = 0) e m <= i < n
implica
(ha ou (i + 1) ÷ k = 0) = (E j : m <= j <= i + 1 : j ÷ k = 0) e m - 1 <= i < n.
a invariância fica demonstrada.

Finalmente a demonstração de que o ciclo termina, i.e., que o ciclo está não só parcialmente correcto mas totalmente correcto, pode ser feita notando que i começa em m - 1 e é incrementado a cada passo, e portanto atingirá n ao fim de um número finito de passos (exactamente n - m + 1, recorde-se que a pré-condição garante que n >= m).

A função completa fica então:

/** Indica se existe um múltiplo de k entre m e n inclusive.
    @pre 0 < k e 0 <= m <= n.
    @post haMultiploEntre = (E j : m <= j <= n : j ÷ k = 0). */
bool haMultiploEntre(int const k, int const m, int const n)
{
    assert(0 < k and 0 <= m and m <= n);

    bool ha = false;

    for(int i = m - 1; i != n; ++i)
        ha = ha or (i + 1) % k == 0;

    return ha;
}

Uma simples mudança de variável permite-nos atrasar o valor de i de uma unidade:

/** Indica se existe um múltiplo de k entre m e n inclusive.
    @pre  0 < k e 0 <= m <= n.
    @post haMultiploEntre = (E j : m <= j <= n : j ÷ k = 0). */
bool haMultiploEntre(int const k, int const m, int const n)
{
    assert(0 < k and 0 <= m and m <= n);

    bool ha = false;

    for(int i = m; i != n + 1; ++i)
        ha = ha or i % k == 0;

    return ha;
}

É óbvio que, se alguma vez o valor de ha se tornar V, nunca mais o deixará de ser, pelo que o ciclo pode ser abreviado, reforçando a guarda (a demonstração formal de que o ciclo continua correcto deixa-se a cargo do aluno).  Ou seja:

/** Indica se existe um múltiplo de k entre m e n inclusive.
    @pre  0 < k e 0 <= m <= n.
    @post haMultiploEntre = (E j : m <= j <= n : j ÷ k = 0). */
bool haMultiploEntre(int const k, int const m, int const n)
{
    assert(0 < k and 0 <= m and m <= n);

    bool ha = false;

    for(int i = m; not ha and i != n + 1; ++i)
        ha = ha or i % k == 0;

    return ha;
}

Esta versão pode ser simplificada observando que no início do passo, com esta guarda reforçada, a variável ha toma sempre o valor F, pelo que a acção pode ser simplificada:

/** Indica se existe um múltiplo de k entre m e n inclusive.
    @pre  0 < k e 0 <= m <= n.
    @post haMultiploEntre = (E j : m <= j <= n : j ÷ k = 0). */
bool haMultiploEntre(int const k, int const m, int const n)
{
    assert(0 < k and 0 <= m and m <= n);

    bool ha = false;

    for(int i = m; not ha and i != n + 1; ++i)
        ha = i % k == 0;

    return ha;
}

Finalmente, uma solução alternativa passa pelo terminar abrupto da função se se encontrar um múltiplo:

/** Indica se existe um múltiplo de k entre m e n inclusive.
    @pre  0 < k e 0 <= m <= n.
    @post haMultiploEntre = (E j : m <= j <= n : j ÷ k = 0). */
bool haMultiploEntre(int const k, int const m, int const n)
{
    assert(0 < k and 0 <= m and m <= n);

    for(int i = m; i != n + 1; ++i)

        if(i % k == 0)
            return true;

    return false;
}

Esta última versão é pouco recomendável, pois o ciclo fica com duas possíveis saídas.

4.b)  Sim!  A condição objectivo é equivalente a CO': m ÷ k = 0 ou m / k <> n / k (divisão inteira!).  Ou seja, há um múltiplo de k entre m e n (inclusive), se e só se m for múltiplo de k ou o quociente de m por k for diferente do quociente de n por k.  Ver ex4b.C.

Para demonstrar que CO' equivale a CO demonstra-se primeiro que CO' implica CO (suficiência) e depois que ¬CO' implica ¬CO (necessidade).

Suponha-se que m ÷ k = 0 ou m / k = g <> n / k = h.  Se o primeiro termo for verdadeiro, é óbvio que existe um múltiplo: o próprio m.  Partindo apenas do segundo termo,  m / k = g <> n / k = h, conclui-se que g k <= m < (g + 1) k e h k <= n < (h + 1) k.  Sabe-se ainda que g < h, o que é o mesmo que g + 1 <= h, que por sua vez implica que (g + 1) k <= h k.  Assim, conclui-se que g k <= m < (g + 1) k <= h k <= n < (h + 1) k.  Ou seja, há de certeza múltiplos de k entre m e n: pelo menos (g + 1) k e h k (que podem ser iguais).

Suponha-se agora que m ÷ k <> 0 e m / k = n / k = h.  Do segundo termo é óbvio que h k <= m < (h + 1) k e que h k <= n < (h + 1) k.  Por outro lado supôs-se que m ÷ k <> 0, ou seja, h k < m < (h + 1) k e h k <= n < (h + 1) k.  Como se sabe que m <= n, conclui-se que h k < m <= n < (h + 1) k.  É evidente então que não há múltiplos de k entre m e n nestas circunstâncias.

A função pode portanto ser escrita como:

/** Indica se existe um múltiplo de k entre m e n inclusive.
    @pre  0 < k e 0 <= m <= n.
    @post haMultiploEntre = (E j : m <= j <= n : j ÷ k = 0). */
bool haMultiploEntre(int const k, int const m, int const n)
{
    assert(0 < k and 0 <= m and m <= n);

    return m % k == 0 or m / k != n / k;

}

Desafio ao aluno: Consegue simplificar adicionalmente esta função?