思路
题目意思就是让我们求一个集合,集合中的字母来自你划分段落的最后一个字母,要求这个集合的大小越小越好,为了方便表述,我称其为集合 T T T。
观察题目可以发现,任意一段长为 k k k 的子串,必须至少有一个组成这个子串的字母存在于集合 T T T。我们可以通过反证法来验证:如果存在一段长度为 k k k 的子串,其字母都不在集合 T T T 内,则说明这 k k k 段根本没被切割,就算紧接着的字母被确认为最后一个字母,也至少会产生一个长度为 k + 1 k + 1 k+1 的单词。
因此,我们可以把题目转述为,寻找一个集合 T T T , T T T ∩ \cap ∩ 每一段长度为 k k k 的子串 ≠ \neq = ∅ \emptyset ∅(也就是至少有一个字母存在于集合 T T T 中),求最小大小的 T T T。
用这个思路去正着求 T T T,会发现不优化的枚举做法时间复杂度很大(但是确实有优化方法能把 T T T 正着枚举出来,这里不加以讨论)。我们不妨倒过来想,我们去寻找 T T T 的对立面,也就是 T T T 的补集 C U T C_UT CUT 。只要把不合法的 C U T C_UT CUT 全找出来,剩下的就全为合法的 T T T 了。
我们把任意一段长为 k k k 的子串中的出现过的字母组成一个集合 S S S (如子串 A B B ABB ABB 其集合 S S S 为 A , B {A,B} A,B )。再此观察上述分析,我们可以发现集合 S S S 的补集 C U S C_US CUS 一定是不合法的,其补集的子集也是不合法的(因为其补集以及子集一定不包含 S S S 内的字母,显然是不合法的)。我们遍历所有的 C U S C_US CUS 将其补集以及补集的子集一一标记,剩下的就都是合法的。我们再找剩下合法的里面出现字母最少的,就是答案了。
代码实现中我通过状压(也就是二进制)来表示集合(如十进制的 7 7 7 = 二进制的 0111 0111 0111 ,也就是表示集合 A , B , C {A,B,C} A,B,C ;十进制的 5 5 5 = 二进制 0101 0101 0101,也就是集合 A , C {A,C} A,C),如不懂可以看代码中的注释,里面有详细说明。
void solve(){
int n,c,k;
cin >> n >> c >> k;
string str;
cin >> str;
vector<bool>s(1ll << c); //用状压01的方式记录每k段字符串出现过的字母种类 如A为01 B为10 AB则为11
vector<int>pos(c,-1); //记录所有字母的最近出现位置 用于快速求数组s
for(int i = 0;i < k - 1;++i){ //预处理 pos
pos[str[i] - 'A'] = i;
}
for(int i = k - 1;i < n;++i){
pos[str[i] - 'A'] = i;
int mask = 0;
for(int j = 0;j < c;++j){
if(pos[j] >= i - k + 1) //如果这个字母出现的位置在[i - k + 1,i]内,则说明在这段中出现了,把他记录下来
mask |= (1 << j);
}
s[mask] = 1; //这段出现的字母全记下来后,置为true 说明存在这样着一段有这几种字母子串
}
s[1 << (str[n - 1] - 'A')] = 1; //因为末尾一定是要成为case的,所以单独拉出来置为true,也就是说末尾一定是答案字母中的其中一个字母
vector<bool>bad(1ll << c); //存不合法的情况
for(int i = (1 << c) - 1;i >= 0;--i){
bad[ ((1ll << c) - 1) ^ i ] = s[i]; //((1ll << c) - 1) ^ i就是i的补集,如果这个字母集合为s[i]的补集以及补集的子集,则说明这个集合一定没出现过s[i]段中的任何字母,其一定是非法的
}
for(int i = (1 << c) - 1;i >= 0;--i){
if(bad[i]){ //这里是把非法集合的所有子集都给找出来置为1,因为子集肯定比他的父亲小,为了把所有子集找出来,一定要从大到小遍历
for(int j = 0;j < c;++j){
if(i & (1ll << j)){
bad[i - (1ll << j)] = 1;
}
}
}
}
int ans = c;
for(int i = 1;i < (1 << c);++i){
if(!bad[i]) { //把不合法的找出来后,剩下的一定是合法的,我们找其出现字母种类最小的即可
ans = min(ans,(int)__builtin_popcount(i)); //__builtin_popcount()为内置函数,用于数一个数以二进制表示时出现1的个数
}
}
cout << ans << endl;
}