/***********************************************************************
This file is part of KEEL-software, the Data Mining tool for regression,
classification, clustering, pattern mining and so on.
Copyright (C) 2004-2010
F. Herrera (herrera@decsai.ugr.es)
L. S�nchez (luciano@uniovi.es)
J. Alcal�-Fdez (jalcala@decsai.ugr.es)
S. Garc�a (sglopez@ujaen.es)
A. Fern�ndez (alberto.fernandez@ujaen.es)
J. Luengo (julianlm@decsai.ugr.es)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/
**********************************************************************/
package keel.Algorithms.Decision_Trees.SLIQ;
import java.io.*;
import java.lang.reflect.Array;
import java.util.*;
/**
Implementaci�n en Java del algoritmo SLIQ
Basada parcialmente en el c�digo del algoritmo ID3 de Crist�bal Romero Morales (UCO)
@author Francisco Charte Ojeda (pr�ctica ICO de la UJA)
@version 1.0 (28/12/09 - 10/1/10)
*/
public class SLIQ extends Algorithm {
/** Ra�z del �rbol */
Node root;
// Para el proceso de divisi�n
Node subnodoIzquierdo, subnodoDerecho;
/** N�mero de nodos existentes en el �rbol */
int NumberOfNodes;
/** N�mero de hojas en el �rbol */
int NumberOfLeafs;
/** Lista con las clases */
ListaClases[] listaClases;
/** Listas con los atributos ordenados */
Vector<ListaAtributos>[] listas;
/** Lista de nodos pendientes de procesar durante el crecimiento del �rbol */
Queue<Node> listaNodos;
/** Constructor.
*
* @param paramFile El archivo de par�metros.
*
*/
public SLIQ(String paramFile) {
try {
// Inicia el temporizador
startTime = System.currentTimeMillis();
// Establecer las opciones de ejecuci�n del algoritmo
StreamTokenizer tokenizer = new StreamTokenizer(new BufferedReader(new FileReader(paramFile)));
initTokenizer(tokenizer);
setOptions(tokenizer);
// Inicializa el dataset a procesar
modelDataset = new Dataset(modelFileName, true);
// Obtener los conjuntos de datos de entrenamiento y prueba
trainDataset = new Dataset(trainFileName, false);
testDataset = new Dataset(testFileName, false);
// Se generan la lista de clases y las listas de atributos, ya ordenadas
generaListas();
// El �rbol est� vac�o
NumberOfNodes = 0;
NumberOfLeafs = 0;
// Genera el �rbol seg�n el algoritmo SLIQ.
generateTree();
// Imprimir los resultados generados
printTrain();
printTest();
printResult();
} catch (Exception e) {
e.printStackTrace();
System.err.println(e.getMessage());
System.exit(-1);
}
}
/** Construye las listas por atributo y lista de clases usadas en SLIQ
*
*/
protected void generaListas() {
int n = 0; // Para contabilizar las clases
// La lista de clases tendr� tantas entradas como muestras de datos en el dataset
listaClases = new ListaClases[modelDataset.numItemsets()];
// La lista de listas de atributos tendr� un vector por atributo
listas = (Vector<ListaAtributos>[]) Array.newInstance(Vector.class, modelDataset.numAttributes());
// y cada elemento de la lista ser� un vector que hay que crear
for (int indice = 0; indice < modelDataset.numAttributes(); indice++) {
listas[indice] = new Vector<ListaAtributos>();
}
// Se recorren todas las muestras de datos
Enumeration datos = modelDataset.enumerateItemsets();
Itemset dato;
while (datos.hasMoreElements()) {
// Agregar el valor de clase a la lista de clases
dato = (Itemset) datos.nextElement();
listaClases[n] = new ListaClases((int) dato.getClassValue(), root);
// Agregar cada atributo a la lista correspondiente seg�n el atributo
Enumeration atributos = modelDataset.enumerateAttributes();
while (atributos.hasMoreElements()) {
Attribute atributo = (Attribute) atributos.nextElement();
// Se introduce el atributo con el �ndice que corresponde a la clase
listas[atributo.getIndex()].add(new ListaAtributos(dato.getValue(atributo.getIndex()), n));
}
n++;
}
// Ahora hay que ordenar las listas de atributos
for (int indice = 0; indice < modelDataset.numAttributes(); indice++) {
// Los atributos discretos (categ�ricos) no hay que ordenarlos
if (modelDataset.getAttribute(indice).isContinuous()) {
Collections.sort(listas[indice], new ListaAtributos.Comparador());
}
}
}
/** M�todo que genera el �rbol seg�n el algoritmo SLIQ
*
*/
public void generateTree() {
// Se crea el nodo ra�z para todas las clases
root = new Node(listaClases.length);
// Asoci�ndoles las listas de atributos con todos los valores
root.setData(listas);
// y se agregan todas las clases
for (int indice = 0; indice < listaClases.length; indice++) {
listaClases[indice].hoja = root;
root.agregaElemento(listaClases[indice].clase);
}
// Se crea la cola de nodos para procesar el �rbol en anchura, no en profundidad
listaNodos = new LinkedList<Node>();
listaNodos.add(root); // y se agrega la ra�z
// Mientras haya nodos a procesar en la cola
while (!listaNodos.isEmpty()) {
Node nodo = listaNodos.poll(); // Obtener el nodo a procesar
// Si no es un nodo puro y a�n no ha sido dividido
if (!nodo.esHoja() && nodo.numChildren() == 0) {
// Recorrer todos los atributos
for (int indice = 0; indice < nodo.getData().length; indice++) {
if (indice != modelDataset.getClassIndex()) {
// calculando el mejor corte posible
calculaMejorCorte(indice, nodo);
}
}
// Y a continuaci�n aplicar ese corte a los nodos a dividir
for (int indice = 0; indice < listaClases.length; indice++) {
// que son aquellos no considerados hoja y a los que se ha agregado hijos
if (indice != modelDataset.getClassIndex() &&
!listaClases[indice].hoja.esHoja() &&
listaClases[indice].hoja.numChildren() != 0) {
aplicaMejorCorte(listaClases[indice].hoja);
}
} // for
} //else
} // while
// Realizar la poda
podaArbol();
} // generateTree
/** M�todo que lleva a cabo la poda del �rbol tras la fase de crecimiento
*
*/
protected void podaArbol() {
int Lt = 2;
// Se inicia la primera fase de la poda (pasos 1 y 2)
root.calculaCoste(1);
// Partir desde los nodos hoja
for (int indice = 0; indice < listaClases.length; indice++) {
// Obtener la referencia al nodo padre
Node padre = listaClases[indice].hoja.getParent();
do {
// y comprobar si el coste justifica la poda de ambos hijos
if (padre.getCoste() < padre.getChildren(0).getCoste() + padre.getChildren(1).getCoste()) {
podaNodoCompleto(padre); // Aplicar la primera fase de poda
// Si se ha podado salir de este bucle y reiniciar el bucle exterior
indice = -1;
break;
}
padre = padre.getParent(); // Subir por el �rbol
} while (padre != null); // Hasta alcanzar la ra�z
}
// Se inicia la segunda fase de la poda (pasos 2 a 4)
Vector<Node> listaNodos = new Vector<Node>();
for(int indice = 0; indice < listaClases.length; indice++)
if(!listaNodos.contains(listaClases[indice].hoja.getParent()))
listaNodos.add(listaClases[indice].hoja.getParent());
// Recorrer los nodos hoja, en este caso no hay que subir por el �rbol
//for (int indice = 0; indice < listaClases.length; indice++) {
for(int indice = 0; indice < listaNodos.size(); indice++) {
// Obtener la referencia al nodo padre, del que cuelga este nodo y otro
Node padre = listaNodos.get(indice); // listaClases[indice].hoja.getParent();
// Si ya se le ha podado una rama
// if ( padre == null ||
// padre.getChildren(0) == null || padre.getChildren(1) == null) {
// continue; // no procesarlo
// }
padre.calculaCoste(2); // Calcular el coste adecuado para la segunda fase
padre.getChildren(0).calculaCoste(2);
padre.getChildren(1).calculaCoste(2);
int costeAmbos = padre.getCoste(); // Coste de tener ambos hijos
// Costes teniendo solamente uno, se resta el coste de tener un hijo
// y se considera el error que se a�adir�a
int costeIzq = costeAmbos - Lt + padre.costeError(padre.getChildren(0));
int costeDch = costeAmbos - Lt + padre.costeError(padre.getChildren(1));
if (costeIzq < costeAmbos) // Si el coste de tener solamente el izquierdo
{
podaNodoParcial(padre, 1); // es menor que ambos, se poda el derecho
} else if (costeDch < costeAmbos) // y viceversa
{
podaNodoParcial(padre, 0);
}
}
}
/** M�todo encargado de podar el nodo que se recibe como par�metro, eliminando sus hijos
*
* @param padre Nodo del que se podar�n los dos hijos
*/
protected void podaNodoCompleto(Node padre) {
// Se podan los dos hijos
podaNodoParcial(padre, 0);
podaNodoParcial(padre, 1);
// Marcar padre como nodo hoja, aunque tenga datos de m�s de una clase
padre.setHoja(true);
}
/** M�todo encargado de podar un nodo hijo de un nodo padre
*
* @param padre Nodo del que se va a podar
* @param indHijo �ndice del nodo hijo a podar: 0-izquierdo, 1-derecho
*/
protected void podaNodoParcial(Node padre, int indHijo) {
// Referencia al hijo que corresponda
Node hijo = padre.getChildren(indHijo);
// Hay que agregar al nodo padre los datos del hijo
agregaDatos(padre, hijo);
// Eliminar el nodo hijo
padre.getChildren()[indHijo] = null;
}
/** M�todo que se encarga de agregar los datos de un hijo que va a
* podarse a la lista de datos de su padre
*
* @param padre Nodo al que se agregar�n los datos
* @param hijo Nodo del que se toman los datos
*/
protected void agregaDatos(Node padre, Node hijo) {
// Se recorre la lista de datos que contiene el hijo, agregando los
// valores de sus atributos al padre
for (int atributo = 0; atributo < hijo.getData().length; atributo++) {
Vector<ListaAtributos> lista = hijo.getData()[atributo];
for (int indice = 0; indice < lista.size(); indice++) {
// Se contabiliza en el histograma
padre.agregaElemento(listaClases[lista.get(indice).indice].clase);
// y se agrega a la lista de atributos correspondiente
padre.getData()[atributo].add(lista.get(indice));
// Se actualiza la entrada en la lista de clases para que apunte al nodo padre
listaClases[lista.get(indice).indice].hoja = padre;
}
}
// Actualizar la clase principal en el padre, que puede ahora ser otra
padre.actualizaClasePrincipal();
}
/** M�todo encargado de comprobar cu�l es el mejor corte para un cierto
* atributo en un nodo dado
*
* @param indAtributo �ndice del atributo por el que se cortar�a
* @param nodo Nodo que se pretende dividir
*/
protected void calculaMejorCorte(int indAtributo, Node nodo) {
// Se recorre la lista ordenada de valores para el atributo �ndAtributo
for (int indice = 0; indice < nodo.getData()[indAtributo].size() - 1; indice++) {
// Se obtiene el nodo hoja que pertenece al valor examinado
Node nodoHoja = listaClases[nodo.getData()[indAtributo].get(indice).indice].hoja;
// Si es un nodo impuro que a�n no ha sido dividido
if (!nodoHoja.esHoja()) {
if (nodoHoja.numChildren() == 0) { // Si no tiene a�n hijos
nodoHoja.divide(); // dividirlo
// Y agregar a la lista de nodos pendientes de procesar
listaNodos.add(nodoHoja.getChildren(0));
listaNodos.add(nodoHoja.getChildren(1));
}
// Probar el corte por el atributo y valor indicados
nodoHoja.pruebaCorte(indAtributo, listaClases,
nodo.getData()[indAtributo].get(indice).valor,
nodo.getData()[indAtributo].get(indice + 1).valor);
}
}
}
/** M�todo que aplica en un nodo el mejor corte obtenido previamente
*
* @param nodo Nodo a dividir
*/
protected void aplicaMejorCorte(Node nodo) {
if (modelDataset.getAttribute(nodo.getDecompositionAttribute()).isDiscret())
aplicaMejorCorteDiscreto(nodo);
else aplicaMejorCorteContinuo(nodo);
}
/** M�todo que divide un nodo por un atributo discreto
*
* @param nodo Nodo a dividir
*/
protected void aplicaMejorCorteDiscreto(Node nodo) {
// �ndice del atributo por el que se dividir�
int indAtributo = nodo.getDecompositionAttribute();
// Se toman las listas de atributos ordenadas del nodo a dividir
Vector<ListaAtributos>[] listaI = nodo.getData().clone();
Vector<ListaAtributos>[] listaD = nodo.getData().clone();
// Y se generan nuevas listas inicialmente vac�as
for (int indice = 0; indice < listaI.length; indice++) {
listaI[indice] = new Vector<ListaAtributos>();
listaD[indice] = new Vector<ListaAtributos>();
}
// Referencias a los nodos hijo entre los que se repartir�n los datos
Node nodoI = nodo.getChildren(0), nodoD = nodo.getChildren(1);
// Procesar la lista de valores correspondiente a cada atributo
// y dividirla entre los dos nodos seg�n el criterio � subconjunto
for (int atributo = 0; atributo < nodo.getData().length; atributo++) {
// Lista de valores a dividir
Vector<ListaAtributos> lista = nodo.getData()[atributo];
for (int indice = 0; indice < lista.size(); indice++) {
// �ndice del mejor subconjunto encontrado
int indSubconjunto = (int) nodo.getDecompositionValue();
// Obtener el subconjunto que corresponde a indSubconjunto
while (indSubconjunto > 0) {
if (indSubconjunto % 2 != 0) {
// indSubconjunto es el �ndice del atributo que quedar�a a la izquierda
if (modelDataset.itemset(lista.get(indice).indice).getValue(indAtributo) == indSubconjunto) {
nodoI.agregaElemento(listaClases[lista.get(indice).indice].clase);
listaI[atributo].add(lista.get(indice));
// Se actualiza la entrada en la lista de clases para que apunte al nuevo nodo
listaClases[lista.get(indice).indice].hoja = nodoI;
}
} else {
if (modelDataset.itemset(lista.get(indice).indice).getValue(indAtributo) == indSubconjunto) {
// indSubconjunto es el �ndice del atributo que quedar�a a la derecha
nodoD.agregaElemento(listaClases[lista.get(indice).indice].clase);
listaD[atributo].add(lista.get(indice));
// Se actualiza la entrada en la lista de clases para que apunte al nuevo nodo
listaClases[lista.get(indice).indice].hoja = nodoD;
}
}
indSubconjunto /= 2; // Partir el conjunto en dos subconjuntos
}
}
}
// Facilitar a cada nodo su lista de atributos y valores
nodoI.setData(listaI);
nodoD.setData(listaD);
}
/** M�todo que divide un nodo por un atributo continuo
*
* @param nodo Nodo a dividir
*/
protected void aplicaMejorCorteContinuo(Node nodo) {
// �ndice del atributo por el que se dividir�
int indAtributo = nodo.getDecompositionAttribute();
// Valor por el que se dividir�
double valor = nodo.getDecompositionValue();
// Se toman las listas de atributos ordenadas del nodo a dividir
Vector<ListaAtributos>[] listaI = nodo.getData().clone();
Vector<ListaAtributos>[] listaD = nodo.getData().clone();
// Y se generan nuevas listas inicialmente vac�as
for (int indice = 0; indice < listaI.length; indice++) {
listaI[indice] = new Vector<ListaAtributos>();
listaD[indice] = new Vector<ListaAtributos>();
}
// Referencias a los nodos hijo entre los que se repartir�n los datos
Node nodoI = nodo.getChildren(0), nodoD = nodo.getChildren(1);
// Procesar la lista de valores correspondiente a cada atributo
// y dividirla entre los dos nodos seg�n el criterio <= valor
for (int atributo = 0; atributo < nodo.getData().length; atributo++) {
// Lista de valores a dividir
Vector<ListaAtributos> lista = nodo.getData()[atributo];
for (int indice = 0; indice < lista.size(); indice++) {
if (modelDataset.itemset(lista.get(indice).indice).getValue(indAtributo) <= valor) {
nodoI.agregaElemento(listaClases[lista.get(indice).indice].clase);
listaI[atributo].add(lista.get(indice));
// Se actualiza la entrada en la lista de clases para que apunte al nuevo nodo
listaClases[lista.get(indice).indice].hoja = nodoI;
} else {
nodoD.agregaElemento(listaClases[lista.get(indice).indice].clase);
listaD[atributo].add(lista.get(indice));
// Se actualiza la entrada en la lista de clases para que apunte al nuevo nodo
listaClases[lista.get(indice).indice].hoja = nodoD;
}
}
}
// Facilitar a cada nodo su lista de atributos y valores
nodoI.setData(listaI);
nodoD.setData(listaD);
/**** Posiblemente nodo.data ya no sea necesario y pueda eliminarse,
* reduciendo la ocupaci�n en memoria
*/
}
/** M�todo que comprueba la clase a la que corresponder�a una muestra seg�n el �rbol generado
*
* @param itemset La muestra a evaluar
* @param node El nodo que est� recorri�ndose.
*
* @return El �ndice de la clase predicha.
*/
public int evaluateItemset(Itemset itemset, Node node) {
try {
// Si el nodo es una hoja
if (node.esHoja() ||
// o a pesar de no serlo tiene un solo hijo que corresponde a la condici�n a evaluar
itemset.getValue(node.getDecompositionAttribute()) <= node.getDecompositionValue() && node.getChildren(0) == null ||
itemset.getValue(node.getDecompositionAttribute()) > node.getDecompositionValue() && node.getChildren(1) == null) {
return node.getClase(); // Se devuelve el �ndice de clase
}
} catch (Exception e) {
e.printStackTrace();
}
// Evaluar los nodos hijo.
if (itemset.getValue(node.getDecompositionAttribute()) <= node.getDecompositionValue()) {
return (evaluateItemset(itemset, node.getChildren()[0]));
} else {
return (evaluateItemset(itemset, node.getChildren()[1]));
}
}
/** Funci�n para contar el n�mero de nodos y de hojas en el �rbol
*
* @param node El nodo actual.
*/
public void cuentaNodosHojas(Node node) {
NumberOfNodes++; // Contabilizar el n�mero total de nodos
// Descendiendo por el �rbol
if (node.getChildren(0) != null) {
cuentaNodosHojas(node.getChildren(0));
}
if (node.getChildren(1) != null) {
cuentaNodosHojas(node.getChildren(1));
}
// Hasta alcanzar las hojas
if (node.esHoja()) {
NumberOfLeafs++;
}
}
/** Escribe en el archivo los resultados de entrenamiento y pruebas.
*
* @exception Si no es posible escribir en el archivo.
*/
public void printResult() throws IOException {
long totalTime = (System.currentTimeMillis() - startTime) / 1000;
long seconds = totalTime % 60;
long minutes = ((totalTime - seconds) % 3600) / 60;
String tree = "";
PrintWriter resultPrint;
cuentaNodosHojas(root);
tree += "\n@TotalNumberOfNodes " + NumberOfNodes;
tree += "\n@NumberOfLeafs " + NumberOfLeafs;
tree += "\n\n@NumberOfItemsetsTraining " + trainDataset.numItemsets();
tree += "\n@NumberOfCorrectlyClassifiedTraining " + correct;
tree += "\n@PercentageOfCorrectlyClassifiedTraining " + (float) (correct * 100.0) / (float) trainDataset.numItemsets() + "%";
tree += "\n@NumberOfInCorrectlyClassifiedTraining " + (trainDataset.numItemsets() - correct);
tree += "\n@PercentageOfInCorrectlyClassifiedTraining " + (float) ((trainDataset.numItemsets() - correct) * 100.0) / (float) trainDataset.numItemsets() + "%";
tree += "\n\n@NumberOfItemsetsTest " + testDataset.numItemsets();
tree += "\n@NumberOfCorrectlyClassifiedTest " + testCorrect;
tree += "\n@PercentageOfCorrectlyClassifiedTest " + (float) (testCorrect * 100.0) / (float) testDataset.numItemsets() + "%";
tree += "\n@NumberOfInCorrectlyClassifiedTest " + (testDataset.numItemsets() - testCorrect);
tree += "\n@PercentageOfInCorrectlyClassifiedTest " + (float) ((testDataset.numItemsets() - testCorrect) * 100.0) / (float) testDataset.numItemsets() + "%";
tree += "\n\n@ElapsedTime " + (totalTime - minutes * 60 - seconds) / 3600 + ":" + minutes / 60 + ":" + seconds;
resultPrint = new PrintWriter(new FileWriter(resultFileName));
resultPrint.print(getHeader() + "\n@decisiontree\n\n" + tree);
resultPrint.close();
}
/** Eval�a el dataset de entrenamiento y escribe el resultado en un archivo.
*
*/
public void printTrain() {
String text = getHeader();
for (int i = 0; i < trainDataset.numItemsets(); i++) {
try {
Itemset itemset = trainDataset.itemset(i);
int cl = evaluateItemset(itemset, root);
if (cl == (int) itemset.getValue(trainDataset.getClassIndex())) {
correct++;
}
text += trainDataset.getClassAttribute().value(cl) + " " +
trainDataset.getClassAttribute().value(((int) itemset.getClassValue())) + "\n";
} catch (Exception e) {
e.printStackTrace();
System.err.println(e.getMessage());
}
}
try {
PrintWriter print = new PrintWriter(new FileWriter(trainOutputFileName));
print.print(text);
print.close();
} catch (IOException e) {
System.err.println("No es posible abrir el archivo de salida de entrenamiento: " + e.getMessage());
}
}
/** Eval�a el dataset de pruebas y escribe el resultado en un archivo
*
*/
public void printTest() {
String text = getHeader();
for (int i = 0; i < testDataset.numItemsets(); i++) {
try {
int cl = (int) evaluateItemset(testDataset.itemset(i), root);
Itemset itemset = testDataset.itemset(i);
if (cl == (int) itemset.getValue(testDataset.getClassIndex())) {
testCorrect++;
}
text += testDataset.getClassAttribute().value(((int) itemset.getClassValue())) + " " +
testDataset.getClassAttribute().value(cl) + "\n";
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
try {
PrintWriter print = new PrintWriter(new FileWriter(testOutputFileName));
print.print(text);
print.close();
} catch (IOException e) {
System.err.println("No es posible abrir el archivo de salida de pruebas.");
}
}
/** M�todo encargado de leer las opciones de ejecuci�n del archivo y establecer los par�metros adecuados.
*
* @param options El StreamTokenizer que lee del archivo de par�metros.
*
* @throws Exception En caso de que el formato del archivo no sea el correcto.
*/
protected void setOptions(StreamTokenizer options) throws Exception {
options.nextToken();
// Comprobar que el archivo comienza con el token 'algorithm'
if (options.sval.equalsIgnoreCase("algorithm")) {
options.nextToken(); // Saltar el s?mbolo =
options.nextToken(); // y tomar el nombre del algoritmo
// que debe ser 'SLIQ'
if (!options.sval.equalsIgnoreCase("SLIQ")) {
throw new Exception("El nombre del algoritmo no es correcto.");
}
options.nextToken();
options.nextToken();
// Recuperar los nombres de los archivos de entrada
if (options.sval.equalsIgnoreCase("inputData")) {
options.nextToken();
options.nextToken();
modelFileName = options.sval;
if (options.nextToken() != StreamTokenizer.TT_EOL) {
trainFileName = options.sval;
options.nextToken();
testFileName = options.sval;
if (options.nextToken() != StreamTokenizer.TT_EOL) {
trainFileName = modelFileName;
options.nextToken();
}
}
} else {
throw new Exception("El archivo debe comenzar con la palabra 'inputData'.");
}
// Avanzar en el archivo hasta la marca 'outputData'
while (true) {
if (options.nextToken() == StreamTokenizer.TT_EOF) {
throw new Exception("No se han indicado archivos de salida.");
}
if (options.sval == null) {
continue;
} else if (options.sval.equalsIgnoreCase("outputData")) {
break;
}
}
/* Recuperar los nombres de los archivos de salida */
options.nextToken();
options.nextToken();
trainOutputFileName = options.sval;
options.nextToken();
testOutputFileName = options.sval;
options.nextToken();
resultFileName = options.sval;
} else {
throw new Exception("El archivo debe comenzar con la palabra 'algorithm' seguida del nombre del algoritmo.");
}
} // setOptions
/** Funci�n main.
*
* @param args El archivo de par�metros.
*/
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("\nError: debe especificar el archivo de par�metros\n\tuso: java -jar SLIQ.jar archivoparametros.txt");
System.exit(-1);
} else {
new SLIQ(args[0]);
}
}
} // sliq