NOIP 模拟赛 10 比赛总结
分数:\(100 + 70 + 30 + 15 = 215\)
我钦定这几次模拟赛的质量是严格单调递减的。
由于今天有熨斗比赛,很多大佬都没有参与这次模拟赛,竟使我意外捞到了 rank 1。作为最后一场正式的模拟赛,也算是完结撒花了吧。
T1
这道题,没有高精度就是水,有了高精度就是屎。
考虑一个递归函数 \(f(x)\):
- 当 \(x = 1\) 时,返回 \(\texttt{2(0)}\)。
- 当 \(x = 2\) 时,返回 \(\texttt{2}\)。
- 对于其它情况,将 \(x\) 转为二进制。记 \(x = 2^{a_1} + 2^{a_2} + \cdots + 2^{a_n}\)。对于这个多项式中的每一项,如果 \(a_i = 1\),就在要返回的字符串后面加上 \(\texttt{+2(0)}\);如果 \(a_i = 2\),就加上 \(\texttt{2}\);否则,就加上 \(\texttt{+2(}f(x)\texttt{)}\)。
别忘了这道题要用高精就行了。
#include <bits/stdc++.h>
#define int long long
const int N = 1e5+10;
std::string n;
std::pair<std::string, int> divideBy2(std::string x) {
int len = x.size();
for (auto &ch : x) {
ch -= '0';
}
std::string res;
res.resize(x.size());
int a = 0;
for (int i = 0; i < len; i++) {
res[i] = (a * 10 + x[i]) / 2;
a = (a * 10 + x[i]) % 2;
}
int resLen = 0;
while (res[resLen] == 0 && resLen < len) {
resLen++;
}
for (auto &ch : res) ch += '0';
return {res.substr(resLen).empty() ? "0" : res.substr(resLen), x.back() % 2};
}
std::string solve(std::string n) {
if (n == "1") {
return "2(0)";
} else if (n == "2") {
return "2";
} else {
std::vector<int> res;
int i = 0;
while (n != "0") {
std::pair<std::string, int> tmp = divide2(n);
if (tmp.second == 1) res.push_back(i);
n = tmp.first;
i++;
}
std::reverse(res.begin(), res.end());
std::string next;
for (int i = 0; i < res.size(); i++) {
if (i != 0) next.push_back('+');
if (res[i] == 0) {
next = next + "2(0)";
} else if (res[i] == 1) {
next = next + "2";
} else next = next + "2(" + solve(std::to_string(res[i])) + ")";
}
return next;
}
}
signed main() {
freopen("pow.in", "r", stdin);
freopen("pow.out", "w", stdout);
std::ios::sync_with_stdio(false); std::cin.tie(0);
std::cin >> n;
std::cout << solve(n) << '\n';
return 0;
}
T2
出题人真是太良心了,竟然给了暴力做法 70 分。
首先,我们可以预处理前缀哈希,用于快速判断子串是否重复,当发现重复子串时,在之前出现的位置对标记 \(-1\),这样在后续 DP 计算时,重复的子串只会计数一次。
接着,我们定义 \(\operatorname{ans}(l, r)\) 表示子串 \([l, r]\) 中不同子串的数量。有状态转移方程:
意为去掉左端点的子串的不同子串数加上去掉右端点的子串的不同子串数,减去重复计算的部分,再加上当前子串本身。可以看出这是一个区间 DP,因此我们外层循环区间长度(\(r - l + 1\)),内层循环 \(l\)。
为了更清晰一点,我附上了由 DeepSeek 倾情注释的代码(代码本身是我自己写的):
#include <bits/stdc++.h>
#define int long long
const int N = 3010, BASE = 19260817; // 定义常数:最大长度和哈希基数
int n, q; std::string s;
unsigned long long hash[N], basePow[N]; // 哈希数组和基数幂数组
int ans[N][N]; // DP数组,ans[l][r]存储子串s[l...r]的答案
unsigned long long getHash(int l, int r) {
return hash[r] - hash[l-1] * basePow[r-l+1]; // 计算子串s[l...r]的哈希值
}
std::unordered_map<unsigned long long, int> mp; // 用于记录哈希值对应的起始位置
signed main() {
freopen("substring.in", "r", stdin);
freopen("substring.out", "w", stdout);
std::ios::sync_with_stdio(false); std::cin.tie(0);
// 读入数据
std::cin >> n >> q;
std::cin >> s;
s = " " + s; // 让字符串下标从1开始
// 预处理哈希
basePow[0] = 1;
for (int i = 1; i <= n; i++) {
hash[i] = hash[i-1] * BASE + s[i]; // 计算前缀哈希
basePow[i] = basePow[i-1] * BASE; // 计算BASE的幂次
}
// 关键步骤:处理重复子串
for (int len = 1; len <= n; len++) { // 枚举所有可能的子串长度
mp.clear(); // 清空哈希表
for (int l = 1; l + len - 1 <= n; l++) { // 枚举起始位置
int r = l + len - 1;
unsigned long long tmp = getHash(l, r); // 计算当前子串哈希
// 如果这个子串之前出现过,在之前出现的位置做标记
// 表示这个子串不应该被重复计数
ans[mp[tmp]][r]--;
// 记录当前子串的最新出现位置
mp[tmp] = l;
}
}
// DP计算所有区间的答案
for (int l = n; l >= 1; l--) { // 从后往前枚举左端点
for (int r = l; r <= n; r++) { // 从前往后枚举右端点
// 容斥原理:ans[l][r] = ans[l+1][r] + ans[l][r-1] - ans[l+1][r-1] + 1
// +1 是因为当前子串s[l...r]本身是一个新的子串
ans[l][r] += ans[l+1][r] + ans[l][r-1] - ans[l+1][r-1] + 1;
}
}
// 处理查询
while (q--) {
int l, r;
std::cin >> l >> r;
std::cout << ans[l][r] << '\n'; // 直接输出预处理好的答案
}
return 0;
}
T3
这道题题解的想法可谓妙哉。
本题的关键在于如何去重,也就是对于有多种组合方式的新咒语,怎么保证其只被计算一次。我们可以构建两棵字典树 \(tr_1, tr_2\),将所有单词正序插入 \(tr_1\),即维护其前缀;倒序插入 \(tr_2\),即维护其后缀。在 \(tr_1\) 上进行 DFS,访问到一个节点时,如果它的子节点中没有某个字母(设为 \(c\)),那么就让答案加上以 \(c\) 开头的不同后缀的数量,也就是 \(tr_2\) 中字母为 \(c\) 的结点数量。
但这样会忽略一个问题:如果存在一个存在一个以 \(c\) 结尾的词,那么即使一个节点的子节点中包含 \(c\),也可以通过拼接得到这个前缀加 \(c\) 的结果,如 \(\texttt{abc}\) 和 \(\texttt{dc}\) 可以拼接出 \(\texttt{abc}\),造成重复,这是刚才的算法所避免不了的。所以我们记录每个字符串的尾字母。此时如果一个词的长度 \(|S| \ge 2\),那么它能够被自己的前 \(|S|-1\) 个字符和最后一个字符连起来得到。但如果 \(|S| = 1\),这个词就无法被构造出来,而它确实存在于新的词典中,需要将 \(\mathrm{ans}\) 加上 \(1\)。
#include <bits/stdc++.h>
#define int long long
const int N = 4e5+10;
struct Trie {
int cnt, letterCnt[26], trie[N][26];
void insert(std::string s, bool isRev) {
if (isRev) std::reverse(s.begin(), s.end());
int p = 0;
for (auto &ch : s) {
int c = ch - 'a';
if (!trie[p][c]) {
trie[p][c] = ++cnt;
if (isRev) letterCnt[c]++;
}
p = trie[p][c];
}
}
} tr1, tr2;
int ans;
int n;
bool end[26], singleLetterStart[26];
std::string s[N];
signed main() {
freopen("magic.in", "r", stdin);
freopen("magic.out", "w", stdout);
std::ios::sync_with_stdio(false); std::cin.tie(0);
std::cin >> n;
for (int i = 1; i <= n; i++) {
std::cin >> s[i];
tr1.insert(s[i], false);
tr2.insert(s[i], true);
int len = s[i].size();
end[s[i].back() - 'a'] = true;
if (len == 1 && !singleLetterStart[s[i][0]-'a']) {
ans++;
singleLetterStart[s[i][0]-'a'] = true;
}
}
for (int p = 1; p <= tr1.cnt; p++) {
for (int i = 0; i < 26; i++) {
if (!tr1.trie[p][i]) ans += tr2.letterCnt[i];
else if (end[i]) ans++;
}
}
std::cout << ans << '\n';
return 0;
}
总结
这场比赛虽然很屎,但也一定程度上助长了我考前的信心。
希望 NOIP rp++ 吧。

浙公网安备 33010602011771号