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 , is defined as the minimum number of contiguous substrings, each consisting of identical characters, into which can be partitioned. For example, because can be partitioned as where each bracketed segment consists of identical characters.
Skibidus gives you a binary string and queries. In each query, a single character of the string is flipped (i.e. changes to and changes to ); changes are saved after the query is processed. After each query, output the sum over all where is a non-empty subsequence of , modulo .
A binary string consists of only characters and .
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 () — the number of test cases.
The first line of each test case contains a binary string ().
The following line of each test case contains an integer () — the number of queries.
The following line contains integers (), denoting is flipped for the 'th query.
It is guaranteed that the sum of and the sum of over all test cases does not exceed .
Output
For each test case, output integers on a single line — the answer after each query modulo .
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, becomes after the first query. Let's calculate the answer for each subsequence:
The sum of these values is , modulo .
解题思路
给出一种比较简单粗暴的 DDP 做法。
对于统计类题目尤其是涉及到取模时,可以考虑贡献法加组合数学,或者动态规划。先考虑没有修改操作时字符串的答案(所有子序列 段数量的总和),试了一下发现动态规划可做(之后还会给出贡献法的做法)。
定义 表示前 个字符中以 结尾的子序列的 段数量总和; 表示前 个字符中以 结尾的子序列的总数。根据第 个字符 接到结尾是 的子序列或空串进行状态转移。其中当 时:
当 时:
最后整个字符串 的答案就是 。
当动态规划涉及到修改操作时,就要考虑用矩阵来维护状态转移。因为会用到线段树来维护矩阵的乘积,修改操作只需修改矩阵,而不需要重新 dp。定义
那么当 时,上述的状态转移方程就可以表示为 。当 时,状态转移方程就可以表示为 。
因此有 ,其中 。
用线段树去维护区间 矩阵乘积的结果,即 。那么当修改第 个字符时,需要在线段树上把区间 的矩阵修改成另外一个矩阵即可。
最后答案就是 。
AC 代码如下,时间复杂度为 :
#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;
}
再给出官方题解的做法。考虑段数通过哪些贡献得到的,容易知道在 串中,如果存在相邻两个字符不同,那么段数就会加 。因此在原串 中,如果存在 ,则对答案的贡献就是以这两个字符为相邻字符的子序列的数量,即 。
当不涉及修改操作时,枚举 ,同时维护 表示前缀中满足 的 的总和; 表示前缀中满足 的 的总和。那么 与前缀不同的字符作为子序列相邻字符时对答案的贡献就是 。时间复杂度是 。
通过上面的方法可以求出原串 的答案 。当修改 时,我们只需从 中减去 对答案的贡献即可。其中包括与前缀和后缀中的 相邻的两个部分。前缀部分的贡献是 ,后缀部分的贡献是 。然后再对 加上修改后的贡献 以及 (此时还没对 进行修改)。由于涉及到修改操作,因此我们用树状数组来分别维护 关于 和 的前缀和。
AC 代码如下,时间复杂度为 :
#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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2024-02-10 E. Klever Permutation
2023-02-10 E. Sum Over Zero
2022-02-10 蹄球