2024 ccpc 网络赛题解
比赛链接:https://codeforces.com/gym/105336
L. 网络预选赛
题意
给出一个 n ∗ m n*m n∗m 的字符矩阵,问该矩阵内存在多少个子矩阵 [ c c p c ] [\begin{array}{} c \ c \\ p \ c \end{array}] [c cp c]。
数据范围
2 ≤ n , m ≤ 500 2 \le n,m \le 500 2≤n,m≤500
思路
两重循环遍历字符矩阵,找到所有为 [ c c p c ] [\begin{array}{} c \ c \\ p \ c \end{array}] [c cp c] 的子矩阵即可
复杂度
时间复杂度 O ( n ∗ m ) O(n*m) O(n∗m),空间复杂度 O ( n ∗ m ) O(n*m) O(n∗m)
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 505;
int n, m;
string g[N];
void solve()
{
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> g[i];
}
int ans = 0;
for (int i = 0; i + 1 < n; i++) {
for (int j = 0; j + 1 < m; j++) {
if (g[i][j] == 'c' && g[i][j + 1] == 'c'
&& g[i + 1][j] == 'p' && g[i + 1][j + 1] == 'c')
ans++;
}
}
cout << ans;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while (T--) {
solve();
}
}
延伸思考
如果是对于一个
n
∗
m
n*m
n∗m 的字符矩阵
S
S
S,要查询一个
a
∗
b
a*b
a∗b 的字符矩阵
T
T
T 在
S
S
S 中出现的次数?(
1
≤
a
≤
n
,
1
≤
b
≤
m
1 \le a \le n,1 \le b \le m
1≤a≤n,1≤b≤m)
例题链接:https://vjudge.net/problem/UVA-11019
字符串哈希
思路
可以对二维的字符矩阵处理哈希值,和一维的字符串哈希类似,不同的是处理二维的时候,是先处理其中一维度的哈希值,然后把该维的值作为另一维哈希值中某一位上的数。
处理
T
T
T 的哈希值,先对
T
T
T 的每一行,以
b
a
s
e
1
base_1
base1为底数进行字符串哈希的处理,得到
v
1
,
v
2
,
.
.
.
,
v
a
v_1,v_2,...,v_a
v1,v2,...,va,然后再取
b
a
s
e
2
base_2
base2为底数,
v
1
∗
b
a
s
e
2
a
−
1
+
v
2
∗
b
a
s
e
2
a
−
2
+
.
.
.
+
v
n
v_1 * base_2^{a-1}+v_2*base_2^{a-2} + ... + v_n
v1∗base2a−1+v2∗base2a−2+...+vn即为
T
T
T的哈希值,这步的时间复杂度为
O
(
a
∗
b
)
O(a*b)
O(a∗b)。
然后处理出
S
S
S每一行的前缀字符串哈希值,用于求某行中子串的哈希值。
若 S S S中的 a ∗ b a*b a∗b的子矩阵的右下角 ( i , j ) (i,j) (i,j)(表示第 i i i行第 j j j列,下同),令 h a s h ( i , l , r ) hash(i,l,r) hash(i,l,r)为 S S S中第 i i i行中区间为 [ l , r ] [l,r] [l,r]的子串的哈希值,则该子矩阵的哈希值为 h a s h ( i − a + 1 , j − b + 1 , j ) ∗ b a s e 2 a − 1 + . . . + h a s h ( i , j − b + 1 , j ) hash(i-a+1,j-b+1,j)*base_2^{a-1}+... + hash(i,j-b+1,j) hash(i−a+1,j−b+1,j)∗base2a−1+...+hash(i,j−b+1,j)。
枚举子矩阵的右下角 ( i , j ) (i,j) (i,j),看是否存在子矩阵哈希值和 T T T的哈希值相同,枚举顺序是先列后行,因为按上面这个哈希规则,从右下角为 ( i , j ) (i,j) (i,j)的子矩阵转移到右下角为 ( i + 1 , j ) (i+1,j) (i+1,j)的子矩阵,子矩阵的哈希值变化很少,就是减去了 h a s h ( i − a + 1 , j − b + 1 , j ) ∗ b a s e 2 a − 1 hash(i-a+1,j-b+1,j)*base_2^{a-1} hash(i−a+1,j−b+1,j)∗base2a−1这一项,然后剩下的部分乘上 b a s e 2 base_2 base2,再增加 h a s h ( i + 1 , j − b + 1 , j ) hash(i+1,j-b+1,j) hash(i+1,j−b+1,j),就变成右下角为 ( i + 1 , j ) (i+1,j) (i+1,j)的子矩阵的哈希值,而如果先行后列,哈希值中涉及到 a a a行,每一行的值就要改动,因此先列后行枚举是较优的,最多枚举 ( n − a + 1 ) ∗ ( m − b + 1 ) (n-a+1)*(m-b+1) (n−a+1)∗(m−b+1)个右下角就可以枚举完所有的子矩阵,复杂度是 O ( n ∗ m ) O(n*m) O(n∗m)的。
复杂度
时间复杂度为 O ( n ∗ m + a ∗ b ) O(n*m+a*b) O(n∗m+a∗b),空间复杂度为 O ( n ∗ m ) O(n*m) O(n∗m)
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef unsigned long long ull;
const int N = 1e3 + 1, M = 1e2 + 5;
const ull base1 = 13331, base2 = 1e9 + 7;
int n, m, a, b, q;
ull p, p1, s[N][N];
string g[N], g1[M];
ull gethash(int l, int r, ull h[])
{
return h[r] - h[l - 1] * p;
}
void solve()
{
cin >> n >> m;
// 读入n*m的字符矩阵
for (int i = 1; i <= n; i++) {
cin >> g[i];
g[i] = ' ' + g[i];
}
cin >> a >> b;
ull val = 0;
// 读入a*b的字符矩阵,同时处理出该矩阵的哈希值val
for (int i = 1; i <= a; i++) {
cin >> g1[i];
ull cur = 0;
for (char c : g1[i]) {
cur = cur * base1 + c;
}
val = val * base2 + cur;
}
if (a > n || b > m) {
cout << 0 << '\n';
return;
}
// 处理n*m的字符矩阵每一行的哈希值
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
s[i][j] = s[i][j - 1] * base1 + g[i][j];
}
}
// p = base1的b次方,在求n*m矩阵中某一行长度为b的子串的哈希值中用到
p = 1;
for (int i = 1; i <= b; i++) {
p *= base1;
}
// p1 = base2的a-1次方,在计算n*m矩阵中a*b的子矩阵的哈希值用到
p1 = 1;
for (int i = 1; i < a; i++) {
p1 *= base2;
}
// cnt 为出现次数
int cnt = 0;
// 双重循环 (i,j) 枚举的是a*b的子矩阵的右下角
for (int j = b; j <= m; j++) {
ull cur = 0;
// cur 为当前枚举的a*b的子矩阵的哈希值
for (int i = 1; i <= n; i++) {
cur = cur * base2 + gethash(j - b + 1, j, s[i]);
if (i >= a) {
// 找到一个子矩阵哈希值和 T的哈希值val 相同
if (val == cur)
cnt++;
cur -= p1 * gethash(j - b + 1, j, s[i - a + 1]);
}
}
}
cout << cnt << '\n';
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
cin >> T;
while (T--) {
solve();
}
}
kmp
思路
如果要看 S S S中以 ( i , j ) (i,j) (i,j)为左上角,大小为 a ∗ b a*b a∗b的子矩阵是否为 T T T,一般的想法都是从第 i i i行开始,看第 i i i行中区间为 [ j , j + b − 1 ] [j,j+b-1] [j,j+b−1]对应的子串是否和 T T T的第一行相同,如果相同,就看第 i + 1 i+1 i+1行中区间为 [ j , j + b − 1 ] [j,j+b-1] [j,j+b−1]对应的子串是否和 T T T的第二行相同,如果不同,说明以 ( i , j ) (i, j) (i,j)为左上角的 a ∗ b a*b a∗b的子矩阵一定不为 T T T,结束匹配,若可以匹配到第 i + a − 1 i+a-1 i+a−1行,且该行区间为 [ j , j + b − 1 ] [j,j+b-1] [j,j+b−1]对应的子串和 T T T的第 a a a行相同,那么该子矩阵就为 T T T。
从这个思路出发,可以取
S
S
S的每一行与
T
T
T的第一行进行匹配,找出在
S
S
S每行的哪些位置开始,长度为
b
b
b的子串为
T
T
T的第一行,然后保留这些位置。
接着,取
S
S
S的每一行与
T
T
T的第二行进行匹配,找出在
S
S
S每行的哪些位置开始,长度为
b
b
b的子串为
T
T
T的第二行,如果
(
i
,
j
)
(i,j)
(i,j)为第二轮找到的位置,且
(
i
−
1
,
j
)
(i-1,j)
(i−1,j)为第一轮找到的位置,说明从
(
i
,
j
)
(i,j)
(i,j)开始向下匹配,至少能与
T
T
T的前两行匹配,对于
(
i
,
j
)
(i,j)
(i,j)这样的位置进行保留。
然后进行第三轮,第三轮也是类似的,,如果
(
i
,
j
)
(i,j)
(i,j)为第三轮找到的位置,且
(
i
−
1
,
j
)
(i-1,j)
(i−1,j)为第二轮保留的位置,说明从
(
i
,
j
)
(i,j)
(i,j)开始向下匹配,至少能与
T
T
T的前三行匹配,对于
(
i
,
j
)
(i,j)
(i,j)这样的位置进行保留。
如果到第
k
k
k轮没有可以保留的位置,说明
S
S
S中不存在能匹配
T
T
T前
k
k
k行的子矩阵,也说明
S
S
S中不存在
k
k
k,否则如果到第
a
a
a轮都有可以保留的位置,说明
S
S
S中存在能匹配
T
T
T前
a
a
a行的子矩阵,即
S
S
S中存在
T
T
T。
在实际代码实现中,因为每一轮要保留的位置只与前一轮保留的位置相关,所以每进行一轮后,可以把上一轮保留的位置清空,这样就能保证进行每一轮时,当前保留的位置是近似
n
∗
m
n*m
n∗m的,从而优化空间复杂度。
使用 k m p kmp kmp进行字符串匹配,每行匹配的复杂度是 O ( m + b ) O(m+b) O(m+b),最多进行 a a a轮匹配,每轮最多匹配的行数接近 n n n,所以最坏复杂度是 O ( a ∗ n ∗ ( m + b ) ) O(a*n*(m+b)) O(a∗n∗(m+b))
复杂度
时间复杂度 O ( a ∗ n ∗ ( m + b ) ) O(a*n*(m+b)) O(a∗n∗(m+b)),空间复杂度 O ( n ∗ m ) O(n*m) O(n∗m)
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef unsigned long long ull;
const int N = 1e3 + 1, M = 1e2 + 5;
// st[i][j] = 1 表示上一轮保留了(i,j), =0 则反之
int n, m, a, b, ne[M], st[N][N];
string g[N], g1[M];
// 求s的前缀数组ne
void init(string& s)
{
for (int i = 2, j = 0; i <= b; i++) {
while (j && s[i] != s[j + 1])
j = ne[j];
if (s[i] == s[j + 1])
j++;
ne[i] = j;
}
}
void solve()
{
cin >> n >> m;
// 读入n*m的字符矩阵
for (int i = 1; i <= n; i++) {
cin >> g[i];
g[i] = ' ' + g[i];
}
cin >> a >> b;
// 读入a*b的字符矩阵
for (int i = 1; i <= a; i++) {
cin >> g1[i];
g1[i] = ' ' + g1[i];
}
if (a > n || b > m) {
cout << 0 << '\n';
return;
}
for (int i1 = 1; i1 <= a; i1++) {
// 计算T第i1行的前缀数组
init(g1[i1]);
vector<array<int, 3>> pos;
// 记录该轮找到的位置
for (int i = 1; i <= n; i++) {
for (int j = 1, j1 = 0; j <= m; j++) {
while (j1 && g[i][j] != g1[i1][j1 + 1])
j1 = ne[j1];
if (g[i][j] == g1[i1][j1 + 1])
j1++;
if (j1 == b) {
pos.push_back({ i, j, 0 });
}
}
}
// 如果该轮没有找到位置则不存在子矩阵为T
if (!pos.size()) {
cout << 0 << '\n';
return;
}
for (auto& it : pos) {
int i = it[0], j = it[1];
if (st[i - 1][j] || i1 == 1) {
it[2] = 1;
}
}
// 清空上一轮保留的位置
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
st[i][j] = 0;
}
}
// 该轮保留的位置进行标记
for (auto it : pos) {
int i = it[0], j = it[1];
if (it[2])
st[i][j] = 1;
}
}
int cnt = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cnt += st[i][j];
}
}
cout << cnt << '\n';
// cnt为 T 在 S中的出现次数
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
cin >> T;
while (T--) {
solve();
}
}
B. 军训 II
题意
给出一个长度为 n n n 的序列,定义序列的 “不整齐度” 为 ∑ l = 1 n ∑ r = l n m a x ( a l , a l + 1 , . . . , a r ) − m i n ( a l , a l + 1 , . . . , a r ) \sum^n_{l=1}\sum^n_{r=l}max(a_l,a_{l+1},...,a_r)-min(a_l,a_{l+1},...,a_r) ∑l=1n∑r=lnmax(al,al+1,...,ar)−min(al,al+1,...,ar),即为所有区间的极差和。
可以将序列进行重新排列,问重新排列后序列可以取到的 “不整齐度” 最小值和对应的方案数。
(注:这里的重新排列指对数的下标进行重新排列,因此,如果在原序列中
a
i
=
a
j
a_i = a_j
ai=aj,
(
a
i
,
a
j
)
(a_i,a_j)
(ai,aj),
(
a
j
,
a
i
)
(a_j,a_i)
(aj,ai) 是两种不同的排列方案)
(答案对 998244353 998244353 998244353 取模)
数据范围
1 ≤ n ≤ 1 0 3 , 1 ≤ a i ≤ 1 0 6 1 \le n \le 10^3, 1 \le a_i \le 10^6 1≤n≤103,1≤ai≤106
思路
结论
当序列为升序或者降序的时候,不整齐度为最小值。
证明如下:
当序列长度为
n
n
n 的时候,若序列的最大值为
x
x
x,那么不存在比
x
x
x 大的数,
x
x
x 对不整齐度的贡献只有当
x
x
x 为区间最大值的情况,只会产生正贡献。
如果 x x x 的下标为 i i i,那么会有 i ∗ ( n − i + 1 ) i*(n-i+1) i∗(n−i+1) 个区间包含 x x x, x x x 对不整齐度的贡献为 i ∗ ( n − i + 1 ) ∗ x i*(n-i+1)*x i∗(n−i+1)∗x,因为 x x x 为常数,所以该贡献的大小取决于 i ∗ ( n − i + 1 ) = − i 2 + ( n + 1 ) ∗ i i*(n-i+1) = -i^2+(n+1)*i i∗(n−i+1)=−i2+(n+1)∗i,可以发现这是一个凸函数,它的极大值点在 n + 1 2 \frac{n+1}{2} 2n+1,在两侧取得最小值,所以考虑把最大值先放到最左边或者最右边。
接着序列的长度就变成了 n − 1 n-1 n−1,如果当前序列的最大值为 x 1 x_1 x1,那么如果不考虑 x x x 的话, x 1 x_1 x1 也是选择最左侧或者最右侧放是最优的(注意,如果与 x x x 放在同一侧,比如都放在最右侧,这里 x 1 x_1 x1 是要放在 x x x 的左边一位,反之同理),如果 x 1 x_1 x1 与 x x x 不放在同一侧,如果 n > 2 n>2 n>2,那么 x 1 x_1 x1 和 x x x 之间存在不大于 x 1 x_1 x1 的数,从而使得 x 1 x_1 x1 无法作为区间的最小值,只有作为区间最大值贡献的部分,如果 x 1 x_1 x1 和 x x x 放在同一侧,与 x x x 相邻,那么 x 1 x_1 x1 原先作为作为区间最大值贡献的部分不变,且可以作为区间最小值产生负贡献,因此,把次大值放在与最大值同一侧是最优的。
之后长度减一,可以发现仍然是相同的子问题,把序列的当前最大值,与之前安排的序列最大值放在同一侧,是最优的做法,这样放置之后,序列必然要么降序要么升序。
通过以上结论,可以之间对序列进行升序排序,然后从小到大枚举区间的左,右端点。
如果当前枚举的区间端点为
i
i
i,那么当
i
i
i 为区间右端点时,
a
i
a_i
ai 会作为区间的最大值,对应的左端点选择区间为
[
1
,
i
]
[1,i]
[1,i],有
i
i
i 种选择,对 “不整齐度” 的贡献为
i
∗
a
i
i*a_i
i∗ai,当
i
i
i 为左端点时,
a
i
a_i
ai 会作为区间的最小值,对应的右端点选择区间为
[
i
,
n
]
[i,n]
[i,n],有
n
−
i
+
1
n-i+1
n−i+1 种选择,对 “不整齐度” 的贡献为
−
(
n
−
i
+
1
)
∗
a
i
-(n-i+1)*a_i
−(n−i+1)∗ai,把所有的贡献加起来即为可以取到的最小 “不整齐度” 。
当序列升序或者降序时,相等的数必然是连续的一段,对于相等的数,虽然这些数相等,但是它们对应的原下标不同,如果放在不同的位置,则对应不同的情况。在这一段中对这些相等的数进行任意排列,“不整齐度” 仍然是最小的,所以如果序列中 x x x 的出现次数为 y y y,那么对应的排列情况为 y ! y! y! 种,利用乘法原理,对序列中,存在的不重复的数的数量的阶乘,进行累乘,得到的乘积 v a l val val 即为升序或者降序时的情况数。
如果序列中只存在一种数,那么升序和降序都是一样的,此时答案为 v a l val val,否则,升序和降序不一样,此时的答案为 v a l ∗ 2 val*2 val∗2。
复杂度
时间复杂度为
O
(
n
log
n
)
O(n \log n)
O(nlogn),主要是排序的复杂度
空间复杂度为
O
(
n
)
O(n)
O(n)
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e3 + 5, mod = 998244353;
int n;
set<int> nums;
int a[N], f[N];
void solve()
{
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i++) {
cin >> a[i];
nums.insert(a[i]);
// 通过set去重,set的大小 nums.size() 即为序列中数的种类数量
f[i] = i * f[i - 1] % mod;
// 预处理阶乘
}
sort(a + 1, a + 1 + n);
int mi = 0;
for (int i = 1; i <= n; i++) {
mi += a[i] * (i - 1) - a[i] * (n - i);
// 正贡献: a[i] * (i-1)
// 负贡献: -a[i] * (n-i)
}
int cnt = 1;
for (int i = 1; i <= n; i++) {
int j = i;
while (j <= n && a[i] == a[j])
j++;
// 通过双指针找到相等的数的区间长度 j-i
cnt = (cnt * f[j - i]) % mod;
i = j - 1;
}
if (nums.size() > 1)
cnt = cnt * 2 % mod;
cout << mi << ' ' << cnt;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while (T--) {
solve();
}
}
K. 取沙子游戏
题意
存在 n n n 粒沙子, A l i c e Alice Alice 和 B o b Bob Bob 两人轮流取沙子, A l i c e Alice Alice 先取,每次取的数量不难超过 k k k,且为前面所有取的沙子数量的公因数,先取完沙子的人获胜。
问两个人都采取最优策略, A l i c e Alice Alice 是否可以获胜。
数据范围
1 ≤ T ≤ 1 0 4 1 \le T \le 10^4 1≤T≤104,表示数据组数, 1 ≤ n , k ≤ 1 0 9 1 \le n,k \le 10^9 1≤n,k≤109
思路
结论
如果 l o w b i t ( n ) ≤ k lowbit(n) \le k lowbit(n)≤k,那么 A l i c e Alice Alice 必胜,否则 B o b Bob Bob 必胜。(注:如果 n n n 在二进制下存在 1 1 1 的最低位为 i i i,那么 l o w b i t ( n ) = 2 i lowbit(n) = 2^i lowbit(n)=2i。)
证明
令 n = 2 p 1 + 2 p 2 + 2 p 3 + . . . 2 p m ( p 1 < p 2 < p 3 < . . . < p m ) n = 2^{p_1} + 2^{p_2} + 2^{p_3} + ... 2^{p_m}(p_1 < p_2 < p_3 < ... <p_m) n=2p1+2p2+2p3+...2pm(p1<p2<p3<...<pm)。
如果 l o w b i t ( n ) ≤ k lowbit(n) \le k lowbit(n)≤k,那么 A l i c e Alice Alice 可以先取 l o w b i t ( n ) lowbit(n) lowbit(n),取走之后, n = 2 p 2 + 2 p 3 + . . + 2 p m n = 2^{p_2} + 2^{p_3}+..+2^{p_m} n=2p2+2p3+..+2pm。
因为 p 1 p_1 p1 最小为 0 0 0,所以 p 2 , p 3 , . . . , p m p_2,p_3,...,p_m p2,p3,...,pm 都大于 0 0 0,此时 n n n 必然为偶数,所以在接下来 B o b Bob Bob 每次操作之后, A l i c e Alice Alice 可以重复 B o b Bob Bob 的取的数,由于偶数可以取半,所以按这样的策略 n n n 必然能取完,且因为 A l i c e Alice Alice 是在 B o b Bob Bob 操作之后取,所以取完的时候必然是轮到 A l i c e Alice Alice。
否则,如果
l
o
w
b
i
t
(
n
)
>
k
lowbit(n) > k
lowbit(n)>k,那么
A
l
i
c
e
Alice
Alice 起初取任意数
x
(
x
≤
k
)
x(x \le k)
x(x≤k),因为
l
o
w
b
i
t
(
x
)
≤
k
<
l
o
w
b
i
t
(
n
)
lowbit(x) \le k < lowbit(n)
lowbit(x)≤k<lowbit(n),
n
n
n
在
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x) 对应二进制位上的数为
0
0
0,减去
x
x
x 之后,
n
n
n 在
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x) 对应的二进制位上的数必然会变成
1
1
1,此时
l
o
w
b
i
t
(
n
−
x
)
=
l
o
w
b
i
t
(
x
)
≤
k
lowbit(n-x) = lowbit(x) \le k
lowbit(n−x)=lowbit(x)≤k,
B
o
b
Bob
Bob 就处于必胜的状态,
A
l
i
c
e
Alice
Alice 必输。
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, k;
void solve()
{
cin >> n >> k;
// 因为1e9 < 2的32次方,所以枚举从低到高32位
for (int i = 0; i <= 32; i++) {
// 找存在1的最低位
if ((n >> i) & 1) {
int w = 1ll << i;
// w = 2的i次幂 = lowbit(n)
cout << (w <= k ? "Alice" : "Bob") << '\n';
return;
}
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
cin >> T;
while (T--) {
solve();
}
}
D. 编码器-解码器
题意
给出两个字符串 S , T S,T S,T。
若 S S S 的长度为 n n n,则进行 n n n 轮计算,得到一个字符串 R n R_n Rn。
计算方式如下,
R i = { S i , i = 1 R i − 1 + S i + R i − 1 , i > 1 R_i = \begin{cases} S_i, i=1 \\ R_{i-1} + S_i + R_{i-1},i>1 \end{cases} Ri={Si,i=1Ri−1+Si+Ri−1,i>1
比如, S = a b c S=abc S=abc,则 R 1 = S 1 = a , R 2 = R 1 + S 2 + R 1 = a b a , R 3 = R 2 + S 3 + R 2 = a b a c a b a R_1 = S_1 = a,R_2 = R_1 + S_2 + R_1 = aba,R_3 = R_2 + S_3 + R_2 = abacaba R1=S1=a,R2=R1+S2+R1=aba,R3=R2+S3+R2=abacaba。
问在
R
n
R_n
Rn 中,
T
T
T 以咨询了形式出现的次数?
(答案对
998244353
998244353
998244353 取模)
数据范围
1 ≤ ∣ S ∣ , ∣ T ∣ ≤ 100 1 \le |S|,|T| \le 100 1≤∣S∣,∣T∣≤100
思路
注意到, R i = R i − 1 + S i + R i − 1 R_i = R_{i-1} + S_i + R_{i-1} Ri=Ri−1+Si+Ri−1,令前面的 R i − 1 R_{i-1} Ri−1 为 P 1 P_1 P1,后面的 R i − 1 R_{i-1} Ri−1 为 P 2 P_2 P2,则 R i = P 1 + S i + P 2 R_i = P_1 + S_i + P_2 Ri=P1+Si+P2。
如果 T T T 在 R i R_i Ri 中以子序列存在,如果不考虑 S i S_i Si,那么 T T T 必然是存在一部分 T 1 T_1 T1 在 P 1 P_1 P1 中,存在一部分 T 2 T_2 T2 在 P 2 P_2 P2 中( T 1 , T 2 T_1,T_2 T1,T2 满足 T = T 1 + T 2 T = T_1 + T_2 T=T1+T2);或者都在 P 1 P_1 P1 中;或者都在 P 2 P_2 P2 中,后面两种情况可以看作 T 1 T_1 T1 或者 T 2 T_2 T2 为空串的情况。
如果 T 1 T_1 T1 在 P 1 P_1 P1 中以子序列出现的次数为 v 1 v_1 v1, T 2 T_2 T2 在 P 2 P_2 P2 中以子序列出现的次数为 v 2 v_2 v2,那么在不考虑 S i S_i Si 的情况下,通过乘法原理可得, T T T 在 R i R_i Ri 中以子序列出现的次数为 v 1 ∗ v 2 v_1 * v_2 v1∗v2。
如果考虑 S i S_i Si,其实是同理的,引用上面的定义, T 1 , T 2 T_1,T_2 T1,T2 分别为 T T T 在 P 1 , P 2 P_1,P_2 P1,P2 中存在的部分,此时 T 1 , S i , T 2 T_1,S_i,T_2 T1,Si,T2 满足 T = T 1 + S i + T 2 T = T_1 + S_i + T_2 T=T1+Si+T2,那么在考虑 S i S_i Si ,且 S i S_i Si 可以作为子序列的一部分的情况下,若 T 1 T_1 T1 在 P 1 P_1 P1 中以子序列出现的次数为 w 1 w_1 w1, T 2 T_2 T2 在 P 2 P_2 P2 中以子序列出现的次数为 w 2 w_2 w2, T T T 在 R i R_i Ri 中以子序列出现的次数为 w 1 ∗ w 2 w_1 * w_2 w1∗w2。
因为 T 1 , T 2 T_1,T_2 T1,T2 是不确定的,对应着 T T T 的多个子串,且要计算子串在 R i R_i Ri 中以子序列出现的次数,也涉及到其他子串,所以要计算 T T T 所有的子串在 R i R_i Ri 中以子序列出现的次数,才能通过子串的次数推出 T T T 的次数。
子串和上面 T T T 的计算方法是一样的,存在着从 R i − 1 R_{i-1} Ri−1 推到 R i R_i Ri,从 T T T 的小子串推到 T T T 的大子串这样的关系,可以考虑用动态规划解决这个问题。
令 f ( i , j , k ) f(i,j,k) f(i,j,k) 为在 R i R_i Ri 中, T T T 中从下标 j j j 开始,长度为 k k k 的子串,以子序列形式出现的次数。
若 ∣ S ∣ = n , ∣ T ∣ = m |S| = n,|T| =m ∣S∣=n,∣T∣=m,则 1 ≤ i ≤ n , 1 ≤ j ≤ m , 0 ≤ k ≤ m − j + 1 1 \le i \le n,1 \le j \le m,0 \le k \le m-j+1 1≤i≤n,1≤j≤m,0≤k≤m−j+1
在上面的讨论过程中,涉及到空串的情况,因为这是一种合法情况,所以在初始化的时候,设置 f ( i , j , 0 ) = 1 ( 1 ≤ i ≤ n , 1 ≤ j ≤ m + 1 ) f(i,j,0) = 1(1 \le i \le n,1 \le j \le m+1) f(i,j,0)=1(1≤i≤n,1≤j≤m+1)。( j j j 最大为 m + 1 m+1 m+1 的原因,下面状态转移会解释到)
当 i = 1 i=1 i=1 时,因为 R 1 = S 1 R_1 = S_1 R1=S1,所以如果 S 1 = T j S_1 = T_j S1=Tj,则 T j T_j Tj 可以作为 R 1 R_1 R1 的子序列, f ( 1 , j , 1 ) = 1 f(1,j,1) = 1 f(1,j,1)=1。
当 i > 1 i>1 i>1 时, f ( i , j , k ) f(i,j,k) f(i,j,k) 对应的状态会分为两种情况:
1,不考虑 S i S_i Si
若从下标 j j j 开始,长度为 x x x 的部分在 P 1 P_1 P1 中,则剩下的部分,即从下标 j + x j+x j+x 开始,长度为 k − x k-x k−x 的部分在 P 2 P_2 P2 中。
因为 P 1 = P 2 = R i − 1 P_1 = P_2 = R_{i-1} P1=P2=Ri−1,因此长度为 x x x 的部分在 P 1 P_1 P1 中以子序列出现的次数为 f ( i − 1 , j , x ) f(i-1,j,x) f(i−1,j,x),从下标 j + x j+x j+x 开始,长度为 k − x k-x k−x 的部分在 P 2 P_2 P2 中以子序列出现的次数为 f ( i − 1 , j + x , k − x ) f(i-1,j+x,k-x) f(i−1,j+x,k−x),该情况对应的子序列出现次数则为 f ( i − 1 , j , x ) ∗ f ( i − 1 , j + x , k − x ) f(i-1,j,x)*f(i-1,j+x,k-x) f(i−1,j,x)∗f(i−1,j+x,k−x)。
因为 x x x 的取值范围为 [ 0 , k ] [0,k] [0,k],所以不考虑 S i S_i Si,从下标 j j j 开始,长度为 k k k 的子串,在 R i R_i Ri 中以子序列形式出现的次数为 ∑ x = 0 k f ( i − 1 , j , x ) ∗ f ( i − 1 , j + x , k − x ) \sum_{x=0}^k f(i-1,j,x)*f(i-1,j+x,k-x) ∑x=0kf(i−1,j,x)∗f(i−1,j+x,k−x)。
注意,在计算类似 f ( i , 1 , m ) f(i,1,m) f(i,1,m) 涉及到右边界的状态时,会涉及到 f ( i − 1 , m + 1 , 0 ) f(i-1,m+1,0) f(i−1,m+1,0),这里 f ( i , 1 , m ) f(i,1,m) f(i,1,m) 会涉及到 f ( i − 1 , 1 , m ) , f ( i − 1 , m + 1 , 0 ) f(i-1,1,m),f(i-1,m+1,0) f(i−1,1,m),f(i−1,m+1,0),表示 T T T 从下标 1 1 1 开始,长度为 m m m 的部分都在 P 1 P_1 P1 的情况,这是合法的情况,所以要设置 f ( i − 1 , m + 1 , 0 ) = 1 f(i-1,m+1,0)=1 f(i−1,m+1,0)=1,否则默认为 0 0 0 可能会使得一些状态没有算进去。
2,考虑
S
i
S_i
Si
从下标
j
j
j 开始,长度为
x
−
1
x-1
x−1 的部分在
P
1
P_1
P1 中,然后
T
j
+
x
−
1
T_{j+x-1}
Tj+x−1 为
S
i
S_i
Si,从下标
j
+
x
j+x
j+x 开始,长度为
k
−
x
k-x
k−x 的部分在
P
2
P_2
P2 中。
和不考虑 S i S_i Si 的情况是同理的,就是需要判断 T j + x − 1 T_{j+x-1} Tj+x−1 是否和 S i S_i Si 相同,如果不同的话,那么对应情况不成立,否则对应情况的以子序列出现的次数为 f ( i − 1 , j , x − 1 ) ∗ f ( i − 1 , j + x , k − x ) f(i-1,j,x-1)*f(i-1,j+x,k-x) f(i−1,j,x−1)∗f(i−1,j+x,k−x)。
因为 x − 1 x-1 x−1 最小为 0 0 0,所以 x x x 的取值范围为 [ x , k ] [x,k] [x,k],因此考虑 S i S_i Si,,从下标 j j j 开始,长度为 k k k 的子串,在 R i R_i Ri 中以子序列形式出现的次数为 ∑ x = 1 k f ( i − 1 , j , x − 1 ) ∗ f ( i − 1 , j + x , k − x ) ∗ [ S i = = T j + x − 1 ] \sum_{x=1}^k f(i-1,j,x-1)*f(i-1,j+x,k-x)*[S_i == T_{j+x-1}] ∑x=1kf(i−1,j,x−1)∗f(i−1,j+x,k−x)∗[Si==Tj+x−1]。( [ ] [] [] 表示艾佛森括号,若 x x x 成立则 [ x ] = 1 [x]=1 [x]=1,否则 [ x ] = 0 [x]=0 [x]=0)
综上,当 i > 1 i>1 i>1 时, f ( i , j , k ) = ∑ x = 0 k f ( i − 1 , j , x ) ∗ f ( i − 1 , j + x , k − x ) + ∑ x = 1 k f ( i − 1 , j , x − 1 ) ∗ f ( i − 1 , j + x , k − x ) ∗ [ S i = = T j + x − 1 ] f(i,j,k) = \sum_{x=0}^k f(i-1,j,x)*f(i-1,j+x,k-x) + \sum_{x=1}^k f(i-1,j,x-1)*f(i-1,j+x,k-x)*[S_i == T_{j+x-1}] f(i,j,k)=∑x=0kf(i−1,j,x)∗f(i−1,j+x,k−x)+∑x=1kf(i−1,j,x−1)∗f(i−1,j+x,k−x)∗[Si==Tj+x−1]
复杂度
时间复杂度
O
(
∣
S
∣
∣
T
∣
3
)
O(|S||T|^3)
O(∣S∣∣T∣3),遍历所有的状态复杂度为
O
(
∣
S
∣
∣
T
∣
2
)
O(|S||T|^2)
O(∣S∣∣T∣2),状态计算的复杂度为
O
(
∣
T
∣
)
O(|T|)
O(∣T∣)
空间复杂度
O
(
∣
S
∣
∣
T
∣
3
)
O(|S||T|^3)
O(∣S∣∣T∣3)
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e2 + 5, mod = 998244353;
int n, m;
string s, t;
int f[N][N][N];
void solve()
{
cin >> s >> t;
n = s.size(), m = t.size();
s = ' ' + s, t = ' ' + t;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m + 1; j++) {
f[i][j][0] = 1;
}
}
for (int i = 1; i <= m; i++) {
f[1][i][1] = s[1] == t[i];
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int k = 1; j + k - 1 <= m; k++) {
for (int x = 0; x <= k; x++) {
f[i][j][k] = (f[i][j][k] + f[i - 1][j][x] * f[i - 1][j + x][k - x] % mod) % mod;
if (x > 0 && s[i] == t[j + x - 1]) {
f[i][j][k] = (f[i][j][k] + f[i - 1][j][x - 1] * f[i - 1][j + x][k - x] % mod) % mod;
}
}
}
}
}
cout << f[n][1][m];
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while (T--) {
solve();
}
}
E. 随机过程
题意
有
n
n
n 个长度为
m
m
m 的字符串,每个字符串的每一位字符都是
[
a
,
z
]
[a,z]
[a,z] 中等概率随机选一个。
将
n
n
n 个字符串插入字典树,问字典树上节点个数的最大值与期望。
(答案对 998244353 998244353 998244353 取模)
数据范围
1 ≤ n , m ≤ 1 0 5 1 \le n,m \le 10^5 1≤n,m≤105
思路
如果到达字典树的某个节点,要插入字符串的某个字符时,如果该节点存在对应字符的子节点,就会走向子节点,然后插入下一个字符,否则就会新建子节点后再操作。
因为字符集为 [ a , z ] [a,z] [a,z],所以一个节点最多会存在 26 26 26 个子节点,第 i i i 层的最大节点数为 2 6 i 26^i 26i。
如果 2 6 i ≥ n 26^i \ge n 26i≥n,则插入 n n n 个字符串,总能新建 n n n 个不同节点插入 n n n 个字符串的第 m m m 位字符,否则最多新建 2 6 i 26^i 26i 个节点。
由于字符串的长度为 m m m,算上根节点的一层,字典树的层数为 m + 1 m+1 m+1,因此字典树中节点个数的最大值为 ∑ i = 0 m m i n ( 2 6 i , n ) \sum_{i=0}^m min(26^i,n) ∑i=0mmin(26i,n)。
对于字典树第 i i i 层的一个节点,令从根节点到该节点的边所构成的字符串为 T T T,若只插入一个字符串,该节点要存在的话, T T T 必然为插入的字符串的前缀,因为 T T T 的长度为 i i i,所以该节点存在的概率为 1 2 6 i \frac{1}{26^i} 26i1,不存在的概率为 1 − 1 2 6 i 1-\frac{1}{26^i} 1−26i1。
但是此时的问题是插入 n n n 个字符串,如果该节点存在,那么可能是有一个字符串的前缀为 T T T,有两个字符串的前缀为 T T T,…,有 n n n 个字符串的前缀为 T T T,情况很多不好求,但是可以不存在的概率是好求的,该节点不存在意味着 n n n 个字符串都不存在前缀 T T T,只有一种情况,对应概率为 ( 1 − 1 2 6 i ) n (1-\frac{1}{26^i})^n (1−26i1)n,因此字典树第 i i i 层的某个节点存在的概率为 1 − ( 1 − 1 2 6 i ) n 1-(1-\frac{1}{26^i})^n 1−(1−26i1)n。
因为第 i i i 层的最多存在的节点数为 2 6 i 26^i 26i,而每个节点都是等概率的,所以第 i i i 层的期望节点数为 2 6 i ∗ ( 1 − ( 1 − 1 2 6 i ) n ) 26^i*(1-(1-\frac{1}{26^i})^n) 26i∗(1−(1−26i1)n),字典树有 m m m 层,则字典树的期望节点数为 ∑ i = 0 m 2 6 i ∗ ( 1 − ( 1 − 1 2 6 i ) n ) \sum_{i=0}^m 26^i*(1-(1-\frac{1}{26^i})^n) ∑i=0m26i∗(1−(1−26i1)n)。
因为答案对 998244353 998244353 998244353 取模,所以 1 2 6 i \frac{1}{26^i} 26i1 需要转换为在 998244353 998244353 998244353 模意义下的乘法逆元,可以利用费马小定理和快速幂求解,求每层的节点数也可以用快速幂求解。
复杂度
时间复杂度为
O
(
m
log
n
)
O(m \log n)
O(mlogn),
O
(
m
)
O(m)
O(m) 为遍历层数的复杂度,
O
(
log
n
)
O(\log n)
O(logn) 是计算每一层期望节点数使用的快速幂的复杂度。
空间复杂度为
O
(
1
)
O(1)
O(1)
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 998244353;
int n, m;
int qmi(int a, int b)
{
a %= mod;
int res = 1;
while (b) {
if (b & 1)
res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res;
}
int inv(int x)
{
return qmi(x, mod - 2);
}
void solve()
{
cin >> n >> m;
int ma = 1, cur = 1;
for (int i = 1; i <= m; i++) {
cur = min(cur * 26, n);
ma = (ma + cur) % mod;
}
cout << ma << ' ';
int sum = 0;
for (int i = 0; i <= m; i++) {
int p = qmi(1 - qmi(inv(26), i) + mod, n);
// 不存在的概率
int q = (1 - p + mod) % mod;
// 存在的概率
int num = qmi(26, i);
// 该层节点数
sum = (sum + num * q % mod) % mod;
}
cout << sum;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while (T--) {
solve();
}
}
J. 找最小
题意
给出两个长度为 n n n 的序列 a , b a,b a,b,可以若干次选择一个下标 i i i,然后交换 a i , b i a_i,b_i ai,bi。
令 f ( A ) = ⊕ i = 1 n A i f(A) = \oplus^n_{i=1} A_i f(A)=⊕i=1nAi,问若干次交换之后, m a x ( f ( a ) , f ( b ) ) max(f(a),f(b)) max(f(a),f(b)) 最小可能是多少?
数据范围
1
≤
T
≤
1
0
5
1 \le T \le 10^5
1≤T≤105,
T
T
T 表示数据组数
1
≤
n
,
∑
n
≤
1
0
6
,
0
≤
a
i
<
2
31
1 \le n,\sum n \le 10^6,0 \le a_i < 2^{31}
1≤n,∑n≤106,0≤ai<231
思路
设最开始未进行交换时候的 f ( a ) = x a , f ( b ) = x b f(a) = xa,f(b) = xb f(a)=xa,f(b)=xb。
如果交换 a i , b i a_i,b_i ai,bi,通过异或的性质 x ⊕ x = 0 x \oplus x = 0 x⊕x=0,那么交换之后 f ( a ) = x a ⊕ a i ⊕ b i = ( a 1 ⊕ a 2 ⊕ . . . ⊕ a n ) ⊕ a i ⊕ b i = a 1 ⊕ a 2 ⊕ . . . ⊕ b i ⊕ . . . ⊕ a n , f ( b ) = x b ⊕ a i ⊕ b i f(a) = xa \oplus a_i \oplus b_i =(a_1 \oplus a_2 \oplus ... \oplus a_n) \oplus a_i \oplus b_i = a_1 \oplus a_2 \oplus ... \oplus b_i \oplus ... \oplus a_n,f(b) = xb \oplus a_i \oplus b_i f(a)=xa⊕ai⊕bi=(a1⊕a2⊕...⊕an)⊕ai⊕bi=a1⊕a2⊕...⊕bi⊕...⊕an,f(b)=xb⊕ai⊕bi。
设 c i = a i ⊕ b i c_i = a_i \oplus b_i ci=ai⊕bi,那么选择若干个下标进行交换,就相当于选择 c c c 中的子序列,若选择的子序列异或和为 v v v,那么交换后 f ( a ) = x a ⊕ v , f ( b ) = x b ⊕ v f(a)=xa \oplus v,f(b) = xb \oplus v f(a)=xa⊕v,f(b)=xb⊕v,此时问题就转换为找出一个 c c c 的子序列异或和 v v v,能使得 m a x ( x a ⊕ v , x b ⊕ v ) max(xa \oplus v,xb \oplus v) max(xa⊕v,xb⊕v) 最小化。
子序列异或问题可以用异或线性基解决,异或线性基就是通过序列构造的一个集合,该集合任意一个子集的异或和都对应序列中某个子序列的异或和。
异或线性基中,每个元素都有不同的二进制的最高有效位,即每个元素在二进制下最高位的 1 1 1 所在的位数是不同的。
因为越高位权重越大,所以应该优先消去 x a , x b xa,xb xa,xb 高位上的 1 1 1,因此可以按最高有效位,从高到低地遍历通过 c c c 构造出的线性基中的元素,如果 x a , x b xa,xb xa,xb 与当前遍历到的元素进行异或后,使得 m a x ( x a , x b ) max(xa,xb) max(xa,xb) 变小,那么就异或上该元素,这样遍历一次之后,得到的 m a x ( x a , x b ) max(xa,xb) max(xa,xb) 即为可以取到的最小值。
复杂度
时间复杂度为
O
(
n
log
2
a
i
)
O(n \log_2 a_i)
O(nlog2ai),因为涉及遍历二进制位,所以通过序列构造线性基的复杂度为
O
(
n
log
2
a
i
)
O(n \log_2 a_i)
O(nlog2ai)。
空间复杂度为
O
(
m
a
x
(
log
2
c
i
)
)
O(max(\log_2 c_i))
O(max(log2ci)),因为线性基中的元素最高有效位不同,所以线性基中元素数量最多为
c
i
c_i
ci 的最大二进制位数。
代码实现
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 5, M = 32;
int n;
int a[N], b[N], p[M];
// 向线性基中插入x
void insert(int x)
{
for (int i = M - 1; i >= 0; i--) {
if ((x >> i) & 1) {
if (!p[i]) {
p[i] = x;
break;
}
x ^= p[i];
}
}
}
void solve()
{
cin >> n;
int sa = 0, sb = 0;
// sa,sb为序列a,b的初始异或和
memset(p, 0, sizeof(p));
for (int i = 1; i <= n; i++) {
cin >> a[i];
sa ^= a[i];
}
for (int i = 1; i <= n; i++) {
cin >> b[i];
sb ^= b[i];
insert(a[i] ^ b[i]);
}
for (int i = M - 1; i >= 0; i--) {
int xa = sa ^ p[i];
int xb = sb ^ p[i];
if (max(xa, xb) < max(sa, sb)) {
sa = xa, sb = xb;
}
}
cout << max(sa, sb) << '\n';
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T = 1;
cin >> T;
while (T--) {
solve();
}
}