Prog 1 - InCo | SolucionesComentadas

Instituto de Computación
Curso de Programación 1 - Soluciones comentadas

En este material se ilustran, mediante ejemplos tomados de soluciones de estudiantes, algunos aspectos de buena práctica de programación. Las letras y soluciones de parciales y exámenes de años anteriores a las que se hace referencia, son accesibles vía Web.

1.  Eficiencia

Entre las buenas prácticas de programación, se menciona que los algoritmos sean "razonablemente eficientes".

Un punto importante a tener en cuenta, en este sentido, es la correcta elección de la estructura de iteración apropiada para cada problema.

La eficiencia tiene que ver con la cantidad de operaciones que se realizan en una solución. Una solución que, aún siendo correcta desde el punto de vista del resultado, realice operaciones "de más" no se considera "razonablemente eficiente"

En las secciones siguientes se ejemplifican estos conceptos.

1.1  Selección apropiada de las estructuras de iteración

Considere el siguiente ejercicio:

Dada la siguiente definición de tipos para representar cadenas de caracteres de largo N y M:

     CONST N = . . .;
     . . .
     TYPE
             CadenaN = ARRAY[1..N] of Char;

Escriba una función que, dados un carácter y una cadena de largo N, determine si el carácter pertenece a la cadena.

    FUNCTION pertenece(c : Char; cadN : CadenaN) : Boolean;

Una solución común pero INCORRECTA para este ejercicio es la siguiente:

function pertenece(c : Char; cadN : CadenaN) : Boolean;
(* SOLUCIÓN INCORRECTA*)

var i : Integer;
    encontre : Boolean;

begin

   encontre := FALSE;

   for i := 1 to N do
    if cadN[i] = c then
      encontre := TRUE;

   pertenece := encontre

end;

Esta solución, en donde se utiliza la estructura de iteración for para recorrer de forma completa el arreglo, es ineficiente. Este error es considerado grave en una evaluación.

La iteración que debe realizarse es una búsqueda, una vez que se encuentra el elemento c en cadN la iteración debe terminar. Lo correcto es utilizar una estructura de iteración CONDICIONAL, es decir, while o repeat.

Dado que asumimos evaluación por circuito corto, podemos utilizar while y diseñar una solución compacta que realice la comparación del carácter c con los elementos del arreglo en la misma condición de iteración:

function pertenece(c : Char; cadN : CadenaN) : Boolean;

var i : Integer;

begin

   i := 1;

   while (i <= N) and (cadN[i] <> c) do (* itero mientras no encuentre c *)
      i := i+1;

   pertenece := i <= N  (* si se cumple i <= N , encontré c *)

end;

1.2  Número de operaciones

Veamos los siguientes ejemplos:

Observe que el uso de la función sqrt en la definición de EsPrimo tiene el efecto de disminuir el número de operaciones ejecutadas:

function EsPrimo(numero: integer): boolean;
var
  i,tope: integer;

  function divide(n,m: integer): boolean;
  begin
    divide := m mod n = 0;
  end;

begin
  i := 2;
  tope := trunc(sqrt(numero));
  while (i<=tope) and not divide(i,numero) do
      i := i+1;

  EsPrimo := i > tope;
end;

Sea el ejercicio 1 del segundo parcial de 2005:

Escribir un programa que lea de la entrada un número n entero mayor o igual que 0 y un número x real distinto de 0, y despliegue el valor de la siguiente suma:

El programa sólo puede hacer sumas, multiplicaciones y divisiones. La cantidad de multiplicaciones no puede ser mayor que el doble de N y la cantidad de divisiones no puede ser mayor que N. No se pueden utilizar subprogramas auxiliares ni invocar subprogramas predefinidos salvo los de entrada y salida.

se pide calcular una sumatoria, exigiendo:

  • "La cantidad de multiplicaciones no puede ser mayor que el doble de N y la cantidad de divisiones no puede ser mayor que N".

