Neste segundo treino vamos resolver e submeter o problema UVa 256.
Primeiro leiam o enunciado do problema.
Vamos resolver o problema de duas formas diferentes: por força bruta e depois usando um pouco de matemática para obter uma solução mais inteligente.
Analisando o enunciado percebemos que existem apenas quatro instâncias do problema, para 2, 4, 6, e 8 dígitos. Podíamos simplesmente calcular estas quatro soluções, armazená-las num array de Strings
, e imprimir consoante o que fosse pedido. Ou podemos ser um pouco mais preguiçosos (a grande virtude do programador, se bem usada!) e só calcular se for pedido, mas sem nos esquecermos de guardar a solução porque, como se pode ver no exemplo do enunciado, é possível pedir uma instância mais que uma vez.
Como são pedidos poucos dígitos, vamos primeiro fazer à bruta e calcular todas as hipóteses!
Este excerto de código faz esse trabalho:
StringBuffer sb = new StringBuffer(); // keep solutions in a StringBuffer String format = "%0" + digits + "d"; // useful for leading zeros int maxValue = (int)Math.pow(10, digits); int split = (int)Math.pow(10, digits/2); for(int i=0; i < maxValue; i++) { // given that 'i' is evenly split in parts 'ab', // we wish to include only when i == (a+b)^2 // eg: 3025 == (30+25)^2, with i=3025, a=30, b=25 int a = i/split, b = i%split; if (i == (a+b)*(a+b)) // the condition is met, add to current solutions sb.append(String.format(format, i)+"\n"); }
Vejam se entendem este ciclo, ele é a essência desta solução do problema!
Reparem que se usou a função String.format para adicionar os zeros à esquerda (leading zeros em inglês). Dá jeito saber o que as classes standard do Java podem fazer: isso poupa-nos muito trabalhinho!
Agora só nos resta adicionar o resto do código para ler o input e escrever o output.
public class UVa_256_Quirksome_Squares { public static void main(String[] args) { /// REMOVE BEFORE SUBMITION /////////////////////////////////// if (1>0) // if true: read from files; else: read from System.in try { // redirect System.in and System.out to in/out text files System.setIn (new FileInputStream("data/uva0256.in.txt" )); System.setOut(new PrintStream("data/uva0256.out.txt") ); } catch (Exception e) {} /////////////////////////////////////////////////////////////// Scanner sc = new Scanner(System.in); // there are only four solutions, // let's compute and save them in case the input repeats itself String[] sols = new String[4]; while (sc.hasNextInt()) { // solve one problem instance int digits = sc.nextInt(); if (sols[digits/2-1]==null) { // if not computed yet... StringBuffer sb = new StringBuffer(); // for increased performance String format = "%0" + digits + "d"; // useful for leading zeros int maxValue = (int)Math.pow(10, digits); int split = (int)Math.pow(10, digits/2); // This cycle is where 99.9% of the computation occurs, // so it must be as light as possible for(int i=0; i < maxValue; i++) { // given that 'i' is evenly split in parts 'ab', // we wish to include only when i == (a+b)^2 // eg: 3025 == (30+25)^2, with i=3025, a=30, b=25 int a = i/split, b = i%split; if (i == (a+b)*(a+b)) // the condition is met, add to current solutions sb.append(String.format(format, i)+"\n"); } sols[digits/2-1] = sb.toString(); // save solutions for future use } System.out.print(sols[digits/2-1]); } sc.close(); } }
Reparem também que as soluções que vão sendo calculadas são guardadas. Quando uma nova instância é pedida, verificamos se ela já existe ou não para evitar repetir computações. Este tipo de tática é muitíssimo útil e nós vamos aprender a usá-la para resolver problemas muito mais complicados (e úteis)!
Com este código, fomos ao UVa e submetemos (experimentem!). A solução foi aceite e demorou 0.45 segundos. Nada mau para uma força bruta!
Mas não deixa de ser uma solução pouco elegante. Podemos fazer melhor? Podemos!
Para isso vamos analisar o problema em termos de Álgebra (como é? A Álgebra serve para alguma coisa?)
A solução do problema passa por encontrar números \(i\) que são compostos por sequência de dígitos \(ab\) tal que:
$$i = (a+b)^2$$Por exemplo, no enunciado \(i=3025\) composto por \(a=30\) e \(b=25\) é solução porque \(3025=(30+25)^2\)
Ora se i tem d dígitos, a e b são compostos por \(d/2\) dígitos, e o valor máximo de a e b é majorado por \(b_{max}=10^{d/2}\).
Assim temos:
$$i = b_{max} \times a + b$$Os valores de a e b têm de satisfazer:
$$(a+b)^2 = b_{max} \times a + b$$desenvolvendo (e agora vem a Álgebra: respirar fundo...):
$$a^2 + 2ab + b^2 = b_{max} \times a + b$$ $$a^2 + (2b-b_{max}) \times a + b^2- b = 0$$(já acabou?)
Obtemos um polinómio de grau 2 que queremos resolver em a. Como fazer? Para uma instância do problema, geramos os b possíveis (o que é muito menos do que todos os i possíveis) e resolvemos a equação (usando a famosa fórmula resolvente). As soluções inteiras serão aquelas que vamos guardar e imprimir!
Sugiro que tentem implementar esta solução por vocês antes de analisar a solução seguinte:
. . .
Nesta solução incluiu-se uma função auxiliar solveSecondDegreeEquation
para calcular as raízes via a fórmula resolvente.
public class UVa_256_Quirksome_SquaresMath { /** * This method solves ax^2 + bx + c = 0 * @return an array with the solutions of the equation * (max 2 solutions; null if there are no solutions) */ public static double[] solveSecondDegreeEquation(double a, double b, double c) { // Result values for x double[] solutions = null; double q = b * b - 4 * a * c; if (q > 0 && a != 0) { // case there are two roots solutions = new double[2]; solutions[0] = (-b + Math.sqrt(q))/ (2 * a); solutions[1] = (-b - Math.sqrt(q))/ (2 * a); } else if (q == 0) { // case there is one root solutions = new double[1]; solutions[0] = (-b) / (2 * a); } else { // case there are no roots } return solutions; } ///////////////////////////////////////////////////////////// public static void main(String[] args) { /// REMOVE BEFORE SUBMITION /////////////////////////////////// if (1>0) // if true: read from files; else: read from System.in try { // redirect System.in and System.out to in/out text files System.setIn (new FileInputStream("data/uva0256.in.txt" )); System.setOut(new PrintStream("data/uva0256.out.txt") ); } catch (Exception e) {} /////////////////////////////////////////////////////////////// Scanner sc = new Scanner(System.in); // there are only four solutions, // let's compute and save them in case the input repeats itself String[] sols = new String[4]; while (sc.hasNextInt()) { // solve one problem instance int digits = sc.nextInt(); if (sols[digits/2-1]==null) { // if not computed yet... List
foundSols = new LinkedList ();; int bMax = (int)Math.pow(10, digits/2); StringBuffer sb = new StringBuffer(); // for increased performance String format = "%0" + digits + ".0f"; // useful for leading zeros for(int b=0; b<bMax; b++) { // get quadratic solutions double[] aSols = solveSecondDegreeEquation(1,2*b-bMax,b*b-b); if (aSols!=null) // if there are solutions... for(double a : aSols) // only want integer solutions and with (digits/2) digits if ((int)a == a && a < bMax) // compose solution & add it to list foundSols.add(String.format(format, a*bMax + b)); } Collections.sort(foundSols); // sort solutions for presentation purposes for(String sol : foundSols) // add them into one string sb.append(sol+"\n"); sols[digits/2-1] = sb.toString(); // save solution for future use } System.out.print(sols[digits/2-1]); } sc.close(); } }
Quando se submete esta nova versão observamos de imediato o ganho de desempenho. Em vez dos 0.45 segundos, demorou-se 0.07 segundos! A segunda versão foi sete vezes mais rápida. Esta diferença seria ainda maior se o problema admitisse números com 10 ou mais dígitos (nesse caso, a primeira solução de força bruta seria rejeitada com um TL, i.e., Time Limit Exceeded, porque iria demorar muito mais que os três segundos admissíveis ao cálculo de uma solução.
O uso de conceitos matemáticos é um trunfo na resolução de muitos problemas. É um tema comum nos concursos de programação e a maioria nem precisa de matemática muito avançada. Neste exemplo precisámos apenas de material dado no 2º e 3º ciclo de escolaridade.
O que precisamos é de um espírito crítico quando analisamos um novo problema. E isso vem com a prática. Há medida que forem treinando vão aprender a usar estas técnicas, muitas das quais já conhecem mas que se foram esquecendo por falta de uso. Eis uma boa oportunidade para reverter essa situação. Podem consultar o capítulo 5 do livro do Halim que apresenta bastantes exemplos deste assunto. Com tempo iremos abordar alguns deles nos treinos.
Happy Coding!
Last modified: Thursday, 13 October 2016, 2:20 PM