题目说明
所谓字母算式,就是用字母表示的算式,
规则是相同字母对应相同数字,不同字母对应不同数字,
并且第一位字母的对应数字不能是 0。譬如给定算式 We * love = CodeIQ,则可以对应填上下面这些数字以使之成立。
W = 7, e = 4, l = 3, o = 8, v = 0, C = 2, d = 1, I = 9, Q = 6
这样一来,我们就能得到 74 * 3804 = 281496 这样的等式。
使前面那个字母算式成立的解法只有这一种。求使下面这个字母算式成立的解法有多少种?
READ + WRITE + TALK = SKILL
思路
1.根据字母算式拆分出不重复的字母作为可选字母,0~9这十个数字作为可选数字
2.用两个集合分别保存可选字母和可选数字
3.用递归依次将每个字母替换成数字,将字母算式=>数字算式
4.判断算式是否成立
代码1
public static int count = 0; // 统计最终结果有多少个
public static void main(String[] args) {
// 字母算式
String pattern = "READ + WRITE + TALK = SKILL";
pattern = pattern.replace(" ", ""); // 去除空格
String str = pattern.replaceAll("[^a-zA-Z]", ""); // 去除+和=
System.out.println(str); // READWRITETALKSKILL
// 可用字符
List<String> sList = new LinkedList<>();
for (int i = 0; i < str.length(); i++) {
String s = str.charAt(i) + "";
if (!sList.contains(s)) { // 去重添加
sList.add(s);
}
}
System.out.println(sList); // [R, E, A, D, W, I, T, L, K, S]
// 可用数字
List<Integer> nList = new LinkedList<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
calc(pattern, sList, nList);
System.out.println("count = " + count); // 输出最终结果
}
/**
* 每层递归替换一种字母,替换到最后一层时得到纯数字算式,计算算式是否成立
* @param pattern 当前的字母算式(逐步被替换成数字算式)
* @param sList 可选字母
* @param nList 可选数字
*/
private static void calc(String pattern, List<String> sList, List<Integer> nList) {
if (pattern == null || sList == null || nList == null)
throw new IllegalArgumentException("入参不合法!");
// 字符用完啦,字母算式完全替换成数字算式了。开始判断算式是否成立
if (sList.size() == 0) {
String[] ss = pattern.split("[^0-9]"); // 按照+和=切分算式,得到4个数字
// 此处的逻辑与字母算式耦合
int a = Integer.parseInt(ss[0]);
int b = Integer.parseInt(ss[1]);
int c = Integer.parseInt(ss[2]);
int d = Integer.parseInt(ss[3]);
if (a + b + c == d) {
// 打印出来瞅瞅成功的算式是哪些
System.out.println(a + " + " + b + " + " + c + " = " + d);
count ++;
}
return;
}
String s = sList.remove(0); // 取出当前集合中的第一个字母,并移除
for (int j = 0; j < nList.size(); j++) {
int n = nList.get(j); // 获取位置j上的数字
if (n == 0 && "RWTS".contains(s)) { // 首位不为0
continue;
}
nList.remove(j); // 移除位置j上的数字
calc(pattern.replace(s, n + ""), sList, nList);
nList.add(j, n); // 还原位置j上的数字
}
sList.add(0, s); // 还原第一个字母
}
代码2
代码1中的部分逻辑与字母算式严重耦合,意味着更换字母算式后需要修改多处代码。
以下代码的可复用性更好,直接更换为想要的字母算式即可。
鉴于Java中没有ruby那样的eval方法,所以需要自己实现相关代码。
main方法
public static int count = 0; // 统计最终结果有多少个
public static void main(String[] args) {
// 字母算式
String pattern = "READ + WRITE + TALK = SKILL";
pattern = pattern.replace(" ", ""); // 去除空格
String str = pattern.replaceAll("[^a-zA-Z]", ""); // 去除+和=
System.out.println(str); // READWRITETALKSKILL
// 首字母
String[] ss = pattern.split("[^a-zA-Z]"); // 切分出每个单词
String head = "";
for(String s : ss){
head += s.charAt(0); // 取每个单词的首字母
}
System.out.println(head); // RWTS
// 可用字符
List<String> sList = new LinkedList<>();
for (int i = 0; i < str.length(); i++) {
String s = str.charAt(i) + "";
if (!sList.contains(s)) { // 去重添加
sList.add(s);
}
}
System.out.println(sList); // [R, E, A, D, W, I, T, L, K, S]
// 可用数字
List<Integer> nList = new LinkedList<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
calc(pattern, sList, nList, head);
System.out.println("count = " + count); // 输出最终结果
}
递归方法
/**
* 每层递归替换一种字母,替换到最后一层时得到算式,计算算式是否成立
* @param pattern 当前的字母算式(逐步被替换成数字算式)
* @param sList 可选字母
* @param nList 可选数字
*/
private static void calc(String pattern, List<String> sList, List<Integer> nList, String head) {
if (pattern == null || sList == null || nList == null) throw new IllegalArgumentException("入参不合法!");
// 字符用完啦,字母算式完全替换成数字算式了。开始判断算式是否成立
if (sList.size() == 0) {
String[] ss = pattern.split("="); // 通过等号将算式切分成两部分
if(Double.compare(CalcUtil.evaluate(ss[0]),Integer.parseInt(ss[1])) == 0){ // CalcUtil.evaluate是自定义的方法
System.out.println(pattern);
count ++;
}
return;
}
String s = sList.remove(0); // 取出当前集合中的第一个字母,并移除
for (int j = 0; j < nList.size(); j++) {
int n = nList.get(j); // 获取位置j上的数字
if(n == 0 && head.contains(s)) { // 首字母不可为0
continue;
}
nList.remove(j); // 移除位置j上的数字
calc(pattern.replace(s, n + ""), sList, nList, head);
nList.add(j, n); // 还原位置j上的数字
}
sList.add(0, s); // 还原第一个字母
}
CalcUtil.java(首次用于:《程序员的算法趣题:Q02 数列的四则运算(Java版)》)
package com.lanying.Q13有多少种满足字母算式的解法;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Double.NaN;
public class CalcUtil {
// 按照四则运算优先级,计算表达式的值,并返回
public static Double evaluate(String exp) {
/* 1.中缀表达式 --> 后缀表达式
1) 通过正则表达式切分算数表达式
2) 从左往右遍历中缀表达式的每个元素:数字和运算符
3) 若是数字,直接存入List
4) 若是符号,则①判断优先级②出栈③入栈:
i. 判断当前符号与栈顶符号(最近一次存入的符号)的优先级
ii. 如果该符号是右括号,或者优先级低于栈顶符号,则栈顶元素依次出栈并存入StringBuilder
iii. 然后将当前符号入栈
5) 遍历结束后,出栈所有运算符
*/
Stack<Double> nums = new Stack<>(); // 存放数字
Stack<String> op = new Stack<>(); // 存放符号
List<String> list = new LinkedList<String>(); // 保存数字和+-*/符号
// 拆分出数字和操作符
// \d+(\.\d+)? 表示数字部分,小括号里的是小数部分,?表示0~1次
// (?<!\d)-? 表示数字部分前面的负号;?表示负号可以没有,或者有一个;(?<!\d)表示不能以数字开头,即这个负号的前面不能是数字(否则-就是减号,而不是负号了)
Pattern p = Pattern.compile("(?<!\\d)-?\\d+(\\.\\d+)?|[+\\-*/()]"); // Java中的正则表达式用\\表示转义字符
Matcher m = p.matcher(exp);
while (m.find()) {
String s = m.group(); // 依次找出每个元素(数字or字符)
// 操作符
if (s.matches("[+\\-*/()]")) {
switch (s) {
case "(": // "(" 入栈
op.push("(");
break;
case ")": // ")" 出栈,直到"("
String top = null; // 栈顶的运算符
while (!(top = op.pop()).equals("(")) {
list.add(top);
}
break;
default: // 四则运算符,比较优先级,出栈比当前符号优先级高的符号,然后入栈当前符号
while (!op.empty() && opPriority(s) <= opPriority(op.peek())) { // 自定义的方法opPriority(xx)
list.add(op.pop()); // 出栈
}
op.push(s); // 入栈当前运算符
break;
}
} else { // 数字
list.add(s);
}
}
// 出栈所有运算符
while (!op.isEmpty()) {
list.add(op.pop());
}
//------- 至此,后缀表达式的每个部分均已存入list ------
// 2.计算后缀表达式的值
Double res = NaN; // not a number; 需要导包import static java.lang.Double.NaN;
for (String e : list) { // 依次取出list中的每个操作数和运算符
if (e.matches("[+\\-*/()]")) { // +-*/运算符,取出nums栈顶的两个数字进行运算,然后将结果存入nums
double next = nums.pop();
double pre = nums.pop();
res = calc(pre, next, e); // 自定义的方法calc(xx)
nums.push(res);
} else { // 数字,直接存入nums
nums.push(Double.parseDouble(e));
}
}
return res;
}
// 四则运算
private static double calc(double pre, double next, String op) {
switch (op) {
case "+":
return pre + next;
case "-":
return pre - next;
case "*":
return pre * next;
case "/":
return pre / next; // Java中两个double相除,0为除数不会报错
default:
break;
}
throw new IllegalArgumentException("不支持的操作符!");
}
// 计算运算符的优先级。此处指定:数字越大,优先级越高
private static int opPriority(String op) {
if (op == null) return 0;
switch (op) {
case "(":
return 1;
case "+":
case "-":
return 2;
case "*":
case "/":
return 3;
default:
throw new IllegalArgumentException("不支持的操作符!");
}
}
}
结果
1632 + 41976 + 7380 = 50988
2543 + 72065 + 6491 = 81099
4905 + 24689 + 8017 = 37611
5094 + 75310 + 1962 = 82366
5096 + 35710 + 1982 = 42788
5180 + 65921 + 2843 = 73944
5270 + 85132 + 3764 = 94166
7092 + 37510 + 1986 = 46588
7092 + 47310 + 1986 = 56388
9728 + 19467 + 6205 = 35400
count = 10