卡牌翻面求和问题 题解 | 豆包MarsCode AI刷题

139 阅读2分钟

问题描述

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

例如:
输入:n = 3 ,a = [1, 2, 3] ,b = [2, 3, 2]
输出:3

思路历程

首先我们可以发现,所有求和都是在模三意义下进行的,所以所有卡面数值实际上只有:1201、2、0 (因为所有数和的模等于所有数模的和),那么实际上是不是只有六种卡牌:0-0,1-1,2-2,0-1,0-2,1-2,很容易分别进行计数即可进行排列组合?

那么问题来了,如何排列组合?

我们很容易想到一个 O(n3)O(n^3) 的做法:三层嵌套循环枚举两面不同的卡牌的正反张数,进行先组合乘积的计数(因为容易证明牌的顺序不影响结果,先组合计数第 k 种卡牌选 i 张朝上的方案数,三层组合方案数在取模意义下相乘即可)剪枝就是模 3 不为 0 的和不用进行计数。伪代码:

for(i: 0-1牌的数量a)    
    for(j: 0-2牌的数量b)    
           for(k: 1-2牌的数量c){    
                和%3 == 0 ? ans += $C_a^i * C_b^j * C_c^k$ : continue;    
}

优化?

发现,可以进行凑数,每一种牌能达成的模意义下和只有 0,1,2 三种情况,对于某一情况下,新加的牌只需要凑模意义下余数相加为 3 即可。

你想到了什么?

这不就是动态规划吗?模意义下余数为结果,新加的手牌就是状态转移过程。

那么前面的思路就可以直接被优化掉了,直接在序列上 dp 即可。
以 f [i][j] 表示前i个牌的余数为j的方案数量。 状态转移就是选择正面或背面:

for (int i = 1; i <= n; ++i) {
    // 当前卡牌的正面和背面数字
    int front = a[i - 1];
    int back = b[i - 1];

    // 更新dp数组
    for (int j = 0; j < 3; ++j) {
        // 选择正面
        dp[i][(j + front) % 3] = (dp[i][(j + front) % 3] + dp[i - 1][j]) % MOD;
        // 选择背面
        dp[i][(j + back) % 3] = (dp[i][(j + back) % 3] + dp[i - 1][j]) % MOD;
    }
}

新加入的牌得到的新模意义下余数所拥有的方案数为旧模意义下余数结果方案数,注意不能只增加构造为模意义下0的结果,而是要把所有模意义下余数方案全部转移,确保状态转移正确性。边界条件:f[0][0]=1,答案就为f[n][0]

505a6d6ed4a21187dab567fa272bb6e1_720.jpg

那是不是很显然?