68.【必备】从递归入手三维动态规划

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解069【必备】从递归入手三维动态规划_哔哩哔哩_bilibili

一.一和零

题目:一和零

算法原理

  • 整体原理
    • 这是一个多维费用背包问题,目标是在给定二进制字符串数组strs以及限制条件(最多有m0n1)下,找出最大子集的长度。
    • 主要运用了动态规划思想,通过对不同状态的记录和计算,逐步推导出最优解。动态规划的关键在于避免重复计算子问题,这里通过记忆化搜索或者直接构建状态转移方程来实现。
  • 具体步骤
    • zerosAndOnes函数
      • 原理:遍历输入字符串str中的每个字符,统计其中01的数量。如果字符为0,则将全局变量zeros加1;如果字符为1,则将全局变量ones加1。
      • 目的:为后续判断字符串是否满足01的数量限制提供基础数据。
    • findMaxForm1f1函数
      • findMaxForm1:作为递归计算的入口函数,它调用f1函数开始计算。
      • f1
        • 递归终止条件:当i等于strs.length时,意味着已经遍历完所有字符串,此时返回0。
        • 状态转移:
          • 不选择当前字符串strs[i]时,递归调用f1(strs, i + 1, z, o)得到p1
          • 选择当前字符串strs[i]时,先调用zerosAndOnes(strs[i])统计01的个数。如果zeros <= zones <= o,则递归调用f1(strs, i + 1, z - zeros, o - ones)并加1得到p2
          • 最终结果为Math.max(p1, p2),即选择和不选择当前字符串两种情况中的较大值。
    • findMaxForm2f2函数(记忆化搜索)
      • findMaxForm2
        • 初始化:创建一个三维数组dp,大小为[strs.length][m + 1][n + 1],并将所有元素初始化为 - 1,用于记忆化搜索。然后调用f2函数开始计算。
      • f2
        • 递归终止条件:当i等于strs.length时,返回0。
        • 记忆化处理:如果dp[i][z][o]不等于 - 1,表示该状态已经计算过,直接返回dp[i][z][o]
        • 状态转移:与f1类似,有不选择当前字符串(p1)和选择当前字符串(p2)两种情况。计算出p1p2后,取较大值ans,并更新dp[i][z][o]=ans,最后返回ans
    • findMaxForm3函数
      • 初始化:创建一个三维数组dp,大小为[len + 1][m + 1][n + 1],其中lenstrs的长度。
      • 状态转移:从后往前遍历strs数组。对于每个字符串strs[i],先调用zerosAndOnes(strs[i])统计01的个数。然后对于每个可能的z0m)和o0n),计算不选择当前字符串(p1)和选择当前字符串(p2)两种情况下的最大值,更新dp[i][z][o]=Math.max(p1, p2)。最后返回dp[0][m][n]
    • findMaxForm4函数
      • 初始化:创建一个二维数组dp,大小为[m + 1][n + 1]
      • 状态转移:遍历strs中的每个字符串s,先调用zerosAndOnes(s)统计01的个数。然后从mzerosnones倒序遍历zo,更新dp[z][o]=Math.max(dp[z][o], 1 + dp[z - zeros][o - ones])。最后返回dp[m][n]

代码实现

// 一和零(多维费用背包)
// 给你一个二进制字符串数组 strs 和两个整数 m 和 n
// 请你找出并返回 strs 的最大子集的长度
// 该子集中 最多 有 m 个 0 和 n 个 1
// 如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集
// 测试链接 : https://leetcode.cn/problems/ones-and-zeroes/
public class Code01_OnesAndZeroes {

    public static int zeros, ones;

    // 统计一个字符串中0的1的数量
    // 0的数量赋值给全局变量zeros
    // 1的数量赋值给全局变量ones
    public static void zerosAndOnes(String str) {
        zeros = 0;
        ones = 0;
        for (int i = 0; i < str.length(); i++) {
            if (str.charAt(i) == '0') {
                zeros++;
            } else {
                ones++;
            }
        }
    }

    public static int findMaxForm1(String[] strs, int m, int n) {
        return f1(strs, 0, m, n);
    }

