题解:P11404 [RMI 2020] 蝶变 / Brperm
题目链接:P11404 [RMI 2020] 蝶变 / Brperm
:::info[题外话]{open}
这道题放到了我们的 NOIP 模拟赛中,也是给我看懵了,发现不了一点性质,只会打个暴力。结束后题解说这是什么“蝴蝶变换”。但是“蝴蝶变换”这个点貌似主要用到诸如快速傅里叶变换的多项式中?我也不知道,我不会多项式。所以应用到字符串的这种情况还是挺少见的,挺好一题。但是我实在看不懂老师给的题解,于是在同学 bhs 的讲解下有了这篇题解。
:::
:::info[推导过程]
我先来解释一下题面传达的意思。
假设我们现在有一个长度为
蝶变的定义是:
其中
举个栗子。例如
| 0 | 000 | 000 |
| 1 | 001 | 100 |
| 2 | 010 | 010 |
| 3 | 011 | 110 |
| 4 | 100 | 001 |
| 5 | 101 | 101 |
| 6 | 110 | 011 |
| 7 | 111 | 111 |
此时我们进行蝶变之后的数组为:
我们现在设原数组的下标为:
那么它的蝶变下标即为:
到这里,运用我们惊人的注意力,可以发现,原下标的最低位
- 如果
j 是偶数,那么b_0=0 ,翻转后的下标最高位为0 ; - 如果
j 是奇数,那么b_0=1 ,翻转后的下标最高位为1 。
所以我们现在发现了一个性质:
那么这意味着,原数组的偶数位置,蝶变之后会落在前半部分;原数组的奇数位置,蝶变之后会落在后半部分。因此我们可以考虑将原数组按照奇偶拆分。
我们再把原数组按照奇偶拆分。具体地,我们定义:
很明显,它们的长度都是
现在我们来证明:
证明如下:
我们上面说到,原数组偶数位置的元素,蝶变后一定落在前半部分。设偶数位置对应的原下标为
翻转后:
去掉最前面的
而这恰好就是
因此蝶变结果的前半部分,恰好是
同理,原数组奇数位置的元素,蝶变后一定落在后半部分。我们继续设原下标
同样进行翻转后变为:
它的最高位是
我们可以看出来,蝶变结果的后半部分,恰好是
我们将上面两部分合起来:
证毕。
我们定义普通字符串哈希:
其中
我们假设两个字符串
这也是字符串哈希的基础公式,并且很显然,在此不再解释了。
因此我们可以类比的得到:
那么现在蝶变哈希递推式就顺利地推出来了。
最后只需要判断原数组的哈希值与进行蝴蝶变换后的数组哈希值是否相等即可。
到这里,我们可以发现一个问题:
我们考虑定义:
表示从位置
它对应的原始序列是:
按照上面我们推导的,把它拆成奇偶位置:
- 偶数位置:
[s_i, s_{i+2^{d+1}}, s_{i+2 \cdot 2^{d+1}}, \ldots] - 奇数位置:
[s_{i+2^d}, s_{i+2^d+2^{d+1}}, \ldots]
然后我们就会发现:
- 偶数位置对应
f_{k-1, d+1, i} ; - 奇数位置对应
f_{k-1, d+1, i+2^d} 。
根据我上面写出的字符串拼接哈希公式得到:
当
这样就可以解决上面的问题啦。
但我们发现这样开数组空间会直接爆掉。可以发现,计算
那么,对于每个固定的
也就是沿着
进一步地,由于计算当前层时只需要上一层的结果,因此我们不必保存整条对角线上的所有状态,只需要使用两个数组分别维护上一层和当前层的结果,每次计算完成后交换两个数组即可。
这样我们就可以解决空间问题啦。
最后我们只需要预处理一下普通字符串哈希:
那么长度为
而我们预处理得到的蝶变哈希是
| 这里我们一般使用双模哈希来降低哈希冲突的概率。 |
|---|
:::info[复杂度分析]
我们令
- 前缀哈希:
O(N) - 蝶变哈希预处理:
O(N \log^2 N) - 每次询问:
O(1) - 保存每种长度的最终蝶变组合:
O(N \log N) 空间。 - 两个滚动数组:
O(N) 空间。
因此,我们最终的时间复杂度为
:::
以上就是本道题的全部推导过程啦,下面给出参考代码。
:::success[参考代码]
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 500010;
const int K = 20;
const int MOD1 = 1e9 + 7;
const int MOD2 = 998244353;
const int B1 = 911382323;
const int B2 = 1331;
int n;
int pw1[K], pw2[K];
int pre1[N], pre2[N];
struct Hash
{
int x, y;
} dp[N], ndp[N], ans[K][N];
void init(int N, const char s[])
{
n = N;
pw1[0] = B1;
pw2[0] = B2;
for (int i = 1; i < K; i++)
{
pw1[i] = 1ll * pw1[i - 1] * pw1[i - 1] % MOD1;
pw2[i] = 1ll * pw2[i - 1] * pw2[i - 1] % MOD2;
}
pre1[0] = pre2[0] = 0;
for (int i = 0; i < n; i++)
{
int x = s[i] - 'a' + 1;
pre1[i + 1] = (1ll * pre1[i] * B1 + x) % MOD1;
pre2[i + 1] = (1ll * pre2[i] * B2 + x) % MOD2;
}
int mx = 0;
while ((1 << (mx + 1)) <= n) mx++;
for (int k = 1; k <= mx; k++)
{
Hash *cur = dp, *nxt = ndp;
for (int i = 0; i < n; i++)
{
dp[i].x = s[i] - 'a' + 1;
dp[i].y = s[i] - 'a' + 1;
}
for (int r = 1; r <= k; r++)
{
int step = 1 << (k - r);
int ed = n - (1 << k) + step - 1;
for (int i = 0; i <= ed; i++)
{
nxt[i].x = (1ll * cur[i].x * pw1[r - 1] + cur[i + step].x) % MOD1;
nxt[i].y = (1ll * cur[i].y * pw2[r - 1] + cur[i + step].y) % MOD2;
}
swap(cur, nxt);
}
for (int i = 0; i + (1 << k) <= n; i++) ans[k][i] = cur[i];
}
}
int query(int i, int k)
{
int len = 1 << k;
if (i + len > n) return 0;
if (k == 0) return 1;
int x = (pre1[i + len] - 1ll * pre1[i] * pw1[k] % MOD1 + MOD1) % MOD1;
int y = (pre2[i + len] - 1ll * pre2[i] * pw2[k] % MOD2 + MOD2) % MOD2;
return x == ans[k][i].x && y == ans[k][i].y;
}
:::
以上为本篇的全部内容,感谢您的耐心阅读,希望本篇文章对您有所帮助。