ABC419_E,F题解

比赛网址

E

E - Subarray Sum Divisibility

题意

给定一个长度为N的整数序列A(每个元素满足0≤A_i<M)
通过多次操作(每次选择一个元素A_i加1),使得所有长度为L的连续子数组的和均为M的倍数。
要求求出达到该目标所需的最小操作次数。

题解

问题分析

我们需要通过最少的操作(每次将一个元素加1),使得所有长度为L的连续子数组的和都是M的倍数。由于每次操作只能增加元素值,且初始元素满足 \(0 \leq A_i < M\),最终每个元素调整后的值可表示为 \(A_i + k_i\)\((k_i \geq 0)\),且调整后的子数组和需满足 \(\sum (A_i + k_i) \equiv 0 \pmod{M}\)


关键观察:分组与模运算

所有长度为L的子数组的和可以分解为L个“位置组”的和。具体来说,对于位置 \(i\)(从0开始),其在所有子数组中的出现规律为:\(i, i+L, i+2L, \dots\)。因此,我们可以将原数组分为L个组 \(groups[0 \dots L-1]\),其中 \(groups[t]\) 包含所有满足 \(i \equiv t \pmod{L}\) 的元素 \(A_i\)

例如,当 \(L=3\) 时,\(groups[0] = {A_0, A_3, A_6, \dots}\)\(groups[1] = {A_1, A_4, A_7, \dots}\)\(groups[2] = {A_2, A_5, A_8, \dots}\)

每个子数组的和恰好包含每个组中的一个元素(例如,子数组 \([A_0, A_1, A_2]\) 包含 \(groups[0][0], groups[1][0], groups[2][0]\);子数组 \([A_1, A_2, A_3]\) 包含 \(groups[1][0], groups[2][0], groups[0][1]\))。因此,所有子数组和模M相等的条件等价于: 每个组内的元素调整后的值之和模M相等


动态规划解法

我们需要为每个组选择一个目标余数 \(r_t\)(调整后该组所有元素的和模M等于 \(r_t\)),使得所有 \(r_0 + r_1 + \dots + r_{L-1} \equiv 0 \pmod{M}\),且总操作次数最少。

步骤1:计算每组调整到余数r的成本

对于每个组 \(groups[t]\),若选择目标余数 \(r\),则每个元素 \(a \in groups[t]\) 需要调整 \((r - a + M) % M\) 次(因为 \(a\) 最终需满足 \(a + k \equiv r \pmod{M}\),即 \(k \equiv (r - a) \pmod{M}\),且 \(k \geq 0\))。总成本 \(cost[t][r]\) 是该组所有元素调整到余数r的操作次数之和。

步骤2:动态规划状态转移

定义 \(dp[t][s]\) 为前t个组调整后,总余数为s的最小操作次数。初始时 \(dp[0][0] = 0\)(前0个组总余数为0,无需操作),其余状态为无穷大。

状态转移方程:对于第t个组(从1到L),枚举前t-1个组的总余数 \(s_{ ext{prev}}\) 和当前组的目标余数 \(r\),则新的总余数 \(s_{ ext{new}} = (s_{ ext{prev}} + r) % M\),更新 \(dp[t][s_{ ext{new}}] = \min(dp[t][s_{ ext{new}}], dp[t-1][s_{ ext{prev}}] + cost[t-1][r])\)

最终答案为 \(dp[L][0]\)(所有L个组调整后总余数为0的最小操作次数)。

参考代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
ll N, M, L;
vector<ll> A;
vector<vector<ll>> groups, cost;
ll dp[505][505];

