题解 AT3859 【[AGC020E] Encoding Subsets】
installb
2020-05-11 20:32:39
## AGC020E
[题](https://atcoder.jp/contests/agc020/tasks/agc020_e)
我是先想的如果 `1` 不能变 `0` 应该怎么做,明显是个区间 DP。
$f_{i,j}$ 代表 $[i,j]$ 方案数,$g_{i,j}$ 代表缩成一个括号(以及只有一个字符的情况,`0` 或 `1`)$f$ 转移的时候枚举最后一个括号位置。$g$ 转移就枚举区间长度 $len$ 的约数($len$ 除外)把这些缩成一个括号。
这题 $f$ 还是一样的转移方式,不过算 $g$ 的时候不同了,需要转移自的 $f$ 需要是所有截取部分 AND 起来的值,可能会产生新的字符串,所以 DP 状态里就记字符串而不是区间了,记忆化搜索即可。
这个东西看起来复杂度很大但其实是对的,首先 $f$ 刷出来的 $f$ 不用考虑,因为 $f$ 刷出来的 $f$ 刷出来的 $f$ 一定都是原来 $f$ 的字串,还只有 $n$ 个。而 $f$ 也会产生 $g$,并且 $f$ 的每一个子串都会产生一个 $g$ 的计算,而 $g$ 又会产生新的 $f$,这些产生出来的 $f$ 是必须全部重新算的。
但是每次产生出来的 $f$ 长度至多是原来 $g$ 的一半,三次 $g$ 产生 $f$ 操作以后,长度就必定 $\leq 13$ 了。长度为 $n$ 的 01串只有 $2^n$ 个,当 $n\leq 13$ 的时候这其实并不大。
所以我们只需要考虑 $g$ 变 $f$ 一次和两次的情况就行了,而且分成 $f$ 的次数两次乘起来还得小于 $8$,不然就长度太小了。其实最后生成的串可以看作是最早的原串取了几个字串拼起来的,这里用两次操作长度都减半举例好了。
![YJbb0s.png](https://s1.ax1x.com/2020/05/11/YJbb0s.png)
只用 $i,j,k$ 就可以表示一个状态,那么状态数不会超过 $n^3$。
两次分别为分两段和分三段其实是一样的,状态数也只有 $n^3$ 级别种。
实际值是远小于理论值的。
一共有 $n^3+2^{\frac{n}{8}}$ 个状态,转移用时 $n^2$,时间复杂度 $O(n^5+n^2 2^{\frac{n}{8}})$
代码写得很丑()
```cpp
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <map>
#include <string>
using namespace std;
typedef long long LL;
const LL N = 998244353;
map <string,LL> mp[2];
string u;
LL f(LL id,string s){
if(mp[id].find(s) != mp[id].end()) return mp[id][s];
LL ret = 0,len;
len = s.length();
if(id){
for(LL i = 0;i < len;i ++) ret = (ret + f(1,s.substr(0,i)) * f(0,s.substr(i,len))) % N;
mp[id][s] = ret; return ret;
}
else{
for(LL i = 1;i < len;i ++){
if(len % i) continue;
string t = "";
for(LL j = 0;j < i;j ++) t += '1';
for(LL j = 0;j < len;j += i){
for(LL k = 0;k < i;k ++){
if(s[j + k] == '0') t[k] = '0';
}
}
ret += f(1,t); ret %= N;
}
mp[id][s] = ret; return ret;
}
}
int main(){
mp[0][""] = mp[1][""] = 1;
mp[0]["0"] = 1; mp[0]["1"] = 2;
mp[1]["0"] = 1; mp[1]["1"] = 2;
cin >> u;
cout << f(1,u) << '\n';
return 0;
}
```