H. Bro Thinks He's Him
H. Bro Thinks He's Him
Skibidus thinks he's Him! He proved it by solving this difficult task. Can you also prove yourself?
Given a binary string$^{\text{∗}}$ $t$, $f(t)$ is defined as the minimum number of contiguous substrings, each consisting of identical characters, into which $t$ can be partitioned. For example, $f(\texttt{00110001}) = 4$ because $t$ can be partitioned as $\texttt{[00][11][000][1]}$ where each bracketed segment consists of identical characters.
Skibidus gives you a binary string $s$ and $q$ queries. In each query, a single character of the string is flipped (i.e. $\texttt{0}$ changes to $\texttt{1}$ and $\texttt{1}$ changes to $\texttt{0}$); changes are saved after the query is processed. After each query, output the sum over all $f(b)$ where $b$ is a non-empty subsequence$^{\text{†}}$ of $s$, modulo $998\,244\,353$.
$^{\text{∗}}$A binary string consists of only characters $\texttt{0}$ and $\texttt{1}$.
$^{\text{†}}$A subsequence of a string is a string which can be obtained by removing several (possibly zero) characters from the original string.
Input
The first line contains an integer $t$ ($1 \leq t \leq 10^4$) — the number of test cases.
The first line of each test case contains a binary string $s$ ($1 \leq |s| \leq 2 \cdot 10^5$).
The following line of each test case contains an integer $q$ ($1 \leq q \leq 2 \cdot 10^5$) — the number of queries.
The following line contains $q$ integers $v_1, v_2, \ldots, v_q$ ($1 \leq v_i \leq |s|$), denoting $s_{v_i}$ is flipped for the $i$'th query.
It is guaranteed that the sum of $|s|$ and the sum of $q$ over all test cases does not exceed $2 \cdot 10^5$.
Output
For each test case, output $q$ integers on a single line — the answer after each query modulo $998\,244\,353$.
Example
Input
3
101
2
1 3
10110
3
1 2 3
101110101
5
7 2 4 4 1
Output
10 7
61 59 67
1495 1169 1417 1169 1396
Note
In the first test case, $s$ becomes $\texttt{001}$ after the first query. Let's calculate the answer for each subsequence:
- $f(s_1) = f(\texttt{0}) = 1$
- $f(s_2) = f(\texttt{0}) = 1$
- $f(s_3) = f(\texttt{1}) = 1$
- $f(s_1 s_2) = f(\texttt{00}) = 1$
- $f(s_1 s_3) = f(\texttt{01}) = 2$
- $f(s_2 s_3) = f(\texttt{01}) = 2$
- $f(s_1 s_2 s_3) = f(\texttt{001}) = 2$
The sum of these values is $10$, modulo $998\,244\,353$.
解题思路
给出一种比较简单粗暴的 DDP 做法。
对于统计类题目尤其是涉及到取模时,可以考虑贡献法加组合数学,或者动态规划。先考虑没有修改操作时字符串的答案(所有子序列 $01$ 段数量的总和),试了一下发现动态规划可做(之后还会给出贡献法的做法)。
定义 $f(i,0/1)$ 表示前 $i$ 个字符中以 $0/1$ 结尾的子序列的 $01$ 段数量总和;$g(i,0/1)$ 表示前 $i$ 个字符中以 $0/1$ 结尾的子序列的总数。根据第 $i$ 个字符 $s_i \in \{0,1\}$ 接到结尾是 $0/1$ 的子序列或空串进行状态转移。其中当 $s_i = 0$ 时:
\begin{cases}
f(i,0) = 2 \cdot f(i-1,0) + f(i-1,1) + g(i-1,1) + 1 \\
g(i,0) = 2 \cdot g(i-1,0) + g(i-1,1) + 1 \\
f(i,1) = f(i-1,1) \\
g(i,1) = g(i-1,1)
\end{cases}
当 $s_i = 1$ 时:
\begin{cases}
f(i,0) = f(i-1,0) \\
g(i,0) = g(i-1,1) \\
f(i,1) = 2 \cdot f(i-1,1) + f(i-1,0) + g(i-1,0) + 1 \\
g(i,1) = 2 \cdot g(i-1,1) + g(i-1,0) + 1
\end{cases}
最后整个字符串 $s$ 的答案就是 $f(n,0) + f(n,1)$。
当动态规划涉及到修改操作时,就要考虑用矩阵来维护状态转移。因为会用到线段树来维护矩阵的乘积,修改操作只需修改矩阵,而不需要重新 dp。定义
$$F_i = \begin{bmatrix} f(i,0) & f(i,1) & g(i,0) & g(i,1) & 1 \end{bmatrix}, \quad G_0 = \begin{bmatrix} 2 & 0 & 0 & 0 & 0 \\ 1 & 1 & 0 & 0 & 0 \\ 0 & 0 & 2 & 0 & 0 \\ 1 & 0 & 1 & 1 & 0 \\ 1 & 0 & 1 & 0 & 1\end{bmatrix}, \quad G_1 = \begin{bmatrix} 1 & 1 & 0 & 0 & 0 \\ 0 & 2 & 0 & 0 & 0 \\ 0 & 1 & 1 & 1 & 0 \\ 0 & 0 & 0 & 2 & 0 \\ 0 & 1 & 0 & 1 & 1\end{bmatrix}$$
那么当 $s_i = 0$ 时,上述的状态转移方程就可以表示为 $F_i = F_{i-1} \times G_0$。当 $s_i = 1$ 时,状态转移方程就可以表示为 $F_i = F_{i-1} \times G_1$。
因此有 $F_n = F_0 \times \prod\limits_{i=1}^{n}G_{s_i}$,其中 $F_0 = \begin{bmatrix} 0 & 0 & 0 & 0 & 1 \end{bmatrix}$。
用线段树去维护区间 $[l,r]$ 矩阵乘积的结果,即 $\prod\limits_{i=l}^{r}G_{s_i}$。那么当修改第 $x$ 个字符时,需要在线段树上把区间 $[x,x]$ 的矩阵修改成另外一个矩阵即可。
最后答案就是 $F_n[0][0] + F_n[0][1]$。
AC 代码如下,时间复杂度为 $O\left(5^3(n + q\log{n})\right)$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5, mod = 998244353;
char s[N];
struct Matrix {
array<array<int, 5>, 5> a;
Matrix(array<array<int, 5>, 5> b = {0}) {
a = b;
}
auto& operator[](int x) {
return a[x];
}
Matrix operator*(Matrix b) {
Matrix c;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
for (int k = 0; k < 5; k++) {
c[i][j] = (c[i][j] + 1ll * a[i][k] * b[k][j]) % mod;
}
}
}
return c;
}
}g[2];
struct Node {
int l, r;
Matrix f;
}tr[N * 4];
void build(int u, int l, int r) {
tr[u] = {l, r};
if (l == r) {
tr[u].f = g[s[l] & 1];
}
else {
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
tr[u].f = tr[u << 1].f * tr[u << 1 | 1].f;
}
}
void modify(int u, int x) {
if (tr[u].l == tr[u].r) {
tr[u].f = g[s[x] & 1];
}
else {
if (x <= tr[u].l + tr[u].r >> 1) modify(u << 1, x);
else modify(u << 1 | 1, x);
tr[u].f = tr[u << 1].f * tr[u << 1 | 1].f;
}
}
void solve() {
int n, m;
cin >> s >> m;
n = strlen(s);
memmove(s + 1, s, n + 1);
g[0] = Matrix({
2, 0, 0, 0, 0,
1, 1, 0, 0, 0,
0, 0, 2, 0, 0,
1, 0, 1, 1, 0,
1, 0, 1, 0, 1
});
g[1] = Matrix({
1, 1, 0, 0, 0,
0, 2, 0, 0, 0,
0, 1, 1, 1, 0,
0, 0, 0, 2, 0,
0, 1, 0, 1, 1
});
build(1, 1, n);
Matrix f({0, 0, 0, 0, 1});
while (m--) {
int x;
cin >> x;
s[x] ^= 1;
modify(1, x);
Matrix t = f * tr[1].f;
cout << (t[0][0] + t[0][1]) % mod << ' ';
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
再给出官方题解的做法。考虑段数通过哪些贡献得到的,容易知道在 $01$ 串中,如果存在相邻两个字符不同,那么段数就会加 $1$。因此在原串 $s$ 中,如果存在 $s_i \ne s_j$,则对答案的贡献就是以这两个字符为相邻字符的子序列的数量,即 $2^{i-1} \cdot 2^{n-j}$。
当不涉及修改操作时,枚举 $i$,同时维护 $g_0$ 表示前缀中满足 $s_j = 0$ 的 $2^{j-1}$ 的总和;$g_1$ 表示前缀中满足 $s_j = 1$ 的 $2^{j-1}$ 的总和。那么 $s_i$ 与前缀不同的字符作为子序列相邻字符时对答案的贡献就是 $2^{n-i} \cdot g_{\neg s_i}$。时间复杂度是 $O(n)$。
通过上面的方法可以求出原串 $s$ 的答案 $\text{ans}$。当修改 $s_x$ 时,我们只需从 $\text{ans}$ 中减去 $s_x$ 对答案的贡献即可。其中包括与前缀和后缀中的 $\neg s_x$ 相邻的两个部分。前缀部分的贡献是 $2^{n-x}\sum\limits_{i=1}^{x-1}{[s_i \ne s_x]2^{i-1}}$,后缀部分的贡献是 $2^{x-1}\sum\limits_{i=x+1}^{n}{[s_i \ne s_x]2^{n-i}}$。然后再对 $\text{ans}$ 加上修改后的贡献 $2^{n-x}\sum\limits_{i=1}^{x-1}{[s_i = s_x]2^{i-1}}$ 以及 $2^{x-1}\sum\limits_{i=x+1}^{n}{[s_i = s_x]2^{n-i}}$(此时还没对 $s_x$ 进行修改)。由于涉及到修改操作,因此我们用树状数组来分别维护 $0/1$ 关于 $2^{i-1}$ 和 $2^{n-i}$ 的前缀和。
AC 代码如下,时间复杂度为 $O((n+q)\log{n})$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5, mod = 998244353;
int n, m;
char s[N];
int p[N];
int tr1[2][N], tr2[2][N];
int lowbit(int x) {
return x & -x;
}
void add(int *tr, int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) {
tr[i] = (tr[i] + c) % mod;
}
}
int query(int *tr, int x) {
int ret = 0;
for (int i = x; i; i -= lowbit(i)) {
ret = (ret + tr[i]) % mod;
}
return ret;
}
void solve() {
cin >> s >> m;
n = strlen(s);
memmove(s + 1, s, n + 1);
p[0] = 1;
for (int i = 1; i <= n; i++) {
p[i] = 2ll * p[i - 1] % mod;
}
memset(tr1[0], 0, n + 1 << 2);
memset(tr1[1], 0, n + 1 << 2);
memset(tr2[0], 0, n + 1 << 2);
memset(tr2[1], 0, n + 1 << 2);
int ret = p[n] - 1; // 每个子序列至少有一段
for (int i = 1; i <= n; i++) {
add(tr1[s[i] & 1], i, p[i - 1]);
add(tr2[s[i] & 1], i, p[n - i]);
ret = (ret + 1ll * p[n - i] * query(tr1[~s[i] & 1], i - 1)) % mod;
}
while (m--) {
int x;
cin >> x;
ret = (ret - 1ll * p[n - x] * query(tr1[~s[x] & 1], x - 1) - p[x - 1] * LL(query(tr2[~s[x] & 1], n) - query(tr2[~s[x] & 1], x))) % mod;
ret = (ret + 1ll * p[n - x] * query(tr1[s[x] & 1], x - 1) + p[x - 1] * LL(query(tr2[s[x] & 1], n) - query(tr2[s[x] & 1], x))) % mod;
ret = (ret + mod) % mod;
cout << ret << ' ';
add(tr1[s[x] & 1], x, -p[x - 1]);
add(tr1[~s[x] & 1], x, p[x - 1]);
add(tr2[s[x] & 1], x, -p[n - x]);
add(tr2[~s[x] & 1], x, p[n - x]);
s[x] ^= 1;
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
参考资料
Codeforces Round 1003 (Div. 4) Editorial:https://codeforces.com/blog/entry/139178
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/18707011

浙公网安备 33010602011771号