    // strs[i....]自由选择,希望零的数量不超过z、一的数量不超过o
    // 最多能选多少个字符串
    public static int f1(String[] strs, int i, int z, int o) {
        if (i == strs.length) {
            // 没有字符串了
            return 0;
        }
        // 不使用当前的strs[i]字符串
        int p1 = f1(strs, i + 1, z, o);
        // 使用当前的strs[i]字符串
        int p2 = 0;
        zerosAndOnes(strs[i]);
        if (zeros <= z && ones <= o) {
            p2 = 1 + f1(strs, i + 1, z - zeros, o - ones);
        }
        return Math.max(p1, p2);
    }

    // 记忆化搜索
    public static int findMaxForm2(String[] strs, int m, int n) {
        int[][][] dp = new int[strs.length][m + 1][n + 1];
        for (int i = 0; i < strs.length; i++) {
            for (int z = 0; z <= m; z++) {
                for (int o = 0; o <= n; o++) {
                    dp[i][z][o] = -1;
                }
            }
        }
        return f2(strs, 0, m, n, dp);
    }

    public static int f2(String[] strs, int i, int z, int o, int[][][] dp) {
        if (i == strs.length) {
            return 0;
        }
        if (dp[i][z][o] != -1) {
            return dp[i][z][o];
        }
        int p1 = f2(strs, i + 1, z, o, dp);
        int p2 = 0;
        zerosAndOnes(strs[i]);
        if (zeros <= z && ones <= o) {
            p2 = 1 + f2(strs, i + 1, z - zeros, o - ones, dp);
        }
        int ans = Math.max(p1, p2);
        dp[i][z][o] = ans;
        return ans;
    }

    public static int findMaxForm3(String[] strs, int m, int n) {
        int len = strs.length;
        int[][][] dp = new int[len + 1][m + 1][n + 1];
        for (int i = len - 1; i >= 0; i--) {
            zerosAndOnes(strs[i]);
            for (int z = 0, p1, p2; z <= m; z++) {
                for (int o = 0; o <= n; o++) {
                    p1 = dp[i + 1][z][o];
                    p2 = 0;
                    if (zeros <= z && ones <= o) {
                        p2 = 1 + dp[i + 1][z - zeros][o - ones];
                    }
                    dp[i][z][o] = Math.max(p1, p2);
                }
            }
        }
        return dp[0][m][n];
    }

    public static int findMaxForm4(String[] strs, int m, int n) {
        // 代表i == len
        int[][] dp = new int[m + 1][n + 1];
        for (String s : strs) {
            // 每个字符串逐渐遍历即可
            // 更新每一层的表
            // 和之前的遍历没有区别
            zerosAndOnes(s);
            for (int z = m; z >= zeros; z--) {
                for (int o = n; o >= ones; o--) {
                    dp[z][o] = Math.max(dp[z][o], 1 + dp[z - zeros][o - ones]);
                }
            }
        }
        return dp[m][n];
    }

}

二.盈利计划

题目:盈利计划

算法原理

  • 整体原理
    • 这是一个多维费用背包问题,涉及员工数量(类似背包容量)和利润要求(另一维度的限制条件)。目标是计算满足员工数量不超过n且利润不少于minProfit的计划数量。通过不同的函数实现了递归求解、记忆化搜索和基于动态规划的优化求解。
  • 具体步骤
    • profitableSchemes1f1函数
      • profitableSchemes1:作为递归计算的入口函数,调用f1函数开始计算。
      • f1
        • 递归终止条件
          • r <= 0(员工额度耗尽)时,如果s <= 0(利润已达标)则返回1,表示有一种可行方案;否则返回0。
          • i == g.length(工作耗尽)时,如果s <= 0则返回1,否则返回0。
        • 状态转移
          • 不选择当前工作i时,递归调用f1(g, p, i + 1, r, s)得到p1
          • 选择当前工作i时,如果g[i] <= r(员工数量足够),则递归调用f1(g, p, i + 1, r - g[i], s - p[i])得到p2
          • 最终结果为p1 + p2,即选择和不选择当前工作的方案数之和。
    • profitableSchemes2f2函数(记忆化搜索)
      • profitableSchemes2
        • 初始化:创建一个三维数组dp,大小为[m][n + 1][minProfit + 1],并将所有元素初始化为 - 1,用于记忆化搜索。然后调用f2函数开始计算。
      • f2
        • 递归终止条件
          • r <= 0时,如果s == 0(利润刚好达标)则返回1,否则返回0。
          • i == g.length时,如果s == 0则返回1,否则返回0。
        • 记忆化处理:如果dp[i][r][s]不等于 - 1,表示该状态已经计算过,直接返回dp[i][r][s]
        • 状态转移:与f1类似,不选择当前工作得到p1,选择当前工作(当g[i] <= r时)得到p2。计算出p1p2后,计算(p1 + p2) % mod得到ans,更新dp[i][r][s]=ans,最后返回ans
    • profitableSchemes3函数
      • 初始化:创建一个二维数组dp,大小为[n + 1][minProfit + 1],并将dp[r][0](所有员工数量r下,利润为0的情况)初始化为1。
      • 状态转移:从后往前遍历工作。对于每个工作i,从n到0倒序遍历r(员工数量),从minProfit到0倒序遍历s(利润)。不选择当前工作时p1 = dp[r][s],选择当前工作(当group[i] <= r时)p2 = dp[r - group[i]][Math.max(0, s - profit[i])],然后更新dp[r][s]=(p1 + p2) % mod。最后返回dp[n][minProfit]

