题目链接: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
i−1个字符组成的不相同的子序列后,肯定是满足已0结尾这个条件的,再加上单独的0自己也是不同于当前能获得子序列的一个子序列。那有个问题,那我对前
i
−
1
i-1
i−1构成的以0结尾的子序列不追加0不也是满足以0结尾这个条件吗?我们从由前i-1个字符组成的所有以0结尾子序列结尾连续0的个数去考虑。对结尾只含有1个0的子序列都可以从以1结尾的子序列追加一个0构造(特殊的对单个0我们最后加上了一个单独为0的方案),对结尾包含l个0的子序列显然可以由结尾包含
l
−
1
l-1
l−1个0的子序列追加0构成,所以所有对前
i
−
1
i-1
i−1个字符构成的以0结尾的都重复了,即我们直接将0追加再前
i
−
1
i-1
i−1个字符构成的子序列后再加上特殊的单独的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[i−1][0]+dp[i−1][1]+1
d
p
[
i
]
[
1
]
=
d
p
[
i
−
1
]
[
1
]
dp[i][1] = dp[i - 1][1]
dp[i][1]=dp[i−1][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[i−1][1]+dp[i−1][0]+1
d
p
[
i
]
[
0
]
=
d
p
[
i
−
1
]
[
0
]
dp[i][0] = dp[i - 1][0]
dp[i][0]=dp[i−1][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[i−1][0]+dp[i−1][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[i−1][1]+dp[i−1][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