ABC419_E,F题解
E
题意
给定一个长度为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
题意
求长度为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,遍历所有可能的 mask和 state,尝试添加字符 c(a-z):
- 通过AC自动机的转移函数
Trie[state][c]得到下一个状态next_state。 - 新的
mask为原mask与next_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_count(node_count为AC自动机节点数,最多约1000),转移次数为L × (1<<N) × node_count × 26。在N≤8、L≤100时,该复杂度可接受。

浙公网安备 33010602011771号