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 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(00110001)=4 because t can be partitioned as [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. 0 changes to 1 and 1 changes to 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 of s, modulo 998244353.

A binary string consists of only characters 0 and 1.

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 (1t104) — the number of test cases.

The first line of each test case contains a binary string s (1|s|2105).

The following line of each test case contains an integer q (1q2105) — the number of queries.

The following line contains q integers v1,v2,,vq (1vi|s|), denoting svi 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 2105.

Output

For each test case, output q integers on a single line — the answer after each query modulo 998244353.

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 001 after the first query. Let's calculate the answer for each subsequence:

  • f(s1)=f(0)=1
  • f(s2)=f(0)=1
  • f(s3)=f(1)=1
  • f(s1s2)=f(00)=1
  • f(s1s3)=f(01)=2
  • f(s2s3)=f(01)=2
  • f(s1s2s3)=f(001)=2

The sum of these values is 10, modulo 998244353.

 

解题思路

  给出一种比较简单粗暴的 DDP 做法。

  对于统计类题目尤其是涉及到取模时,可以考虑贡献法加组合数学,或者动态规划。先考虑没有修改操作时字符串的答案(所有子序列 01 段数量的总和),试了一下发现动态规划可做(之后还会给出贡献法的做法)。

  定义 f(i,0/1) 表示前 i 个字符中以 0/1 结尾的子序列的 01 段数量总和;g(i,0/1) 表示前 i 个字符中以 0/1 结尾的子序列的总数。根据第 i 个字符 si{0,1} 接到结尾是 0/1 的子序列或空串进行状态转移。其中当 si=0 时:

{f(i,0)=2f(i1,0)+f(i1,1)+g(i1,1)+1g(i,0)=2g(i1,0)+g(i1,1)+1f(i,1)=f(i1,1)g(i,1)=g(i1,1)

  当 si=1 时:

{f(i,0)=f(i1,0)g(i,0)=g(i1,1)f(i,1)=2f(i1,1)+f(i1,0)+g(i1,0)+1g(i,1)=2g(i1,1)+g(i1,0)+1

  最后整个字符串 s 的答案就是 f(n,0)+f(n,1)

  当动态规划涉及到修改操作时,就要考虑用矩阵来维护状态转移。因为会用到线段树来维护矩阵的乘积,修改操作只需修改矩阵,而不需要重新 dp。定义

Fi=[f(i,0)f(i,1)g(i,0)g(i,1)1],G0=[2000011000002001011010101],G1=[1100002000011100002001011]

  那么当 si=0 时,上述的状态转移方程就可以表示为 Fi=Fi1×G0。当 si=1 时,状态转移方程就可以表示为 Fi=Fi1×G1

  因此有 Fn=F0×i=1nGsi,其中 F0=[00001]

  用线段树去维护区间 [l,r] 矩阵乘积的结果,即 i=lrGsi。那么当修改第 x 个字符时,需要在线段树上把区间 [x,x] 的矩阵修改成另外一个矩阵即可。

  最后答案就是 Fn[0][0]+Fn[0][1]

  AC 代码如下,时间复杂度为 O(53(n+qlogn))

#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 中,如果存在 sisj,则对答案的贡献就是以这两个字符为相邻字符的子序列的数量,即 2i12nj

  当不涉及修改操作时,枚举 i,同时维护 g0 表示前缀中满足 sj=02j1 的总和;g1 表示前缀中满足 sj=12j1 的总和。那么 si 与前缀不同的字符作为子序列相邻字符时对答案的贡献就是 2nig¬si。时间复杂度是 O(n)

  通过上面的方法可以求出原串 s 的答案 ans。当修改 sx 时,我们只需从 ans 中减去 sx 对答案的贡献即可。其中包括与前缀和后缀中的 ¬sx 相邻的两个部分。前缀部分的贡献是 2nxi=1x1[sisx]2i1,后缀部分的贡献是 2x1i=x+1n[sisx]2ni。然后再对 ans 加上修改后的贡献 2nxi=1x1[si=sx]2i1 以及 2x1i=x+1n[si=sx]2ni(此时还没对 sx 进行修改)。由于涉及到修改操作,因此我们用树状数组来分别维护 0/1 关于 2i12ni 的前缀和。

  AC 代码如下,时间复杂度为 O((n+q)logn)

#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

posted @   onlyblues  阅读(82)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的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 蹄球
Web Analytics
点击右上角即可分享
微信分享提示