int main() {
    cin >> N >> M >> L;
    A.resize(N);
    for (ll i = 0; i < N; ++i) cin >> A[i];

    // 步骤1:将数组分为L个组
    groups.resize(L);
    for (ll t = 0; t < L; ++t)
        for (ll i = t; i < N; i += L)
            groups[t].push_back(A[i]);

    // 步骤2:计算每组调整到余数r的成本
    cost.resize(L, vector<ll>(M));
    for (ll t = 0; t < L; ++t)
        for (ll r = 0; r < M; ++r) {
            ll c = 0;
            for (ll a : groups[t])
                c += (r - a + M) % M;  // 调整次数:(r - a) mod M(保证非负)
            cost[t][r] = c;
        }

    // 步骤3:动态规划初始化
    for (ll s = 0; s < M; ++s) dp[0][s] = INF;
    dp[0][0] = 0;

    // 步骤4:状态转移
    for (ll t = 1; t <= L; ++t) {
        for (ll s = 0; s < M; ++s) dp[t][s] = INF;  // 初始化为无穷大
        for (ll s_prev = 0; s_prev < M; ++s_prev) {
            if (dp[t-1][s_prev] == INF) continue;
            for (ll r = 0; r < M; ++r) {  // 枚举当前组的目标余数r
                ll s_new = (s_prev + r) % M;
                dp[t][s_new] = min(dp[t][s_new], dp[t-1][s_prev] + cost[t-1][r]);
            }
        }
    }

    cout << dp[L][0] << endl;  // 所有L个组总余数为0的最小操作次数
    return 0;
}

复杂度分析

  • 分组:\(O(N)\)
  • 成本计算:\(O(L \cdot M \cdot \frac{N}{L}) = O(NM)\)(每组平均有 \(\frac{N}{L}\) 个元素)
  • 动态规划:\(O(L \cdot M^2)\)(状态数 \(L \cdot M\),每个状态转移 \(M\) 次)
  • 总复杂度为 \(O(NM + L \cdot M^2)\),在 \(N, M \leq 500\) 时可接受

F

F - All Included

题意

求长度为L的小写英文字符串中,同时包含所有N个输入字符串作为子串的数量,结果对998244353取模

题解

题目要求

计算长度为L的小写英文字符串中,同时包含所有给定N个字符串作为子串的数量(结果对998244353取模)。


核心思路

本题需高效处理多模式串的包含问题,采用 AC自动机(Aho-Corasick Automaton) 结合 动态规划(DP) 的方法:

  • AC自动机 :用于快速匹配多个模式串,记录当前状态下已匹配的模式串集合。
  • 动态规划 :跟踪字符串构建过程中已匹配的模式串集合(位掩码表示)和自动机状态,统计合法字符串数量。

代码关键步骤解析

1. AC自动机构建(build_automaton函数)

AC自动机是多模式匹配的核心结构,包含以下部分:

  • Trie树 :存储所有模式串的前缀。每个节点表示一个状态,边表示字符转移。
  • 失败指针(fail) :类似KMP算法的失败函数,用于匹配失败时跳转到其他可能的匹配路径。
  • 输出函数(output) :记录每个节点对应的模式串集合(位掩码),表示到达该节点时已匹配的模式串。

构建步骤

  • 初始化Trie树,插入所有模式串,每个模式串的结束节点标记其对应的位掩码(如第i个模式串对应 1<<i)。
  • BFS计算失败指针:每个节点的失败指针指向其父节点失败指针的对应字符子节点。
  • 合并输出:每个节点的输出集合需包含其失败指针路径上的所有输出(确保匹配到更短的模式串)。
2. 动态规划(DP)设计

定义状态 dp[pos][mask][state]

  • pos:当前构建的字符串长度(0到L)。
  • mask:已匹配的模式串集合(位掩码,如 mask=0b101表示第0和第2个模式串已匹配)。
  • state:当前在AC自动机中的状态(节点)。
  • 值:长度为 pos、状态为 state、已匹配 mask的字符串数量。

