当前位置: 首页 > article >正文

2024 ccpc 网络赛题解

比赛链接:https://codeforces.com/gym/105336

L. 网络预选赛

题意

给出一个 n ∗ m n*m nm 的字符矩阵,问该矩阵内存在多少个子矩阵 [ 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 2n,m500

思路

两重循环遍历字符矩阵,找到所有为 [ c   c p   c ] [\begin{array}{} c \ c \\ p \ c \end{array}] [c cp c] 的子矩阵即可

复杂度

时间复杂度 O ( n ∗ m ) O(n*m) O(nm),空间复杂度 O ( n ∗ m ) O(n*m) O(nm)

代码实现

#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 nm 的字符矩阵 S S S,要查询一个 a ∗ b a*b ab 的字符矩阵 T T T S S S 中出现的次数?( 1 ≤ a ≤ n , 1 ≤ b ≤ m 1 \le a \le n,1 \le b \le m 1an,1bm
例题链接: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 v1base2a1+v2base2a2+...+vn即为 T T T的哈希值,这步的时间复杂度为 O ( a ∗ b ) O(a*b) O(ab)
然后处理出 S S S每一行的前缀字符串哈希值,用于求某行中子串的哈希值。

S S S中的 a ∗ b a*b ab的子矩阵的右下角 ( 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(ia+1,jb+1,j)base2a1+...+hash(i,jb+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(ia+1,jb+1,j)base2a1这一项,然后剩下的部分乘上 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,jb+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) (na+1)(mb+1)个右下角就可以枚举完所有的子矩阵,复杂度是 O ( n ∗ m ) O(n*m) O(nm)的。

复杂度

时间复杂度为 O ( n ∗ m + a ∗ b ) O(n*m+a*b) O(nm+ab),空间复杂度为 O ( n ∗ m ) O(n*m) O(nm)

代码实现
#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 ab的子矩阵是否为 T T T,一般的想法都是从第 i i i行开始,看第 i i i行中区间为 [ j , j + b − 1 ] [j,j+b-1] [j,j+b1]对应的子串是否和 T T T的第一行相同,如果相同,就看第 i + 1 i+1 i+1行中区间为 [ j , j + b − 1 ] [j,j+b-1] [j,j+b1]对应的子串是否和 T T T的第二行相同,如果不同,说明以 ( i , j ) (i, j) (i,j)为左上角的 a ∗ b a*b ab的子矩阵一定不为 T T T,结束匹配,若可以匹配到第 i + a − 1 i+a-1 i+a1行,且该行区间为 [ j , j + b − 1 ] [j,j+b-1] [j,j+b1]对应的子串和 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) (i1,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) (i1,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 nm的,从而优化空间复杂度。

使用 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(an(m+b))

复杂度

时间复杂度 O ( a ∗ n ∗ ( m + b ) ) O(a*n*(m+b)) O(an(m+b)),空间复杂度 O ( n ∗ m ) O(n*m) O(nm)

代码实现
#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=1nr=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 1n103,1ai106

思路

结论
当序列为升序或者降序的时候,不整齐度为最小值。

证明如下
当序列长度为 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(ni+1) 个区间包含 x x x x x x 对不整齐度的贡献为 i ∗ ( n − i + 1 ) ∗ x i*(n-i+1)*x i(ni+1)x,因为 x x x 为常数,所以该贡献的大小取决于 i ∗ ( n − i + 1 ) = − i 2 + ( n + 1 ) ∗ i i*(n-i+1) = -i^2+(n+1)*i i(ni+1)=i2+(n+1)i,可以发现这是一个凸函数,它的极大值点在 n + 1 2 \frac{n+1}{2} 2n+1,在两侧取得最小值,所以考虑把最大值先放到最左边或者最右边。

接着序列的长度就变成了 n − 1 n-1 n1,如果当前序列的最大值为 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 iai,当 i i i 为左端点时, a i a_i ai 会作为区间的最小值,对应的右端点选择区间为 [ i , n ] [i,n] [i,n],有 n − i + 1 n-i+1 ni+1 种选择,对 “不整齐度” 的贡献为 − ( n − i + 1 ) ∗ a i -(n-i+1)*a_i (ni+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 val2

复杂度

时间复杂度为 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 1T104,表示数据组数, 1 ≤ n , k ≤ 1 0 9 1 \le n,k \le 10^9 1n,k109

思路

结论

如果 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(xk),因为 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(nx)=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=1Ri1+Si+Ri1i>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 1S,T100

思路

注意到, R i = R i − 1 + S i + R i − 1 R_i = R_{i-1} + S_i + R_{i-1} Ri=Ri1+Si+Ri1,令前面的 R i − 1 R_{i-1} Ri1 P 1 P_1 P1,后面的 R i − 1 R_{i-1} Ri1 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 v1v2

如果考虑 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 w1w2

因为 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} Ri1 推到 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 1in,1jm,0kmj+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(1in,1jm+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 kx 的部分在 P 2 P_2 P2 中。

因为 P 1 = P 2 = R i − 1 P_1 = P_2 = R_{i-1} P1=P2=Ri1,因此长度为 x x x 的部分在 P 1 P_1 P1 中以子序列出现的次数为 f ( i − 1 , j , x ) f(i-1,j,x) f(i1,j,x),从下标 j + x j+x j+x 开始,长度为 k − x k-x kx 的部分在 P 2 P_2 P2 中以子序列出现的次数为 f ( i − 1 , j + x , k − x ) f(i-1,j+x,k-x) f(i1,j+x,kx),该情况对应的子序列出现次数则为 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(i1,j,x)f(i1,j+x,kx)

因为 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(i1,j,x)f(i1,j+x,kx)

注意,在计算类似 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(i1,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(i1,1,m),f(i1,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(i1,m+1,0)=1,否则默认为 0 0 0 可能会使得一些状态没有算进去。

2,考虑 S i S_i Si
从下标 j j j 开始,长度为 x − 1 x-1 x1 的部分在 P 1 P_1 P1 中,然后 T j + x − 1 T_{j+x-1} Tj+x1 S i S_i Si,从下标 j + x j+x j+x 开始,长度为 k − x k-x kx 的部分在 P 2 P_2 P2 中。

和不考虑 S i S_i Si 的情况是同理的,就是需要判断 T j + x − 1 T_{j+x-1} Tj+x1 是否和 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(i1,j,x1)f(i1,j+x,kx)

因为 x − 1 x-1 x1 最小为 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(i1,j,x1)f(i1,j+x,kx)[Si==Tj+x1]。( [ ] [] [] 表示艾佛森括号,若 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(i1,j,x)f(i1,j+x,kx)+x=1kf(i1,j,x1)f(i1,j+x,kx)[Si==Tj+x1]

复杂度

时间复杂度 O ( ∣ S ∣ ∣ T ∣ 3 ) O(|S||T|^3) O(S∣∣T3),遍历所有的状态复杂度为 O ( ∣ S ∣ ∣ T ∣ 2 ) O(|S||T|^2) O(S∣∣T2),状态计算的复杂度为 O ( ∣ T ∣ ) O(|T|) O(T)
空间复杂度 O ( ∣ S ∣ ∣ T ∣ 3 ) O(|S||T|^3) O(S∣∣T3)

代码实现

#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 1n,m105

思路

如果到达字典树的某个节点,要插入字符串的某个字符时,如果该节点存在对应字符的子节点,就会走向子节点,然后插入下一个字符,否则就会新建子节点后再操作。

因为字符集为 [ 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 26in,则插入 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} 126i1

但是此时的问题是插入 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 (126i1)n,因此字典树第 i i i 层的某个节点存在的概率为 1 − ( 1 − 1 2 6 i ) n 1-(1-\frac{1}{26^i})^n 1(126i1)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(126i1)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(126i1)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 1T105 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} 1n,n106,0ai<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 xx=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)=xaaibi=(a1a2...an)aibi=a1a2...bi...an,f(b)=xbaibi

c i = a i ⊕ b i c_i = a_i \oplus b_i ci=aibi,那么选择若干个下标进行交换,就相当于选择 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)=xav,f(b)=xbv,此时问题就转换为找出一个 c c c 的子序列异或和 v v v,能使得 m a x ( x a ⊕ v , x b ⊕ v ) max(xa \oplus v,xb \oplus v) max(xav,xbv) 最小化。

子序列异或问题可以用异或线性基解决,异或线性基就是通过序列构造的一个集合,该集合任意一个子集的异或和都对应序列中某个子序列的异或和。

异或线性基中,每个元素都有不同的二进制的最高有效位,即每个元素在二进制下最高位的 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();
    }
}

http://www.kler.cn/a/310047.html

相关文章:

  • 如何管理PHP API版本
  • 嵌入式开发之线程
  • Flutter 插件 sliding_up_panel 实现从底部滑出的面板
  • SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder”
  • React diff算法和Vue diff算法的主要区别
  • 算法详解——链表的归并排序非递归解法
  • SEO之页面优化(一-页面标题2)
  • Java操作数栈分析
  • 【JAVA基础】实现Tomcat基本功能
  • PyQt5-QCheckBox-开关按钮
  • Maven详细介绍
  • 【postgres】笔记
  • 重修设计模式-结构型-适配器模式
  • Doker学习笔记--黑马
  • Unity从2018.1版本开始,可以采用内置JSON进行存档和读档
  • windows C++ 并行编程-异步代理库概述
  • git 删除远程分支的几种写法
  • 基于stm32的四旋翼无人机控制系统设计系统设计与实现
  • vs2022配置opencv==4.9.0(C++)
  • 所有用贪心的算法和所有用动态规划(dp)的算法合集
  • Linux C高级 day1
  • 【线程】线程的控制
  • 【React Native】路由和导航
  • 【PLW004】基于Python网络爬虫与推荐算法的新闻推荐平台v1.0(Python+Django+NLP+Vue+MySQL前后端分离)
  • 【浅水模型MATLAB】尝试复刻SCI论文中的溃坝流算例
  • 如何使用go生成可执行文件