AGC026_C 题解

题意简述

给出一个长度为 $2n(1\le n\le18)$ 的全部由小写字母构成的字符串 $S$。对其红蓝染色,求使得红色字母组成的字符串翻转后与蓝色字母组成的字符串完全相同的染色方案数。

题目分析

折半搜索的经典题了吧。

很明显直接枚举染色情况时间复杂度会炸,所以我们考虑更优的做法。

我们先来看看什么是折半搜索。

双向同时搜索(折半搜索,又称 Meet in the middle 算法)的基本思路是从状态图上的起点和终点同时开始进行广搜或深搜。如果发现搜索的两端相遇了,那么可以认为是获得了可行解。

暴力搜索的复杂度往往是指数级的,而改用折半搜索后复杂度的指数可以减半,即让复杂度从 $O(a^b)$ 降到 $O(a^{b/2})$。

——OI Wiki

本题就可以采用折半搜索。具体来说,可以将 $S$ 分为前后长度都为 $n$ 的两段 $S_0$ 和 $S_1$。两段的染色情况可以分别考虑。设前半段的红串为 $red_0$,蓝串为 $blue_0$;后半段红串为 $red_1$,蓝串为 $blue_1$。注意到:

$$\left\{ \begin{aligned} &|red_0|+|red_1|=|blue_0|+|blue_1|=n\\ &|red_0|+|blue_0|=|red_1|+|blue_1|=n\\ \end{aligned} \right.~$$

得到$$\left\{ \begin{aligned} &|red_1|=|blue_0|\\ &|red_0|=|blue_1|\\ \end{aligned} \right.~$$

又有 $red_0+red_1=(blue_0+blue_1)'$,因此有$$\left\{ \begin{aligned} &red_1=blue_0'\\ &red_0=blue_1'\\ \end{aligned} \right.~$$

也就是说,前半段的红串是后半段的蓝串的反串,前半段的蓝串是后半段红串的反串(本部分证明参考了 Ezio__Auditore 大佬的题解,在此膜拜大佬)

那么我们就可以先在前半段暴力枚举每个字母的染色情况,把红串和蓝串一起哈希一下存到哈希表里(可以通过 map 实现);然后在后半段倒着暴力枚举字母的染色情况,把蓝串和红串一起哈希一下,把哈希表里的方案数加到答案里。

最后就是字符串哈希的实现。本题字符串较短,冲突概率相对较小,因此只需要把字符串看作一个 $p$ 进制数($p$ 为质数)对一个模数取模即可。由于取模运算太慢,我们直接用 unsigned long long 存储,这样自然溢出就可以实现对 $2^{64}$ 的取模了,目前最优解。

代码实现

#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long
map<pair<ull,ull>,int>mp;//哈希表 
int n;
long long ans;
string s[2];
void dfs(int x,ull hs1/*红串哈希值*/,ull hs2/*蓝串哈希值*/,bool fg) 
{
    if(x>n) 
    {
        if(fg)
            ans+=mp[{hs1,hs2}];//统计方案数
        else
            mp[{hs2,hs1}]++; //前半段的红串蓝串丢到哈希表里 
        return;
    } 
    dfs(x+1,hs1*131ull/*一个质数 p*/+s[fg][x-1],hs2,fg);//第 x 个字符染成红的 
    dfs(x+1,hs1,hs2*131ull+s[fg][x-1],fg);//第 x 个字符染成蓝的
}
int main() 
{
    ios::sync_with_stdio(0);
    cin.tie();
    cout.tie();
    cin>>n>>s[0];
    s[1]=s[0].substr(n,n);
    reverse(s[1].begin(),s[1].end());//方便操作,直接把后半段翻转 
    s[0]=s[0].substr(0,n);//取前半段 
    dfs(1,0,0,0);
    dfs(1,0,0,1);
    cout<<ans;
    return 0;
}
posted @ 2023-07-10 22:16  Hadtsti  阅读(1)  评论(0编辑  收藏  举报  来源