I did it. The result is a monstrous thing.
The code below is divided into several parts:
Lexical analysis - Divides a numerical expression into a sequence of tokens. A token may be either a number consisting of a sequence of 5s and 7s or one of the symbols +
, -
, *
, /
, (
or )
.
Syntactic trees - Represented by the functional interface Operacao
and by the methods that return such a lot. Responsible for interpreting syntactically well-formed expressions. Can generate an error in case of division by zero or non-integer division.
Parsing - Discover the structure of a given expression. Parsing is based on a context-free language and the analyser is recursive descendant LL-1 with the following grammar:
- additive (initial symbol) multiplicative (cont_additive)*
- cont_aditivo (
+
| -
) multiplicative
- unary multiplicative (multiplicative counter)*
- multiplicative count (
*
| /
) unary
- unary (
+
| -
) parentheses
- parentheses
(
additive )
| number
String generation of random expressions - Converts a number to a string of symbols consisting of 5
, 7
, +
, -
, *
, /
, (
and )
.
Evaluation of the number of parentheses and the degree of complication of the strings.
Expression optimizer - find the best expression that generates the desired number:
Searches for an increasing number of characters, starting at 1 and going up to 12 and generating by brute force all strings sequences with the symbols mentioned in the corresponding size. It only looks for a larger string if it has not found any smaller string that fits.
The strings that serve are those that can be compiled and interpreted without generating lexical, syntactic or interpretation errors and that the result of the evaluation is equal to the number sought.
When finding a string with a certain size, keep looking for a better one with the same size. If the new string has the same size and fewer parentheses than the previous one or has the same size and number of parentheses but less complication, test it when compiling and interpreting.
The program tries to optimize all numbers from 0 to 100. The statement says it is up to 1000, but as this business runs on the basis of brute force, then it takes him to finish and I had no bag to wait until he solve the 1000. Syntactic parser code could also be better and a little more organized, but that should be good enough.
Here’s the code:
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.LinkedHashMap;
/**
* @author Victor Williams Stafusa da Silva
*/
public class Expressoes {
private static class ExpressaoMalformadaException extends Exception {}
private static class AvaliacaoExpressaoException extends Exception {}
// Árvore sintática e interpretação:
@FunctionalInterface
private interface Operacao {
public int calcular() throws AvaliacaoExpressaoException;
}
private static Operacao soma(Operacao a, Operacao b) {
return () -> a.calcular() + b.calcular();
}
private static Operacao sub(Operacao a, Operacao b) {
return () -> a.calcular() - b.calcular();
}
private static Operacao mult(Operacao a, Operacao b) {
return () -> a.calcular() * b.calcular();
}
private static Operacao div(Operacao a, Operacao b) {
return () -> {
int ac = a.calcular();
int bc = b.calcular();
if (bc == 0 || ac % bc != 0) throw new AvaliacaoExpressaoException();
return ac / bc;
};
}
private static Operacao op(String simbolo, Operacao a, Operacao b) {
switch (simbolo) {
case "+": return soma(a, b);
case "-": return sub(a, b);
case "*": return mult(a, b);
case "/": return div(a, b);
default: throw new AssertionError();
}
}
private static Operacao simples(int i) {
return () -> i;
}
// Análise léxica:
private static List<String> tokenize(String expressao)
throws ExpressaoMalformadaException
{
List<String> tokens = new ArrayList<>(expressao.length());
StringBuilder proximoToken = new StringBuilder(expressao.length());
for (int i = 0; i < expressao.length(); i++) {
char c = expressao.charAt(i);
if ("()+-*/".indexOf(c) != -1) {
if (proximoToken.length() != 0) {
tokens.add(proximoToken.toString());
proximoToken = new StringBuilder(expressao.length() - i);
}
tokens.add(String.valueOf(c));
} else if ("57".indexOf(c) != -1) {
proximoToken.append(c);
} else {
throw new ExpressaoMalformadaException();
}
}
if (proximoToken.length() != 0) tokens.add(proximoToken.toString());
return tokens;
}
// Análise sintática:
private static class Subexpressao {
private final Operacao op;
private final List<String> resto;
public Subexpressao(Operacao op, List<String> resto) {
this.op = op;
this.resto = resto;
}
}
private static class Continuacao {
private final String simbolo;
private final Operacao op;
private final List<String> resto;
public Continuacao(String simbolo, Operacao op, List<String> resto) {
this.simbolo = simbolo;
this.op = op;
this.resto = resto;
}
}
private static class Simbolo {
private final String op;
private final List<String> resto;
public Simbolo(String op, List<String> resto) {
this.op = op;
this.resto = resto;
}
}
private static Subexpressao parseAditivo(List<String> tokens) {
Subexpressao a = parseMultiplicativo(tokens);
if (a == null) return null;
List<String> resto = a.resto;
List<Continuacao> outros = new ArrayList<>();
while (true) {
Continuacao proximo = parseContinuacaoAditivo(resto);
if (proximo == null) break;
outros.add(proximo);
resto = proximo.resto;
}
for (Continuacao c : outros) {
a = new Subexpressao(op(c.simbolo, a.op, c.op), c.resto);
}
return a;
}
private static Continuacao parseContinuacaoAditivo(List<String> tokens) {
Simbolo sinal = parseTerminal("+", tokens);
if (sinal == null) sinal = parseTerminal("-", tokens);
if (sinal == null) return null;
Subexpressao b = parseMultiplicativo(sinal.resto);
if (b == null) return null;
return new Continuacao(sinal.op, b.op, b.resto);
}
private static Subexpressao parseMultiplicativo(List<String> tokens) {
Subexpressao a = parseUnario(tokens);
if (a == null) return null;
List<String> resto = a.resto;
List<Continuacao> outros = new ArrayList<>();
while (true) {
Continuacao proximo = parseContinuacaoMultiplicativo(resto);
if (proximo == null) break;
outros.add(proximo);
resto = proximo.resto;
}
for (Continuacao c : outros) {
a = new Subexpressao(op(c.simbolo, a.op, c.op), c.resto);
}
return a;
}
private static Continuacao parseContinuacaoMultiplicativo(List<String> tokens) {
Simbolo sinal = parseTerminal("*", tokens);
if (sinal == null) sinal = parseTerminal("/", tokens);
if (sinal == null) return null;
Subexpressao b = parseUnario(sinal.resto);
if (b == null) return null;
return new Continuacao(sinal.op, b.op, b.resto);
}
private static Subexpressao parseUnario(List<String> tokens) {
Simbolo sinal = parseTerminal("+", tokens);
if (sinal == null) sinal = parseTerminal("-", tokens);
if (sinal == null) return parseParenteses(tokens);
Subexpressao v = parseParenteses(sinal.resto);
if (v == null) return null;
return new Subexpressao(op(sinal.op, simples(0), v.op), v.resto);
}
private static Subexpressao parseParenteses(List<String> tokens) {
Simbolo abre = parseTerminal("(", tokens);
if (abre == null) return parseNum(tokens);
Subexpressao dentro = parseAditivo(abre.resto);
if (dentro == null) return null;
Simbolo fecha = parseTerminal(")", dentro.resto);
if (fecha == null) return null;
return new Subexpressao(dentro.op, fecha.resto);
}
private static Subexpressao parseNum(List<String> tokens) {
if (tokens.isEmpty()) return null;
String first = tokens.get(0);
int t;
try {
t = Integer.parseInt(first);
} catch (NumberFormatException e) {
return null;
}
return new Subexpressao(simples(t), tokens.subList(1, tokens.size()));
}
private static Simbolo parseTerminal(String s, List<String> tokens) {
if (tokens.isEmpty()) return null;
String first = tokens.get(0);
if (!s.equals(first)) return null;
return new Simbolo(s, tokens.subList(1, tokens.size()));
}
private static Subexpressao analiseSintatica(List<String> tokens)
throws ExpressaoMalformadaException
{
Subexpressao raiz = parseAditivo(tokens);
if (raiz == null) throw new ExpressaoMalformadaException();
if (!raiz.resto.isEmpty()) throw new ExpressaoMalformadaException();
return raiz;
}
// Interpretador:
private static int interpretar(String expressao)
throws ExpressaoMalformadaException, AvaliacaoExpressaoException
{
return analiseSintatica(tokenize(expressao)).op.calcular();
}
// Gerador de expressões:
private static final String SIMBOLOS = "57()+-*/";
private static final int TAMANHO_SIMBOLOS = SIMBOLOS.length();
private static String gerarExpressao(int chute, int tamanho) {
char[] c = new char[tamanho];
for (int i = 0; i < tamanho; i++) {
int r = chute % TAMANHO_SIMBOLOS;
c[i] = SIMBOLOS.charAt(r);
chute /= TAMANHO_SIMBOLOS;
}
return new String(c);
}
private static int complicacao(String x) {
int comp = 0;
for (char c : x.toCharArray()) {
if (c == '5' || c == '7') comp++;
}
return comp;
}
private static int contaPar(String x) {
int comp = 0;
for (char c : x.toCharArray()) {
if (c == '(' || c == ')') comp++;
}
return comp;
}
private static int pow(int base, int expoente) {
return expoente == 0 ? 1 : base * pow(base, expoente - 1);
}
// Otimizador de expressões:
private static String acharMelhorString(int valor) {
String melhor = "";
int menosComplicado = 999999;
int menosParenteses = 999999;
int menor = melhor.length();
for (int tamanho = 1; tamanho < 12; tamanho++) {
boolean achou = false;
int max = pow(TAMANHO_SIMBOLOS, tamanho);
for (int i = 0; i < max; i++) {
String g = gerarExpressao(i, tamanho);
int par = contaPar(g);
if (par > menosParenteses) continue;
int complicado = complicacao(g);
if (par == menosParenteses && complicado >= menosComplicado) continue;
try {
if (valor == interpretar(g)) {
melhor = g;
menosComplicado = complicado;
menosParenteses = par;
System.out.println("Achei: " + valor + " = " + g);
}
} catch (ExpressaoMalformadaException | AvaliacaoExpressaoException e) {
// Ignora e continua.
}
}
if (!melhor.isEmpty()) return melhor;
}
return "";
}
private static Map<Integer, String> tabelar(int min, int max) {
Map<Integer, String> tabela = new LinkedHashMap<>(max - min + 1);
for (int i = min; i <= max; i++) {
tabela.put(i, acharMelhorString(i));
}
return tabela;
}
public static void main(String[] args) {
Map<Integer, String> tabela = tabelar(0, 100);
for (Map.Entry<Integer,String> entry : tabela.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
The way out is like this:
Achei: 0 = 5-5
Achei: 1 = 5/5
Achei: 2 = 7-5
Achei: 3 = 5-7+5
Achei: 4 = 5-5/5
Achei: 5 = 5
Achei: 6 = 5/5+5
Achei: 7 = 7
Achei: 8 = 7+5/5
Achei: 9 = 7+7-5
Achei: 10 = 5+5
Achei: 11 = 55/5
Achei: 12 = 7+5
Achei: 13 = 75-7-55
Achei: 13 = 7+5/5+5
Achei: 14 = 7+7
Achei: 15 = 75/5
Achei: 16 = 55/5+5
Achei: 17 = 7+5+5
Achei: 18 = 75-57
Achei: 18 = 5*5-7
Achei: 19 = 7+7+5
Achei: 20 = 75-55
Achei: 20 = 5*5-5
Achei: 21 = 7+7+7
Achei: 22 = 77-55
Achei: 23 = 5-57+75
Achei: 23 = 5*5-7+5
Achei: 24 = 7+7+5+5
Achei: 25 = 5*5
Achei: 26 = 75-7*7
Achei: 27 = 7+75-55
Achei: 27 = 7+5*5-5
Achei: 28 = 7*5-7
Achei: 29 = 7+77-55
Achei: 30 = 5*5+5
Achei: 31 = 775/5/5
Achei: 32 = 7+5*5
Achei: 33 = 7*5-7+5
Achei: 34 = 7*5-5/5
Achei: 35 = 7*5
Achei: 36 = 5/5+7*5
Achei: 37 = 7+5*5+5
Achei: 38 = 55-7-5-5
Achei: 39 = 7*7-5-5
Achei: 40 = 7*5+5
Achei: 41 = 55-7-7
Achei: 42 = 7+7*5
Achei: 43 = 55-7-5
Achei: 44 = 7*7-5
Achei: 45 = 55-5-5
Achei: 46 = 57-55/5
Achei: 47 = 57-5-5
Achei: 48 = 55-7
Achei: 49 = 7*7
Achei: 50 = 55-5
Achei: 51 = 7*7+7-5
Achei: 52 = 57-5
Achei: 53 = 5-7+55
Achei: 54 = 7*7+5
Achei: 55 = 55
Achei: 56 = 7*7+7
Achei: 57 = 57
Achei: 58 = 57+5/5
Achei: 59 = 7+57-5
Achei: 60 = 5+55
Achei: 61 = 75-7-7
Achei: 62 = 7+55
Achei: 63 = 75-7-5
Achei: 64 = 7+57
Achei: 65 = 5+5+55
Achei: 66 = 55/5+55
Achei: 67 = 7+5+55
Achei: 68 = 75-7
Achei: 69 = 7+7+55
Achei: 70 = 75-5
Achei: 71 = 7+7+57
Achei: 72 = 77-5
Achei: 73 = 5-7+75
Achei: 74 = 75-5/5
Achei: 75 = 75
Achei: 76 = 5/5+75
Achei: 77 = 77
Achei: 78 = 77+5/5
Achei: 79 = 7+77-5
Achei: 80 = 5+75
Achei: 81 = 5/5+5+75
Achei: 82 = 7+75
Achei: 83 = 7*5-7+55
Achei: 84 = 7+77
Achei: 85 = 5+5+75
Achei: 86 = 55/5+75
Achei: 87 = 7+5+75
Achei: 88 = 77+55/5
Achei: 89 = 7+7+75
Achei: 90 = 7*5+55
Achei: 91 = 7+7+77
Achei: 92 = 57+7*5
Achei: 93 = 75-57+75
Achei: 93 = 5*5-7+75
Achei: 94 = 7+7+5+75
Achei: 95 = 7*5+5+55
Achei: 96 = 755/5-55
Achei: 96 = 7+7+7+75
Achei: 97 = 7+7*5+55
Achei: 98 = 7*7+7*7
Achei: 99 = 7*7-5+55
Achei: 100 = 5*5+75
0: 5-5
1: 5/5
2: 7-5
3: 5-7+5
4: 5-5/5
5: 5
6: 5/5+5
7: 7
8: 7+5/5
9: 7+7-5
10: 5+5
11: 55/5
12: 7+5
13: 7+5/5+5
14: 7+7
15: 75/5
16: 55/5+5
17: 7+5+5
18: 5*5-7
19: 7+7+5
20: 5*5-5
21: 7+7+7
22: 77-55
23: 5*5-7+5
24: 7+7+5+5
25: 5*5
26: 75-7*7
27: 7+5*5-5
28: 7*5-7
29: 7+77-55
30: 5*5+5
31: 775/5/5
32: 7+5*5
33: 7*5-7+5
34: 7*5-5/5
35: 7*5
36: 5/5+7*5
37: 7+5*5+5
38: 55-7-5-5
39: 7*7-5-5
40: 7*5+5
41: 55-7-7
42: 7+7*5
43: 55-7-5
44: 7*7-5
45: 55-5-5
46: 57-55/5
47: 57-5-5
48: 55-7
49: 7*7
50: 55-5
51: 7*7+7-5
52: 57-5
53: 5-7+55
54: 7*7+5
55: 55
56: 7*7+7
57: 57
58: 57+5/5
59: 7+57-5
60: 5+55
61: 75-7-7
62: 7+55
63: 75-7-5
64: 7+57
65: 5+5+55
66: 55/5+55
67: 7+5+55
68: 75-7
69: 7+7+55
70: 75-5
71: 7+7+57
72: 77-5
73: 5-7+75
74: 75-5/5
75: 75
76: 5/5+75
77: 77
78: 77+5/5
79: 7+77-5
80: 5+75
81: 5/5+5+75
82: 7+75
83: 7*5-7+55
84: 7+77
85: 5+5+75
86: 55/5+75
87: 7+5+75
88: 77+55/5
89: 7+7+75
90: 7*5+55
91: 7+7+77
92: 57+7*5
93: 5*5-7+75
94: 7+7+5+75
95: 7*5+5+55
96: 7+7+7+75
97: 7+7*5+55
98: 7*7+7*7
99: 7*7-5+55
100: 5*5+75
These lines that start with "I found" are to see what he’s doing. Interesting that you can see the optimizer working, as in this example below where an expression was found for the 18 with 5 characters and complication degree 4, but when searching for a better expression, one with a complication degree 3 was found later:
Achei: 18 = 75-57
Achei: 18 = 5*5-7
Interestingly, none of the expressions generated up to 100 use parentheses (they are expensive because each pair of parentheses leaves the expressions longer in 2 characters). Even your example gives this:
31 = 7 - ( 5 * 5 ) + 7 * 7
But the show thought so:
31 = 775 / 5 / 5
And in this case there, the degree of complication is equal, but the expression size and the number of parentheses is lower.
Your problem is somewhat difficult and laborious. Unfortunately, the implementation you have made is rather confusing and I personally doubt that this is an approach that can solve the problem. I would end up implementing something from scratch without taking anything from you.
– Victor Stafusa
There are three criteria there to minimize the expression: size, number of parentheses and degree of complication. What is their order? For example, if I have a smaller expression, but with more parentheses, which one is better? If I have an expression with less parentheses but greater degree of complication, which is the best?
– Victor Stafusa
I don’t know if that’s the intention, but there is a rule in mathematics that every natural number can be written by a multiplication of prime numbers. If you discover the composition in primes of the numbers from 1 to 1000 you can check which are these numbers and find out if it is possible to get them only with these elements that you have. If possible you can follow this idea to solve the algorithm. (It’s just an idea)
– Pagotti