状态转移 : 对于每个位置 pos,遍历所有可能的 maskstate,尝试添加字符 c(a-z):

  • 通过AC自动机的转移函数 Trie[state][c]得到下一个状态 next_state
  • 新的 mask为原 masknext_state的输出集合的按位或(合并新匹配的模式串)。
  • 转移方程:dp[pos+1][new_mask][next_state] += dp[pos][mask][state](模998244353)。
3. 结果统计

最终需统计所有长度为L、且 mask为全1(即 (1<<N)-1,所有模式串都被匹配)的状态的数量之和。


参考代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 998244353;
const int MAX_NODES = 1000;  // AC自动机最大节点数
ll N, L;
vector<string> S;
int trie[MAX_NODES][26];  // Trie树,trie[u][c]表示节点u经字符c转移后的节点
int fail[MAX_NODES];       // 失败指针
int output[MAX_NODES];     // 输出集合(位掩码)
ll node_count;
ll dp[101][1 << 8][MAX_NODES];  // DP数组:[长度][已匹配掩码][自动机状态]

// 构建AC自动机
void build_automaton() {
    memset(trie, -1, sizeof(trie));
    memset(fail, 0, sizeof(fail));
    memset(output, 0, sizeof(output));
    node_count = 1;
    // 插入所有模式串到Trie树
    for (ll i = 0; i < N; ++i) {
        string &s = S[i];
        ll cur = 0;
        for (char c : s) {
            ll idx = c - 'a';
            if (trie[cur][idx] == -1)  // 新建节点
                trie[cur][idx] = node_count++;
            cur = trie[cur][idx];
        }
        output[cur] |= (1 << i);  // 标记模式串i的结束节点
    }
    // BFS计算失败指针
    queue<ll> q;
    for (ll i = 0; i < 26; ++i) {
        if (trie[0][i] != -1) {
            q.push(trie[0][i]);
            fail[trie[0][i]] = 0;
        } else
            trie[0][i] = 0;  // 根节点的失败指针指向自己
    }
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        for (ll c = 0; c < 26; ++c) {
            ll v = trie[u][c];
            if (v != -1) {
                ll f = fail[u];
                fail[v] = trie[f][c];  // 失败指针跳转
                output[v] |= output[fail[v]];  // 合并输出集合
                q.push(v);
            } else
                trie[u][c] = trie[fail[u]][c];  // 转移失败时跳转到失败指针的对应转移
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cin >> N >> L;
    S.resize(N);
    for (ll i = 0; i < N; ++i) cin >> S[i];
    build_automaton();
    memset(dp, 0, sizeof(dp));
    dp[0][0][0] = 1;  // 初始状态:长度0,未匹配任何模式串,位于根节点
    // 动态规划转移
    for (ll pos = 0; pos < L; ++pos) {
        for (ll mask = 0; mask < (1 << N); ++mask) {
            for (ll state = 0; state < node_count; ++state) {
                if (dp[pos][mask][state]) {
                    // 尝试添加每个可能的字符(a-z)
                    for (ll c = 0; c < 26; ++c) {
                        ll next_state = trie[state][c];
                        ll new_mask = mask | output[next_state];
                        dp[pos + 1][new_mask][next_state] = 
                            (dp[pos + 1][new_mask][next_state] + dp[pos][mask][state]) % MOD;
                    }
                }
            }
        }
    }
    // 统计所有长度为L且匹配所有模式串的情况
    ll ans = 0, full_mask = (1 << N) - 1;
    for (ll state = 0; state < node_count; ++state)
        ans = (ans + dp[L][full_mask][state]) % MOD;
    cout << ans;
    return 0;
}

复杂度分析

  • AC自动机构建:时间复杂度为 O(M)(M为所有模式串总长度)。
  • 动态规划:状态数为 L × (1<<N) × node_countnode_count为AC自动机节点数,最多约1000),转移次数为 L × (1<<N) × node_count × 26。在 N≤8L≤100时,该复杂度可接受。
posted @ 2025-08-17 08:57  Nthtofade  阅读(160)  评论(0)    收藏  举报