卡牌翻面求和问题2种方法解题 | 豆包MarsCode AI刷题

76 阅读5分钟

问题描述

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

测试样例

  • 样例1:
  • 输入:n = 3, a = [1, 2, 3], b = [2, 3, 2]
  • 输出:3
  • 样例2:
  • 输入:n = 4, a = [3, 1, 2, 4], b = [1, 2, 3, 1]
  • 输出:6
  • 样例3:
  • 输入:n = 5, a = [1, 2, 3, 4, 5], b = [1, 2, 3, 4, 5]
  • 输出:32

方法思路

动态规划 (DP)

  1. 状态定义:
  • dp[i][j] 表示前 i 张卡牌中,当前和为 j 的方案数。

  • j 的取值范围是 [0, 2],因为我们需要计算和对3取模的结果。

  • 状态转移:

  • 对于第 i 张卡牌,有两种选择:使用正面 a[i-1] 或反面 b[i-1]。

  • dp[i][j] = (dp[i-1][(j - a[i-1] % 3 + 3) % 3] + dp[i-1][(j - b[i-1] % 3 + 3) % 3]) % MOD。

  • 初始化:

  • dp[0][0] = 1,表示没有卡牌时,和为0的方案数为1。

  • 最终结果:

  • dp[n][0],表示前 n 张卡牌中,和为0(即能被3整除)的方案数。

递归加记忆化搜索

  1. 递归:
  • 使用递归来遍历每张卡牌,并决定是使用正面还是反面的数字。
  • 记忆化搜索:
  • 为了避免重复计算,使用一个 Map 来存储已经计算过的状态(即当前处理到的卡牌索引和当前的数字和)。
  • 状态表示:
  • 用一个字符串 key 来表示当前的状态,key 由当前卡牌的索引和当前的数字和组成。
  • 递归终止条件:
  • 当处理完所有卡牌时(即 index == n),检查当前的数字和是否是3的倍数。如果是,则返回1,否则返回0。
  • 状态转移:
  • 对于每张卡牌,有两种选择:使用正面的数字或反面的数字。递归地计算这两种选择的结果,并将结果相加。

代码实现

动态规划 (DP)

public class Main {
    private static final int MOD = 1000000007;

    private static int solution(int n, int[] a, int[] b) {
        int[][] dp = new int[n + 1][3];
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int front = a[i - 1] % 3;
            int back = b[i - 1] % 3;
            for (int j = 0; j < 3; j++) {
                dp[i][j] = (dp[i - 1][(j - front + 3) % 3] + dp[i - 1][(j - back + 3) % 3]) % MOD;
            }
        }
        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);
    }
}

递归加记忆化搜索

import java.util.HashMap;
import java.util.Map;

public class Main {
    private static final int MOD = 1000000007;

    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);
    }

    private static int solution(int n, int[] a, int[] b) {
        Map<String, Integer> memo = new HashMap<>();
        return helper(n, 0, 0, a, b, memo);
    }

    private static int helper(int n, int index, int currentSum, int[] a, int[] b, Map<String, Integer> memo) {
        if (index == n) {
            return currentSum % 3 == 0 ? 1 : 0;
        }

        String key = index + "," + currentSum;
        if (memo.containsKey(key)) {
            return memo.get(key);
        }

        int front = a[index] % 3;
        int back = b[index] % 3;

        int result = (helper(n, index + 1, (currentSum + front) % 3, a, b, memo) +
                     helper(n, index + 1, (currentSum + back) % 3, a, b, memo)) % MOD;

        memo.put(key, result);
        return result;
    }
}

方法对比

时间复杂度

  • 动态规划 (DP):O(n * 3) = O(n)。因为我们需要遍历 n 个元素,并且每个元素有 3 种状态。
  • 递归加记忆化搜索:O(n * 3) = O(n)。helper 函数每次调用两个子 helper 函数,看起来像是一个二叉树的结构,可能会让人误以为时间复杂度是O(2 ^ n) ,但实际上,由于我们使用了记忆化搜索,许多子问题会被重复计算并存储在哈希表中,从而避免了重复计算。实际上每个子问题只会被计算一次,递归树的深度为 n,每个节点有 3 种状态。

空间复杂度

  • 动态规划 (DP):O(n * 3) = O(n)。因为我们使用了一个大小为 (n + 1) * 3 的二维数组 dp。
  • 递归加记忆化搜索:O(n * 3) = O(n)。因为我们需要存储每个子问题的结果,最坏情况下会有 n * 3 个不同的子问题。

实现难易

  • 动态规划 (DP):中等。需要正确初始化 dp 数组,并且在循环中正确更新状态。
  • 递归加记忆化搜索:较低。需要正确处理递归调用和记忆化存储。

理解难易

  • 动态规划 (DP):中等。需要理解状态转移的过程和如何通过当前状态推导出下一个状态。
  • 递归加记忆化搜索:较高。需要理解递归的思想和记忆化的机制。

综合评价

维度动态规划 (DP)递归加记忆化搜索
时间复杂度O(n)O(n)
空间复杂度O(n)O(n)
实现难易中等较低
理解难易中等较高

总结

动态规划:

  • 优点:代码结构清晰,动态规划递推逻辑直观。
  • 缺点:实现相对复杂,需要手动管理状态数组,可能会有一些额外的代码开销。

递归加记忆化搜索:

  • 优点:实现相对简单,递归的写法并不复杂。
  • 缺点:需要处理递归调用和记忆化存储,理解上可能有一定难度。