A continuación se muestran dos soluciones que no respetan esa exigencia pedida en la letra, lo cual es un error grave (se señalan otros errors también):

  program Suma(input,output);
 (* SOLUCIÓN INCORRECTA*)
  var
     n,j,z,i:Integer;          (* los nombres de las variables no son adecuados *)
     x,k: Real;     

 begin 
     readln (n,x);
     for j := 2 to n do
        begin
           z := j;
           repeat
              z := z * (z-1)
           until ((z-1) = 1)            (* hace más de 2*n multiplicaciones *)
           k := x;
           i := j;
           repeat 
              k := k * x;
              i := i-1
           until (i = 0)                 (* hace más de 2*n multiplicaciones *)
           x := x + (k DIV z)            (* usa DIV *)
     end;
     writeln (x);
end.

Otra forma de hacer más de 2 * n multiplicaciones:

(* SOLUCIÓN INCORRECTA*)
var x, termino,suma:Real;
    j,i,n,z: Integer;
begin
    read (n,x);
    suma := 1;
    termino := 1;
    z := 1;
    for  i := 1 to n do
      begin
         for j := 1 to i do
           z := z*i;                  (* hace más de 2 * n multiplicaciones *)                                
         termino := (termino * x) / z    
         z := 1;
         suma := suma + termino;
      end;
      writeln (suma);
end.  

Solución correcta:

program Suma(input,output);
var x, potencia, suma: real;
     i, n, factorial: integer;
begin
    readLn(n); readLn(x);
    factorial:= 1;  
    potencia:= 1;
    suma:= 1;
    for i:= 1 to n do
       begin
           potencia:= potencia * x;
           factorial:= factorial * i;
           suma:= suma + potencia / factorial;
       end;
    writeLn(suma);
end.

Sea el ejercicio 1 del examen de julio 2006:

Dado un número positivo n, se define s(n) como la suma de todos los divisores positivos de n, sin incluir al propio n.

s(15)=1+3+5=9

s(9) = 1+3=4

s(4)=1+2=3

s(3)=1

s(1)=0

Dos números m y n son amigables si se cumple que:

  • s(n) = m
  • s(m) = n

Ejemplo: 220 y 284 son amigables.

Escribir una función que determine si dos números dados son amigables:

 function amigables(m,n: integer): boolean;

Se supone m y n números naturales distintos de 0. La solución que se implemente debe realizar la menor cantidad posible de operaciones (sumas, divisiones, etc.)

Otras parejas de números amigables son: 1184 1210, 2620 2924, 6232 6368, 17296 18416.
He aquí una solución
function sumaDivisoresIgual(x,y:integer): boolean; (* devuelve true si s(x) = y *)
var i,suma,mitad: integer;
begin
   i:= 1;
   mitad:= x div 2;
   suma:= 0;
   while (i <= mitad) and (suma <= y) do
     begin
       if (x mod i = 0) then
           suma:= suma + i;
        i:= i + 1;
     end;
   sumaDivisoresIgual := suma = y;
end;
function amigables(m,n:integer): boolean;
begin (* evaluación por circuito corto *)
   amigables := sumaDivisoresIgual(m,n) AND sumaDivisoresIgual(n,m);
end;

Observar que una solución con for como la siguiente:

FUNCTION amigables (m, n: integer): Boolean;
VAR
    tope, suma, i : Integer;
BEGIN
    tope := n DIV 2;
    suma := 0;
    FOR i:=1 TO tope DO
        IF (n MOD i) = 0 THEN
            suma := suma + i;
amigables := (suma = m)
END;

no tiene en cuenta que si la suma de divisores de n sobrepasa el valor de m,
debe detenerse la iteración y los números no son amigables.

1.3  Búsquedas en arreglos teniendo en cuenta propiedades de los elementos

A veces, la búsqueda de un elemento en un arreglo puede hacerse más eficientemente considerando propiedades del elemento que se busca. En general, realizar la búsqueda ignorando dicha propiedad se considera error leve o medio (salvo que se pida expresamente en la letra, en cuyo caso, sería error grave).

Sea el ejercicio 1 del examen de julio 2007:

El siguiente arreglo:

Estuds_por_digito = ARRAY [0 .. 9] OF Integer;

se utiliza para registrar la cantidad de estudiantes que asisten a una universidad, según el último dígito de su cédula. En la celda con índice i, se guarda la cantidad de estudiantes con cédula terminada en i.

Escribir en Pascal la función May_dig_par que devuelve el mayor dígito par, para el cual hay una cantidad de estudiantes menor a un valor entero dado.

Observar que lo que está en negrita indica propiedades del elemento que pueden usarse para hacer una búsqueda más eficiente, tal como está planteado en la solución propuesta:

FUNCTION May_dig_par(estudiantes:Estuds_por_digito; valor:Integer) : Integer;

VAR indice : Integer;

BEGIN

    (* inicializo el indice con el mayor digito par posible *)

    indice := 8;


    (* busco una celda del arreglo que cumpla la condicion, 
      empezando por el mayor indice, decrementando de a dos 
      para mirar solo los pares 
     *)

    WHILE (indice >= 0) AND (estudiantes[indice] >= valor) DO
        indice := indice - 2;


    (* retorno resultado *)

    IF indice >= 0 THEN
        May_dig_par := indice	
    ELSE
        May_dig_par := -1

END;

¿Cómo sería una búsqueda ineficiente?

1.4  Arreglos: Recorridas múltiples, copias innecesarias, etc.

Es importante NO ABUSAR del uso de arreglos. Por ejemplo, usar un arreglo auxiliar para guardar datos de la entrada y después operar con ellos, cuando se pueden realizar las operaciones a medida que se van ingresando, es un error grave. En general, el uso de MAXINT como tamaño de un arreglo se considera error grave.

Sea el ejercicio 2 del examen de julio 2006:

Sean una secuencia de valores:

  • (x1, ...,xk,...,xn)

Llamamos permutación circular de esa secuencia, a toda secuencia de la forma:

  • (xk,...,xn,x1,...,xk-1)

donde k es un valor comprendido entre 1 y n.

Al valor k se le llama índice de la permutación

Ejemplos: Todas las permutaciones circulares de la secuencia ahora son:

  • ahora (índice 1)
  • horaa (índice 2)
  • oraah (índice 3)
  • raaho (índice 4)
  • aahor (índice 5)

Se considera la siguiente definición de tipo:

 type
    ArregloChar = array [1..Max] of char;

donde Max es una constante mayor que 1.

Se pide escribir una función:

 function circular(A,B: ArregloChar): integer;

Esta función determina si el arreglo A es una permutación circular del arreglo B y en ese caso retorna el índice de la permutación. Retorna 0 si no es una permutación circular.

Una solución común es copiar el arreglo A a uno auxiliar, que se va transformando en todas las posibles permutaciones y comparando con B. Sin embargo, si se utiliza la información que se da en la letra sobre el índice, se ve que se puede resolver el problema sin ese arreglo auxiliar, como se plantea en la solución dada:

const Max=...;

type ArregloChar = array [1..Max] of char;


function circularIndice(indice: integer; A,B: ArregloChar) : boolean;
{ retorna true si es A es una permutación circular de B
  de índice i }
   function siguiente(i: integer): integer;
   { auxiliar que permite la recorrida circular del arreglo }
   begin
      if i < Max then
        i:= i+1
      else
        i:= 1;
siguiente := i; end; var k,j: integer; begin k:= indice; j:= 1; while (j<=Max) and (A[j] = B[k]) do begin j:= j+1; k:= siguiente(k); end; circularIndice:= j > Max; end; function circular(A,B:ArregloChar): integer; var i:integer; begin i:=1; while (i <= Max) and not circularIndice(i,A,B) do i:=i+1; if i <= Max then circular:= i else circular:= 0; end;

2.  Errores de rango

2.1  Salirse del rango de un arreglo

