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

官方题解

A题 模数的世界

大致猜一下,肯定能构造出 \(x,y\) ,使得答案为 \(p-1\) 。(\(a=b=0\) 的时候没办法构造,答案只能为 \(0\)

现在尝试证明:令 \(a\not= b\)\(a\geq b\)

  1. \(b=0\) 的时候很容易构造:令 \(x=(p-1)m,y=(p-1)n\),我们现在尝试使得 \(\gcd(m,n)=1,x\%p=a,y\%p=0\)

    显然可以令 \(n=p\),现在开始构造 \(m\)\(x\%p=(pm-m)\%p=(-m)\%p=a\),所以 \(-m=kp+a\) ,也就是 \(m=-kp-a\), 不妨令 \(k=-1\),则 \(m=p-a\) 。显然 \(\gcd(p,p-a)=1\)

    综上,可以构造 \(x=(p-a)(p-1),y=p(p-1)\)

  2. \(b\not=0\) 的情况很难整:

    \(x=(p-1)m,y=(p-1)n\),则易得 \(m=k_1p-a,n=k_2p-b\),保证 \(k_1,k_2\) 均为正整数。

    那么现在就尝试证明:存在 \(k_1,k_2\) ,使得 \(\gcd(k_1p-a,k_2p-b)=1\)

    这里有点难想(我建议考场上直接写一个暴力怼一下就完事了):

    不妨令 \(k_2=1\),则 \(n=p-b\)

    有一个不太显然的结论:对于 \(k_1=1,2,...,p-b\)\((k_1p-a)\%(p-b)\) 的值各不相同

    证明:倘若存在 \(1 \leq i<j \leq p-b\) ,且 \(ip-a\)\(jp-a\) 关于 \(p-b\) 同余,那么 \((j-i)p\)\(p-b\) 的倍数。

    显然 \(\gcd(p,p-b)=1\),又因为 \(1 \leq j -i < p-b\) ,故 \(\gcd(j-i,p-b)=1\) ,所以显然 \(\gcd((j-i)p,p-b)=1\) ,即两者必定互质,与假设矛盾,故不存在。

    那么很显然结论成立,那么必然存在 \(1 \leq k_1 \leq p-b\) ,使得 \(k_1p-a\)\(1\) 关于 \(p-b\) 同余。

    那么原证明成立。

综上,得到结论:

  1. \(a=b=0\) 时,\(ans=0\)
  2. \(a\not=0\text{ or }b\not=0\) 时,\(ans=p-1\)

求解的话就直接 \(exgcd\) 了,或者想我一样直接枚举(复杂度 \(O(T(p - b))\))(逃

#include <bits/stdc++.h>
using namespace std;
#define LL long long
LL gcd(LL a, LL b) {
    if (!b) return a;
    return gcd(b, a % b);
}
void solve() {
    LL a, b, p;
    scanf("%lld%lld%lld", &a, &b, &p);
    if (a == 0 && b == 0) { //第一次写了个 a=0,给我人都调傻了
        printf("0 %lld %lld\n", p, p);
        return;
    }
    //如果 a=0 或 b=0,那么在 i=1 的情况下就直接成立了
    //所以这么写,以减少码量
    for (LL i = 1; i <= p - b; ++i)
        if (gcd(i * p - a, p - b) == 1) {
            printf("%lld %lld %lld\n", p - 1, (p - 1) * (i * p - a), (p - 1) * (p - b));
            return;
        }
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) solve();
    return 0;
}

B题 内卷

C题 重力坠击 (暴力DFS)

看这数据规模,捞的一,直接硬模拟就完事了

但是我一开始没上手写,因为理解错题意了,以为如果能消灭全部敌人的话,必须尽可能的少攻击,所以每次都会判定一下,这样复杂度就上来了,但实际上并没有这个限制,我们仅需要尽量的多消灭敌人,所以能攻击就攻击就完事了。

枚举 \(k\) 个坐标,然后对 \(n\) 个炸弹一一判定,总复杂度为 \(O(225^kn)\)

#include<bits/stdc++.h>
using namespace std;
int n, k, R;
struct node {
    int x, y, r;
} a[15];
bool check(int x, int y, int p) {
    return (x - a[p].x) * (x - a[p].x)
        +  (y - a[p].y) * (y - a[p].y)
        <= (R + a[p].r) * (R + a[p].r);
}
int dx[5], dy[5], ans;
int calc() {
    int cnt = 0;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= k; ++j)
            if (check(dx[j], dy[j], i)) {
                ++cnt; break;
            }
    return cnt;
}
void dfs(int d) {
    if (d == k + 1) {
        ans = max(ans, calc());
        return;
    }
    for (int i = -7; i <= 7; ++i)
        for (int j = -7; j <= 7; ++j)
            dx[d] = i, dy[d] = j, dfs(d + 1);
    return;
}
int main()
{
    scanf("%d%d%d", &n, &k, &R);
    for (int i = 1; i <= n; ++i)
        scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].r);
    dfs(1);
    printf("%d", ans);
    return 0;
}

