You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何棋盘骑士概率问题的递归记忆化解法出错?

骑士在棋盘上的概率问题分析

问题来源

来自LeetCode的「Knight Probability in Chessboard」题目

问题描述

骑士从指定位置出发,每次随机选择8种可能的移动方式之一,总共移动k次,求最终仍留在棋盘上的概率(每次移动的概率为1/8)。

原解法思路

假设无循环路径且每层有2种有效移动时,概率本质为 $(1/8)^k × 有效路径的叶子节点数$;考虑循环路径时,计划通过记忆化利用深度差计算贡献:若节点x在剩余步数为kOfX时被访问过,其对父节点的贡献为 $(1/8)^{k - kOfX}$。

原代码实现

public class KnightProbabilityInChessboard {
    class Pair{
        int row, col;
        int k;
        Pair(int row, int col, int k){
            this.row = row;
            this.col = col;
            this.k = k;
        }

        Pair(int row, int col){
            this.row = row;
            this.col = col;
        }
    }
    // 执行入口
    public double knightProbability(int n, int k, int row, int column) {
        int isVisited[][] = new int[n][n];
        double value[][][] = new double[n][n][k + 1];
        knightProbabilitycalc2(k , value, isVisited, row, column, new ArrayList<>());
        return value[row][column][k];
    }

    // 核心逻辑
    void knightProbabilitycalc2(int k, double value[][][], int visited[][], int row, int column, List<Pair> parents) {

         // 记忆化判断:若节点已被访问,复用已有计算结果
        if (visited[row][column] > k && k!= 0) {
            for(int i = parents.size() - 1; i >= 0; --i){
                Pair parent = parents.get(i);
                double res =  Math.pow((1/8.0), parent.k - k - 1) * value[row][column][k];
                value[parent.row][parent.col][parent.k - k] += res;
            }
            return;
        }

        // 未到最后一步,递归搜索后续移动
        if(k != 0) {
            int n = value.length;
            List<Pair> validMoves = getAllMoves(row, column).stream().filter((move) -> isValid(move.row, move.col, n - 1, n - 1)).collect(Collectors.toList());
            parents.add(new Pair(row, column, k));
            for (Pair move : validMoves) {
                knightProbabilitycalc2(k - 1, value, visited, move.row, move.col, parents);
            }
            parents.remove(parents.size() - 1);
        }

        // 递归结束后,更新所有父节点的概率值
        for(int i = parents.size() - 1; i >= 0; --i){
            Pair parent = parents.get(i);
            double res =  Math.pow((1/8.0), parent.k - k);
            value[parent.row][parent.col][parent.k - k] += res;
        }
        // 标记该位置当前剩余步数已计算完成
        visited[row][column] = k;
    }


    List<Pair> getAllMoves(int row, int col){
        List<Pair> validMoves = new ArrayList<>();
        validMoves.add(new Pair(row - 2, col - 1));
        validMoves.add(new Pair(row - 2, col + 1));
        validMoves.add(new Pair(row + 2, col + 1));
        validMoves.add(new Pair(row + 2, col - 1));
        validMoves.add(new Pair(row + 1, col - 2));
        validMoves.add(new Pair(row + 1, col + 2));
        validMoves.add(new Pair(row - 1, col - 2));
        validMoves.add(new Pair(row - 1, col + 2));
        return validMoves;
    }

    boolean isValid(int r, int c, int maxR, int maxC){
        return (0 <= r &&  r <= maxR  && 0 <= c && c <= maxC);
    }

}

问题现象

代码在k≤3时能正确运行,但k≥4时结果严重偏低。例如n=8、k=30时,预期输出0.0019,实际输出为e-25量级。已确认移动生成和有效性检查逻辑无误,问题出在递归记忆化部分。

错误原因分析

  1. 记忆化判断逻辑完全错误
    原代码用visited[row][column] > k判断是否复用已有结果,但visited是二维数组,仅记录某个位置最后处理的剩余步数k值。实际上,value[row][col][k]表示位置(row,col)剩余k步时留在棋盘的概率,每个(k, row, col)都是独立状态,二维的visited无法区分不同剩余步数的状态,导致错误复用了无关状态的计算结果。比如之前处理过k=5的状态,现在处理k=3时,会错误触发记忆化,复用k=5的结果,而这两个状态完全不同。

  2. 记忆化时的贡献计算指数错误
    触发记忆化时,代码计算贡献用了parent.k - k - 1作为指数,正确的指数应该是parent.k - k(父节点剩余步数减当前节点剩余步数,即从父节点到当前节点走了几步)。指数少1意味着概率被多除以8,直接导致结果被大幅缩小。

  3. visited数组设计不合理
    由于每个状态是三维的(剩余步数、行、列),仅用二维数组记录无法区分不同剩余步数的状态,应该用三维的标记数组,或者直接检查value[row][col][k]是否已完成计算(比如初始化为-1,计算后赋值,判断是否不等于-1)。

内容的提问来源于stack exchange,提问作者tester test

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 09:26:13