Riepilogo
Questa lezione non contiene codice, e non per distrazione: l’algoritmo precede il programma, e chi non sa descrivere con precisione che cosa si deve fare non saprà dirlo neppure in C++. Prima si insegna a specificare un problema, cioè a dichiarare quali dati riceve, quale risultato deve restituire e quale relazione li lega, e si mostra che uno stesso enunciato ammette specifiche diverse. Poi si definisce l’algoritmo come procedimento finito, preciso e generale, e si scrive in passi numerati quello della divisione per sottrazioni successive. Si insegna a seguire a mano un algoritmo con una tabella di traccia, che sarà per tutto il corso il primo strumento per capire e per fare il debug. Si presentano i tre modi di combinare i passi (sequenza, decisione e ripetizione), con i quali si scrive ogni algoritmo, e si impara a giustificare che una ripetizione termina mediante una quantità che decresce a ogni iterazione e non può scendere indefinitamente.
Obiettivi di apprendimento
- Specificare un problema semplice indicandone gli input, gli output e le condizioni che devono soddisfare.
- Descrivere un algoritmo in passi numerati, senza ambiguità, con decisioni e ripetizioni esplicite.
- Seguire a mano un algoritmo con una tabella di traccia e determinarne il risultato per un input dato.
- Distinguere, in un procedimento dato, la sequenza, la decisione e la ripetizione, e giustificare che una ripetizione termina.
Che cos’è un problema computazionale
Un problema computazionale è una relazione fra due categorie di dati: quelli che si ricevono, chiamati input, e quelli che si devono restituire, chiamati output. Risolverlo non consiste nell’ottenere una risposta per un caso, ma nel disporre di un metodo che, per ogni input ammissibile, produca un output che soddisfi la relazione. «Quanto fa 17 diviso 5?» è una domanda; «dati due interi, ottenerne il quoziente e il resto» è un problema, perché parla di tutte le coppie di interi allo stesso tempo.
Ora, il linguaggio comune è impreciso, e lo stesso enunciato può essere inteso in vari modi. Per questo, prima di risolvere, si specifica: si scrive che cosa entra, che cosa esce e quali condizioni deve soddisfare ciascuna cosa. La specifica ha tre parti. L’input elenca i dati con la loro categoria (interi, decimali, parole) e le condizioni che si richiedono loro, che si chiamano precondizione: chi usa la soluzione si impegna a rispettarle. L’output elenca i risultati. La relazione, o postcondizione, dice che cosa deve soddisfare l’output rispetto all’input: chi scrive la soluzione si impegna a garantirla ogni volta che la precondizione sia stata rispettata. Ne segue che la specifica è un contratto fra chi pone il problema e chi lo risolve, e che fuori dalla precondizione la soluzione non promette nulla.
Enunciato comune: «trovare il maggiore di tre numeri». Specifica:
- Input: tre numeri , , (interi o decimali, senza altra condizione).
- Output: un numero .
- Relazione: è uno dei tre dati, e nessuno di essi è maggiore di ; cioè , e .
La relazione non dice «il maggiore dei tre», che è proprio ciò che si voleva precisare, ma due condizioni verificabili. La prima esige che sia uno dei dati (senza di essa, il numero 1000 soddisferebbe la seconda per quasi ogni input); la seconda, che nessuno lo superi. E la specifica risolve di passaggio un dubbio che l’enunciato lasciava aperto: se due dati sono a pari merito per il massimo, come in , l’output è , e non c’è alcun errore in questo.
Uno stesso enunciato ammette specifiche diverse, e scegliere fra di esse è una decisione, non una svista. Il caso più istruttivo è quello della divisione intera. Per interi e , tutti sono d’accordo: il quoziente e il resto sono gli unici interi che soddisfano
Così, per e sono e , perché . Ma l’enunciato «dividere due interi» non dice che cosa fare se è negativo, e la domanda non è oziosa: per e ci sono due risposte ragionevoli.
Prima specifica: il resto non è mai negativo, . Allora , e l’output è , . È quella che si studia in matematica, nella lezione «Divisibilità e divisione con resto» del corso Algebra e Geometria I.
Seconda specifica: il quoziente è il risultato della divisione con i decimali, privato della parte decimale, cioè arrotondato verso lo zero; il resto è ciò che manca, e ha il segno del dividendo. Allora si tronca a , e : l’output è , .
Entrambe soddisfano ; differiscono nella condizione che si impone al resto. Nessuna delle due è un errore, ma un programma che ne usi una credendo di usare l’altra darà risultati sbagliati con i negativi. Anticipiamo un dato: C++ adotta la seconda, come si vedrà nella lezione Aritmetica; chi si aspetta la prima avrà una sorpresa.
C’è una terza opzione, che è la più frequente nella pratica: escludere il caso con la precondizione. Se la specifica esige , il problema dei negativi scompare, non perché sia stato risolto, ma perché chi usa la soluzione si è impegnato a non porlo. Ciò che non si può fare è lasciare la questione indecisa. E per la stessa ragione la specifica esige : con non esistono e che soddisfino la relazione, e il contratto deve dirlo.
Un errore frequente consiste nello specificare con un esempio: «l’output di 17 e 5 è 3 e 2». Un esempio illustra, ma non specifica, perché non dice che cosa accade con gli altri input; infinite relazioni diverse coincidono su un caso. La specifica si scrive come condizione generale, e gli esempi servono poi a verificarla.
Algoritmi
Una volta specificato il problema, si cerca un metodo per risolverlo. Un algoritmo è una sequenza di istruzioni che, applicata a qualunque input che soddisfi la precondizione, produce in un numero finito di passi un output che soddisfa la postcondizione. La definizione contiene tre requisiti.
- Finitezza. L’algoritmo si scrive con un numero finito di istruzioni e, per ogni input ammissibile, termina dopo averne eseguito un numero finito. Un procedimento che per qualche input continua per sempre non è un algoritmo per quell’input.
- Precisione. Ogni istruzione ha un unico significato, e chi la esegue sa in ogni momento che cosa fare e qual è l’istruzione successiva. Nessuna decisione è lasciata al giudizio di chi esegue.
- Generalità. L’algoritmo risolve il problema per tutti gli input ammissibili, non solo per un caso.
Con una certa libertà, si chiama algoritmo anche un procedimento preciso e che termina, ma che per qualche input ammissibile non soddisfa la postcondizione: a rigore è un algoritmo per un altro problema, e si dirà che è scorretto per il problema specificato. Così si parlerà nei problemi di questa lezione, che confutano algoritmi scorretti con un controesempio.
Una ricetta di cucina somiglia a un algoritmo, ed è per questo il paragone abituale; ma quasi nessuna lo è. «Aggiungere un pizzico di sale» non è preciso, perché un pizzico non è una quantità; «cuocere in forno finché non è dorato» dipende da un giudizio che due persone darebbero in modo diverso; «sbattere varie volte» non dice quante. Un cuoco colma queste lacune con la sua esperienza; un computer non ne ha nessuna. Ne segue il requisito che governa tutto il corso: un algoritmo deve poter essere seguito da chi non capisce il problema, senza indovinare nulla. Se per eseguire un passo bisogna sapere a che cosa serve, il passo è scritto male.
Gli algoritmi di questo corso si scrivono prima in passi numerati, in italiano, con alcune convenzioni. I dati che cambiano durante l’algoritmo si indicano con lettere o parole, come o , e si chiamano variabili. Si scrive « assume il valore » per dire che si calcola con il valore attuale di e il risultato diventa il nuovo valore di ; il valore precedente si perde. Nella forma abbreviata, che si usa quando si prendono appunti, la stessa istruzione si scrive con una freccia verso sinistra, che sulla tastiera è r <- r - b. Le istruzioni si eseguono nell’ordine del loro numero, salvo che una di esse dica altrimenti.
Il problema è quello specificato nella sezione precedente: input, due interi e ; output, due interi e con e . L’idea è quella di ripartire: si tolgono da gruppi di finché è possibile, si contano i gruppi tolti, e ciò che avanza è il resto.
- assume il valore , e assume il valore .
- Finché vale , ripeti i passi 3 e 4; quando smette di valere, prosegui al passo 5.
- assume il valore .
- assume il valore ; torna al passo 2.
- Restituisci e , e termina.
Il passo 2 è una domanda che si pone ogni volta: prima della prima iterazione e dopo ciascuna. I passi 3 e 4 formano il corpo della ripetizione, ciò che si ripete. Nulla è lasciato al giudizio di chi esegue: in ogni momento c’è una sola istruzione successiva.
L’algoritmo è lento (per dividere un milione per tre compie più di trecentomila iterazioni), e il metodo scolastico della divisione è molto più rapido; ma è corretto, e la sua semplicità lo rende ideale per imparare a ragionare sugli algoritmi. Il corso tornerà sulla differenza fra un algoritmo corretto e uno efficiente nella lezione Il costo di un algoritmo.
Conviene, infine, separare l’algoritmo dalla sua scrittura. L’algoritmo della divisione per sottrazioni è lo stesso se lo si scrive in italiano, in un diagramma, in C++ o in qualunque altro linguaggio di programmazione: ciò che lo definisce è la sequenza delle operazioni, non le parole. Un programma è la scrittura di un algoritmo in un linguaggio che una macchina può eseguire, e la lezione Programmi, compilatori e C++ spiegherà come avviene questa esecuzione. Se è così, un programma può essere sbagliato in due modi molto diversi: perché l’algoritmo è scorretto, e allora nessun linguaggio lo salva, oppure perché l’algoritmo è corretto ma è stato scritto male. Distinguere i due casi è metà del lavoro di debugging, ed è per questo che il corso scrive sempre prima l’algoritmo.
Seguire un algoritmo a mano
Leggere un algoritmo non basta per sapere che cosa fa; bisogna eseguirlo. Seguirlo a mano, come farebbe la macchina, senza saltare passi né indovinare risultati, è l’abilità più utile di un programmatore: con essa si verifica un algoritmo prima di scriverlo, si prevede l’output di un programma prima di eseguirlo e si trova l’errore di uno che non funziona.
Lo strumento è la tabella di traccia: una colonna per il numero del passo che si esegue, una colonna per ogni variabile e, se conviene, una per le condizioni che si valutano; e una riga per ogni passo eseguito. In ogni riga si annota il nuovo valore della variabile che è cambiata in quel passo e si lascia in bianco ciò che non è cambiato, in modo che il valore attuale di una variabile sia l’ultimo scritto nella sua colonna. Le condizioni si annotano con il loro risultato.
Seguiamo la divisione per sottrazioni successive con e . La freccia della figura indica il passo appena eseguito; la linea tratteggiata ricorda che il passo 4 torna al 2.
Dimostrazione
Passo 1: si danno i valori iniziali. L’input soddisfa la precondizione, perché e .
Prima iterazione. Nel passo 2 si verifica che , quindi si esegue il corpo: il passo 3 toglie un gruppo di cinque e il passo 4 lo conta.
Seconda iterazione: , e il corpo si esegue di nuovo.
Terza iterazione: , e il corpo si esegue di nuovo.
Nel passo 2 la condizione non è soddisfatta, perché : l’algoritmo prosegue al passo 5 e restituisce e . L’output soddisfa la postcondizione.
La tabella completa ha dodici righe: il passo 1, tre iterazioni di tre righe ciascuna (passi 2, 3 e 4), la verifica che fallisce e la restituzione.
Due osservazioni sulla tabella. La prima: il numero di iterazioni è uguale al quoziente, , e il passo 2 è stato eseguito una volta più del corpo, volte; ciò accade sempre in una ripetizione di questa forma, perché l’ultima verifica è quella che fallisce. La seconda: la traccia insegna qualcosa che il testo dell’algoritmo non dice a prima vista; se , per esempio e , la condizione fallisce la prima volta, il corpo non si esegue nessuna volta, e l’output è , , che è corretto, perché . Dimenticare il caso in cui la ripetizione non si esegue è uno degli errori più frequenti nel progettare algoritmi, e la traccia con input piccoli lo scopre.
La traccia di un caso non dimostra che l’algoritmo sia corretto: verifica un caso. Ma una traccia ben scelta (un caso comune, un caso limite, il caso in cui la ripetizione non si esegue) scopre la maggior parte degli errori, e quella che fallisce dice dove sono. La lezione Progettare cicli corretti insegnerà a passare dai casi a una dimostrazione.
Sequenza, decisione e ripetizione
Visti da vicino, gli algoritmi si costruiscono con pochissimi pezzi. Ci sono istruzioni elementari (dare un valore a una variabile, restituire un risultato) e tre modi di combinarle.
- Sequenza: un’istruzione dopo l’altra, in ordine. I passi 3 e 4 della divisione sono una sequenza: prima si sottrae, poi si conta.
- Decisione: a seconda che una condizione sia soddisfatta o no, si esegue una parte o un’altra (oppure una parte o niente). «Se , assume il valore » è una decisione.
- Ripetizione: finché una condizione è soddisfatta, si esegue una parte ancora e ancora. Il passo 2 della divisione, con il suo corpo, è una ripetizione.
Ogni forma ha un solo ingresso e una sola uscita, e il suo interno può essere a sua volta una qualunque delle tre: il corpo di una ripetizione può contenere decisioni, un ramo di una decisione può contenere una ripetizione, e così via. Se è così, con le tre forme si costruiscono algoritmi di qualunque complessità, come con pochi pezzi che si incastrano fra loro si costruisce qualunque edificio. Che bastino a esprimere ogni algoritmo è un risultato noto della teoria della programmazione (lo dimostrarono Corrado Böhm e Giuseppe Jacopini nel 1966); qui non lo si dimostra, ma tutto il corso lo conferma: i programmi che vi si scriveranno, per quanto lunghi siano, non useranno altro che queste tre forme, scritte in C++ come if per la decisione e while o for per la ripetizione, nelle lezioni Decisioni e Cicli.
Una notazione utile per vedere la forma di un algoritmo è il diagramma di flusso: ogni istruzione è un rettangolo, ogni condizione un rombo con due uscite (soddisfatta, che la figura etichetta , e non soddisfatta, ), e le frecce dicono qual è il passo successivo. È una notazione, non un fine: serve a cogliere con un colpo d’occhio un algoritmo piccolo, ma non scala, e nessuno scrive un programma grande disegnandolo.
La figura disegna le tre forme con istruzioni generiche e e una condizione , e poi le compone nel diagramma della divisione per sottrazioni successive.
Dimostrazione
Sequenza: si esegue e poi ; c’è un solo ingresso e una sola uscita.
Decisione: se è soddisfatta, si esegue ; altrimenti, . Entrambi i rami si riuniscono in una sola uscita.
Ripetizione: si valuta ; se è soddisfatta, si esegue e si torna a valutare ; altrimenti, si esce. La condizione si valuta una volta più del corpo.
Composizione: la divisione per sottrazioni successive è una sequenza (i valori iniziali, la ripetizione, la restituzione) la cui ripetizione ha per corpo un’altra sequenza (sottrarre, contare).
Distinguere le tre forme in un procedimento scritto in linguaggio comune è il primo passo per tradurlo. Il segnale di una decisione è un «se», un «nel caso in cui» o un «a seconda di»; quello di una ripetizione, un «finché», un «fino a quando», un «per ogni» o un «tante volte quante». Un procedimento senza nessuna di queste parole è una sequenza, e se deve trattare input di dimensione variabile (una lista di numeri, un testo), gli manca una ripetizione.
Termina?
La finitezza fa parte della definizione di algoritmo, e una ripetizione è l’unico luogo in cui può perdersi: una sequenza di cinque passi esegue cinque passi, e una decisione esegue uno dei suoi rami; ma una ripetizione esegue il suo corpo tante volte quante la condizione è soddisfatta, e nulla garantisce in anticipo che smetta di esserlo.
Per giustificare che una ripetizione termina si cerca una quantità, calcolata con le variabili, che soddisfi due condizioni: è un numero naturale (intero e non negativo) finché la ripetizione continua, e decresce strettamente a ogni iterazione. Se è così, la ripetizione termina: una successione di naturali strettamente decrescente non può essere infinita, perché ogni iterazione la abbassa almeno di uno e da un naturale non ci sono più di gradini fino allo zero. Tale quantità si chiama variante o funzione limitante, e la lezione Progettare cicli corretti la userà in modo sistematico.
Nella divisione per sottrazioni successive, la variante è . Finché la ripetizione continua, , quindi è un naturale. A ogni iterazione, assume il valore , e poiché (precondizione), il nuovo valore è strettamente minore. Pertanto la ripetizione termina, e lo fa al più dopo iterazioni (di fatto, esattamente dopo iterazioni, il quoziente).
Si osservi dove si è usata la precondizione . Se si esegue l’algoritmo con , il passo 3 lascia invariato, la condizione è sempre soddisfatta e la ripetizione non termina mai: l’algoritmo non è sbagliato, ma ha ricevuto un input che il contratto escludeva. E se fosse negativo, crescerebbe a ogni iterazione. La precondizione non è un ornamento: è esattamente ciò che serve perché il ragionamento funzioni.
Le due condizioni della variante sono necessarie, ed è facile sbagliare su una qualunque delle due. Si consideri questo procedimento, che pretende di decidere se un naturale è pari: «finché , assume il valore ; se si arriva allo zero, era pari». La quantità decresce a ogni iterazione, eppure il procedimento non termina se è dispari: con passa per , , , , e la condizione non fallisce mai. Ciò che non è soddisfatto è l’altra condizione: smette di essere naturale, perché la condizione della ripetizione non gli impedisce di scendere sotto lo zero. La correzione consiste nel ripetere finché e nel decidere alla fine secondo il valore che rimane, o ; ora è naturale a ogni iterazione, perché si sottrae solo quando , e decresce: il procedimento termina.
Decidere se una ripetizione termina non è sempre facile. Un esempio celebre: partendo da un naturale , finché , se è pari lo si divide per due, e se è dispari lo si sostituisce con . Con la successione è , e termina. Nessuno ha trovato un per cui non termini, e lo si è verificato con i computer per quantità enormi di valori; ma nessuno è riuscito a dimostrare che termini sempre: è la congettura di Collatz, un problema aperto della matematica. Nessuna quantità nota decresce a ogni iterazione. La morale per chi programma è modesta e ferma: un algoritmo di cui non si sa giustificare la terminazione non è finito.
Errori frequenti
- Passi ambigui. «Ripetere varie volte», «finché va bene», «prendere un numero adeguato». Ogni passo deve poter essere eseguito senza capire il problema; altrimenti, è scritto male.
- Dimenticare il caso in cui la ripetizione non si esegue. Ogni ripetizione con la condizione all’inizio può eseguire il suo corpo zero volte. Bisogna chiedersi sempre che cosa restituisce l’algoritmo in quel caso, e la traccia con un input minimo lo dice.
- Specificare con un esempio. «Per 17 e 5 dà 3 e 2» non è una specifica; la relazione si scrive come condizione generale, e l’esempio la illustra.
- Confondere l’algoritmo con il suo programma. Un programma che non funziona può avere sbagliato l’algoritmo o la sua scrittura. Chi non ha scritto prima l’algoritmo non sa quale delle due cose cercare.
- Dare per scontata la terminazione. Che una quantità decresca non basta: deve essere inoltre naturale finché si ripete.
Problemi risolti
Scrivere un algoritmo che soddisfi la specifica del massimo di tre numeri della prima sezione, e seguirlo a mano per gli input e .
Soluzione
Idea: si percorre l’input conservando il maggiore visto finora in una variabile . Primo passo: assume il valore . Dopo di esso, è il massimo dei dati visti, che sono solo .
Secondo passo: se , assume il valore . Terzo passo: se , assume il valore . Quarto passo: restituire . Sono tre istruzioni in sequenza, due delle quali decisioni senza ramo alternativo.
Traccia per : dopo il primo passo ; nel secondo, è falso e non cambia; nel terzo, è falso e non cambia. Si restituisce .
Traccia per : dopo il primo passo ; nel secondo, e passa a valere ; nel terzo, è falso e non cambia. Si restituisce , che è corretto: il pari merito non richiede un trattamento a parte.
Giustificazione: dopo ogni passo, è uno dei dati visti e nessuno di essi lo supera. È vero dopo il primo, e ogni decisione lo conserva: se il dato nuovo è maggiore, diventa ; altrimenti, non supera . Alla fine sono stati visti tutti e tre, e soddisfa la postcondizione.
Se si fosse scritto al posto di , il risultato sarebbe lo stesso, perché in caso di pari merito si assegna un valore uguale. La scelta fra e avrebbe importanza se si chiedesse anche la posizione del massimo: con si restituisce la prima delle posizioni a pari merito, e con l’ultima; la formula chiama la posizione .
Nel calendario gregoriano, un anno è bisestile se è divisibile per , salvo quelli divisibili per , che lo sono solo se sono anche divisibili per . Uno studente propone l’algoritmo «se l’anno è divisibile per , restituire sì; altrimenti, restituire no». Trovare un anno in cui fallisce e scrivere un algoritmo corretto, con la sua specifica.
Soluzione
Specifica. Input: un intero (il calendario gregoriano è in vigore dall’ottobre 1582). Output: sì o no. Relazione: l’output è sì esattamente quando è divisibile per e non per , oppure è divisibile per .
L’algoritmo proposto indovina nella maggior parte degli anni, e per questo sembra corretto. Per confutarlo basta un anno divisibile per e non per : è divisibile per , e l’algoritmo dice sì; ma il non fu bisestile.
Algoritmo corretto, in tre decisioni concatenate. Passo 1: se è divisibile per , restituire sì e terminare. Passo 2: se è divisibile per , restituire no e terminare. Passo 3: se è divisibile per , restituire sì; altrimenti, restituire no.
L’ordine delle domande è essenziale: vanno dalla regola più particolare alla più generale. Se si chiedesse prima della divisibilità per , l’anno riceverebbe sì prima di arrivare all’eccezione. Traccia per quattro anni, uno per ciascun caso: (passo 1: sì), (passo 2: no), (passo 3: sì), (passo 3: no).
L’insegnamento non riguarda solo i calendari: un algoritmo si confuta con un solo controesempio, e i controesempi si cercano ai bordi delle regole, non nei casi comuni. Provare con , e non avrebbe scoperto nulla.
Seguire a mano la divisione per sottrazioni successive con e , e con e . Poi giustificare che per ogni input ammissibile l’algoritmo termina dopo esattamente iterazioni e che il suo output soddisfa la postcondizione.
Soluzione
Traccia con , : passa per , , , , , e per , , , , . La quinta verifica, , fallisce. Output: , . Qui il caso limite è la divisione esatta: la quarta iterazione avviene perché , ed è per questo che la condizione deve essere e non .
Traccia con , : la prima verifica, , fallisce; il corpo non si esegue nessuna volta. Output: , , che è corretto.
Una relazione che si conserva. Prima della prima iterazione, . A ogni iterazione diminuisce di e aumenta di , quindi non cambia. Pertanto l’uguaglianza vale dopo ogni iterazione.
Terminazione. Finché si ripete, , e è un naturale; a ogni iterazione diminuisce di . Una successione strettamente decrescente di naturali è finita: la ripetizione termina.
Al termine, la condizione è fallita: . Inoltre , perché si sottrae solo quando . Insieme alla relazione che si conserva, l’output soddisfa la postcondizione; e poiché conta le iterazioni, il numero di iterazioni è il quoziente.
Esercizi in linea
Seguire a mano l’algoritmo seguente e scrivere nella casella il valore che restituisce, solo il numero.
- assume il valore , e assume il valore .
- Finché vale , ripeti i passi 3 e 4; quando smette di valere, prosegui al passo 5.
- Se è pari, assume il valore ; se è dispari, assume il valore .
- assume il valore ; torna al passo 2.
- Restituisci .
Il browser controlla la tua risposta all’istante, senza valutarla.
Suggerimento
Fai la tabella: passa da a nella prima iterazione. Conta un’iterazione per ogni cambio di , finché non vale .
Vedi la soluzione
7L’algoritmo seguente, scritto nella forma abbreviata, deve calcolare il quoziente e il resto della divisione per sottrazioni successive, ma gli manca la condizione della ripetizione. Sostituire la riga di trattini bassi con la condizione corretta, senza cambiare il resto.
Il browser controlla la tua risposta all’istante, senza valutarla.
Suggerimento
Segui a mano l’algoritmo con e : la seconda iterazione deve avvenire anche se il resto vale esattamente .
Vedi la soluzione
Input: a (intero, a >= 0), b (intero, b > 0)1. q <- 02. r <- a3. finché r >= b ripeti: r <- r - b q <- q + 14. restituisci q, rProblemi proposti
Ogni problema ha un suggerimento, che conviene aprire solo dopo averlo tentato, e una risposta, che compare dopo aver aperto il suggerimento.
Scrivere la specifica del problema «ordinare tre numeri» (input, output e relazione) e indicare quali decisioni, non prese dall’enunciato, essa obbliga a prendere. Poi verificare che la specifica scarta questi due output per l’input : e , e che ammette .
Suggerimento
Un output «ordinato» può non avere nulla a che fare con l’input. Quale condizione lega i numeri dell’output a quelli dell’input, compresi quelli ripetuti? Crescente o decrescente? Stretto o no?
Risposta
Input: tre numeri , , . Output: tre numeri , , . Relazione: , e è un riordinamento di , cioè contiene gli stessi valori con le stesse ripetizioni. Decisioni che l’enunciato non prendeva: il verso (qui, dal minore al maggiore); che l’ordine non è stretto ( e non , perché con valori ripetuti l’ordine stretto è impossibile); e che l’output deve essere un riordinamento. è ordinato ma contiene un che non c’era; ha quattro valori; soddisfa entrambe le condizioni. Senza la condizione di riordinamento, sarebbe un output corretto per qualunque input.
Scrivere un algoritmo in passi numerati che riceva un naturale e ne restituisca le cifre dall’ultima alla prima (per , restituisce , , , ), usando solo il quoziente e il resto della divisione per . Seguirlo a mano per e per , e giustificare che termina per ogni .
Suggerimento
Il resto della divisione per è l’ultima cifra, e il quoziente è il numero senza di essa. Decidi se la condizione si chiede prima o dopo aver restituito una cifra, e guarda che cosa accade con , che ha una cifra.
Risposta
Un algoritmo corretto: (1) restituire il resto della divisione di per ; (2) assume il valore del quoziente della divisione di per ; (3) se , tornare al passo 1; altrimenti, terminare. La domanda viene dopo il corpo, quindi il corpo si esegue almeno una volta: per restituisce , che è la sua unica cifra. Se si chiedesse all’inizio («finché »), per non si restituirebbe nulla; è l’errore di dimenticare il caso in cui la ripetizione non si esegue. Traccia per : si restituiscono , , , , e passa per , , , . Terminazione: la variante è , un naturale; se si torna al passo 1 è perché , e per il quoziente per è strettamente minore di , quindi decresce strettamente a ogni iterazione.
Si dispone di monete da , e unità, in quantità illimitata. Specifica: input, un naturale ; output, tre naturali , , con e la minor quantità possibile di monete, . Una persona propone l’algoritmo seguente. Trovare un input in cui fallisce, spiegare ogni difetto e correggerlo.
- , e assumono il valore .
- Se , assume il valore e assume il valore .
- Finché : assume il valore e assume il valore .
- Finché : assume il valore e assume il valore .
- Restituisci , e .
Suggerimento
La somma delle monete è sempre ; ciò che può fallire è la quantità. Prova un multiplo esatto di e un importo superiore a , e chiediti quale modo di combinare i passi usa il passo 2 e quale dovrebbe usare.
Risposta
Ci sono due difetti. Primo, il passo 3 usa : con non restituisce nessuna moneta da e ne restituisce dieci da , invece di una. Secondo, il passo 2 è una decisione dove serve una ripetizione: con , l’algoritmo così com’è scritto restituisce una moneta da , sei da e dieci da (diciassette monete), e anche con il passo 3 già corretto ne restituirebbe una da e sette da (otto), invece di due da e due da (quattro). L’input mostra entrambi i difetti: l’algoritmo restituisce una moneta da , quattro da e dieci da , quindici monete, invece di due. Correzione: passo 2, «finché , …»; passo 3, «finché , …». Così corretto, l’algoritmo prende sempre la moneta più grande che ci sta, e con queste tre monete ciò dà la quantità minima: la cifra delle unità di si può pagare solo con monete da , e di ciò che resta conviene pagare con monete da tutto il possibile, perché cinque da valgono quanto una da . Con altre monete, per esempio da , e per , prendere sempre la più grande non dà più il minimo ( contro ).