D题 Happy New Year! (签到)

白给题,直接暴力或者找规律就行了

#include<bits/stdc++.h>
using namespace std;
int calc(int x) {
    int ans = 0;
    while (x)
        ans += x % 10, x /= 10;
    return ans;
}
int main()
{
    int n;
    cin>>n;
    for (int i = n + 1; ; ++i)
        if (calc(n) == calc(i)) {
            printf("%d", i);
            return 0;
        }
}

E题 买礼物 (线段树)

说起来很惭愧,这些题目明明知道是数据结构题,但是完全不知道怎么转化成我们学过的数据结构(图论题好歹知道先建一个图)

开两个数组 \(Last\)\(Next\)\(Last_i\) 记录 \(i\) 前面最靠近 \(i\) 且值也等于 \(a_i\) 的点的位置,如果没有的话值就记为 \(0\)\(Next\) 同理,没有的话就记为 \(n+1\)(这玩意有点像链表)

到这里,我们就可以转化成线段树的操作了:

  1. 买一个东西:\(Next[Last[i]] = Next[i],Last[Next[i]] = Last[i],Last[i]=0,Next[i]=n+1\),单点操作
  2. 查询:对于区间 \([l,r]\) 里面的一个 \(i\),判断是否 \(Next[i] \leq r\)。那么也就是判断 \(\min\limits_{l\leq i \leq r}Next[i] \leq r\) 是否成立

单点修改+区间 \(\min\),这显然就是线段树的操作了

别问我咋想到这么建的,问就是看官方题解

#include<bits/stdc++.h>
using namespace std;
const int N = 500010;
struct SegmentTree {
    struct node {
        int val, l, r;
    } a[N<<2];
    inline int ls(int x) { return x<<1; }
    inline int rs(int x) { return x<<1|1; }
    void pushup(int x) {
        a[x].val = min(a[ls(x)].val, a[rs(x)].val);
    }
    void build(int *arr, int x, int l, int r) {
        a[x].l = l, a[x].r = r;
        if (l == r) {
            a[x].val = arr[l];
            return;
        }
        int mid = (l + r) >> 1;
        build(arr, ls(x), l, mid);
        build(arr, rs(x), mid + 1, r);
        pushup(x);
    }
    void change(int p, int val, int x = 1) {
        if (a[x].l == a[x].r) {
            a[x].val = val;
            return;
        }
        int mid = (a[x].l + a[x].r) >> 1;
        if (p <= mid) change(p, val, ls(x));
        else          change(p, val, rs(x));
        pushup(x);
    }
    int query(int l, int r, int x = 1) {
        if (l <= a[x].l && a[x].r <= r)
            return a[x].val;
        int mid = (a[x].l + a[x].r) >> 1;
        int ans = 1e9 + 10;
        if (l <= mid) ans = min(ans, query(l, r, ls(x)));
        if (r >  mid) ans = min(ans, query(l, r, rs(x)));
        return ans;
    }
}Last, Next;
int n, T, a[N];
//
const int MAX_A = 1000010;
int pos[MAX_A], Arr_Last[N], Arr_Next[N];
void ConstructTree()
{
    //Last
    for (int i = 0; i < MAX_A; ++i)
        pos[i] = 0;
    for (int i = 1; i <= n; ++i)
        Arr_Last[i] = pos[a[i]], pos[a[i]] = i;
    Last.build(Arr_Last, 1, 1, n);
    //Next
    for (int i = 0; i < MAX_A; ++i)
        pos[i] = n + 1;
    for (int i = n; i >= 1; --i)
        Arr_Next[i] = pos[a[i]], pos[a[i]] = i;
    Next.build(Arr_Next, 1, 1, n);
}
int main()
{
    scanf("%d%d", &n, &T);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    ConstructTree();
    while (T--) {
        int opt, x, l, r;
        scanf("%d", &opt);
        if (opt == 1) {
            scanf("%d", &x);
            Next.change(Last.query(x, x), Next.query(x, x));
            Last.change(Next.query(x, x), Last.query(x, x));
            Next.change(x, n + 1);
            Last.change(x, 0);
        }
        else if (opt == 2) {
            scanf("%d%d", &l, &r);
            puts(Next.query(l, r) <= r ? "1" : "0");
        }
    }
    return 0;
}

