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

官方题解

A题 九峰与签到题 (签到)

这签到题难度就比别的签到题高一大截:按顺序记录每题的通过率,只有全部时刻正答率在 \(50\%\) 上才算签到(我觉得按照这个标准,这题显然不是)。

#include<bits/stdc++.h>
using namespace std;
int n, m;
int cnt1[30], cnt2[30];
bool vis[30], ans[30];
int main()
{
    cin>>m>>n;
    for (int i = 1; i <= n; ++i)
        ans[i] = true;
    for (int i = 1; i <= m; ++i) {
        int x;
        string s;
        cin>>x>>s;
        vis[x] = 1;
        cnt1[x]++;
        if (s == "AC") cnt2[x]++;
        if (2 * cnt2[x] < cnt1[x]) ans[x] = false;
    }
    bool flag = 0;
    for (int x = 1; x <= n; ++x)
        if (vis[x] && ans[x]) {
            printf("%d ", x);
            flag = 1;
        }
    if (!flag) printf("-1");
    return 0;
}

B题 武辰延的字符串 (二分,字符串哈希)

我一开始直接想出来 \(O(n^3)\) 的算法,直接扔了(逃

  1. 字符串哈希

    对于字符串的比较(尤其是不断对一个字符串的子串的比较),我们不妨使用字符串哈希来进行优化,将复杂度从 \(O(n)\) 降到 \(O(1)\)

  2. 二分

    这个 \(O(n^2)\) 的枚举似乎是无法优化掉的,但是嘛......

    我们假设 \(s_i+s_j=t_{i+j}\),且 \(s_i+s_{j+1}\not= t_{i+j+1}\)(不)难发现:

    1. 对于 \(1\leq k \leq j\),有 \(s_i+s_k=t_{i+k}\)
    2. 对于 \(k > j\),有 \(s_i+s_k\not=t_{i+k}\)

    这个是一个显然的二分性质,所以我们可以通过二分,将第二维的枚举从 \(O(n)\) 降到 \(O(\log n)\)

综上,总复杂度 \(O(n\log n)\),可以通过本题。

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
#define LL long long
int n, m;
char s[N], t[N];
//HASH
#define ULL unsigned long long
const ULL BASE = 131;
ULL hash_s[N], hash_t[N], p[N];
//
int calc(int i) {
    int l = 0, r = m - i;
    while (l < r) {
        int mid = (l + r + 1) >> 1;
        if (hash_s[mid] + hash_s[i] * p[mid] == hash_t[i + mid]) l = mid;
        else r = mid - 1;
    }
    return l;
}
int main()
{
    //INIT
    p[0] = 1;
    for (int i = 1; i < N; i++)
        p[i] = p[i - 1] * BASE;
    //INPUT
    scanf("%s%s", s + 1, t + 1);
    //HASH
    n = strlen(s + 1), m = strlen(t + 1);
    for (int i = 1; i <= n; i++)
        hash_s[i] = hash_s[i - 1] * BASE + (s[i] - 'a' + 1);
    for (int i = 1; i <= m; i++)
        hash_t[i] = hash_t[i - 1] * BASE + (t[i] - 'a' + 1);
    LL ans = 0;
    //就离谱
    //就是这个 i<=m 这个边界条件,让我调了两个小时就离谱
    //严格来讲,是 i<m && i<=n
    for (int i = 1; i<m && i<=n; i++)
    {
        if (s[i] != t[i]) break;
        ans += calc(i);
    }
    printf("%lld", ans);
    return 0;
}

C题 九峰与CFOP

大模拟,写的并不是很来(逃

D题 温澈滢的狗狗

E题 九峰与子序列

F题 魏迟燕的自走棋

如果数据范围小点的话,我想费用流水过去的,可惜小不得(逃

G题 九峰与蛇形填数 (数据结构)

暴力的复杂度是 \(O(mn^2)\),直接 \(T\) 飞(逃。

其实,对于 \(O(n^2)\) 的操作,我们可以将其视为 \(n\) 次复杂度为 \(O(n)\) 的区间修改,将某一段修改为一个等差数列。那么我们就可以尝试将这个修改的复杂度降到 \(O(\log n)\) 甚至 \(O(1)\)

可惜了,这题是修改,如果是加的话,这题就可以通过差分来做了,直接降到 \(O(1)\)一道多次区间加等差数列的题目这是题解)。

今天又看了下题解,发现一个恐怖的现实:似乎修改更简单(逃

对于一个矩阵,我们只需要给每个元素打上同样的标记,然后每个点都可以根据标记,从而 \(O(1)\) 的计算出这个点的值。(打个比方,我给一块区间打上 \(opt:(x,y,k)\) 的标记,那么对于每个点,我们就可以 \(O(1)\) 的推断出来,这个点的值应该是多少)。换言之,我们并不需要在每次修改的时候真的修改到每一个点的值,只需要在最后遍历的时候算出来即可(有点像铺地毯那题了?)

用线段树优化打标记的复杂度,可以将总复杂度压到 \(O(mn\log n)\),并不是很优秀,得花好一会功夫才能卡过去(平均提交 3 次过 1 次)(逃

#include<bits/stdc++.h>
using namespace std;
const int N = 2010, M = 3010;
//
int m;
struct opt {
    int x, y, k;
}Opt[M];
int calc(int x, int y, int id) {
    //这边做了好几个卡常数处理
    if (id == 0) return 0;//注意没打标记的情况
    x = x - Opt[id].x + 1, y = y - Opt[id].y + 1;
    int &k = Opt[id].k;//引用,不建新变量
    if (x & 1) return (x - 1) * k + y;//二进制而不是取模
    else return (x - 1) * k + k - y + 1;
}
//
int n;
struct SegmentTree
{
    struct node {
        int l, r, val, tag;
    } a[N << 2];
    #define ls(x) (x << 1)
    #define rs(x) (x << 1 | 1)
    void build(int x = 1, int l = 1, int r = n) {
        a[x].l = l, a[x].r = r;
        if (l == r) return;
        int mid = (l + r) >> 1;
        build(ls(x), l, mid);
        build(rs(x), mid + 1, r);
    }
    inline void f(int x, int k) {
        a[x].val = a[x].tag = k;
    }
    void pushdown(int x) {
        int &k = a[x].tag;
        if (k) {
            a[ls(x)].val = a[ls(x)].tag = k;
            a[rs(x)].val = a[rs(x)].tag = k;
            k = 0;
        }
    }
    int query(int p, int x = 1) {
        if (a[x].l == a[x].r) return a[x].val;
        pushdown(x);
        int mid = (a[x].l + a[x].r) >> 1;
        if (p <= mid) return query(p, ls(x));
        else return query(p, rs(x));
    }
    void update(int l, int r, int val, int x = 1) {
        if (l <= a[x].l && a[x].r <= r) {
            a[x].val = a[x].tag = val;
            return;
        }
        pushdown(x);
        int mid = (a[x].l + a[x].r) >> 1;
        if (l <= mid) update(l, r, val, ls(x));
        if (r >  mid) update(l, r, val, rs(x));
        return;
    }
}arr[N];

int main()
{
    //input
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; ++i)
        scanf("%d%d%d", &Opt[i].x, &Opt[i].y, &Opt[i].k);
    //solve
    for (int i = 1; i <= n; ++i)
        arr[i].build();
    for (int i = 1; i <= m; ++i) {
        int x = Opt[i].x, y = Opt[i].y, k = Opt[i].k;
        for (int d = 0; d < k; ++d)
            arr[x + d].update(y, y + k - 1, i);
    }
    //output
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j)
            printf("%d ", calc(i, j, arr[i].query(j)));
        puts("");
    }
    return 0;
}

这题还有不少奇奇怪怪的打标记的方式,但是可能有点超出我的能力范围了,溜了溜了

H题 吴楚月的表达式 (树的遍历,表达式求值)

一道树的遍历题,需要在遍历时不断维护每个点的状态。

一个非空表达式前缀可以表示成 \(a+b\) 的形式。

如果后面接了一个 \(+x\),则变成 \((a+b)+x\)

如果后面接了一个 \(−x\),则变成 \((a+b)+(−x)\)

如果后面接了一个 \(∗x\),则变成 \(a+b∗x\)

如果后面接了一个 \(/x\),则变成 \(a+b/x\)

最后还是可以表示成 \(a+b\) 的形式。

因此只需要遍历整棵树维护每个节点对应的 \(a+b\) 即可。

(别问我咋知道这么想的,问就是看的答案)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
int n;
LL v[N];
const LL mod = 1e9 + 7;
//
LL quickpow(LL a, int x) {
    if (x == 0) return 1;
    if (x == 1) return a;
    LL half = quickpow(a, x / 2);
    if (x % 2 == 0) return half * half % mod;
    return half * half % mod * a % mod;
}
LL Div(LL b, LL a) {
    return b * quickpow(a, mod - 2) % mod;
}
//
int fa[N];
char opt[N];
vector<int> tree[N];
//
LL a[N], b[N];
void dfs(int x) {
    for (int i = 0; i < tree[x].size(); ++i) {
        int to = tree[x][i];
        switch (opt[to]) {
        case '+':
            a[to] = (a[x] + b[x]) % mod;
            b[to] = v[to] % mod;
            break;
        case '-':
            a[to] = (a[x] + b[x]) % mod;
            b[to] = (-v[to] + mod) % mod;
            break;
        case '*':
            a[to] = a[x];
            b[to] = b[x] * v[to] % mod;
            break;
        case '/':
            a[to] = a[x];
            b[to] = Div(b[x], v[to]);
            break;
        }
        dfs(to);
    }
}
int main()
{
    //input
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%lld", &v[i]);
    for (int i = 2; i <= n; ++i) {
        scanf("%d", &fa[i]);
        tree[fa[i]].push_back(i);
    }
    scanf("%s", opt + 2);
    //solve
    a[1] = 0, b[1] = v[1];
    dfs(1);
    //output
    for (int i = 1; i <= n; ++i)
        printf("%lld ", (a[i] + b[i]) % mod);
    return 0;
}

I题 九峰与分割序列

J题 邬澄瑶的公约数 (数论)

我一开始还想着先把他们的 \(\gcd\) 求出来,然后操作操作,后来发现想太多了(逃

\(\gcd\),辗转相除是一个办法,但是我们不妨从另外一个角度(也就是定义)来求:对于这些数,分别进行质因数分解,然后对于每一个质因数,判断其出现次数的最小值,然后累乘即可。

例如 \(12=2^2*3,18=2*3^2,15=3*5\)

那么 \(\gcd(12,18,15)=2^0*3^1*5^0=3\)

那么我们只要依次实现三个部分:打出素数表(素数筛),质因数分解,累乘(快速幂)。

三个步骤的复杂度各不相同(素数筛的复杂度是 \(O(n\log\log n)\),质因数分解的复杂度是 \(O(n\sqrt{n})\),累乘的复杂度是 \(O(nm)\)\(m\)\(10^4\) 以内的质数的数量),但是根据具体跑出来的数据,反正可以 \(A\) 掉这题。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
//
const int N = 10010, C =1510;
const LL mod = 1e9 + 7;
LL quickpow(LL a, LL b) {
    if (b == 0) return 1;
    if (b == 1) return a;
    LL half = quickpow(a, b / 2);
    if (b % 2 == 0) return half * half % mod;
    return half * half % mod * a % mod;
}
//
int cnt = 0;
int id[N], prime[N];
void GeneratePrime(int n) {
    int vis[N];
    memset(vis, 0, sizeof(vis));
    for (int i = 2; i <= n; ++i) {
        if (vis[i]) continue;
        id[i] = ++cnt, prime[cnt] = i;
        for (int j = 2; i * j <= n; ++j) vis[i * j] = 1;
    }
}
void divide(int x, int *arr, int p) {
    for (int i = 1; prime[i] <= x && i <= cnt; ++i)
        if (x % prime[i] == 0) {
            while (x % prime[i] == 0)
                x /= prime[i], arr[i]++;
            arr[i] *= p;
        }
}
//
int n, a[N], d[N][C];
int main()
{
    GeneratePrime(10000);
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    for (int i = 1; i <= n; ++i) {
        int p;
        scanf("%d", &p);
        divide(a[i], d[i], p);
    }
    LL ans = 1;
    for (int i = 1; i <= cnt; ++i) {
        int Min = 100010;
        for (int k = 1; k <= n; ++k)
            Min = min(Min, d[k][i]);
        ans = (ans * quickpow(prime[i], Min)) % mod;
    }
    printf("%lld", ans);
    return 0;
}
posted @ 2021-03-13 14:29  cyhforlight  阅读(73)  评论(0编辑  收藏  举报