卡牌翻面求和问题(递归)

108 阅读4分钟

问题描述

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

例如:如果有3张卡牌,正反面数字分别为 (1,2)(2,3) 和 (3,2),你需要找到所有满足这3张卡牌正面或背面朝上的数字之和可以被3整除的组合数。


测试样例

样例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


public class Main {
    private static int ans;//答案
    private static int arrLen;//所有卡牌的数量

    /**
     * 
     * @param lastSum   前面已经算进去的卡牌数量的和
     * @param sumCount  已经算进去的卡牌的数量
     * @param a         剔除已经使用的正面卡牌
     * @param b         剔除已经使用的反面卡牌
     */
    public static void recallFunc(int lastSum,int sumCount,int[] a,int[] b)
    {
        int length=a.length;
        //front是拿卡牌正面相加,back是拿反面相加
        int front,back;
        int[] newa,newb;
        //每次进来就代表数组的长度大于0,接下来的操作是去一个数加进去,所以卡牌数量加1
        sumCount++;
        for(int i=0;i<length;i++){

            //取卡牌的正反面,如果这个卡牌的正反面的数一样大怎么办,算一个还是两个?这里算两个
            front=lastSum+a[i];
            back=lastSum+b[i];

            //length==1就代表已经将最后一个数加进去了,表示所有卡牌的情况
            if(sumCount==arrLen){
                if(front%3==0){
                    ans++;
                }
                if(back%3==0){
                    ans++;
                }
                if(ans>=1000000007){
                    ans-=1000000007;
                }
                return;
            }

            if(length-1-i==0){
                return;
            }
            //创建新的数组
            newa=new int[length-1-i];
            newb=new int[length-1-i];
            int index=i+1;
  
//TODO:创建的新数组的元素必须是之前没有用过的元素,否则会出现组合的重复

            for(int j=0;index<length;index++){
                newa[j]=a[index];
                newb[j]=b[index];
                j++;
            }
            

            recallFunc(front,sumCount,newa,newb);
            recallFunc(back,sumCount,newa,newb);
        }
    }
    public static int solution(int n, int[] a, int[] b) {
        // write code here
        ans=0;
        arrLen=a.length;
        recallFunc(0,0,a,b);
        //System.out.println("总数是:"+ans);
        return ans; // Placeholder return
    }



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

代码分析: 这段Java代码解决了一个关于卡牌选择的问题,目的是找出所有可能的组合,使得所有向上的数字之和可以被3整除。以下是代码的详细分析:

  1. 类和方法定义
    • Main 类包含了主方法和辅助递归方法 recallFunc
    • ans 是一个静态变量,用于存储满足条件的组合数。
    • arrLen 是一个静态变量,表示卡牌的总数。
  2. 递归方法 recallFunc
    • 参数 lastSum 表示前面已经计算过的卡牌数字之和。
    • 参数 sumCount 表示已经计算过的卡牌数量。
    • 参数 ab 分别表示卡牌正反面的数字数组,递归过程中会逐步缩小这两个数组。
  3. 递归逻辑
    • 在每次递归调用中,sumCount 加1,表示处理下一张卡牌。
    • 对于每张卡牌,计算选择正面和反面的和(frontback)。
    • 如果已经处理完所有卡牌(sumCount == arrLen),检查 frontback 是否可以被3整除,并更新 ans
    • 如果还有卡牌未处理,递归调用 recallFunc,分别传入 frontback 作为新的 lastSum
    • 在递归调用之前,创建新的数组 newanewb,这些数组不包含当前处理的卡牌,以避免重复计算。

代码总结:

  • 递归与回溯:递归用于探索所有可能的组合,而回溯体现在每次递归调用后创建新的数组 newanewb,这些数组排除了当前处理的卡牌,确保了每张卡牌只被选择一次。

  • 边界条件:递归终止条件是当 sumCount 等于 arrLen,此时检查当前的和是否满足条件。

  • 模运算:为了避免结果过大,代码中使用了模运算 ans %= 1000000007 来保证结果在合理范围内。 存在的问题:

  • 性能问题:递归方法中的数组复制操作是性能瓶颈,因为它涉及到大量的内存操作。 改进建议: