O algoritmo funciona por brute force. Porém, existem algumas validações que aceleram o esforço computacional que ele necessita, como restrição do domínio de notas; retirada prévia de casos que realmente não existem possibilidades de notas; para o cálculo de uma nota apenas, retorno do melhor caso; entre outros aspectos posteriormente explicados.
A classe Nota possui 3 atributos: peso, valor e dominio_de_nota. O peso representa o peso total daquela nota em relação à média final disciplina. O valor representa o quanto o aluno tirou naquela prova/trabalho. E o dominio_de_nota representa quais são os possíveis valores para aquela nota ([0, 0.5, 1, 1.5, ..., 9.5, 10] ou alguma sublista deste domínio maior). Esta variável é inicializada como sendo o valor da lista mencionada (armazenada em DOMINIO_DE_NOTAS da mesma classe). Ela serve para definir os possíveis valores de notas que aquela prova pode possuir para atingir a média desejada. Esta definição pode diminuir as possibilidades que esta nota pode ter, reduzindo o esforço computacional.
Ela possui validação para os seguintes atributos:
-
valida_valor: verifica se o este atributo recebido éfloatouint; e se o valor pertence aoDOMINIO_DE_NOTAS; -
valida_peso: verifica se o peso é None (necessariamente a nota precisa ter um peso); se o peso éfloat, e se o peso está entre o seguinte intervalo: [0, 1]
Além disso, ela possui alguns métodos:
-
randomiza_dominio: pega a listadominio_da_notae a randomiza; -
limita_dominio: pega um valor mínimo e máximo escolhido no input, e faz uma secção da listadominio_da_nota. Vale ressalta que esta lista deve ser a geral (ou seja, uma cópia deDOMINIO_DE_NOTAS), além de não estar randomizada. Esta validação é feita pelo métodovalida_limitacao_de_dominio; -
restaura_dominio: volta o domínio da nota para o original (cópia deDOMINIO_DE_NOTAS); -
valida_limitacao_de_dominio: valida se odominio_da_notaestá no formato original (ou seja, se ele não foi seccionado ainda); se o inputvalor_máximorealmente é maior que o inputvalor_minimo; e se o domíno já não foi randomizado.
Esta classe tem a funcionalidade de apresentar funções que o algorítmo utiliza, como cálculo de médias, desvio padrão, e funções utilizadas no debug do código (como a exibição de uma lista de notas). Os métodos de debug não serão comentados nesta descrição, mas podem ser vistos na classe.
Os métodos que esta classe possui são:
-
media_aritimetica: faz o cálculo da média aritimética das notas (sem considerar os pesos entre elas); -
media: calcula a média ponderada entre as notas, validando se as notas possuem a soma dos seus pesos igual a 1. -
desvio_padrao: calcula o desvio padrão entre as notas, utilizando-se do método anteriormedia; -
distancia_entre_notas: verifica se todas as notas distam de um valordistancia_minda média aritimética entre as notas (para verificar a dispersão entre as notas); -
minimo_valor_no_dominio: verifica qual o menor valor que a nota pode ter para ser possível com que a média desejada seja atingida. O cálculo da equação utilizada por esta função estará descrito no tópico "Lógica do algorítmo". Se o valor fornecido for menor que o menor valor mínimo de uma nota (0.0), a função retorna "0.0". Caso o valor fornecido seja maior que o maior valor que uma nota pode ter (10.0), esta função retorna "-1", indicando que é impossível encontrar uma possibilidade que a média desejada seja atingida; -
maximo_valor_no_dominio: verifica qual o maior valor que a nota pode ter para ser possível com que a média desejada seja atingida. O cálculo da equação utilizada por esta função estará descrito no tópico "Lógica do algorítmo". Se o valor fornecido for maior que o maior valor máximo de uma nota (10.0), a função retorna "10.0". Não há uma validação para se existe uma possibilidade do valor máximo ser menor que "0.0", uma vez que o próprio algoritmo verifica a melhor possibilidade, e encontrará que esta nota deve ser "0.0";
O boletim possui 4 atributos: tenho, que representa uma lista de notas que tenho; quero, que representa uma lista de notas que quero; idx_tenho que representa o index na lista tenho do primeiro trabalho na lista (se for igual ao tamanho da lista representa que não tem trabalho); e idx_quero que tem a mesma ideia de idx_tenho só que para a lista quero. Ela recebe provas_que_tenho, provas_que_quero, trabalhos_que_tenho e trabalhos_que_quero, sendo todas listas de Nota. Esta classe foi criada a fim de se separar a lista de provas com a lista de trabalhos, possuindo algumas características que esta divisão necessita, como a média das provas, média dos trabalhos, entre outros. Ela foi criada justamente por uma descoberta de que o sistema que calcula a média final faz a média de provas e a média de trabalhos separadas, arrendondando elas, e após isso faz a média final.
Esta classe possui as seguintes validações:
-
valida_lista_de_notas: verifica se a lista de notas passadas são do tipo list(Nota); -
valida_preenchimento: para fazer o cálculo das médias, é verificado se as listas foram preenchidas, para que o usuário não faça uma média com notas vazias, uma vez quequero` inicialmente é uma lista de notas com valor nulo (pois é o que o algoritmo quer encontrar); -
valida_pesos: utilizado para verificar se todas as notas passadas possuem a soma de pesos igual a 1;
Os métodos implementados nesta classe são:
-
provas: retorna a lista das provas passadas no boletim, utilizando os atributosidx_tenhoeidx_quero; -
trabalhos: retorna a lista dos trabalhos passadas no boletim, utilizando os atributosidx_tenhoeidx_quero; -
provas_que_quero: retorna uma sublista dequeroapenas com as provas, utilizandoidx_quero; -
trabalhos_que_quero: retorna uma sublista dequero, apenas com os trabalhos, utilizandoidx_quero; -
media_provas: faz a média das provas, se utilizando do métodoprovas(). Ele verifica se as provas foram preencidas ou não (ou seja, se as notas possuem valor); -
media_trabalhos: faz a média dos trabalhos, se utilizando do métodotrabalhos(). Ele verifica se os trabalhos foram preencidos ou não (ou seja, se as notas possuem valor); -
media_final: representa a média final a partir do boletim dado: calcula individualmente a média das provas e dos trabalhos (se utilizando dos métodos anteriores), e depois os soma para resultar na média final. -
media_final_externo: calcula a média final para uma lista de notas que tenho e que quero externo ao objeto instantciado (sendo um método estático). Ele instancia um novo boletim com as tais notas e os respectivos indexes, e utiliza-se do métodomedia_finalpara gerar a média final das notas passadas.
Como dito anteriormente, o algoritmo funciona por força bruta. A fim de ficar mais explícito o entendimento do código, os parágrafos a seguir serão separados pelo conjunto das linhas que eles estão explicando
Cria-se uma lista para representar as possíveis combinações de notas escolhidas randomicamente.
Primeiramente, ele faz a validação se é possível obter notas a fim de se atingir a média desejada, preenchendo todas as notas que o algoritmo bbusca pelo valor máximo que podem ter (10.0). Se o valor dessa média calculada for menor que o valor da média desejada, significa que não existem combinações possíveis nas quais a média desejada seja batida e, portanto, o algoritmo retorna None.
Agora, se iniciará o primeiro laço de repetição. A classe Solucionador possui um atributo aumento_range, que aumentará conforme não se encontram notas possíveis para atingir uma certa nota (caso que ocorreu em um dos testes feitos). Imagina-se que, dentre as notas que o aluno já tem e as que ele quer tirar, não exista uma combinação possível para que ele tire 6.0, mas existe uma combinação para que tire 7.0. Este loop tem a funcionalidade de identificar estes casos. Se o algoritmo, até o máximo valor de uma média possível (10.0), não encontre uma combinação possível de notas, ele retorna None (linha 181).
Vale ressaltar que o aumento_range varia da seguinte forma: imagine-se que media_desejada = 6.0. Então, na primeira repetição, o valor mínimo de uma média possível é 6.0 - Solucionador.ERR_MAX = 6.0 - 0.04 = 5.96, e o valor máximo de uma média possível é 6.0 + Solucionador.ERR_MAX = 6.0 + 0.04 = 6.04. Se não foi encontrada nenhuma nota, o valor mínimo continua sendo 5.96, mas deseja-se que o valor máximo seja 6.5. Logo, o valor de Solucionador.aumento_range para que 6.04 + aumento_range = 6.5 é 0.46. Após esta repetição, se não for encontrada nenhuma combinação, deseja-se que o valor máximo seja 7.0 e, para tal, aumento_range = 0.96, e assim por diante.
Para cada repetição (visto posteriormente), será feita uma limitação no domínio das notas. A explicação ficará mais clara futuramente, mas este trecho do código garante que para cada entrada no primeiro laço, os domínios voltem ao original (ou seja, as notas podem apresentar os seguintes valores: [0, 0.5, 1.0, ..., 9.5, 10.0], sendo este o domínio original).
O trecho a seguir leva em conta o cálculo do mínimo e máximo valor que uma nota pode ter dentre as diversas possíveis (apresentadas no domínio original) para alcançar o valor máximo:
Assumindo que a separação entre prova e trabalho não existe, e assumindo
Vale ressaltar que o mais preciso seria definir não pelo módulo, mas sim que essa diferença pode variar de -0.04 à 0. Porém, foi utilizado este conceito a fim de se facilitar as contas, sendo que o aumento do intervalo (-0.04 à +0.04) aceita mais combinações de notas, não invalidando a solução.
Inicialmente, se verificará qual é o valor mínimo que essa nota n_x deve possuir a fim de que seja possível se obter uma média final. Então, sabe-se que outra validação para que uma combinação de notas seja possível é representada pela seguinte expressão:
na qual representa que todas as notas que quero devem distanciar da média aritimética entre elas em um valor máximo
Como se quer minimizar
Ao analisar
Assumindo esta condição para que
Para facilitar as contas, será englobado as seguintes constantes:
Logo:
Isolando
Como o resultado buscado é um valor no domínio de notas, que é discreto:
Agora, deve-se analisar o valor máximo, que compreende uma distância fixa para a média aritimética entre as notas que o aluno quer buscar. Analisando a equação
onde a subtração é escolhida para representar o menor valor de
Ao analisar
Substituindo
Para facilitar as contas, serão englobadas as mesmas constantes. Portanto:
Isolando
Como deve-se descobrir o valor máximo de
Portanto, as duas equações que serão montadas na classe Utils serão
Com as constantes, a partir das equações
Um problema ocorre caso queira calcular uma nota apenas, no qual len(notas_que_quero) = 0. Portanto, será calculada uma fórmula minimizada para o cálculo de apenas uma nota. Ela será representada pelo valor mínimo que esta nota pode assumir dentro do domínio de notas. Portanto, para uma nota
Comparando com as equações
Assumindo domínio discreto:
Demonstrado todas as fórmulas necessárias para o cálculo dos limites dos domínios que uma certa nota pode possuir, é possível prosseguir com a explicação do algoritmo. O laço de repetição verifica o mínimo e máximo valor que cada nota pode possuir a fim de se obter uma combinação válida de notas. Chamando a função minimo_valor_no_dominio e maximo_valor_no_dominio, se calculam tais valores para cada nota. Caso o cálculo do valor mínimo retorne -1, indica-se que o valor mínimo para a nota procurada naquela iteração foi maior que o valor máximo do domínio de notas (10.0), ou seja, é impossível se obter aquela nota a fim de se obter a média desejada (pois ela nunca alcançará o valor mínimo). Este laço de repetição também possui a análise para uma nota apenas. Neste caso, ao verificar que se quer apenas uma nota, o algoritmo pega o valor mínimo e o retorna como resposta dentro do objeto Boletim inicializado anteriormente. Caso mais de uma nota seja buscada, o algoritmo se utiliza da função limita_dominio para restringir o domínio de cada nota procurada.
Para cada nota procurada, após limitar o domínio de cada nota, estes são randomizados a fim de não se analisar os domínios em forma crescente.
Cria-se a lista idx_possiveis_notas, que representam qual o index do domínio de notas de cada nota que quero que será analisado por vez. Ficará mais claro como que esta lista de indexes será utilizada posteriormente. Esta linha preenche esta lista com '0', representando que cada nota começará com o valor do index '0' de seu domínio, previamente randomizado.
Passa pela lista de notas que quero e atribui a cada atributo valor de cada nota o primeiro valor do domínio previamente limitado e randomizado, utilizando a lista idx_possiveis_notas.
Cria-se uma variável que representa se todas as notas foram verificadas, ou seja, se todas as combinações, para aquelas limitações de domínio, foram verificadas. Inicialmente, esta variável é False, pois ainda não se verificou todas as combinações.
Inicia-se o segundo laço de repetição, que faz a verificação, para um certo aumento_range, se existem, pelo menos, uma combinação válida de notas.
Este condicional verifica se todas as notas foram verificadas. Esta verificação ocorre se todos os valores de notas são os últimos valores do domínio de notas de cada nota, fazendo com que a lista idx_possiveis_notas represente, para cada nota, o último index do domínio de notas delas.
Calcula-se a média final a partir do método da classe Boletim.
Verifica se a combinação em análise, no segundo laço de repetição, é valida. Inicialmente, veririca-se se a média varia de 0.04 negativamente em relação à média desejada, e 0.04 + aumento_range positivamente em relação à média desejada. Caso seja verdadeiro, verifica-se se a distância entre as notas varia de um valor Solucionador.MENOR_DIST da média aritimética entre as notas procuradas. Vale ressaltar que ambos os condicionais são utilizados no raciocínio do desenvolvimento do cálculo do valor mínimo e máximo de cada nota (explicados anteriormente), sendo o primeiro condicional exemplificado pela equação tupla na lista notas_possiveis.
O condicional inicial verifica se o tamanho máximo de notas_possíveis (delimitado pelo parâmetro Solucionador.NOTAS_TOTAIS do algoritmo) foi atingido. Tal parâmetro foi criado a fim de se acelerar o processamento do algoritmo. Caso este valor seja atingido, verifica-se a melhor combinação dentre as notas_possiveis. Inicialmente, escolhe-se a primeira combinação como a melhor e, em seguida, compara esta combinação com as demais. Caso verifique-se que outra combinação seja melhor, esta nova combinação substitui a anterior, até se percorrer a lista inteira. Para verificar se uma combinação é melhor que a outra, verifica-se a combinação de menor desvio padrão entre as notas procuradas. Caso o desvio padrão seja igual (existem testes nos quais acontece isso), verifica-se, dentre o intervalo delimitado por aumento_range, qual combinação possui a menor média final, utilizando o método media_final_externo de Boletim. No final deste condicional, retorna-se o boletim com a melhor combinação.
Caso este laço de repetição chegue ao fim (sem atingir o limite de notas), a lista idx_possiveis_notas é alterada. O objetivo deste trecho é somar em '1' o último index de nota e, caso ultrapasse o maior index do domínio daquela nota, soma-se '1' no index anterior, e zera o último index. Caso o penultimo index ultrapasse o maior index do domínio daquela nota, soma-se '1' no index anterior e zera o penúltimo index, e assim por diante. A fim de se explicar o objetivo deste trecho de código, alguns exemplos serão expostos:
ex1: [0, 0, 0, ..., 0, 0] --(+1)--> [0, 0, 0, ..., 0, 1]
ex2: [0, 0, 0, ..., 0, 19] --(+1)--> [0, 0, 0, ..., 0, 20] -> [0, 0, 0, ..., 1, 0]
ex3: [0, 0, 0, ..., 0, 19, 19] --(+1)--> [0, 0, 0, ..., 0, 19, 20] -> [0, 0, 0, ..., 0, 20, 0] -> [0, 0, 0, ..., 1, 0, 0]
Caso o primeiro laço de repetição chegue ao fim, deve-se verificar se, dentro daquele aumento_range, foi encontrada, pelo menos, uma nota possível. Caso seja encontrada, entra-se em um laço de repetição para verificar a melhor combinação de notas, utilizando as análises feitas nas linhas [108-132] explicadas anteriormente.
Caso não tenha sido encontrada nenhuma nota, este condicional verificará se media_desejada + Solucionador.ERR_MAX + Solucionador.aumento_range é um múltiplo inteiro de 0.5. Caso não seja, ele fará a primeira soma do aumento_range, a fim de que este condicional, na próxima verificação, não seja executado.
Caso aumento_range já tenha sido somado uma vez, entra-se neste condicional a fim de que o valor máximo da média desejada seja múltipla de 0.5.
Caso o algoritmo não encontre nenhuma nota possível, após as diversas verificações, retorna-se Nulo.
- Bruno Vilardi - Brvilardi 👷♂️
- Hector Guerrini - hectorguerrini 🧙♂️
- Vitor Soller - VgsStudio 🐱💻
- Luigi Trevisan - LuigiTrevisan 🍄
- João Branco - JoaoVitorBranco 😎
- João Branco - JoaoVitorBranco 😎
- Pedro Mesquita - pedrogjmesquita 💫