F题 匹配串

G题 糖果 (图的遍历)

如果两个人 \(x,y\) 是朋友,那么他们的糖果数量只能相同。尽可能减少开支,就是每个人都拿 \(\max(a_x,a_y)\) 个糖果。这样就可以保证两个人都满足了最低需求,不互相冲突,而且我们花费最少。

如果 \(z\)\(y\) 的朋友,这时候两个人都要拿 \(\max(\max(a_x,a_y),a_z)\) ,也就是 \(\max(a_x,a_y,a_z)\) 个糖果。

注意了,如果这时候 \(y\) 的糖果数量变了,那么 \(x\) 也要变,保持和 \(y\) 的糖果数量一致。

这时候,我们发现了朋友似乎具有传递性质:\(x\)\(y\)\(y\)\(z\) 分别是朋友,那么 \(x\)\(z\) 也是朋友。

好在我交的快,前脚交,后脚出题人发公告,特意说明这题里面,朋友不具有传递性,也就是我们上面这条推论是错误的。

这个公告害人不浅,导致不少本来正确的人把代码改掉,在新的错误思路里面绕来绕去(就离谱)

实际上,出题人也没说错,朋友不具有传递性,具有传递性的,只是这个糖果的数量具有传递性(即使 \(x\)\(z\) 不是朋友,但是他们在糖果分配上面还是要当作朋友来考虑的)(好一手文字游戏)

实际上,我们转化一下,把每个小朋友视作一个点,朋友关系视作一条无向边,那么在一个连通块里面的所有点都可以视为互为朋友,那么他们的糖果数量,必须是这个连通块里面的小朋友的最低糖果要求的最大值。

那么这题思路就很清楚了:建图,然后跑多次 \(DFS\) 来找出所有连通块,以及每块连通块内部的最大点权,最后合并输出即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n, m;
long long a[N];
 
vector<int> G[N];
int vis[N];
long long cnt, maxv;
void dfs(int x) {
    vis[x] = 1, ++cnt, maxv = max(maxv, a[x]);
    for (int i = 0; i < G[x].size(); ++i) {
        int to = G[x][i];
        if (!vis[to]) dfs(to);
    }
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%lld", &a[i]);
    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    long long ans = 0;
    for (int i = 1; i <= n; ++i) {
        if (vis[i]) continue;
        cnt = 0, maxv = -1;
        dfs(i);
        ans += cnt * maxv;
    }
    printf("%lld", ans);
    return 0;
}

H题 数字串 (模拟)

很显然,想要变换的话,只有两种方法:

  1. 拆:例如 \(w\) 拆成 \(bc\)\(p\) 拆成 \(af\) 之类

  2. 并:上面的逆操作,例如 \(ad\) 拆成 \(n\) 等等

另外,注意 \(j\)\(t\) 这两个特殊字符,他们没法拆,也没法并

这题没啥大思路,就是纯模拟,代码有亿点小细节,烦的一

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
string s1, ans;

int main()
{
    cin>>s1;
    ans = "";
    bool flag = false;
    for (int i = 0; i < s1.length(); ++i) {
        char c = s1[i];
        if (c  <= 'j' || c == 't') ans += c;
        else {
            flag = true;
            if (c == 'k') ans += "aa";
            else if (c == 'l') ans += "ab";
            else if (c == 'm') ans += "ac";
            else if (c == 'n') ans += "ad";
            else if (c == 'o') ans += "ae";
            else if (c == 'p') ans += "af";
            else if (c == 'q') ans += "ag";
            else if (c == 'r') ans += "ah";
            else if (c == 's') ans += "ai";
            else if (c == 'u') ans += "ba";
            else if (c == 'v') ans += "bb";
            else if (c == 'w') ans += "bc";
            else if (c == 'x') ans += "bd";
            else if (c == 'y') ans += "be";
            else if (c == 'z') ans += "bf";
        }
    }
    if (flag) cout<<ans;
    else {
        ans = "";
        int i = 0;
        while (i < s1.length()) {
            bool tmp = false;
            if (i != s1.length() - 1 && flag == false) {
                if (s1[i] == 'a' && s1[i+1] <= 'i') {
                    flag = true;
                    tmp = true;
                    if (s1[i+1] == 'a') ans += "k";
                    else if (s1[i+1] == 'b') ans += "l";
                    else if (s1[i+1] == 'c') ans += "m";
                    else if (s1[i+1] == 'd') ans += "n";
                    else if (s1[i+1] == 'e') ans += "o";
                    else if (s1[i+1] == 'f') ans += "p";
                    else if (s1[i+1] == 'g') ans += "q";
                    else if (s1[i+1] == 'h') ans += "r";
                    else if (s1[i+1] == 'i') ans += "s";
                    i++;
                }
                else if (s1[i] == 'b' && s1[i+1] <= 'f') {
                    flag = true;
                    tmp = true;
                    if (s1[i+1] == 'a') ans += "u";
                    else if (s1[i+1] == 'b') ans += "v";
                    else if (s1[i+1] == 'c') ans += "w";
                    else if (s1[i+1] == 'd') ans += "x";
                    else if (s1[i+1] == 'e') ans += "y";
                    else if (s1[i+1] == 'f') ans += "z";
                    i++;
                }
            }
            if(!tmp) ans += s1[i];
            ++i;
        }
        if (flag) cout<<ans;
        else cout<<-1;
    }
    return 0;
}

