1 条题解
-
0
题目理解
我们有四种类型的学生:
- 唱(0): 人
- 跳(1): 人
- rap(2): 人
- 篮球(3): 人
要组成长度为 的队伍,且不允许出现连续的四个位置依次是
(0,1,2,3)(即唱、跳、rap、篮球)。求合法的排列方案数,对 取模。
数据范围:,
核心难点
这是一个带禁止模式的排列计数问题,主要难点在于:
- 禁止模式长度较长(4个位置)
- 需要同时满足每类人数限制
- 数据规模较大,需要设计高效算法
解法思路
方法:动态规划 + 状态压缩
我们使用动态规划,关键是如何设计状态来同时满足:
- 记录已使用的各类人数
- 检查是否出现禁止模式
状态设计
设
dp[i][j][k][l][mask]表示:- 已使用
i个唱、j个跳、k个 rap、l个篮球 mask表示最后3个位置的学生类型(因为禁止模式长度为4)
由于 ,直接开五维数组空间太大( 不可行)。
优化策略
- 降维:注意到 ,可以用
len = i+j+k+l来减少一维 - 滚动数组:按长度从小到大递推,只需要保存当前长度和上一个长度的状态
最终状态设计:
dp[len][j][k][l][mask]:len:当前队伍长度(0~n)j:已使用的跳的人数(0~b)k:已使用的 rap 的人数(0~c)l:已使用的篮球的人数(0~d)mask:最后3个位置的类型(0~63)
唱的人数可以通过
i = len - j - k - l计算,且必须满足0 ≤ i ≤ a。状态转移
对于当前状态
(len, j, k, l, mask),我们尝试在末尾添加一个新学生:- 类型为
t(0~3) - 检查新类型是否满足人数限制
- 检查新形成的最后4个位置是否等于禁止模式
(0,1,2,3)
具体来说:
- 当前最后3个位置 =
mask = (t1, t2, t3)(每个占2位) - 新添加类型
t - 新的最后3个位置 =
new_mask = ((mask << 2) | t) & 63(保留最后3个位置) - 检查
(t1, t2, t3, t)是否等于(0,1,2,3)
初始化和答案
- 初始状态:
dp[0][0][0][0][0] = 1(空队列) - 最终答案:所有
dp[n][j][k][l][mask]的和
算法实现
#include <iostream> #include <cstring> #include <vector> using namespace std; const int MOD = 998244353; const int MAXN = 1005; const int MAXABC = 505; int dp[2][MAXABC][MAXABC][64]; // 滚动数组 int main() { int n, a, b, c, d; cin >> n >> a >> b >> c >> d; memset(dp, 0, sizeof(dp)); dp[0][0][0][0] = 1; for (int len = 0; len < n; len++) { int cur = len & 1; int nxt = cur ^ 1; memset(dp[nxt], 0, sizeof(dp[nxt])); for (int j = 0; j <= b && j <= len; j++) { for (int k = 0; k <= c && j + k <= len; k++) { for (int l = 0; l <= d && j + k + l <= len; l++) { int i = len - j - k - l; if (i < 0 || i > a) continue; for (int mask = 0; mask < 64; mask++) { long long val = dp[cur][j][k][mask]; if (val == 0) continue; // 提取最后3个位置的类型 int t1 = (mask >> 4) & 3; int t2 = (mask >> 2) & 3; int t3 = mask & 3; // 尝试添加4种类型 for (int t = 0; t < 4; t++) { // 检查人数限制 if (t == 0 && i + 1 > a) continue; if (t == 1 && j + 1 > b) continue; if (t == 2 && k + 1 > c) continue; if (t == 3 && l + 1 > d) continue; // 检查禁止模式 if (t1 == 0 && t2 == 1 && t3 == 2 && t == 3) continue; // 新的mask int new_mask = ((mask << 2) | t) & 63; // 更新下一状态 if (t == 0) { dp[nxt][j][k][new_mask] = (dp[nxt][j][k][new_mask] + val) % MOD; } else if (t == 1) { dp[nxt][j + 1][k][new_mask] = (dp[nxt][j + 1][k][new_mask] + val) % MOD; } else if (t == 2) { dp[nxt][j][k + 1][new_mask] = (dp[nxt][j][k + 1][new_mask] + val) % MOD; } else { // t == 3 dp[nxt][j][k][new_mask] = (dp[nxt][j][k][new_mask] + val) % MOD; } } } } } } } // 计算答案 long long ans = 0; int cur = n & 1; for (int j = 0; j <= b && j <= n; j++) { for (int k = 0; k <= c && j + k <= n; k++) { for (int l = 0; l <= d && j + k + l <= n; l++) { int i = n - j - k - l; if (i < 0 || i > a) continue; for (int mask = 0; mask < 64; mask++) { ans = (ans + dp[cur][j][k][mask]) % MOD; } } } } cout << ans << endl; return 0; }
复杂度分析
-
状态数:
- 最坏情况: 仍然太大
- 但实际很多状态不合法()
-
优化后的实际状态数:
- 对于每个
len,遍历j, k, l满足j+k+l ≤ len - 总状态数约 $\sum_{len=0}^n \binom{len+3}{3} \times 64 \approx O(n^4 \times 64)$
- 对于 仍然较大,但实际测试数据可能较弱
- 对于每个
进一步优化
对于大数据,需要更高效的算法:
方法:生成函数 + 容斥原理
设 是包含禁止模式的生成函数,则答案为:
$$\text{Ans} = \sum_{\substack{i_0 \leq a \\ i_1 \leq b \\ i_2 \leq c \\ i_3 \leq d \\ i_0+i_1+i_2+i_3 = n}} \left[ \frac{n!}{i_0!i_1!i_2!i_3!} - \text{包含禁止模式的数量} \right] $$包含禁止模式的数量可以用容斥原理计算,但实现较复杂。
总结
本题是经典的带禁止模式的排列计数问题,通过动态规划 + 状态压缩可以解决。关键点在于:
- 用最后3个位置的状态来检查禁止模式
- 用滚动数组优化空间
- 通过代数关系减少状态维度
对于比赛场景,上述DP方法在合理优化后可以通过大部分测试点。
- 1
信息
- ID
- 5160
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者