2021牛客寒假算法基础集训营2 解题补题报告

官方题解

A题 牛牛与牛妹的RMQ

单调栈+线段树+树状数组,数据结构小综合

B题 牛牛抓牛妹

分层图+最短路树+状态压缩

C题 牛牛与字符串border

思维题,比较考验智商,溜了溜了

D题 牛牛与整除分块 (找规律)

对于给定的 \(n\),显然小于 \(\sqrt{n}\) 的部分是连续的,我们可以直接硬数。

大于 \(\sqrt{n}\) 的,仔细研究会发现,它们和小于 \(\sqrt{n}\) 的具有某种对称性,具体证明略(因为我是打表看出来的)。

不过有一个特殊点需要考虑:当 \(n\) 是一个完全平方数的时候,需要特判一下(我暴力和规律的程序都没注意到,难怪对拍了一个小时都没拍出问题来)

#include<bits/stdc++.h>
using namespace std;
long long solve(long long n, long long x)
{
    long long k = n / x, s = sqrt(n);
    if (x <= s) return x;
    else if (s * s == n) return 2 * s - k;
    else return s + n /(s + 1) + 1 - k;
}
int main()
{
    long long T, n, x;
     
    scanf("%lld", &T);
    while (T--) {
        scanf("%lld%lld", &n, &x);
        printf("%lld\n", solve(n, x));
    }
     
    return 0;
}

E题 牛牛与跷跷板 (BFS+分组思想+双指针)

这题显然是一个BFS(边权全部为 \(1\) 的最短路),但是朴素建图的复杂度是 \(O(n^2)\) ,显然不行。

我们注意到,两个板子相邻,要么是左右相邻(\(y\) 相同),要么是上下相邻(\(y\) 相差 \(1\)),这意味着我们可以对我们的 \(O(n^2)\) 建图算法进行优化。恰好 \(0 \leq y_i \leq 10^5\) ,那么我们显然可以将所有跷跷板按照 \(y_i\) 分组,然后处理即可。

  1. 左右相邻

    在每一组里面线性处理即可,所有组处理完的总复杂度为 \(O(n)\)

  2. 上下相邻

    类似于双指针(尺取法),\(i,j\) 根据情况自行推进(说起来轻松,但是写起来嘛),复杂度 \(O(n)\)

建完图之后直接 \(BFS\) 即可,复杂度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
struct Block {
    int id, l, r;
    bool operator < (const Block &rhs) const {
        return r < rhs.r;
    }
};
vector<Block> a[N];
bool can(Block u, Block v) {
    if (u.r > v.r) swap(u, v);
    return v.l < u.r;
}
//
namespace BFS {
    vector<int> e[N];
    void addEdge(int u, int v) {
        e[u].push_back(v);
        e[v].push_back(u);
    }

    int d[N];
    queue<int> q;
    void solve() {
        memset(d, -1, sizeof(d));
        d[1] = 0, q.push(1);
        while (!q.empty()) {
            int x = q.front(); q.pop();
            for (int i = 0; i < e[x].size(); ++i) {
                int to = e[x][i];
                if (d[to] == -1)
                    d[to] = d[x] + 1, q.push(to);
            }
        }
    }
}
int main()
{
    //
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        int y, l, r;
        scanf("%d%d%d", &y, &l, &r);
        a[y].push_back((Block){i, l, r});
    }
    //
    for (int i = 0; i < N; ++i)
        sort(a[i].begin(), a[i].end());
    //
    for (int h = 0; h < N; ++h)
        for (int k = 1; k < a[h].size(); ++k)
            if (a[h][k - 1].r == a[h][k].l)
                BFS::addEdge(a[h][k - 1].id, a[h][k].id);
    //
    for (int h = 0; h < N - 1; ++h) {
        int i = 0, j = 0;
        while (i < a[h].size() && j < a[h + 1].size()) {
            if (can(a[h][i], a[h + 1][j]))
                BFS::addEdge(a[h][i].id, a[h + 1][j].id);
            a[h][i] < a[h + 1][j]? ++i : ++j;
        }
    }
    //
    BFS::solve();
    printf("%d", BFS::d[n]);
    return 0;
}

F题 牛牛与交换排序 (双端队列)

这题思路有点小烦,细节有点多,我直接给一个样例:

对于 \(\text{5 2 1 4 3}\),注意到第一位不是 \(1\),那么我们必须将 \(1\) 反转到第一位(必须这时候反转,不然后面反转不了),这一次的区间长度为 \(3\) ,然后变为 \(\text{1 2 5 4 3}\)

第二位就是 \(2\),可以跳过。

第三位不是 \(3\),那么我们必须将 \(3\) 反转到第三位,这次区间长度是 \(3\),然后变为 \(\text{1 2 3 4 5}\)

第四位和第五位正常,略。

那么我们的任务就是不断对某个区间进行反转,而且得随时掌握每个数的所在位置。

