Codeforces Round 961 (Div. 2) D.Cases 详细题解

题目链接

思路

​ 题目意思就是让我们求一个集合,集合中的字母来自你划分段落的最后一个字母,要求这个集合的大小越小越好,为了方便表述,我称其为集合 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;
}
  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值