代码实现

// 盈利计划(多维费用背包)
// 集团里有 n 名员工,他们可以完成各种各样的工作创造利润
// 第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与
// 如果成员参与了其中一项工作,就不能参与另一项工作
// 工作的任何至少产生 minProfit 利润的子集称为 盈利计划
// 并且工作的成员总数最多为 n
// 有多少种计划可以选择?因为答案很大,答案对 1000000007 取模
// 测试链接 : https://leetcode.cn/problems/profitable-schemes/
public class Code02_ProfitableSchemes {

    // n : 员工的额度,不能超
    // p : 利润的额度,不能少
    // group[i] : i号项目需要几个人
    // profit[i] : i号项目产生的利润
    // 返回能做到员工不能超过n,利润不能少于p的计划有多少个
    public static int profitableSchemes1(int n, int minProfit, int[] group, int[] profit) {
        return f1(group, profit, 0, n, minProfit);
    }

    // i : 来到i号工作
    // r : 员工额度还有r人,如果r<=0说明已经没法再选择工作了
    // s : 利润还有s才能达标,如果s<=0说明之前的选择已经让利润达标了
    // 返回 : i.... r、s,有多少种方案
    public static int f1(int[] g, int[] p, int i, int r, int s) {
        if (r <= 0) {
            // 人已经耗尽了,之前可能选了一些工作
            return s <= 0 ? 1 : 0;
        }
        // r > 0
        if (i == g.length) {
            // 工作耗尽了,之前可能选了一些工作
            return s <= 0 ? 1 : 0;
        }
        // 不要当前工作
        int p1 = f1(g, p, i + 1, r, s);
        // 要做当前工作
        int p2 = 0;
        if (g[i] <= r) {
            p2 = f1(g, p, i + 1, r - g[i], s - p[i]);
        }
        return p1 + p2;
    }

    public static int mod = 1000000007;

    public static int profitableSchemes2(int n, int minProfit, int[] group, int[] profit) {
        int m = group.length;
        int[][][] dp = new int[m][n + 1][minProfit + 1];
        for (int a = 0; a < m; a++) {
            for (int b = 0; b <= n; b++) {
                for (int c = 0; c <= minProfit; c++) {
                    dp[a][b][c] = -1;
                }
            }
        }
        return f2(group, profit, 0, n, minProfit, dp);
    }

    public static int f2(int[] g, int[] p, int i, int r, int s, int[][][] dp) {
        if (r <= 0) {
            return s == 0 ? 1 : 0;
        }
        if (i == g.length) {
            return s == 0 ? 1 : 0;
        }
        if (dp[i][r][s] != -1) {
            return dp[i][r][s];
        }
        int p1 = f2(g, p, i + 1, r, s, dp);
        int p2 = 0;
        //  Math.max(0, s - p[i])目的:既然剩余利润是负数和剩余利润是0的效果是一样的
        // 那么就将剩余利润保持在>=0的情况,这样在改成严格位置依赖的时候可以保证剩余利润的下标不会越界
        if (g[i] <= r) {
            p2 = f2(g, p, i + 1, r - g[i], Math.max(0, s - p[i]), dp);
        }
        int ans = (p1 + p2) % mod;
        dp[i][r][s] = ans;
        return ans;
    }

