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\)

求出字典序最小的另一个字符串,满足:

  1. 仅由这 \(m\) 种字符构成
  2. 不是 \(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,但是尽可能的优化了复杂度:

  1. 搜到长度为 \(len\) 的情况时候,才把对应长度的子串放进 vis 里面,尽可能优化了查找的复杂度(对应的,BFS 改成了这种类似迭代加深的形式)
  2. 往 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\) 必须时刻为非负整数)。

问,能否进行若干次操作后,使得数列符合以下要求:

  1. 每个数都是一个质数
  2. 两个相邻数的和也是一个质数

\(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\) 即可。

具体操作如下:

  1. 第一块的左端点 \(L=1\)
  2. 二分找到那个合适的 \(R\),得到第一个区间
  3. 第二个区间的左端点是 \(R+1\),重复上面的操作
  4. 知道某次右端点枚举到 \(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\)

那么有

\[\sum\limits_{i=1}^ni^2\lceil\frac{n}{i}\rceil=\sum\limits_{i=1}^ni^2(\lfloor\frac{n}{i}\rfloor+1)-D \]

前者就是经典的整数分块,后者也可以在求所有因数的时候顺带求出,总复杂度 \(O(T\sqrt{n})\)

//下面只给出怎么求出所有的区间
for (int L = 1, R; L <= n; L = R + 1) {
    R = n / (n / L);
    //do something
}

H题 岸风翻夕浪,舟雪洒寒灯(分治/思维?)

给定一种基于递推式的数字字符串 \(s\)

  1. \(s_1=1\)
  2. \(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\)

现在我们想要给商品加价格,方式如下:

  1. 选定价格因子 \(p\)(必然是正整数)
  2. 给第 \(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)\),无法接受。

我们想到两个旧题目:

  1. 给定一个数列,问是否存在两个数之和为 \(x\)

    先开一个桶来统计一下,然后对于每个数 \(a_i\),在桶里面看看 \(x-a_i\) 是否存在

  2. 对于搜索树过大的题目,我们可以从起点和终点都开始搜索(折半搜索),可以大幅降低复杂度

结合一下,我们就可以想到本题写法:

  1. 折半,将数组分成两半
  2. 对后半段暴力枚举,将所有可能组合对应的价格放到一个数组里面
  3. 枚举前半段,对于每种情况,记价格为 \(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;
}
posted @ 2022-05-19 22:37  cyhforlight  阅读(45)  评论(0编辑  收藏  举报