AtCoder Beginner Contest 246 Ex - 01? Queries

题目链接https://atcoder.jp/contests/abc246/tasks/abc246_h
官方题解https://atcoder.jp/contests/abc246/editorial/3715
给你一个字符串S(只包含0,1,?),求能够组成多少不同的子序列。进行m次修改指定位置的字符,并给出总的能组成的不同的子序列方案数。
先忽略掉修改操作,对一个字符串,我们应该怎么求它能够组成的不同子序列的方案数呢?也即能从S中获得的不同子序列的方案数是多少?
我们考虑dp求解。
状态定义:
d p [ i ] [ 0 ] :由 s [ 1 ] 到 s [ i ] 能够组成的以 0 结尾的不同子序列的方案数 dp[i][0]:由s[1]到s[i]能够组成的以0结尾的不同子序列的方案数 dp[i][0]:由s[1]s[i]能够组成的以0结尾的不同子序列的方案数
d p [ i ] [ 1 ] :由 s [ 1 ] 到 s [ i ] 能够组成的以 1 结尾的不同子序列的方案数 dp[i][1]:由s[1]到s[i]能够组成的以1结尾的不同子序列的方案数 dp[i][1]:由s[1]s[i]能够组成的以1结尾的不同子序列的方案数
状态转移
如果 s [ i ] = 0 : s[i]=0: s[i]=0
我们考虑将0追加在前 i − 1 i-1 i1个字符组成的不相同的子序列后,肯定是满足已0结尾这个条件的,再加上单独的0自己也是不同于当前能获得子序列的一个子序列。那有个问题,那我对前 i − 1 i-1 i1构成的以0结尾的子序列不追加0不也是满足以0结尾这个条件吗?我们从由前i-1个字符组成的所有以0结尾子序列结尾连续0的个数去考虑。对结尾只含有1个0的子序列都可以从以1结尾的子序列追加一个0构造(特殊的对单个0我们最后加上了一个单独为0的方案),对结尾包含l个0的子序列显然可以由结尾包含 l − 1 l-1 l1个0的子序列追加0构成,所以所有对前 i − 1 i-1 i1个字符构成的以0结尾的都重复了,即我们直接将0追加再前 i − 1 i-1 i1个字符构成的子序列后再加上特殊的单独的1即使前i个能构成的以0结尾的子序列方案数。
解释的有点啰嗦,可以自己手模体会下
s [ i ] = 1 / ? s[i]=1/? s[i]=1/?同理
转移方程如下:
1. s [ i ] = 0 s[i]=0 s[i]=0
d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] + d p [ i − 1 ] [ 1 ] + 1 dp[i][0] = dp[i - 1][0] + dp[i - 1][1] + 1 dp[i][0]=dp[i1][0]+dp[i1][1]+1
d p [ i ] [ 1 ] = d p [ i − 1 ] [ 1 ] dp[i][1] = dp[i - 1][1] dp[i][1]=dp[i1][1]
( s [ i ] = 0 s[i]=0 s[i]=0的时候易知s[i]对构成已1为结尾的方案数是没有贡献的)
2. s [ i ] = 1 s[i]=1 s[i]=1
d p [ i ] [ 1 ] = d p [ i − 1 ] [ 1 ] + d p [ i − 1 ] [ 0 ] + 1 dp[i][1] = dp[i - 1][1] + dp[i - 1][0] + 1 dp[i][1]=dp[i1][1]+dp[i1][0]+1
d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] dp[i][0] = dp[i - 1][0] dp[i][0]=dp[i1][0]
3. s [ i ] = ? s[i]=? s[i]=?
d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] + d p [ i − 1 ] [ 1 ] + 1 dp[i][0] = dp[i - 1][0] + dp[i - 1][1] + 1 dp[i][0]=dp[i1][0]+dp[i1][1]+1
d p [ i ] [ 1 ] = d p [ i − 1 ] [ 1 ] + d p [ i − 1 ] [ 0 ] + 1 dp[i][1] = dp[i - 1][1] + dp[i - 1][0] + 1 dp[i][1]=dp[i1][1]+dp[i1][0]+1
至此,我们能够在O(n)的时间内求出不同子序列的方案数 ( d p [ n ] [ 0 ] + d p [ n ] [ 1 ] ) (dp[n][0]+dp[n][1]) (dp[n][0]+dp[n][1])
回到题目要求,需要对S进行Q次修改+查询总方案数,暴力修改再求一遍方案数肯定是不行的。
每次修改一个字符,是不是其实很多计算是重复的?这也是这题很巧妙的地方,利用矩阵向量(姑且叫这个吧 )来进行转移,我们对三种字符构造三个矩阵,通过矩阵乘来进行dp转移
这是三种字符构造的对应的矩阵向量
我们就可以得到
在这里插入图片描述
所以:
在这里插入图片描述
这样,我们计算总方案数就转化成了一些矩阵的乘法,在修改某个位置x的字符时,只需要修改x位置的矩阵,(1-x-1),(x+1,n)的矩阵乘是不变的(矩阵乘法满足结合率),那么我们就能够用线段树来维护这n个矩阵的矩阵乘,在log的时间内修改。
代码如下:(有点丑

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define yes "Yes\n"
#define no "No\n"
const int N = 2e5 + 10;
const int mod = 998244353;
#define pii pair<int, int>

int n, m, a[N], dp[N][2];  // 1~i的字符能构成以0/1为结尾的子序列的方案数
struct matrix {            // 用结构体主要是方便矩阵的直接赋值,矩阵乘
    int a[3][3];           // 向量矩阵
    void init() {          // 初始化为单位矩阵
        memset(a, 0, sizeof a);
        for (int i = 0; i < 3; i++)
            a[i][i] = 1;
    }
} mat[N], base[3];
matrix mul(matrix a, matrix b) {  // 矩阵a左乘b(注意矩阵乘一般是不满足交换律的)
    matrix c;
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            c.a[i][j] = 0;
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            for (int k = 0; k < 3; k++) {
                c.a[i][j] = (c.a[i][j] + a.a[i][k] * b.a[k][j] % mod) % mod;
            }
        }
    }
    return c;
}
//线段树维护矩阵乘
struct SegmentTree { // 这里我将这个线段树封装了一下,当然也可以直接把这个结构体内的代码全部放在外面也是一样的
#define ls (p << 1)      // 左儿子
#define rs (p << 1 | 1)  // 右儿子
    struct node {
        int l, r;
        matrix a;  //[l,r]的矩阵乘
    };
    node tr[N << 2];
    void up(int p) {
        tr[p].a = mul(tr[ls].a, tr[rs].a);  // 左儿子的矩阵左乘右儿子
    }
    void build(int p, int l, int r) {
        tr[p].l = l, tr[p].r = r;
        tr[p].a.init();
        if (l == r) {
            tr[p].a = mat[l];  // mat[l]为l位置对应的矩阵
            return;
        }
        int mid = (l + r) >> 1;
        build(ls, l, mid);
        build(rs, mid + 1, r);
        up(p);
    }
    void upd(int p, int x, matrix y) {  // 将x位置举证修改为矩阵y
        if (tr[p].l == tr[p].r) {
            tr[p].a = y;
            return;
        }
        int mid = (tr[p].l + tr[p].r) >> 1;
        if (x <= mid)
            upd(ls, x, y);
        else
            upd(rs, x, y);
        up(p);
    }
} ST;
string s;
void Print(matrix b) {  // debug方便
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            cout << b.a[i][j] << " \n"[j == 2];
}
void init() { // 初始化构造的三个向量矩阵(写的有亿点麻烦了)
    base[0].a[0][0] = 1, base[0].a[0][1] = 1, base[0].a[0][2] = 1;  // 0
    base[0].a[1][0] = 0, base[0].a[1][1] = 1, base[0].a[1][2] = 0;
    base[0].a[2][0] = 0, base[0].a[2][1] = 0, base[0].a[2][2] = 1;

    base[1].a[0][0] = 1, base[1].a[0][1] = 0, base[1].a[0][2] = 0;  // 1
    base[1].a[1][0] = 1, base[1].a[1][1] = 1, base[1].a[1][2] = 1;
    base[1].a[2][0] = 0, base[1].a[2][1] = 0, base[1].a[2][2] = 1;

    base[2].a[0][0] = 1, base[2].a[0][1] = 1, base[2].a[0][2] = 1;  // ?
    base[2].a[1][0] = 1, base[2].a[1][1] = 1, base[2].a[1][2] = 1;
    base[2].a[2][0] = 0, base[2].a[2][1] = 0, base[2].a[2][2] = 1;
}
void solve() {
    cin >> n >> m;
    cin >> s;
    s = "&" + s;
    init();
    for (int i = 1; i <= n; i++) {  // 每个字符对应的矩阵
        if (s[i] == '0')
            mat[i] = base[0];
        else if (s[i] == '1')
            mat[i] = base[1];
        else
            mat[i] = base[2];
    }
    reverse(mat + 1, mat + n + 1);  // 翻转一下,因为我们要维护的是An*...A1
    ST.build(1, 1, n);              // 建立一颗维护翻转后的A1*..An
    while (m--) {
        int x;
        char c;
        cin >> x >> c;
        matrix temp;
        if (c == '0')
            temp = base[0];
        else if (c == '1')
            temp = base[1];
        else
            temp = base[2];
        ST.upd(1, n - x + 1, temp);     // 翻转后x对应位置为n-x+1
        int ans1 = ST.tr[1].a.a[0][2];  // tr[1].a里面存的是那n个矩阵的矩阵乘,再与初始的3*1矩阵做一次矩阵乘得到答案
        int ans2 = ST.tr[1].a.a[1][2];
        cout << (ans1 + ans2) % mod << "\n";
    }
}
signed main() {
    ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
    int T = 1;
    // cin >> T;
    for (int i = 1; i <= T; i++) {
        solve();
    }
}

线段树维护的是A1-An的矩阵乘也是可以的,相当于从n递推到1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

self_disc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值