    public static int profitableSchemes3(int n, int minProfit, int[] group, int[] profit) {
        // i = 没有工作的时候,i == g.length
        int[][] dp = new int[n + 1][minProfit + 1];
        for (int r = 0; r <= n; r++) {
            dp[r][0] = 1;
        }
        int m = group.length;
        for (int i = m - 1; i >= 0; i--) {
            for (int r = n; r >= 0; r--) {
                for (int s = minProfit; s >= 0; s--) {
                    int p1 = dp[r][s];
                    int p2 = group[i] <= r ? dp[r - group[i]][Math.max(0, s - profit[i])] : 0;
                    dp[r][s] = (p1 + p2) % mod;
                }
            }
        }
        return dp[n][minProfit];
    }

}

 三.骑士在棋盘上的概率

题目: 骑士在棋盘上的概率

算法原理

  • 整体原理
    • 这是一个基于动态规划思想解决概率计算问题的算法。通过记录从棋盘上每个位置出发,剩余不同步数时仍留在棋盘上的概率,逐步递推计算出从给定起始位置出发,经过特定步数后仍留在棋盘上的概率。
  • 具体步骤
    • knightProbability函数
      • 初始化:创建一个三维数组dp,其维度为n * n * (k + 1),其中n是棋盘的边长,k是骑士要移动的步数。将dp数组中的所有元素初始化为 - 1,用于标记未计算的状态。然后调用f函数开始计算。
    • f函数
      • 边界条件
        • 如果当前位置(i, j)不在棋盘内(即i < 0i >= nj < 0j >= n),则返回0,表示从这个位置出发已经不在棋盘上,留在棋盘上的概率为0。
        • 如果dp[i][j][k]不等于 - 1,说明该状态已经计算过,直接返回dp[i][j][k]
      • 基础情况(k = 0
        • k = 0时,即没有剩余步数要走,此时如果当前位置在棋盘内,则留在棋盘上的概率为1,所以ans = 1
      • 状态转移(k > 0
        • k > 0时,骑士有8种可能的移动方向。对于每种移动方向,计算从移动后的位置出发,剩余k - 1步时仍留在棋盘上的概率。由于每种移动方向是等概率的,所以每种方向的概率是从该方向移动后的位置出发的概率除以8。
        • 例如,对于向(i - 2, j + 1)方向移动,计算f(n, i - 2, j + 1, k - 1, dp) / 8,然后将8种可能方向的概率相加得到ans
        • 最后将计算得到的ans赋值给dp[i][j][k],并返回ans,以便在后续计算中可以直接使用这个状态的值。

代码实现 

// 骑士在棋盘上的概率
// n * n的国际象棋棋盘上,一个骑士从单元格(row, col)开始,并尝试进行 k 次移动
// 行和列从0开始,所以左上单元格是 (0,0),右下单元格是 (n-1, n-1)
// 象棋骑士有8种可能的走法。每次移动在基本方向上是两个单元格,然后在正交方向上是一个单元格
// 每次骑士要移动时,它都会随机从8种可能的移动中选择一种,然后移动到那里
// 骑士继续移动,直到它走了 k 步或离开了棋盘
// 返回 骑士在棋盘停止移动后仍留在棋盘上的概率 
// 测试链接 : https://leetcode.cn/problems/knight-probability-in-chessboard/
public class Code03_KnightProbabilityInChessboard {

    public static double knightProbability(int n, int k, int row, int col) {
        double[][][] dp = new double[n][n][k + 1];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                for (int t = 0; t <= k; t++) {
                    dp[i][j][t] = -1;
                }
            }
        }
        return f(n, row, col, k, dp);
    }

    // 从(i,j)出发还有k步要走,返回最后在棋盘上的概率
    public static double f(int n, int i, int j, int k, double[][][] dp) {
        if (i < 0 || i >= n || j < 0 || j >= n) {
            return 0;
        }
        if (dp[i][j][k] != -1) {
            return dp[i][j][k];
        }
        double ans = 0;
        if (k == 0) {
            ans = 1;
        } else {
            ans += (f(n, i - 2, j + 1, k - 1, dp) / 8);
            ans += (f(n, i - 1, j + 2, k - 1, dp) / 8);
            ans += (f(n, i + 1, j + 2, k - 1, dp) / 8);
            ans += (f(n, i + 2, j + 1, k - 1, dp) / 8);
            ans += (f(n, i + 2, j - 1, k - 1, dp) / 8);
            ans += (f(n, i + 1, j - 2, k - 1, dp) / 8);
            ans += (f(n, i - 1, j - 2, k - 1, dp) / 8);
            ans += (f(n, i - 2, j - 1, k - 1, dp) / 8);
        }
        dp[i][j][k] = ans;
        return ans;
    }

}