Cuando se manejan índices de un arreglo, es muy importante tener en cuenta si alguno de ellos puede referenciar una celda inválida del arreglo (fuera del rango). Por ejemplo en el ejercicio 3 del segundo parcial de 2006 se dice:

Sea un arreglo definido de la siguiente manera:

type
       ArregloNum = array [1..N] of integer;

Escribir una función

function EntreNegativos(a: ArregloNum): integer 

que retorna el índice i del primer elemento de a que cumpla:

  • a[i] >= 0
  • a[i-1] < 0
  • a[i+1] < 0

Si no existe tal elemento, retorna N+1

Observar que, para un índice, se maneja el anterior y el posterior al mismo.

En este ejercicio el error más común es "salirse del arreglo", es decir, referenciar una celda inválida del arreglo (fuera del rango) (error grave), como se ilustra en la siguiente solución de la function EntreNegativos.

(* SOLUCIÓN INCORRECTA*)
var i, soluc:Integer;
    existe:boolean;
begin
    i := 2;
    existe := false
    while i <= N and not existe do        (* puede causar salida del rango del arreglo                      
si i=N, pues en i+1 se accede a a[N+1] *) if (a[i] >= 0) and (a[i-1] < 0) and (a[i+1] < 0) then soluc := i else i := i+1; if soluc < N then entreNegativos := soluc else entreNegativos := N + 1; end;

Esta solución tiene además otro error, ¿cuál?

2.1.1 Uso inadecuado de la evaluación de booleanos por circuito corto

La evaluación de booleanos por circuito corto está relacionada con el punto anterior, ya que en un and si la primera condición es falsa, no se evalúa la segunda y eso puede evitar que se referencie una celda del arreglo fuera del rango, como se ilustra en la siguiente solución del examen de febrero de 2006, en el ejercicio 1 parte a).

Se considera las siguientes declaraciones:
const 
   MaxArray = . . .; {algún valor apropiado, mayor que 1 }

type 
   RangoArray = 1..MaxArray;
   Arreglo = array [RangoArray] of Integer;

a) Escribir una función iterativa:
function ordenado(inf,sup: RangoArray; arr: Arreglo): boolean; 
Esta función retorna true si y sólo si la porción del arreglo comprendida entre inf y sup está ordenada de menor a mayor.
Esto significa que para todo valor i que sea mayor o igual que inf y menor que sup se cumple que a[i] <= a[i+1]

Se supone que:
- MaxArray es un entero mayor que 1
- inf y sup son índices válidos dentro del arreglo e inf es menor o igual que sup

function ordenado(inf,sup: RangoArray;  arr: Arreglo): boolean;
var
   i : integer;

begin

   i:= inf;

   while (i<sup) and (arr[i]<=arr[i+1]) do (* evaluación por circuito corto:
                                              cuando i = sup no chequeo la condición a la derecha del and *)
      i:= i+1;

   ordenado:= i = sup;

end; { ordenado }

¿Qué pasaría si la condición se pone al revés: (arr[i]<=arr[i+1]) and (i<sup) ?

2.1.2 Volver a acceder al arreglo luego de la iteración

En soluciones similares a la anterior, es frecuente exceder el rango del arreglo luego de que finaliza la iteración, por chequear la condición arr[i]<=arr[i+1] en vez de chequear i = sup al momento de asignar el resultado de la función. Se ilustra este error en el código siguiente:

function ordenado(inf,sup: RangoArray;  arr: Arreglo): boolean;
(* SOLUCIÓN INCORRECTA*)
var
   i : integer;

begin

   i:= inf;

   while (i<sup) and (arr[i]<=arr[i+1]) do
      i:= i+1;

   ordenado:= arr[i]<=arr[i+1];    (* si el arreglo está ordenado se cumple i=sup, 
                                      por lo que i+1 no es una celda válida *)

end; { ordenado }

2.2  Variables de tipo subrango

