2022 西南科技大学校赛 题解
A题 花非花(马拉车算法)
给定一个长度为 \(n\) 的数字串,对于每个 \(1\leq i\leq n\),都要求出有多少个 \(j\) 符合 \(i\leq j\),且区间 \([i,j]\) 回文。
\(n\leq 10^6\)
一个回文串可以给它左半边的位置都贡献 1 的答案(线段树或者差分啥的来处理区间加),但是要注意重复(如果有多个回文串,且它们的中心都是同一个,那么只取最长的那个)。
那么求回文串的部分,就是经典的马拉车了(字符串哈希也行)。
#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n, m, a[N], p[N * 3], b[N * 3], c[N * 3];
void Manacher() {
int r = 0, mid = 0;
for (int i = 1; i <= m; i++) {
p[i] = i < r ? min(p[2 * mid - i], r - i) : 1;
while (b[i + p[i]] == b[i - p[i]]) ++p[i];
if (i + p[i] > r) r = i + p[i], mid = i;
}
}
int main()
{
scanf("%d", &n);
b[0] = 0, b[1] = -1;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
b[i * 2] = a[i], b[i * 2 + 1] = -1;
}
b[2 * n + 2] = -1;
m = 2 * n + 1;
Manacher();
for (int i = 1; i <= m; i++) {
int d = p[i] - 1;
c[i - d + 1]++, c[i + 1]--;
}
for (int i = 1; i <= m; i++) c[i] += c[i - 1];
for (int i = 1; i <= n; i++)
printf("%d ", c[i * 2]);
return 0;
}
B题 为欢几何(签到)
给定 \(n\) 个字符串,求出他们的首字母组成的新字符串。
\(1\leq n\leq 8,|s|\leq 10\)
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n;
string res = "";
for (int i = 1; i <= n; ++i) {
string s;
cin >> s;
res += s[0];
}
cout << res;
return 0;
}
C题 花空烟水流(BFS)
给定一个长度为 \(n\) 的小写字母字符串 \(s\),串中的不同字符数量为 \(m\)。
求出字典序最小的另一个字符串,满足:
- 仅由这 \(m\) 种字符构成
- 不是 \(s\) 的子串
\(2\leq n\leq 5*10^5,2\leq m\leq 26\)
凭直觉发现,这个字符串的长度不会太高:
对于一个长度为 \(L\) 的字符串,其全部可能的种类为 \(m^L\) 种,而字符串内长度为 \(L\) 的子串的数量大致为 \(n-L+1\) 种(大概是这个规模)。换句话,当 \(m^L>n\) 的时候,长度为 \(L\) 的串里面肯定能找到一个不是 \(s\) 子串的。
那么,我们直接记 \(k=\lceil \log_m^n\rceil\),那么只要枚举长度从 1 到 k,暴力找可能符合要求的串即可,复杂度 \(O(m^k)\),也就是 \(O(n)\) 左右。
但实际上,这只是一个理论复杂度,真的上手写的时候,会碰到各种各样的问题,例如:
判断一个字符串是不是 \(s\) 的子串,意味着我们要枚举出所有 \(s\) 的长度小于等于 \(k\) 的子串的数量,然后塞进一个 map 或者哈希表里面,我们很容易写成一个 \(O(nk)\) 复杂度的枚举代码,然后搭配 STL 的大常数和可能的潜在 \(O(\log n)\) 查找,直接 T 飞。
这是一份使用了 unordered_set 的代码,主要用来理解算法思路(谢天谢地,出题人没出构造数据来卡我):
#include <bits/stdc++.h>
using namespace std;
unordered_set<string> vis;
string BFS(vector<char> &vec) {
queue<string> q;
q.push("");
while (!q.empty()) {
string u = q.front(); q.pop();
for (char c : vec) {
string v = u + c;
if (vis.find(v) == vis.end()) return v;
q.push(v);
}
}
return "-1";
}
int main()
{
//read
int n, m;
string str;
cin >> n >> m >> str;
//init
int H = log(n) / log(m) + 1;
for (int len = 1; len <= H; ++len)
for (int i = 0; i + len - 1 < n; ++i)
vis.insert(str.substr(i, len));
set<char> tmp(str.begin(), str.end());
vector<char> vec(tmp.begin(), tmp.end());
//BFS
cout << BFS(vec) << endl;
return 0;
}
下面这部分用了标准的红黑树实现的 map,但是尽可能的优化了复杂度:
- 搜到长度为 \(len\) 的情况时候,才把对应长度的子串放进 vis 里面,尽可能优化了查找的复杂度(对应的,BFS 改成了这种类似迭代加深的形式)
- 往 vis 里面塞子串的时候,当子串数量达到上限时就退出(例如 \(n=10^5,m=3\) 的情况下,当 \(len=5\) 时候,发现某时刻 vis 里面的数量已经有 \(3^5=243\) 的时候就直接 break,因为所有的子串都出现过了)。但老实说,这玩意其实也可以卡,只能说出题人还是手太软
#include <bits/stdc++.h>
using namespace std;
unordered_set<string> vis;
queue<string> q;
string BFS(vector<char> &vec, int H) {
while (q.front().length() < H) {
string u = q.front(); q.pop();
for (char c : vec) {
string v = u + c;
if (vis.find(v) == vis.end()) return v;
q.push(v);
}
}
return "-1";
}
int main()
{
//read
int n, m;
string str;
cin >> n >> m >> str;
//init
set<char> tmp(str.begin(), str.end());
vector<char> vec(tmp.begin(), tmp.end());
vis.insert("");
int H = log(n) / log(m) + 1;
for (int len = 1; len <= H; ++len)
for (int i = 0; i + len - 1 < n; ++i)
vis.insert(str.substr(i, len));
//BFS
q.push("");
int LIM = 1;
for (int len = 1; ; ++len) {
LIM *= m;
vis.clear();
for (int i = 0; i + len - 1 < n; ++i) {
vis.insert(str.substr(i, len));
if (vis.size() == LIM) break;
}
string res = BFS(vec, len);
if (res != "-1") {
cout << res << endl;
break;
}
}
return 0;
}
D题 似花还似非花
E题 西楼暮,一帘疏雨
F题 青山隐隐,败叶萧萧(数学,思维)
给定一个长度为 \(n\) 的数列 \(\{a_n\}\)。
现在我们可以进行若干次操作(或者不操作),每次都可以给某个数加上 2 或者减去 2(但是 \(a_i\) 必须时刻为非负整数)。
问,能否进行若干次操作后,使得数列符合以下要求:
- 每个数都是一个质数
- 两个相邻数的和也是一个质数
\(T\leq 2*10^3,n\leq 1314,0\leq a_i\leq 1314520\)
考虑到操作不会改变一个数的奇偶性,所以说不可以存在两个奇数或者偶数直接相邻(否则他们两加起来就是一个偶数,不可能是质数)。
当数列是奇偶反复交替的情况时,直接让所有偶数都变成 2,奇数变成 3 即可。
扫一遍即可,时间复杂度 \(O(Tn)\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 2010;
int n, a[N];
bool solve() {
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 2; i <= n; ++i)
if ((a[i] + a[i - 1]) % 2 == 0) return false;
return true;
}
int main()
{
int T;
cin >> T;
while (T--) puts(solve() ? "YES" : "NO");
return 0;
}
G题 几番烟雾,只有花难护(整数分块)
给定 \(n\),求 \(\sum\limits_{i=1}^ni^2\lceil\frac{n}{i}\rceil\) 的值。
\(T\leq 100,n\leq 10^9\)
如果是 \(\lfloor\frac{n}{i}\rfloor\) 的话,那就是典型的整数分块:\(\lfloor\frac{n}{i}\rfloor\) 的取值不超过 \(2\sqrt{n}\) 种,直接求出每一块对应的区间,然后算那段区间对应的平方和,累乘累加即可得出答案。(考虑到前 n 像平方和有公式,所以区间平方和直接类似前缀和一样减一下就行)。
不过,本题中是向上取整,所以我们有两种得到块的方法:
方法一:二分
向下取整中的整数分块,每一块的区间范围都是通过数学公式得到,不过本题中不适用,所以我们直接在每一块中,已知左端点 \(L\) 的情况下,二分最远的 \(R\),使得 \(\lfloor\frac{n}{L}\rfloor=\lfloor\frac{n}{R}\rfloor\) 即可。
具体操作如下:
- 第一块的左端点 \(L=1\)
- 二分找到那个合适的 \(R\),得到第一个区间
- 第二个区间的左端点是 \(R+1\),重复上面的操作
- 知道某次右端点枚举到 \(n\),流程结束
总复杂度为 \(O(T\sqrt{n}\log n)\),刚好卡着时限,挺离谱的(我是出题人我就卡这个)。
#include<bits/stdc++.h>
using namespace std;
//Math Lib
#define LL long long
const LL mod = 998244353;
//直接手动算出 6 在 998244353 下的逆元,就不写快速幂了
const LL inv6 = 166374059;
LL calc(LL n) { return n * (n + 1) % mod * (2 * n + 1) % mod * inv6 % mod; }
LL query(LL L, LL R) { return (calc(R) - calc(L - 1) + mod) % mod; }
//
LL n;
inline int f(int i) { return (n + i - 1) / i; }
int find(int L) {
int l = L - 1, r = n;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (f(L) == f(mid)) l = mid;
else r = mid - 1;
}
return l;
}
int main()
{
int T;
cin >> T;
while (T--) {
cin >> n;
LL res = 0;
for (int L = 1, R; L <= n; L = R + 1)
R = find(L), res = (res + f(L) * query(L, R)) % mod;
cout << res << endl;
}
return 0;
}
方法二:数学化简
假设 \(n\) 的所有因数的集合为 \(S\),那么记 \(D=\sum\limits_{x\in S}x^2\)。
那么有
前者就是经典的整数分块,后者也可以在求所有因数的时候顺带求出,总复杂度 \(O(T\sqrt{n})\)。
//下面只给出怎么求出所有的区间
for (int L = 1, R; L <= n; L = R + 1) {
R = n / (n / L);
//do something
}
H题 岸风翻夕浪,舟雪洒寒灯(分治/思维?)
给定一种基于递推式的数字字符串 \(s\):
- \(s_1=1\)
- \(s_n=s_{n-1}+n+s_{n-1}\)(加号表示字符串连接)
求出 \(n=10^{10^{10^{10}}}\) 时的 \(s_n\) 的第 \(k\) 位。
\(1\leq k\leq 10^{18}\)
显然,\(len(s_n)=2^n-1\)。
我们假设 \(k<2^t\),那么说明 \(s_n\) 的第 \(k\) 位也就是 \(s_t\) 的第 \(k\) 位,那么,怎么缩小范围,来确定值呢?
如果我们发现 \(k=2^x\),说明它恰好是 \(s_{x+1}\) 的中间一位,即 \(x+1\)。
相反,我们假设 \(2^x<k<2^{x+1}\),那么我们截掉前半部分,相当于求 \(s_x\) 的 \(k-2^x\) 位。
找几个数模拟一下,其实就发现,其实就是找 \(k\) 在二进制下最小的为 1 的位 \(x\)(从 0 计数),然后输出 \(x+1\) 即可。
#include<bits/stdc++.h>
using namespace std;
int main()
{
long long n;
cin >> n;
for (int k = 0; k <= 62; ++k)
if ((n >> k) & 1) {
cout << k + 1 << endl;
break;
}
return 0;
}
I题 醉漾轻舟,信流引到花深处(二分+折半枚举)
给定 \(n\) 件商品,第 \(i\) 个商品的价格为 \(a_i\)。
现在我们想要给商品加价格,方式如下:
- 选定价格因子 \(p\)(必然是正整数)
- 给第 \(i\) 件商品的价格增加 \(p*b_i\)
但是,价格的增加不是无限制的:当 \(m\) 元能买到的商品组合的总数(不买也算一种)小于等于 \(k\) 时,大家就会选择不买。
尝试求出,我们能选定的最大价格因子是多少?(没法加价的话就输出 0,保证在不加价格的情况下,购买方案总数符合要求)
\(n\leq 30,0\leq m \leq 10^9,2\leq k\leq 10^9,1\leq a_i\leq 10^6,1\leq b_i\leq 10^5\)
显然,价格越贵,商品组合的总数越少,所以我们直接二分枚举 \(t\) 的大小即可。
那么,接下来就是这样一个子问题了:
给定 \(n\) 个数,问有多少种选择方法,使得选择的数的和小于等于 \(m\)?
check 函数如果暴力统计的话,那么复杂度就是 \(O(2^n)\),无法接受。
我们想到两个旧题目:
-
给定一个数列,问是否存在两个数之和为 \(x\)?
先开一个桶来统计一下,然后对于每个数 \(a_i\),在桶里面看看 \(x-a_i\) 是否存在
-
对于搜索树过大的题目,我们可以从起点和终点都开始搜索(折半搜索),可以大幅降低复杂度
结合一下,我们就可以想到本题写法:
- 折半,将数组分成两半
- 对后半段暴力枚举,将所有可能组合对应的价格放到一个数组里面
- 枚举前半段,对于每种情况,记价格为 \(x\),那么就只要查询数组里面有多少数小于等于 \(m-x\) 了:排序之后每次二分即可
我们记 \(v=\frac{n}{2}\),那么这种 check 方式的复杂度为 \(O(v*2^v)\)。
对了,最好特判一下 \(n=1\) 的情况(check 写的好就不用)。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 40;
int n, k;
LL m, a[N], b[N], c[N];
//
vector<LL> v1, v2;
int tot;
LL arr[1000010];
bool check(int w) {
for (int i = 1; i <= n; ++i)
c[i] = a[i] + w * b[i];
//
v1.clear();
v2.clear();
for (int i = 1; i <= n / 2; ++i)
v1.push_back(c[i]);
for (int i = n / 2 + 1; i <= n; ++i)
v2.push_back(c[i]);
int n1 = v1.size(), n2 = v2.size();
//v2
tot = 0;
for (int i = 0; i < (1 << n2); ++i) {
LL res = 0;
for (int k = 0; k < n2; ++k)
if ((i >> k) & 1) res += v2[k];
arr[++tot] = res;
}
sort(arr + 1, arr + tot + 1);
arr[tot + 1] = 1e15;
//v1
LL ans = 0;
for (int i = 0; i < (1 << n1); ++i) {
//calc1
LL res = 0;
for (int k = 0; k < n1; ++k)
if ((i >> k) & 1) res += v1[k];
//compare
int id = upper_bound(arr + 1, arr + tot + 2, m - res) - arr - 1;
ans += id;
}
return ans >= k;
}
int main()
{
//read
cin >> n >> m >> k;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= n; ++i)
cin >> b[i];
//subtask
if (n == 1) {
cout << max(m - a[1], 0LL) / b[1] << endl;
return 0;
}
int l = 0, r = 1e9;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
J题 满城烟水月微茫,人倚兰舟唱(模拟)
纯纯模拟题,题意和题解直接看原题目和代码就行了。
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
deque<int> q[N];
int main()
{
//read & build
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
int tot, x;
cin >> tot;
while (tot--) {
cin >> x;
q[i].push_back(x);
}
}
//solve
int ans = 0, id = 1;
while (id <= n) {
int x = q[id].front(); q[id].pop_front();
//get
int L = id, R, flag = 0;
for (R = id; R <= n; ++R) {
for (int v : q[R])
if (v > x) { flag = 1, ++ans; break; }
if (flag) break;
}
if (R == n + 1) break;
while (q[R].front() <= x) {
q[R + 1].push_back(q[R].front());
q[R].pop_front();
}
q[R].pop_front();
for (int i = R - 1; i >= L; --i)
while (!q[i].empty()) {
q[R].push_back(q[i].front());
q[i].pop_front();
}
id = q[R].empty() ? R + 1 : R;
}
cout << ans << endl;
return 0;
}
K题 对潇潇暮雨洒江天,一番洗清秋
L题 夜暗方显万颗星,灯明始见一缕尘(数学,思维)
给定一个 \(n\) 行 \(m\) 列的白色方格面,接着我们用一个 \(x\) 行 \(y\) 列的黑色方格面遮盖在上面(必须完全在上面,可以自行选择横着或者竖着)。
问怎样摆放,使得剩下来的白色方格面的白色方格,组成的矩阵最多?输出这个最多的数。
\(1\leq n,m\leq 10^3,1\leq x,y\leq \min(n,m)\)
直接设黑矩阵离两边距离来列一个二元函数,数学推导发现当矩阵摆在最角落的时候效果最佳(别问具体怎么推导,电脑上面敲不来)。
横着还是竖着,这个直接枚举即可。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL n, m, x, y;
LL f(LL a, LL b) {
return a * (a + 1) * b * (b + 1) / 4;
}
LL calc(LL dr, LL dc) {
return f(dr, m) + f(n, dc) - f(dr, dc);
}
int main()
{
cin >> n >> m >> x >> y;
cout << max(calc(n - x, m - y), calc(n - y, m - x)) << endl;
return 0;
}
M题 劝君终日酩酊醉,酒不到刘伶坟上土(数学)
给定一个 \(n\) 行 \(m\) 列的方格桌面,每格都有一坛酒。
现在我们可以进行 \(k\) 次操作,每次操作都是选择拿走某行或者某列上的所有酒。注意,这两类操作不可以同时执行,也就是说不可以连续两次都是拿走某行或者某列的酒。
问,我们至多可以拿走多少酒?
\(T\leq 10^4,1\leq n,m\leq 10^9,0\leq k\leq 2*10^9\)
操作序列基本固定,那我们只需要看第一次操作是选择拿行还是拿列就行了。显然,当行数更大的时候,第一步拿列要比拿行更优,反之亦然。
那么,我们直接算出拿走的行数和列数,随后减去重复的部分即可(记拿走了 \(x\) 行 \(y\) 列,那么得到了 \(xm+yn-xy\) 坛酒)。
注意,上面的公式仅在没有重复拿某行/某列的时候才有效,当拿走的行数超过总行数时(或者另外一种对应情况),就直接输出 \(nm\) 即可。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL solve()
{
LL n, m, k, x, y;
cin >> n >> m >> k;
if (n > m) swap(n, m);
if (k <= 2 * n) {
y = k / 2, x = k - y;
return m * x + n * y - x * y;
}
else return n * m;
}
int main()
{
int T;
cin >> T;
while (T--) cout << solve() << endl;
return 0;
}