四.矩阵中和能被 K 整除的路径

题目:矩阵中和能被 K 整除的路径

算法原理

  • 整体原理
    • 这是一个关于在矩阵中寻找满足特定条件(路径和能被k整除)的路径数量的问题。通过递归、记忆化搜索和动态规划的方式来计算从矩阵左上角(0, 0)到右下角(m - 1, n - 1)满足条件的路径数量。
  • 具体步骤
    • numberOfPaths1f1函数
      • numberOfPaths1:作为递归计算的入口函数,调用f1函数开始计算。
      • f1
        • 递归终止条件:当i == n - 1j == m - 1(到达右下角终点)时,判断当前位置的值grid[i][j]k取模是否等于r,如果是则返回1,表示找到一条满足条件的路径;否则返回0。
        • 计算后续需要凑出的余数need:通过公式(k + r - (grid[i][j] % k)) % k计算从当前位置出发,为了使路径和能被k整除,后续路径和需要对k取模得到的余数。
        • 状态转移
          • i + 1 < n(可以向下走)时,递归调用f1函数计算从(i + 1, j)出发的满足条件的路径数量,记为ans
          • j + 1 < m(可以向右走)时,将从(i, j + 1)出发的满足条件的路径数量与之前的ans相加(并对mod取模),得到新的ans
          • 最后返回ans
    • numberOfPaths2f2函数(记忆化搜索)
      • numberOfPaths2
        • 初始化:创建一个三维数组dp,大小为[n][m][k],并将所有元素初始化为 - 1,用于记忆化搜索。然后调用f2函数开始计算。
      • f2
        • 递归终止条件:与f1相同,当到达右下角时判断grid[i][j]k取模是否等于r,是则返回1,否则返回0。
        • 记忆化处理:如果dp[i][j][r]不等于 - 1,表示该状态已经计算过,直接返回dp[i][j][r]
        • 计算need和状态转移:计算need的方式与f1相同,状态转移也类似,先向下走计算ans,再向右走更新ans(相加并对mod取模),最后将ans赋值给dp[i][j][r]并返回ans
    • numberOfPaths3函数
      • 初始化:创建三维数组dp,并将dp[n - 1][m - 1][grid[n - 1][m - 1] % k]初始化为1,表示到达右下角且路径和对k取模的结果为grid[n - 1][m - 1] % k的路径有1条。
      • 状态转移:
        • 先从右下角向左填充最后一行,对于每个r,计算dp[i][m - 1][r]dp[i + 1][m - 1][(k + r - grid[i][m - 1] % k) % k]
        • 再从右下角向上填充最后一列,对于每个r,计算dp[n - 1][j][r]dp[n - 1][j + 1][(k + r - grid[n - 1][j] % k) % k]
        • 最后从倒数第二行倒数第二列开始,双重循环遍历矩阵。对于每个ijr,计算need,然后更新dp[i][j][r]dp[i + 1][j][need]加上dp[i][j + 1][need](并对mod取模)。
      • 最后返回dp[0][0][0],即从左上角出发满足条件的路径数量。

代码实现

// 矩阵中和能被 K 整除的路径
// 给一个下标从0开始的 n * m 整数矩阵 grid 和一个整数 k
// 从起点(0,0)出发,每步只能往下或者往右,你想要到达终点(m-1, n-1)
// 请你返回路径和能被 k 整除的路径数目
// 答案对 1000000007 取模
// 测试链接 : https://leetcode.cn/problems/paths-in-matrix-whose-sum-is-divisible-by-k/
public class Code04_PathsDivisibleByK {

    public static int mod = 1000000007;

    public static int numberOfPaths1(int[][] grid, int k) {
        int n = grid.length;
        int m = grid[0].length;
        return f1(grid, n, m, k, 0, 0, 0);
    }

    // 当前来到(i,j)位置,最终一定要走到右下角(n-1,m-1)
    // 从(i,j)出发,最终一定要走到右下角(n-1,m-1),有多少条路径,累加和%k的余数是r
    public static int f1(int[][] grid, int n, int m, int k, int i, int j, int r) {
        if (i == n - 1 && j == m - 1) {
            return grid[i][j] % k == r ? 1 : 0;
        }
        // 后续需要凑出来的余数need
        int need = (k + r - (grid[i][j] % k)) % k;
        int ans = 0;
        //往下走
        if (i + 1 < n) {
            ans = f1(grid, n, m, k, i + 1, j, need);
        }
        //往右走
        if (j + 1 < m) {
            ans = (ans + f1(grid, n, m, k, i, j + 1, need)) % mod;
        }
        return ans;
    }