También podemos causar un error por salir de rango si le asignamos un valor no válido a una variable declarada de tipo subrango. Esto sucede a menudo si, al declarar un índice para recorrer un arreglo, le asignamos el tipo subrango correspondiente al tamaño del arreglo. Como, en general, las iteraciones sobre arreglos terminan cuando el índice sobrepasa el valor máximo del arreglo, el último incremento del índice provoca un error de rangos. Este error se ilustra en el ejemplo siguiente, donde  si i : RangoArray, cuando i = M y a[i] <> valor, se incrementa i y se produce el error de salida de rango (RangoArray = 1.. M;).  Si i : Integer, no se produce ese error.

i:= 1;

while (i <= M) and (a[i] <> valor) do
       i:= i+1;

encontrado:= i<=M

Es conveniente declarar estos índices de tipo Integer.

3.  Otros errores frecuentes

3.1  Elección incorrecta de estructuras de iteración, aunque no se afecte la eficiencia

En el siguiente ejemplo, la utilización de while en vez de for, se considera error medio.

Dadas las siguientes declaraciones:

CONST
        M = . . .;
TYPE
        RangoM = 1..M;
        MatrizMxM = ARRAY[RangoM,RangoM] OF Integer;

Escriba un programa para trasponer una matriz de dimensiones MxM utilizando la misma matriz. (Práctico 9 ejercicio 8).

La siguiente solución:

procedure trasponer(var a : MatrizMxM);
(* SOLUCIÓN INCORRECTA*)
var i, j : Integer;
begin
   i := 1;
   while (i <= M) do begin
      j := 1;
      while (j <= M) do begin
         if (i <> j) then
            intercambiar(a[i, j], a[j, i]);
         j := j + 1;
      end;
      i := i + 1;
   end;
end;

tiene un error y una observación de mal estilo de programación.

El mal estilo de programación refiere a que se usa while donde debería usarse for: dado que sabemos a priori que tenemos que recorrer toda la matriz, la solución más adecuada es usar for.

El error consiste en que la condición del segundo while es incorrecta, si dejamos (j <= M) el procedimiento trasponer deja la matriz tal como está. Por ejemplo, los valores a[2,3] y a[3,2] se intercambian cuando (i = 2, j = 3), pero vuelven a intercambiarse cuando (i = 3, j = 2). ¿Qué hay que cambiar en la condición para que el algoritmo se comporte de forma correcta?

Reescriba el procedimiento usando únicamente for.

El procedimiento intercambiar es el siguiente:

procedure intercambiar(var a, b : Integer);
var aux : Integer;
begin
   aux := b;
   b := a;
   a := aux;
end;

3.2  Asignar un registro campo a campo (en vez de hacerlo con una sola asignación).

Ejemplo:

TipoElemento = RECORD
		  codigo : Integer;
		  valor	 : Integer 
	       END;	 

Conjunto = RECORD
		 elemento : ARRAY [1 .. MAXCONJ] OF TipoElemento;
		 tope	  : 0..MAXCONJ
	      END;	  

procedure AgregarElemento(elem: TipoElemento; VAR conj: Conjunto);
(* agrega un elemento a un conjunto *)
(* SOLUCIÓN INCORRECTA*)

BEGIN
   conj.tope := conj.tope+1;
   conj.elemento[conj.tope].codigo := elem.codigo; 
   conj.elemento[conj.tope].valor := elem.valor;   (*lo correcto es: conj.elemento[conj.tope] := elem*)  
END

3.3  if 's innecesarios

En muchos casos, se chequean condiciones innecesarias, lo cual, según el caso, puede ser considerado un error medio o leve.

Se ilustra en la siguiente solución del ejercicio 1 del examen de julio 2009:

Se considera el tipo ArregloEntero definido como sigue:
const
   Max = . . . ;
type
   RangoIndice = 1..Max;
   ArregloEntero = record
         info: array [RangoIndice] of integer;
         tope: 0..Max;
   end;
