2025.1.12 CW 模拟赛
T1
思路
首先可以处理出每对括号的位置.
考虑怎么处理每对括号. 对于相邻的几对括号中的任意一对, 它可以选择的方案数为 \((左侧对数 + 1) \times (右侧对数 + 1)\). 那么我们只需要从左到右扫一遍, 记录一下每个连通块内的括号数量即可.
在实现上, 因为需要做区间加法, 我们使用差分数组记录. 再在统计答案的时候做一遍前缀和就行了.
时间复杂度 \(\mathcal{O}(n)\).
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
using namespace std;
typedef pair<int, int> pii;
constexpr int N = 1e7 + 10, mod = 1e9 + 7;
int len, fa[N];
basic_string<pii> v;
string s;
stack<int> st;
void init() {
cin >> s;
len = s.size(), s = ' ' + s;
iota(fa, fa + len + 1, 0);
for (int i = 1; i <= len; ++i) {
if (s[i] == ')' and !st.empty())
v.push_back({st.top(), i}), st.pop();
else if (s[i] == '(')
st.push(i);
}
}
long long ans[N];
int cnt[N], pre[N], to_l[N];
void calculate() {
for (auto [l, r] : v)
if (to_l[(to_l[r] = l) - 1]) fa[l] = fa[to_l[l - 1]];
int sz = v.size();
for (int i = 0; i ^ sz; ++i) pre[i] = ++cnt[fa[v[i].first]];
for (int i = 0; i ^ sz; ++i) {
int l = v[i].first, r = v[i].second;
ans[l] += 1ll * pre[i] * (cnt[fa[l]] - pre[i] + 1), ans[r + 1] -= 1ll * pre[i] * (cnt[fa[l]] - pre[i] + 1);
}
long long tot = 0;
for (int i = 1; i <= len; ++i) {
ans[i] += ans[i - 1];
tot += 1ll * ans[i] * i % mod;
}
cout << tot << '\n';
}
void solve() {
init();
calculate();
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false);
solve();
return 0;
}
T2
算法
动态规划, 数学.
思路
容易想到令 \(f_{i, 0/1}\) 表示到第 \(i\) 位的时候该段是否是倍数串的答案.
瓶颈在于找到倍数串, 考虑优化. 我们发现其实可以用一种类似于前缀的方法, 具体来说, 设 \(pre_i \gets pre_{i + 1} + 10^{n - i} \times S_i\), 那么区间 \([j, i]\) 为倍数串 \(\iff (pre_j - pre_{i + 1}) \div 10^{n - i} \equiv 0 \mod D\) .
观察 Subtask
, 思考 \(\gcd(D, 10) = 1\) 时怎么做.
显然, 由于 \(D\) 中不含因子 \(2, 5\), 所以上式等价于 \(pre_j \equiv pre_{i + 1} \mod D\), 开个桶记录下桶中前缀和即可.
再来考虑正解, 不妨设 \(D = 2^a \cdot 5^b \cdot x\), 将上式转化为一般情况:
转化根据中国剩余定理得到.
容易发现令 \(\omega = \min(a, b) \le 20\), 显然, 对于一个数若其超过 20 位, 一定有:
解释一下, 这里相当于将前缀和拆开来看, 其实就是一个带 \(10\) 的幂次的 \(S_i\) 之和.
时间复杂度 \(\mathcal{O}(n \omega)\), 其中 \(\omega \le 20\).
T3
算法
动态规划, 前缀和.
思路
先思考一个不考虑重复的 DP
. 设 \(f_{i, j}\) 表示删除第 \(i\) 行第 \(j\) 个的方案数, 那么就可以由第 \(i - 1\) 行的 \([j - k, j + 1]\) 转移而来:
但是这样会有重复, 因为在一块内删除每一个都是一样的, 我们需要删掉重复计算的贡献.
用 \(g_{i, j}\) 表示在第 \(i\) 行 \(j - 1, j\) 重复的部分, 如果 \(s_{i, j - 1} \ne s_{i, j}\) 两种方案显然不会等价, 否则他们相交的贡献区间为 \([j - k, j - 1 + k]\):
那么第 \(i\) 行删除掉第 \(j\) 个的方案其实是 \(f_{i, j} - g_{i, j}\).
直接转移是 \(\mathcal{O}(n^3)\) 的, 需要用前缀和优化一下.
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
using namespace std;
constexpr int N = 3e3 + 10, mod = 1e9 + 7;
int n, m, k;
char s[N][N];
void init() {
cin >> n >> m >> k;
for (int i = 1; i <= n; ++i)
cin >> (s[i] + 1), s[i][0] = '2';
}
int f[N][N], g[N][N];
void calculate() {
for (int j = 1; j <= m; ++j)
f[1][j] = 1, g[1][j] = (s[1][j] == s[1][j - 1]);
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= m; ++j)
f[i - 1][j] = (f[i - 1][j] + f[i - 1][j - 1]) % mod,
g[i - 1][j] = (g[i - 1][j] + g[i - 1][j - 1]) % mod;
for (int j = 1, l, r; j <= m; ++j) {
l = max(1ll, j - k), r = min(m, j + k);
f[i][j] = (f[i - 1][r] - f[i - 1][l - 1] + mod) % mod;
f[i][j] = (f[i][j] - (g[i - 1][r] - g[i - 1][l]) + mod) % mod;
if (s[i][j] != s[i][j - 1]) {
g[i][j] = 0;
continue;
}
r = min(m, j - 1 + k);
g[i][j] = (f[i - 1][r] - f[i - 1][l - 1] + mod) % mod;
g[i][j] = (g[i][j] - (g[i - 1][r] - g[i - 1][l]) + mod) % mod;
}
}
int ans = f[n][1];
for (int j = 2; j <= m; ++j)
ans = (ans + f[n][j] - g[n][j] + mod) % mod;
cout << ans << '\n';
}
void solve() {
init();
calculate();
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false);
solve();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步