    public static int numberOfPaths2(int[][] grid, int k) {
        int n = grid.length;
        int m = grid[0].length;
        int[][][] dp = new int[n][m][k];
        for (int a = 0; a < n; a++) {
            for (int b = 0; b < m; b++) {
                for (int c = 0; c < k; c++) {
                    dp[a][b][c] = -1;
                }
            }
        }
        return f2(grid, n, m, k, 0, 0, 0, dp);
    }

    public static int f2(int[][] grid, int n, int m, int k, int i, int j, int r, int[][][] dp) {
        if (i == n - 1 && j == m - 1) {
            return grid[i][j] % k == r ? 1 : 0;
        }
        if (dp[i][j][r] != -1) {
            return dp[i][j][r];
        }
        int need = (k + r - grid[i][j] % k) % k;
        int ans = 0;
        if (i + 1 < n) {
            ans = f2(grid, n, m, k, i + 1, j, need, dp);
        }
        if (j + 1 < m) {
            ans = (ans + f2(grid, n, m, k, i, j + 1, need, dp)) % mod;
        }
        dp[i][j][r] = ans;
        return ans;
    }

    public static int numberOfPaths3(int[][] grid, int k) {
        int n = grid.length;
        int m = grid[0].length;
        int[][][] dp = new int[n][m][k];
        dp[n - 1][m - 1][grid[n - 1][m - 1] % k] = 1;
        for (int i = n - 2; i >= 0; i--) {
            for (int r = 0; r < k; r++) {
                dp[i][m - 1][r] = dp[i + 1][m - 1][(k + r - grid[i][m - 1] % k) % k];
            }
        }
        for (int j = m - 2; j >= 0; j--) {
            for (int r = 0; r < k; r++) {
                dp[n - 1][j][r] = dp[n - 1][j + 1][(k + r - grid[n - 1][j] % k) % k];
            }
        }
        for (int i = n - 2, need; i >= 0; i--) {
            for (int j = m - 2; j >= 0; j--) {
                for (int r = 0; r < k; r++) {
                    need = (k + r - grid[i][j] % k) % k;
                    dp[i][j][r] = dp[i + 1][j][need];
                    dp[i][j][r] = (dp[i][j][r] + dp[i][j + 1][need]) % mod;
                }
            }
        }
        return dp[0][0][0];
    }

}

 五.扰乱字符串

题目:扰乱字符串