Se solicita escribir una función:
function SubSecuencia(A1,A2: ArregloEntero) : boolean
que retorna true en el caso que los valores almacenados en A1 puedan obtenerse como resultado de borrar elementos (eventualmente ninguno) de A2 manteniendo el orden de los restantes.

Así por ejemplo, si consideramos la secuencia:
10 7 6 66 44 6

Las siguientes son subsecuencias:
10
10 7
10 66 6
6 44
7 6 6
44 6
10 7 6 66 44 6
6 66 6

Las siguientes no son subsecuencias
15
7 10
7 6 6 6
10 10 10
44 6 7

Notar que la secuencia vacía es siempre subsecuencia de cualquier otra y que toda secuencia es subsecuencia de sí misma.
La solución debe realizar una única recorrida de ambos arreglos y no puede utilizar arreglos auxiliares.
function subsecuencia (A1,A2:ArregloEntero) :boolean
(* SOLUCIÓN INCORRECTA*)

var i, j:integer;
seguir:boolean;
begin
    if (A1.tope=0) or (A2.tope=0) then              (* esto es chequeado por la condición del while, 
                                                          este if innecesario se considera error *)
            subsecuencia := true

    else begin
           i:=1; j:= 1;
           while (j <= A1.tope) and (j <= A2.tope)  (* con esta condición no se chequea que las celdas 
                                                                   que quedan alcancen para que sea subsecuencia *)

               ... 


4.  Interpretación incorrecta de la letra

Leer mal la letra de los ejercicios lleva indefectiblemente a errores ya sean graves, medios o leves.

Tomemos como ejemplo el ejercicio 3 del segundo parcial de 2008:

El algoritmo o fórmula de Luhn, es una simple fórmula usada para validar números de identificación, como por ejemplo los números de tarjeta de crédito.

El algoritmo recibe una secuencia de 20 dígitos representados como caracteres y realiza los siguientes cálculos:
- Multiplica por 2 los dígitos que están en lugares pares de la secuencia.
- Suma los dígitos individuales que componen los productos de los lugares pares más los dígitos no afectados del número original.

Si la suma calculada es múltiplo de 10, el resultado del algoritmo es verdadero.
Si la suma calculada no es múltiplo de 10 o alguno de los caracteres de la secuencia no corresponde a un dígito, el resultado del algoritmo es falso.

Se define la siguiente estructura de datos:
CONST
 MAX = 20;
TYPE
 TNumero = array[1..MAX] of char;
Se pide implementar la función que chequee si un número es válido o no según la fórmula de Luhn.
function validarLuhn(numero : TNumero) : boolean;
Ejemplo:

secuencia '1' '7' '3' '4' '2' '6' '7' '8' '9' '0' '4' '2' '3' '9' '5' '6' '7' '8' '9' '0'

Multiplicamos los dígitos de los lugares pares por 2:
1  7  3  4  2  6  7  8  9  0  4  2  3  9  5  6  7  8  9  0
  x2    x2    x2    x2    x2    x2    x2    x2    x2    x2
-----------------------------------------------------------
  14     8    12    16     0     4    18    12    16     0
Sumamos:
dígitos individuales (de los productos) = 1 + 4 + 8 + 1 + 2 + 1 + 6 + 0 + 4 + 1 + 8 + 1 + 2 + 1 + 6 + 0 = 46
dígitos no afectados = 1 + 3 + 2 + 7 + 9 + 4 + 3 + 5 + 7 + 9 = 50
dígitos individuales + dígitos no afectados = 46 + 50 = 96

Resultado: como 96 no es múltiplo de 10, el resultado es falso.

Obviar la exigencia de la letra "... o alguno de los caracteres de la secuencia no corresponde a un dígito ..." induciría a un error grave: recorrer el arreglo completo con un for para ir procesando todos los elementos.

Dado que alguno de los caracteres puede no corresponder a un dígito, la solución correcta sería ir recorriendo el arreglo mientras no haya valores inválidos (no dígitos), lo cual se implementa mediante una iteración condicional. Ver solución.


Última modificación: martes, 30 de noviembre de 2021, 19:39