Cases§
这道题的思路十分巧妙。
考虑到
c很小,可行解的枚举范围并不大,因此我们完全可以通过枚举所有可行解来找到最小的答案。那么,如何判断一个方案是否为可行解呢?
首先,我们需要知道,对于该字符串中的任意一个连续的、长度为
k的子串,必须至少包含一个cases,且结尾必定是cases。如果上述所有情况中,至少有一个字符属于我们的方案,那么这个方案就是可行解。
为了快速判断某个区间内是否存在某个字母,可以通过构造
c个前缀和数组来解决这个问题。对于任意的长度为
k的连续区间,可以用整型的不同二进制位来表示这个区间包含哪些字母,并用数组来存储这种情况是否存在,特别是要单独处理结尾部分。我们可以通过判断一个方案是否非法来确认其合法性。
如何判断一个方案是否非法呢?
自然是找出是否存在一种情况,其包含的字母都不在方案内。
我们可以先通过寻找一种特殊的情况来对一部分方案进行断言。这种情况就是该方案的补集。
如果这种补集存在,意味着该方案一定非法,那么该方案的子集也必定非法。
基于这个思路,我们可以使用动态规划来转移这种状态。
显然,这种方法不会错判,但可能会令人疑惑:会不会漏判呢?
接下来我们探讨一下是否会漏判。
对于某个被断言的非法方案,它肯定包含所有的不满足其补集情况的非法方案。
由于我们枚举了所有可能的方案,那么任意非法方案一定不满足至少
1个情况。因为被断言方案集合包含所有恰好不满足某种情况的非法方案。
因此,所有非法方案一定是某个被断言方案的子集。
#include <bits/stdc++.h> void solve() { int n, c, k; std::cin >> n >> c >> k; std::string s; std::cin >> s; std::vector<std::vector<int>> cnt(n + 1, std::vector<int>(c, 0)); for (int i = 0; i < n; i++) { for (int j = 0; j < c; j++) { cnt[i + 1][j] = cnt[i][j] + ((s[i] - 'A') == j); } } std::vector<bool> exist(1 << c, false); for (int i = 1; i <= n - k + 1; i++) { int mask = 0; for (int j = 0; j < c; j++) { if (cnt[i + k - 1][j] > cnt[i - 1][j]) { mask |= (1 << j); } } exist[mask] = true; } exist[1 << (s.back() - 'A')] = true; std::vector<int> bad(1 << c); for (int i = 0; i < (1 << c); i++) { bad[i] = exist[((1 << c) - 1) ^ i]; } for (int i = (1 << c) - 1; i >= 0; i--) { for (int j = 0; j < c; j++) { bad[i] |= bad[i | (1 << j)]; } } int res = c; for (int i = 0; i < (1 << c); i++) { if (!bad[i]) res = std::min(res, __builtin_popcount(i)); } std::cout << res << '\n'; } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int t; std::cin >> t; while (t--) { solve(); } return 0; }