程序员的算法趣题:Q13 有多少种满足字母算式的解法(Java版)

题目说明

所谓字母算式,就是用字母表示的算式,
规则是相同字母对应相同数字,不同字母对应不同数字,
并且第一位字母的对应数字不能是 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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值