算法原理

  • 整体原理
    • 这是一个判断两个字符串是否为扰乱字符串的问题。扰乱字符串是通过特定的分割和交换子字符串的操作得到的。算法通过递归、记忆化搜索和动态规划的方法来判断两个字符串是否满足扰乱字符串的关系。
  • 具体步骤
    • isScramble1f1函数
      • isScramble1:将输入字符串转换为字符数组后,调用f1函数开始判断。
      • f1
        • 递归终止条件:当l1 == r1(子字符串长度为1)时,判断s1[l1]s2[l2]是否相等,如果相等则返回true,否则返回false
        • 状态转移(不交错情况)
          • 对于s1[l1..r1]s2[l2..r2],通过一个循环,将s1i位置分割,s2j位置分割(ij同步变化)。如果s1的左子串s1[l1..i]s2的左子串s2[l2..j]是扰乱字符串关系,并且s1的右子串s1[i + 1..r1]s2的右子串s2[j + 1..r2]也是扰乱字符串关系,那么返回true
        • 状态转移(交错情况)
          • 对于s1[l1..r1]s2[l2..r2],通过一个循环,将s1i位置分割,s2从右往左在j位置分割(i增加时j减少)。如果s1的左子串s1[l1..i]s2的右子串s2[j..r2]是扰乱字符串关系,并且s1的右子串s1[i + 1..r1]s2的左子串s2[l2..j - 1]也是扰乱字符串关系,那么返回true
        • 如果上述情况都不满足,则返回false
    • isScramble2f2函数
      • isScramble2:将输入字符串转换为字符数组后,调用f2函数开始判断。
      • f2
        • 递归终止条件:当len = 1(子字符串长度为1)时,判断s1[l1]s2[l2]是否相等,如果相等则返回true,否则返回false
        • 状态转移(不交错情况)
          • 对于s1[l1..]s2[l2..]长度为len的子字符串,通过一个循环,将子字符串左边取k个字符,右边取len - k个字符。如果s1左边k个字符和s2左边k个字符是扰乱字符串关系,并且s1右边len - k个字符和s2右边len - k个字符也是扰乱字符串关系,那么返回true
        • 状态转移(交错情况)
          • 通过循环,将s1l1 + 1开始取k个字符,s2l2 + len - 1开始取k个字符,同时s1l1开始取len - k个字符,s2l2开始取len - k个字符。如果这两组子字符串是扰乱字符串关系,那么返回true
        • 如果上述情况都不满足,则返回false
    • isScramble3f3函数(记忆化搜索)
      • isScramble3:将输入字符串转换为字符数组后,创建一个三维数组dp,用于记忆化搜索,然后调用f3函数开始判断。
      • f3
        • 递归终止条件:当len = 1(子字符串长度为1)时,判断s1[l1]s2[l2]是否相等,如果相等则返回true,否则返回false
        • 记忆化处理:如果dp[l1][l2][len]不等于0,表示该状态已经计算过。如果dp[l1][l2][len] == 1则返回true,如果dp[l1][l2][len] == -1则返回false
        • 状态转移(不交错情况)
          • 通过一个循环,将子字符串按照k分割。如果s1左边k个字符和s2左边k个字符是扰乱字符串关系(通过递归调用f3),并且s1右边len - k个字符和s2右边len - k个字符也是扰乱字符串关系,那么将ans设为true并跳出循环。
        • 状态转移(交错情况)
          • 如果ansfalse,则通过循环进行交错判断。如果s1左边k个字符和s2右边k个字符是扰乱字符串关系,并且s1右边len - k个字符和s2左边len - k个字符也是扰乱字符串关系,那么将ans设为true并跳出循环。
        • 最后根据ans的值更新dp[l1][l2][len]anstrue时设为1,否则设为 - 1),并返回ans
    • isScramble4和动态规划实现
      • isScramble4:将输入字符串转换为字符数组后,创建一个三维布尔数组dp。首先初始化len = 1时的情况(即判断单个字符是否相等),然后通过三层嵌套循环,从len = 2开始逐步计算dp[l1][l2][len]
      • 状态转移(不交错情况)
        • 对于dp[l1][l2][len],通过循环将子字符串按照k分割。如果dp[l1][l2][k]dp[l1 + k][l2 + k][len - k]都为true,则将dp[l1][l2][len]设为true并跳出内层循环。
      • 状态转移(交错情况)
        • 如果dp[l1][l2][len]false,则通过循环进行交错判断。如果dp[l1][j][k]dp[i][l2][len - k]都为true,则将dp[l1][l2][len]设为true并跳出内层循环。
      • 最后返回dp[0][0][n],判断整个字符串是否为扰乱字符串关系。

代码实现

// 扰乱字符串
// 使用下面描述的算法可以扰乱字符串 s 得到字符串 t :
// 步骤1 : 如果字符串的长度为 1 ,算法停止
// 步骤2 : 如果字符串的长度 > 1 ,执行下述步骤:
//        在一个随机下标处将字符串分割成两个非空的子字符串
//        已知字符串s,则可以将其分成两个子字符串x和y且满足s=x+y
//        可以决定是要 交换两个子字符串 还是要 保持这两个子字符串的顺序不变
//        即s可能是 s = x + y 或者 s = y + x
//        在x和y这两个子字符串上继续从步骤1开始递归执行此算法
// 给你两个 长度相等 的字符串 s1 和 s2,判断 s2 是否是 s1 的扰乱字符串
// 如果是,返回true ;否则,返回false
// 测试链接 : https://leetcode.cn/problems/scramble-string/
public class Code05_ScrambleString {

    public static boolean isScramble1(String str1, String str2) {
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();
        int n = s1.length;
        return f1(s1, 0, n - 1, s2, 0, n - 1);
    }

