青训营刷题算法学习心得4 | 豆包MarsCode AI 刷题

37 阅读4分钟

学习方法与心得:动态规划求方案数

/**
 * 小M有n张卡牌,
 * 每张卡牌的正反面分别写着不同的数字,
 * 正面是ai,背面是bi,
 * 小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。
 * 你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,
 * 结果需要对10^9+7 取模。
 * 例如:如果有3张卡牌,
 * 正反面数字分别为 (1,2),(2,3) 和 (3,2),
 * 你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。
 */

1. 解题背景

在解决豆包 MarsCode AI 刷题题库中的卡牌反面求和问题的时候,我最开始的想法是用回溯法求出所有的组合方式,再从所有的组合方式中找到可以被整除的方式。这个方法过于暴力,和预料的一样提交超时了。我只好求助于豆包 MarsCode AI。在Ai的帮助下,我学会了使用动态规划的方法来讨论这个问题,时间复杂度一下从o(2^N)降到了o(n^3)。从这道题中,我学到了如何建立递推方程,以及如和更新DP数组。

package JuejinAiCodes;

public class CardAdd {

    /**
     * 小M有n张卡牌,
     * 每张卡牌的正反面分别写着不同的数字,
     * 正面是ai,背面是bi,
     * 小M希望通过选择每张卡牌的一面,使得所有向上的数字之和可以被3整除。
     * 你需要告诉小M,一共有多少种不同的方案可以满足这个条件。由于可能的方案数量过大,
     * 结果需要对10^9+7 取模。
     * 例如:如果有3张卡牌,
     * 正反面数字分别为 (1,2),(2,3) 和 (3,2),
     * 你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。
     */
    private static final int MOD = 1000000007;

    public static int solution(int n, int[] a, int[] b) {
        // DP数组,dp[i][j]表示前i张卡牌的组合,余数为j(j = 0, 1, 2)时的方案数
        int[][] dp = new int[n + 1][3];

        // 初始状态:前0张卡牌,只有一种方案,且和为0(即余数为0)
        dp[0][0] = 1;

        for (int i = 1; i <= n; i++) {
            int x = a[i - 1] % 3; // 卡牌正面的余数
            int y = b[i - 1] % 3; // 卡牌反面的余数

            for (int j = 0; j < 3; j++) {
                // 选择正面或反面,更新余数的方案数
                dp[i][j] = (dp[i - 1][(j - x + 3) % 3] + dp[i - 1][(j - y + 3) % 3]) % MOD;
            }
        }

        // 返回余数为0的方案数
        return dp[n][0];
    }

    public static void main(String[] args) {
        System.out.println(solution(3, new int[]{1, 2, 3}, new int[]{2, 3, 2}) == 3);
        System.out.println(solution(4, new int[]{3, 1, 2, 4}, new int[]{1, 2, 3, 1}) == 6);
        System.out.println(solution(5, new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5}) == 32);
    }
}

2. 解题思路

  1. 整除性与余数

    • 数字能否被3整除,完全取决于它对3的余数。
    • 每张卡牌的正反面数字对3取余后,只有三种可能:012
  2. 动态规划定义

    • dp[i][j]表示前i张卡牌中,选出的数字之和余数为j的方案数(j = 0, 1, 2)。
  3. 转移关系

    • 当加入第i张卡牌时,可以选择正面或反面:

      • 如果选正面,余数变为(j - x + 3) % 3,其中x是正面的余数。
      • 如果选反面,余数变为(j - y + 3) % 3,其中y是反面的余数。
    • 状态转移方程为:

      java
      复制代码
      dp[i][j] = dp[i-1][(j-x+3)%3] + dp[i-1][(j-y+3)%3];
      
  4. 初始状态

    • dp[0][0] = 1:前0张卡牌,和为0的唯一方案。
    • dp[0][1] = dp[0][2] = 0:前0张卡牌和为1或2无方案。
  5. 最终结果

    • dp[n][0]表示前n张卡牌中,数字之和能被3整除的总方案数。

3. 复杂度分析

  1. 时间复杂度

    • O(n * 3):每张卡牌有3种余数,状态转移需要遍历所有卡牌。
    • 因此,时间复杂度为O(n)
  2. 空间复杂度

    • O(n * 3):需要一个二维数组dp存储n张卡牌的3种余数状态。
    • 若使用滚动数组优化,空间复杂度可以降为O(3)