前一个暴力 \(O(n^2)\),用 \(\text{splay}\) 可以降到 \(O(n\log n)\),后一个不知道能不能用 \(\text{splay}\) 优化下来。(不管咋样,都不是很可做)。

其实我们可以先预处理出每次操作的长度 \(k\) ,这样就不用不停维护每个数的位置了。

重点在于翻转的复杂度是 \(O(n)\) 的,这是该题超时的主要因素,必须想办法优化掉。

突然想到了 滑动窗口 一题:一个窗口中增增减减,但是始终保持着一定的性质;这题也是同样的:不断增增减减,始终维护着整个数列的一段区间。那(并不是)很显然,我们不妨用一个队列来模拟这个操作。

将一个队列的元素翻转?那显然是 \(\text{deque}\) 了!我们直接用 \(\text{deque}\) 封装一个新类,简单维护一下反转的功能就好了!(做个标记,来分辨操作到底是从头还是从尾进行)

(实际上,大多数人在维护区间的时候更容易想到双指针,如果是暴力的话,这样写确实更加简单方便,但是当优化反转的复杂度时则会一脸懵;反之,队列的思路在反转上似乎显得更加清晰明了。)

小细节有点多,下面给出代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, k, a[N], pos[N];
bool flag = true;
struct deQue
{
    deque<int> q;
    bool rev;
    bool empty() {
        return q.empty();
    }
    int size() {
        return q.size();
    }
    int front() {
        return rev ? q.back() : q.front();
    }
    int back() {
        return rev ? q.front() : q.back();
    }
    void push_front(int x) {
        rev ? q.push_back(x) : q.push_front(x);
    }
    void push_back(int x) {
        rev ? q.push_front(x) : q.push_back(x);
    }
    void pop_back() {
        rev ? q.pop_front() : q.pop_back();
    }
    void pop_front() {
        rev ? q.pop_back() : q.pop_front();
    }
    void reverse() {
        rev ^= 1;
    }
} q;
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        pos[a[i]] = i;
    }
    bool isOK = true;
    //求出k
    for (int i = 1; i <= n; ++i)
        if (a[i] != i) {
            k = pos[i] - i + 1;
            isOK = false;
            break;
        }
    //已经排好序了
    if (isOK) {
        printf("yes\n1\n");
        return 0;
    }
    //主程序
    for (int i = 1; i <= k; ++i)
        q.push_back(a[i]);
    for (int i = 1; i <= n; ++i) {
        if (q.front() != i && q.size() == k)
            q.reverse();
        if (q.front() != i) {
            flag = false;
            break;
        }
        q.pop_front();
        if (i + k <= n)
            q.push_back(a[i + k]);
    }
    if (flag) printf("yes\n%d\n", k);
    else printf("no\n");
    return 0;
}

G题 牛牛与比赛颁奖 (离散化+扫描线+差分)

如果 \(n\) 的范围能降到 \(10^6\) 这一级,那题目思路就很显然了:直接差分就好了,然后维护出来每个队伍写出来的题目的数量,然后排序之后输出。

如果降到 \(10^7\) 这一级,那么排序会 \(T\) 掉。这时候我们应该转换下思路:我们不需要具体了解每个队伍的得分情况,我们只需要维护排名即可。这样的话,我们不如开一个桶,维护写出 \(i\) 题的队伍的数量为 \(cnt_i\) 即可。

但尴尬的是,\(n\) 的范围是 \(10^9\),在大多数题目里面,必须要上 \(O(\sqrt{n})\) 或者 \(O(1)\) 或者 \(O(\log {n})\)的算法了,但这题显然跟这些都没啥关系。显然,我们只能用一下离散化,把这个规模就硬降下来。

这玩意我也不知道咋用语言来描述,看起来挺像扫描线的,但又不是那回事(逃

建议直接看代码,如果看不懂代码就去看官方题解

#include<bits/stdc++.h>
using namespace std;
const int M = 100010;
int n, m;
struct node {
    int val, pos;
    bool operator < (const node &rhs) const {
        return pos < rhs.pos;
    }
} a[M<<1];
int sum[M];
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; ++i) {
        int l, r;
        scanf("%d%d", &l, &r);
        a[i].pos = l, a[i + m].pos = r + 1;
        a[i].val = 1, a[i + m].val = -1;
    }
    sort(a + 1, a + 2*m + 1);
    int now = 0;
    for (int i = 1; i < 2*m; ++i) {
        now += a[i].val;
        if (a[i].pos != a[i+1].pos)
            sum[now] += a[i+1].pos - a[i].pos;
    }
    int Au = (n + 9) / 10, Ag = (n + 3) / 4, Cu = (n + 1) / 2;
    int ans[3]={0, 0, 0};
    now = 0;
    for (int i = m; i >= 1; --i) {
        if (now < Au) ans[0] += sum[i];
        else if (Au <= now && now < Ag) ans[1] += sum[i];
        else if (Ag <= now && now < Cu) ans[2] += sum[i];
        now += sum[i];
    }
    printf("%d %d %d", ans[0], ans[1], ans[2]);
    return 0;
}