    // s1[l1....r1]
    // s2[l2....r2]
    // 保证l1....r1与l2....r2
    // 是不是扰乱串的关系
    public static boolean f1(char[] s1, int l1, int r1, char[] s2, int l2, int r2) {
        if (l1 == r1) {
            // s1[l1..r1]
            // s2[l2..r2]
            return s1[l1] == s2[l2];
        }
        // s1[l1..i][i+1....r1]
        // s2[l2..j][j+1....r2]
        // 不交错去讨论扰乱关系
        for (int i = l1, j = l2; i < r1; i++, j++) {
            if (f1(s1, l1, i, s2, l2, j) && f1(s1, i + 1, r1, s2, j + 1, r2)) {
                return true;
            }
        }
        // 交错去讨论扰乱关系
        // s1[l1..........i][i+1...r1]
        // s2[l2...j-1][j..........r2]
        for (int i = l1, j = r2; i < r1; i++, j--) {
            if (f1(s1, l1, i, s2, j, r2) && f1(s1, i + 1, r1, s2, l2, j - 1)) {
                return true;
            }
        }
        return false;
    }

    // 依然暴力尝试,只不过四个可变参数,变成了三个
    public static boolean isScramble2(String str1, String str2) {
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();
        int n = s1.length;
        return f2(s1, s2, 0, 0, n);
    }

    public static boolean f2(char[] s1, char[] s2, int l1, int l2, int len) {
        if (len == 1) {
            return s1[l1] == s2[l2];
        }
        // s1[l1.......]  len
        // s2[l2.......]  len
        // 左 : k个   右: len - k 个
        for (int k = 1; k < len; k++) {
            if (f2(s1, s2, l1, l2, k) && f2(s1, s2, l1 + k, l2 + k, len - k)) {
                return true;
            }
        }
        // 交错!
        for (int i = l1 + 1, j = l2 + len - 1, k = 1; k < len; i++, j--, k++) {
            if (f2(s1, s2, l1, j, k) && f2(s1, s2, i, l2, len - k)) {
                return true;
            }
        }
        return false;
    }

    public static boolean isScramble3(String str1, String str2) {
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();
        int n = s1.length;
        // dp[l1][l2][len] : int 0 -> 没展开过
        // dp[l1][l2][len] : int -1 -> 展开过,返回的结果是false
        // dp[l1][l2][len] : int 1 -> 展开过,返回的结果是true
        int[][][] dp = new int[n][n][n + 1];
        return f3(s1, s2, 0, 0, n, dp);
    }

    public static boolean f3(char[] s1, char[] s2, int l1, int l2, int len, int[][][] dp) {
        if (len == 1) {
            return s1[l1] == s2[l2];
        }
        if (dp[l1][l2][len] != 0) {
            return dp[l1][l2][len] == 1;
        }
        boolean ans = false;
        for (int k = 1; k < len; k++) {
            if (f3(s1, s2, l1, l2, k, dp) && f3(s1, s2, l1 + k, l2 + k, len - k, dp)) {
                ans = true;
                break;
            }
        }
        if (!ans) {
            for (int i = l1 + 1, j = l2 + len - 1, k = 1; k < len; i++, j--, k++) {
                if (f3(s1, s2, l1, j, k, dp) && f3(s1, s2, i, l2, len - k, dp)) {
                    ans = true;
                    break;
                }
            }
        }
        dp[l1][l2][len] = ans ? 1 : -1;
        return ans;
    }

    public static boolean isScramble4(String str1, String str2) {
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();
        int n = s1.length;
        boolean[][][] dp = new boolean[n][n][n + 1];
        // 填写len=1层,所有的格子
        for (int l1 = 0; l1 < n; l1++) {
            for (int l2 = 0; l2 < n; l2++) {
                dp[l1][l2][1] = s1[l1] == s2[l2];
            }
        }
        for (int len = 2; len <= n; len++) {
            // 注意如下的边界条件 : l1 <= n - len l2 <= n - len
            for (int l1 = 0; l1 <= n - len; l1++) {
                for (int l2 = 0; l2 <= n - len; l2++) {
                    for (int k = 1; k < len; k++) {
                        if (dp[l1][l2][k] && dp[l1 + k][l2 + k][len - k]) {
                            dp[l1][l2][len] = true;
                            break;
                        }
                    }
                    if (!dp[l1][l2][len]) {
                        for (int i = l1 + 1, j = l2 + len - 1, k = 1; k < len; i++, j--, k++) {
                            if (dp[l1][j][k] && dp[i][l2][len - k]) {
                                dp[l1][l2][len] = true;
                                break;
                            }
                        }
                    }
                }
            }
        }
        return dp[0][0][n];
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值