I题 序列的美观度 (DP/贪心)

法一:DP

\(dp_i\) 为以 \(i\) 位为结尾的子序列的美观度最大值,那么有转移方程:

\(dp_i= \begin{cases} \max dp_j + 1 &\text{if } j < i,a_i=a_j \\ \max dp_j &\text{if } j < i,a_i\not=a_j \end{cases}\)

(这种最基础的 \(DP\) 方程我居然都没有推出来,我感觉我可以埋了)

但是这个方程的朴素复杂度是 \(O(n^2)\)\(T\),所以必须想办法优化。

下一个式子的维护比较方便,直接线性维护就好,主要是上面那个 \(nt\) 式子,我们必须开个桶来记录下(逃

典型的 \(O(n^2)\) 优化成 \(O(n)\)\(DP\) 典范

#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n, a[N], pos[N], dp[N];
int main()
{
    //
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    //
    int Maxv = 0;
    memset(pos, -1, sizeof(pos));
    for (int i = 1; i <= n; ++i) {
        dp[i] = Maxv;
        if (pos[a[i]] != -1)
            dp[i] = max(Maxv, dp[pos[a[i]]] + 1);
        pos[a[i]] = i;
        Maxv = max(Maxv, dp[i]);
    }
    printf("%d", Maxv);
    return 0;
}

法二:贪心

建议看官方题解,我说的不太明白,整体意思就是,尽量选相同数字,如果能选的话,尽量越近越好(好像有点懂,但是并不是很清楚代码上面如何实践

J题 加法和乘法 (数学博弈)

奇数+奇数=偶数,奇数+偶数=奇数,偶数+偶数=偶数

奇数*奇数=奇数,奇数*偶数=偶数,偶数*偶数=偶数

我们将数字对 \(2\) 取模,变成 \(0\)\(1\),似乎也能找到对应的运算:

\(1 xor 1=0,1xor0=1,0xor0=0\)

\(1\&1=1,1\&0=0,0\&0=0\)

那么我们就可以将读进来的数字按照奇偶数分类即可

官方题解给的讲解挺细,我给一个不太一样的思路(正确性不清楚,但反正过了):

显然,当所有数字都是偶数时,牛妹必胜

那么很显然,牛牛必须尽可能的减少偶数,牛妹必须尽可能的增加偶数

那么只需要一个 \(for\) 循环模拟下这个过程就好了,复杂度可以接受

#include<bits/stdc++.h>
using namespace std;
int n, x, y;
int main()
{
    scanf("%d", &n);
    for (int i = 1, tmp; i <= n; ++i) {
        scanf("%d", &tmp);
        tmp % 2 ? x++ : y++;
    }
    int user = 1;
    for (; n > 1; --n, user = 1 - user) {
        if (user == 1) {
            if(y)y--;else x--;
        }
        else {
            if (x > 1) x -= 2, y += 1;
            else if(x == 1) x--;
            else y-=1;
        }
    }
    if (x) puts("NiuNiu");else puts("NiuMei");
    return 0;
}

当然,如果复杂度接受不了呢?

其实也很简单,基于上面的推理,我们可以继续推,得到一个推论:当偶数数量大于等于两个的时候,牛妹必胜(因为此时可以保证两人僵持的情况下,奇数不断消耗,偶数数量不变,一直拖到全部是偶数的情况,详情建议看官方题解)

posted @ 2021-02-07 18:09  cyhforlight  阅读(94)  评论(0编辑  收藏  举报