H题 牛牛与棋盘 (签到)

签到题,有手就行

#include<bits/stdc++.h>
using namespace std;
int n;
int main()
{
    cin>>n;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n / 2; ++j)
            printf(i % 2 ? "01" : "10");
        puts("");
    }
    return 0;
}

I题 牛牛的“质因数” (数论,记忆化搜索/DP)

不管咋样,把所有素数筛出来总没错。

然后......就没有然后了。

老实说,这题当时卡的我想砸键盘,后来张佬一句话点醒了我:“这题可以暴力DFS。”(逃

(并不)显然,根据算术基本定理,每个数的质因数分解方式是唯一的(听起来像是废话)。

另外,我们有一个小思路:如果已知一个数 \(x\) 的表示,即 \(F(x)\) ,同时知道 \(x\) 的最大质因数是 \(p_0\),并且 \(p_1,p_2,......\) 等等都是大于 \(p_0\) 的质因数,那么我们就可以推出 \(p_0x,p_0^2x,p_0^3x,p_1^2x,p_0p_1x\) 等等一系列数字的 \(F\) 表示。

这两个思路并在一起(稍加)思索,我们会发现一个惊人的事实:我们可以 \(O(n)\) 的递推求出区间 \([2,n]\) 内所有数字的 \(F\) 表示(如果 \(DFS\) 一次,每个数会且仅会被扫到一次,实际上就是一个标准的搜索树)。

给出考场代码(严格说,这是一个记忆化搜索,甚至可以称之为动态规划,虽然我觉得就是普通递推):

#include<bits/stdc++.h>
using namespace std;
const int N = 4000010;
const long long mod = 1e9 + 7;
long long n;
int cnt, prime[N], vis[N];
void generate_prime() {
    memset(vis, 0, sizeof(vis));
    cnt = 0;
    for (int i = 2; i <= 4000005; ++i)
        if (!vis[i]) {
            prime[++cnt] = i;
            for (int j = 2; i * j <= 4000005; ++j) vis[i*j] = 1;
        }
}
int g(int x) {
    int val = 1;
    while (x) x /= 10, val *= 10;
    return val;
}
long long res[N];
void dfs(long long val, int p, long long fa) {
    res[val] = (res[fa] * g(prime[p]) + prime[p]) % mod;
    for (int i = p; val * prime[i] <= n && i <= cnt; ++i)
        dfs(val * prime[i], i, val);
}
int main()
{
    generate_prime();
    cin>>n;
    for (int i = 1; prime[i] <= n && i <= cnt; ++i)
        dfs(prime[i], i, 0);
    long long ans = 0;
    for (int i = 2; i <= n; ++i)
        ans = (ans + res[i]) % mod;
    printf("%lld", ans);
    return 0;
}

说是搜索树,其实具体化到本题中,这玩意可以用一个专业点的名词来描述:筛法树(除了 \(1\),每个数都和一个比他小的数有一个关系,这个关系就是一个质数。 \(n\)\(n-1\) 边,我 DNA 动了)

一个大小为 10 的筛法树

这就是一个大小为 \(10\) 的筛法树(借用一下官方题解的图,希望不要介意)(逃

构造方式仔细研究一下,会发现构造方式和线性素数筛有异曲同工之妙(发现一个符合要求的数,然后线性递推,把后面的数字都打上标记),这也就是 筛法树 名字的由来。

J题 牛牛想要成为hacker (构造)

看样例,可以显然找出一种构造方法:一个等比数列,可以保证整个循环里面都找不出来能构成三角形的

可惜的是,题目限制范围,莫得办法(逃

能够保持这种相加不构成三角形的性质的数列,增长最慢的是啥数列?显然是斐波那契数列(证明显然)

当然,斐波那契数列的增长速度也是幂级别的,估计到了第 \(40\) 项左右就会超过 \(10^9\) 的限制了,我们记为 \(cnt\)

这里几种方案:

  1. 构造等差数列

    这玩意就很烦了,不谈

  2. 第一位放弃1,从2开始,后面全部铺1

    我真不知道出题人咋想到这个逆天思路的,但是确实有用,可以保证 \(\text{isTriangle}\) 总调用次数不少于 \(n^2\log n\) 次(\(1\leq i \leq cnt\)\(1 \leq j,k \leq n\),显然 \(cnt > \log n\))。

#include <bits/stdc++.h>
using namespace std;
int n, a[50];
int main()
{
    scanf("%d", &n);
    a[0]= 1, a[1]= 2;
    for (int i = 1; i <= min(40, n); ++i) {
        printf("%d ", a[i]);
        a[i + 1] = a[i] + a[i - 1];
    }
    for (int i = 41; i <= n; i++)
        printf("1 ");
    return 0;
}
posted @ 2021-03-08 22:03  cyhforlight  阅读(101)  评论(0编辑  收藏  举报