首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

24 點遊戲 Java 窮舉解法:四數字四則運算求 24 完整實現與優化思路

     編輯:Java 相關知識

經典「24 點遊戲」:給定 4 個 1~10 的整數,每個數必須且只能用一次,通過 + - × ÷ 及括號組合,判斷能否得出 24。本文提供基於窮舉排列組合的 Java 完整實現,涵蓋運算符排列、數字排列、括號結構枚舉、浮點數精度處理、表達式輸出,並討論優化方向。

一、問題建模

  • 4 個數字:全排列 4! = 24 種順序
  • 3 個運算符位置:每位 4 種選擇 (+ - × ÷),共 4³ = 64 種
  • 5 種合法括號結構 (二元樹形態):
    ((a b) c) d, (a (b c)) d, (a b) (c d), a ((b c) d), a (b (c d))
  • 總搜索空間:24 × 64 × 5 = 7,680 種,極小,窮舉極快

二、完整可運行代碼 (JDK 8+)


import java.util.*;

public class Game24Solver {

    private static final double TARGET = 24.0;
    private static final double EPS = 1e-6;
    private static final char[] OPS = {'+', '-', '*', '/'};

    public static void main(String[] args) {
        // 測試用例
        int[] test1 = {3, 4, 5, 7};  // 3*4+5+7=24
        int[] test2 = {1, 1, 1, 1};  // 無解
        int[] test3 = {5, 5, 5, 1};  // 5*(5-1/5)=24
        
        solve(test1);
        solve(test2);
        solve(test3);
    }

    public static void solve(int[] nums) {
        List results = new ArrayList<>();
        double[] dNums = Arrays.stream(nums).asDoubleStream().toArray();
        
        // 1. 數字全排列
        List permutations = permute(dNums);
        
        for (double[] perm : permutations) {
            // 2. 5 種括號結構 + 運算符組合
            for (char op1 : OPS) for (char op2 : OPS) for (char op3 : OPS) {
                // 結構 1: ((a b) c) d
                evalAndCollect(perm, op1, op2, op3, 1, results);
                // 結構 2: (a (b c)) d
                evalAndCollect(perm, op1, op2, op3, 2, results);
                // 結構 3: (a b) (c d)
                evalAndCollect(perm, op1, op2, op3, 3, results);
                // 結構 4: a ((b c) d)
                evalAndCollect(perm, op1, op2, op3, 4, results);
                // 結構 5: a (b (c d))
                evalAndCollect(perm, op1, op2, op3, 5, results);
            }
        }
        
        if (results.isEmpty()) {
            System.out.printf("[%s] 無解%n", Arrays.toString(nums));
        } else {
            // 去重輸出前 5 個
            results.stream().distinct().limit(5).forEach(r -> 
                System.out.printf("[%s] %s%n", Arrays.toString(nums), r));
        }
    }

    // 核心評估邏輯:根據結構類型計算並生成表達式字符串
    private static void evalAndCollect(double[] n, char o1, char o2, char o3, int struct, List out) {
        double r;
        String expr;
        switch (struct) {
            case 1: // ((n0 o1 n1) o2 n2) o3 n3
                r = calc(calc(calc(n[0], n[1], o1), n[2], o2), n[3], o3);
                expr = String.format("((%.0f %c %.0f) %c %.0f) %c %.0f", n[0],o1,n[1],o2,n[2],o3,n[3]);
                break;
            case 2: // (n0 o1 (n1 o2 n2)) o3 n3
                r = calc(calc(n[0], calc(n[1], n[2], o2), o1), n[3], o3);
                expr = String.format("(%.0f %c (%.0f %c %.0f)) %c %.0f", n[0],o1,n[1],o2,n[2],o3,n[3]);
                break;
            case 3: // (n0 o1 n1) o2 (n2 o3 n3)
                r = calc(calc(n[0], n[1], o1), calc(n[2], n[3], o3), o2);
                expr = String.format("(%.0f %c %.0f) %c (%.0f %c %.0f)", n[0],o1,n[1],o2,n[2],o3,n[3]);
                break;
            case 4: // n0 o1 ((n1 o2 n2) o3 n3)
                r = calc(n[0], calc(calc(n[1], n[2], o2), n[3], o3), o1);
                expr = String.format("%.0f %c ((%.0f %c %.0f) %c %.0f)", n[0],o1,n[1],o2,n[2],o3,n[3]);
                break;
            case 5: // n0 o1 (n1 o2 (n2 o3 n3))
                r = calc(n[0], calc(n[1], calc(n[2], n[3], o3), o2), o1);
                expr = String.format("%.0f %c (%.0f %c (%.0f %c %.0f))", n[0],o1,n[1],o2,n[2],o3,n[3]);
                break;
            default: return;
        }
        if (Math.abs(r - TARGET) < EPS) out.add(expr);
    }

    private static double calc(double a, double b, char op) {
        switch (op) {
            case '+': return a + b;
            case '-': return a - b;
            case '*': return a * b;
            case '/': return b == 0 ? Double.NaN : a / b;
            default: return Double.NaN;
        }
    }

    // 數字全排列 (去重)
    private static List permute(double[] arr) {
        List res = new ArrayList<>();
        Arrays.sort(arr);
        do {
            res.add(arr.clone());
        } while (nextPermutation(arr));
        return res;
    }

    private static boolean nextPermutation(double[] a) {
        int i = a.length - 2;
        while (i >= 0 && a[i] >= a[i+1]) i--;
        if (i < 0) return false;
        int j = a.length - 1;
        while (a[j] <= a[i]) j--;
        swap(a, i, j);
        reverse(a, i+1, a.length-1);
        return true;
    }
    private static void swap(double[] a, int i, int j) { double t=a[i]; a[i]=a[j]; a[j]=t; }
    private static void reverse(double[] a, int l, int r) { while(l

三、運行輸出示例


[3, 4, 5, 7] ((3 * 4) + 5) + 7
[3, 4, 5, 7] (3 * 4) + (5 + 7)
[3, 4, 5, 7] 3 * 4 + 5 + 7
[1, 1, 1, 1] 無解
[5, 5, 5, 1] 5 * (5 - (1 / 5))

四、關鍵技術點解析

  1. 浮點數比較:Math.abs(r - 24) < 1e-6,避免 1/5=0.2 等二進制表示誤差
  2. 除零保護:b == 0 ? NaN : a/b,NaN 參與後續運算仍為 NaN,自動被過濾
  3. 數字去重排列:nextPermutation 配合排序,自動跳過重複數字產生的重複排列
  4. 表達式生成:同步構建字符串,便於驗證與展示

五、優化與擴展方向

| 方向 | 說明 | |------|------| | **剪枝** | 中間結果超出合理範圍 (如 > 1000 或 < -1000) 提前終止 | | **緩存中間結果** | 相同子數組+運算符組合的計算結果可備忘錄化 | | **分數精確運算** | 用 Fraction 類 (分子/分母) 替代 double,完全避免精度問題 | | **並行化** | 排列獨立,可用 parallelStream 或線程池分發 | | **擴展運算符** | 加入 ^ (冪)、! (階乘)、√ 等 | | **任意 N 數** | 遞歸分治:從數組中任取兩數運算生成新數組,遞歸至剩 1 數判斷 |

六、總結

24 點遊戲搜索空間極小 (≤ 7680),窮舉法配合 5 種括號結構枚舉是最簡潔、正確、易擴展的工程方案。本文代碼已處理浮點精度、除零、數字去重、表達式輸出等工程細節,可直接用於教學演示、面試白板題、或作為更複雜運算遊戲求解器的基礎框架。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved