MAC0122 - Princípios de Desenvolvimento de Algoritmos
Departamento de Ciência da Computação do IME-USP
Turma da Engenharia da Computação - POLI
Segundo semestre de 2024
O objetivo deste exercício-programa é exercitar o uso de ordenação e filas de prioridade. O EP5 vai usar algumas coisas desenvolvidas neste EP.
Um problema bastante conhecido nos contextos de produção industrial e de sistemas operacionais é o de escalonamento (scheduling) de tarefas em máquinas. Estamos interessados em uma das versões mais simples do problema: dadas m máquinas idênticas e n tarefas com tempos de execução ou durações pré-determinados, encontrar uma atribuição das tarefas às máquinas que minimize o tempo máximo de operação de qualquer uma das máquinas (makespan ou duração do escalonamento).
Informalmente, o problema do escalonamento em máquinas idênticas consiste no seguinte. Dados inteiros positivos
no_maquinas no_tarefas duracao[0] duracao[1] duracao[2] ... duracao[n0_tarefas-1],onde no_maquinas é o número de máquinas, no_tarefas é o número de tarefas, e para t = 0,1,...,no_tarefas-1, duracao[t] é o tempo de execução da tarefa t, deseja-se distribuir todas as tarefas entre as máquinas (em outras palavras, deseja-se escalonar as tarefas) de modo que o tempo máximo de operação que qualquer uma das máquinas necessita para executar as tarefas atribuídas a ela seja o menor possível.
Exemplo: Considere 3 máquinas (0, 1 e 2) e 7 tarefas com os tempos de execução dados a seguir.
no. da tarefa : 0 1 2 3 4 5 6 duração : 6 2 3 5 4 3 7
Um possível escalonamento dessas tarefas entre as três máquinas é:
maquina: tarefas tempo 0 : 0 3 6 18 1 : 1 4 6 2 : 2 5 6 duração do escalonamento: 18
tarefas:  | 0 | 3 | 6 |
maquina 0: | 6 | 5 | 7 |
1 | 4 | |
maquina 1: | 2 | 4 |
2 | 5 | |
maquina 2: | 3 | 3 |
Um escalonamento de duração mínima para o exemplo acima é:
maquina: tarefas tempo 0 : 0 4 10 1 : 1 2 3 10 2 : 5 6 10 duração do escalonamento: 10
0 | 4 | |
maquina 0: | 6 | 4 |
1 | 2 | 3 | |
maquina 1: | 2 | 3 | 5 |
5 | 6 | |
maquina 2: | 3 | 7 |
O problema do escalonamento de tarefas é um problema computacionalmente difícil. Só se consegue determinar um escalonamento de duração mínima para instâncias de tamanhos bem modestos. Por exemplo, para encontrar um escalonamento de duração mínima de 40 tarefas entre 2 máquinas pode-se consumir horas de computação. Nessa situação, é razoável tentar encontrar rapidamente um escalonamento de duração "não muito grande" em vez de um de duração mínima. Este compromisso entre perda de otimalidade e ganho de eficiência é o objetivo de algoritmos de aproximação.
O primeiro algoritmo de aproximação para o problema do escalonamento de tarefas foi apresentado e analisado por Ronald Graham e usa um critério muito simples: alocar as tarefas uma a uma destinando cada tarefa à maquina menos ocupada. Por esse critério, a escolha da máquina que vai receber determinada tarefa não depende dos tempos das tarefas que ainda não foram atribuídas a nenhuma máquina. O chamado escalonamento de Graham promete encontrar um escalonamento cuja duração é no máximo o dobro da duração mínima de um escalonamento.
Algoritmo de Graham
maquina: tarefas tempo 0 : 0 5 9 1 : 1 3 7 2 : 2 4 6 14 duração do escalonamento: 14
tarefas:  | 0 | 5 |
maquina 0: | 6 | 3 |
1 | 3 | |
maquina 1: | 2 | 5 |
2 | 4 | 6 | |
maquina 2: | 3 | 4 | 7 |
A razão de aproximação nesse caso é 14/10 = 1.4, que é de fato menor que 2, como prometido pelo algoritmo.
Uma variante do algoritmo de Graham que coloca as tarefas em ordem decrescente de tempo antes de começar o processo de escalonamento promete encontrar um escalonamento cuja duração é no máximo 4/3 da duração mínima de um escalonamento.
Ao aplicar a variante do algoritmo de Graham à instância acima, usaremos a ordem a seguir:
no. da tarefa : 0 1 2 3 4 5 6 duração : 6 2 3 5 4 3 7 ordem : 6 0 3 4 2 5 1e obteríamos o seguinte escalonamento:
maquina: tarefas tempo 0 : 6 5 10 1 : 0 2 1 11 2 : 3 4 9 duração do escalonamento: 11
tarefas:  | 6 | 5 |
maquina 0: | 7 | 3 |
0 | 2 | 1 | |
maquina 1: | 6 | 3 | 2 |
3 | 4 | |
maquina 2: | 5 | 4 |
A razão de aproximação nesse caso é 11/10 = 1.1, que é de fato menor que 4/3=1.333..., como prometido pelo algoritmo.
Variante do algoritmo de Graham
Para tanto, nas implementações do algoritmo de Graham, você deve usar duas coisas: uma fila de prioridades e ordenação indireta.
A fila de prioridade você vai usar para determinar o índice de uma máquina menos ocupada a cada iteração do algoritmo. A ordenação indireta você vai usar para implementar a variante do algoritmo de Graham.
/* ************************************************************ */ /* Interface para o EP4: heap.h */ /* ************************************************************ */ /* Nao faca nenhuma alteracao neste arquivo. */ /* ************************************************************ */ struct maquina { int id; int carga; }; typedef struct maquina Maquina; int min(Maquina v[]); /* devolve o id da máquina com a carga mínima do heap v */ void add_to_minkey(int m, Maquina v[], int delta); /* reorganiza o heap v[0..m-1] depois de aumentar a carga da máquina de carga mínima de delta */A sua implementação deve ser eficiente: a função min deve consumir tempo constante e a função add_to_minkey deve consumir tempo proporcional a log m.
Para fazer isso, monte um vetor id[0..n-1] inicialmente com id[i] = i para i = 0,...,n-1. Agora ordene, com um bom algoritmo de ordenação, esse vetor id de modo que, depois da ordenação, duracao[id[0]] <= duracao[id[1]] <= ... <= duracao[id[n-1]]. O vetor duracao não deverá ser alterado.
Concretamente, implemente no seu programa a função
void sortInd(int n, int d[], int id[]); /* ordena o vetor id[0..n-1] de modo que d[id[0]] <= d[id[1]] <= ... <= d[id[n-1]] */Sua função sortInd não deve alterar o vetor d e deve consumir tempo O(n log n) (esperado ou no pior caso).
Além da implementação da biblioteca heap.h e da rotina sortInd, o seu programa deve conter a implementação das seguintes funções:
A saída do seu programa deve ser a impressão de dois escalonamentos: o escalonamento produzido pela rotina Graham e o escalonamento produzido pela rotina sortGraham. Veja nos arquivos exemplos todas as informações que seu programa deve imprimir.
Primeiro exemplo de arquivo de entrada:
3 6 1 5 3 2 7 1Segundo exemplo de arquivo de entrada:
3 7 6 2 1 7 8 2 4Terceiro exemplo de arquivo de entrada:
3 7 6 2 3 5 4 3 7
Saída para o primeiro exemplo de arquivo de entrada:
Nome do arquivo de entrada: input1 m = 3 n = 6 Tarefas: 0 1 2 3 4 5 Duração: 1 5 3 2 7 1 Algoritmo de Graham: Tarefa Maquina 0 0 1 1 2 2 3 0 4 0 5 2 Duracao do escalonamento: 10 Tarefas em ordem decrescente de duracao: 4 1 2 3 0 5 Algoritmo de Graham com ordenação: Tarefa Maquina 0 2 1 1 2 2 3 2 4 0 5 1 Duracao do escalonamento: 7
Saída para o segundo exemplo de arquivo de entrada:
Nome do arquivo de entrada: input2 m = 3 n = 7 Tarefas: 0 1 2 3 4 5 6 Duração: 6 2 1 7 8 2 4 Algoritmo de Graham: Tarefa Maquina 0 0 1 1 2 2 3 2 4 1 5 0 6 0 Duracao do escalonamento: 12 Tarefas em ordem decrescente de duracao: 4 3 0 6 1 5 2 Algoritmo de Graham com ordenação: Tarefa Maquina 0 2 1 1 2 1 3 1 4 0 5 0 6 2 Duracao do escalonamento: 10
Saída para o terceiro exemplo de arquivo de entrada:
Nome do arquivo de entrada: input3 m = 3 n = 7 Tarefas: 0 1 2 3 4 5 6 Duração: 6 2 3 5 4 3 7 Algoritmo de Graham: Tarefa Máquina 0 0 1 1 2 2 3 1 4 2 5 0 6 2 Duracao do escalonamento: 14 Tarefas em ordem decrescente de duracao: 6 0 3 4 2 5 1 Algoritmo de Graham com ordenação: Tarefa Máquina 0 1 1 1 2 1 3 2 4 2 5 0 6 0 Duracao do escalonamento: 11
int main(int argc, char *argv[]) { /* argc = numero de argumentos na linha de comando */ /* argv = vetor de apontadores para strings contendo esses argumentos */ FILE *entrada; /* declaracao da variavel para o arquivo de entrada */ [...] if (argc == 1) { printf("Uso: %s <arquivo de entrada>\n", argv[0]); return -1; } if ((entrada = fopen(argv[1],"r")) == NULL) { printf("%s: arquivo de entrada %s nao pode ser aberto.\n", argv[0], argv[1]); return -1; } [...] /* leitura dos dados */ fclose(entrada); [...] /* restante do programa */ }
fscanf(entrada,"%d ", &numero);
Primeiro faça uma versão preliminar do algoritmo de Graham que não usa heap. Determine qual é a máquina menos carregada em tempo linear no número de máquinas. Apenas essa parte já deve valer 3.0 pontos. Tente estar com isso pronto no fim desta semana.
Depois que fizer isso, você pode escolher: faça primeiro a ordenação indireta e o sortedGraham, ou então faça a implementação do heap.c e o modifique o Graham para usá-la. Cada uma destas partes deve valer 3.5 pontos.
Escreva funções para fazer cada tarefa diferente do EP: por exemplo, escreva uma função que imprime um escalonamento.
A eficiência da sua resolução vai ser levada em conta na sua nota. Use o que foi ensinado nas aulas!
Divirta-se com o EP!