第八届“图灵杯”NEUQ-ACM程序设计竞赛个人赛 解题补题报告

官方题解

A题 切蛋糕

以后再补。

B题 小宝的幸运数组 (前缀和,贪心,Hash)

B 和 C 的子数列,都必须要求连续,这点和我们正常理解的子数列不一样,写的时候注意下。

不管咋样,前缀和维护一下总不会错。

如果 \(O(n^2)\) 枚举肯定会超时,那么我们必须想办法优化。

有这样一个题目,不知道大家有没有做过:

给定一个长度为 \(n\) 的数组 \(a\) 和一个常数 \(C\),问有哪些\(1 \leq i < j \leq n\),满足 \(a_i+a_j=C\)

我们可以 \(O(n^2)\) 枚举,也可以带上原坐标排序后枚举 \(i\)\(j\),然后对另外一个进行二分查找,复杂度 \(O(n \log n)\)。不过,还有另外一种方法:开一个额外数组 \(vis\),使得 \(vis[a[i]]=i\) (我们假定没有重复项,如果有的话就开一个邻接表啥的),然后枚举 \(i\), 之后在 \(vis\) 里面查询是否存在 \(C-a_i\)。纯粹数组的话,复杂度是 \(O(n)\),速度很快,但前提是内存允许我们开一个值域大小的 \(vis\) 数组(其实就是 \(Hash\))。如果内存不够,我们可以使用取模啥的方法来节省内存(但是有可能会冲突),或者使用 \(map\) (但是时间复杂度就上升到了\(O(n \log n)\))。

这题的思路也是类似的:找到 \(0 \leq i < j \leq n +1\),满足 \(\displaystyle\sum\limits_{k=1}^{i}a_i+\displaystyle\sum\limits_{k=j}^{n}a_i\)\(\displaystyle\sum\limits_{k=1}^{n}a_i\) 关于 \(k\) 同余?(这样的话,\(\displaystyle\sum\limits_{k=i+1}^{j-1}a_i\) 就是 \(k\) 的倍数了,也就是找到子数列 \([i+1,j-1]\) 了)

类似的,我们也开一个数组记录一下,然后枚举 \(j\)\(O(1)\) 查找 \(i\),总复杂度 \(O(n)\)

有个小玩意需要注意下:我们希望 \(i\) 越往左边越好(这样区间长度 \(j - i - 1\) 才越长),所以生成 \(Hash\) 数组的时候要从右往左更新,这样可以保证每次查询到的 \(i\) 都是尽可能最左边的。

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, k;
long long a[N], sum[N];
long long t1[N], t2[N];
int Hash[N];
void solve()
{
    //init
    memset(Hash, -1, sizeof(int) * k);
    //input
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i)
        scanf("%lld", &a[i]);
    for (int i = 1; i <= n; ++i)
        sum[i] = sum[i - 1] + a[i];
    //solve
    for (int i = 1; i <= n; ++i)
        t1[i] = sum[i] % k, t2[i] = (sum[n] - sum[i - 1]) % k;
    t2[n+1] = 0;
    for (int i = n; i >= 0; --i)
        Hash[t1[i]] = i;
    int ans = -1;
    for (int j = n + 1; j >= 1; j--) {
        int i = Hash[(sum[n]%k + k - t2[j]) % k];
        if (i != -1 && i + 1 <= j - 1)
            ans = max(ans, j - i - 1);
    }
    printf("%d\n", ans);
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) {
        solve();
    }
    return 0;
}

补充

\(\displaystyle\sum\limits_{i=l}^r a_i=\displaystyle\sum\limits_{i=1}^r a_i-\displaystyle\sum\limits_{i=1}^{l-1} a_i=sum_r-sum_{l-1}\)

所以 \((\displaystyle\sum\limits_{i=l}^r a_i)\%k = sum_r\%k - sum_{l-1}\% k\)

整体思路不变(贪心,\(Hash\) 啥的),但是这种写法会比我上面的写法更简单直接

C题 上进的凡凡 (模拟,贪心)

一个单调不降数列,他的所有子数列全部单调不降。

那么,这题的思路似乎就显而易见了:将整个数组划分成若干个单调递增的子数列,然后分别统计答案(记得开 \(long long\)

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, a[N];
inline long long calc(long long x) {
    return x * (x + 1) / 2;
}
int main()
{
    //input
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    a[n + 1] = -1e9;
    //solve
    long long ans = 0;
    int begin = 1;
    for (int i = 1; i <= n; ++i) {
        if (a[i+1] < a[i]) {
            ans += calc(i - begin + 1);
            begin = i + 1;
        }
    }
    //output
    printf("%lld", ans);
    return 0;
}

D题 Seek the Joker I (数学:巴什博弈)

板子题,给一个题意概括:

\(n\) 个石子,每次可以取走 \(x\) 个,两人轮番取石子,取走最后一个石子者输。如果两人均采取最优策略,问谁能赢?

\(1 \leq x \leq k\)\(k\) 为一常数

对这个知识点感兴趣的可以自己去百度,我直接给出我写这题的思路:

\(n=1\) 时,显然先手必败。

\(2 \leq n \leq k+1\) 时,先手可以拿走 \(n-1\) 个石子,使得场上还剩 \(1\) 个,所以先手必胜

\(n=k+2\) 时,不论怎么拿石子,都会转移到一个先手必胜状态,所以先手必败

\(k+3 \leq n \leq 2(k+1)\) 时,先手可以拿走 \(n-k-2\) 个石子,使得场上还剩下 \(k+2\) 个石子,所以先手必胜

......

综上,显然当 \((n-1) \% (k+1)=0\) 时先手必败,否则先手必胜

#include<bits/stdc++.h>
using namespace std;
bool solve()
{
    int n, k;
    cin>>n>>k;
    if ((n - 1) % (k + 1)) return true;
    else return false;

}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) {
        puts(solve() ? "yo xi no forever!" : "ma la se mi no.1!");
    }
    return 0;
}

E题 Seek the Joker I (数学:威佐夫博弈)

有两堆石子,个数分别为 \(n\)\(m\)。每次可以选一堆取走任意多个石子,或者从两堆同时取走相同个数的石子。两人轮流取石子,先取完所有石子者胜。在两者均采用最优策略的情况下,问谁能赢?

似乎看的并不是很懂,我直接放别人的 \(AC\) 代码得了:

#include <bits/stdc++.h>
using namespace std;
int main()
{
    double mysticalConstant = (1.0 + sqrt(5.0)) / 2.0;
    int T;
    cin >> T;
    while (T--)
    {
        int n, x;
        cin >> n >> x;
        int a = x - 1, b = n - x;
        if (a > b) swap(a, b);
        int temp = (b - a) * mysticalConstant;
        puts((temp != a) ? "yo xi no forever!" : "ma la se mi no.1!");
    }
    return 0;
}

F题 成绩查询ing (字符串处理,卡常)

这个题目的数据不是一般的离谱,我也不知道为啥用 \(set\) 排序就可以 \(AC\),用 \(sort\) 就会 \(TLE\) ......

\(AC\) 代码 和 \(TLE\) 代码都贴上来(注释掉的是 \(TLE\) 代码(跟 \(cin/cout\) 优化没关系))

还有,如果把 \(string\) 换成普通 \(char\)数组也能过,但是最好别用啥 \(Hash\)\(unordered\_map\) 这些花里胡哨的东西了(我就是被这玩意整 \(WA\) 的)

总而言之,\(ACM\) 考卡常题的,出题人多少都沾点那啥

//string版,排序基于set实现(cin/sout优不优化没区别)
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
struct Student {
    string name;
    int grade, sex, number;
}a[N];
int n, m;
//subtask1
map<string,int> subtask1;
//subtask2
//vector<string> G[110];
set<string> subtask2[110];
int main()
{
    cin>>n;
    for (int i = 1; i <= n; ++i) {
        cin>>a[i].name>>a[i].grade>>a[i].sex>>a[i].number;
        subtask1[a[i].name] = i;
        //G[a[i].grade].push_back(a[i].name);
        subtask2[a[i].grade].insert(a[i].name);
    }
    /*
    for (int i = 0; i <= 100; ++i)
        sort(G[i].begin(), G[i].end());
    */
    cin>>m;
    while (m--) {
        int opt, grade;
        string name;
        cin>>opt;
        if (opt == 1) {
            cin>>name;
            Student &tmp = a[subtask1[name]];
            cout<<tmp.grade<<" "<<tmp.number<<" "<<tmp.sex<<endl;
        }
        else {
            cin>>grade;
            /*
            for (int i = 0; i <G[grade].size(); ++i) {
                cout<<G[grade][i]<<endl;
            }
            */
            set<string>::iterator it;
            it=subtask2[grade].begin();
            for(;it!=subtask2[grade].end();it++)
                cout<<*it<<"\n";
        }
    }
    return 0;
}
//char数组版,排序基于sort实现(但是只进行指针交换,不交换具体的字符串,这样速度更快)
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
struct Student {
    int grade, sex, number;
}a[N];
int n, m;
//subtask1
map<string, int> subtask1;
//subtask2
vector<char*> G[110];
bool cmp(char *s1, char *s2) {
    return strcmp(s1, s2) < 0;
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        char *s = (char*) malloc(20 * sizeof(char));
        scanf("%s%d%d%d", s, &a[i].grade, &a[i].sex, &a[i].number);
        subtask1[s] = i;//学到了,char数组的字符串也可以这样直接丢进map里面
        G[a[i].grade].push_back(s);
    }
    for (int i = 0; i <= 100; ++i)
        sort(G[i].begin(), G[i].end(), cmp);
    scanf("%d", &m);
    int opt, grade;
    char name[20];
    while (m--) {
        scanf("%d", &opt);
        if (opt == 1) {
            scanf("%s", name);
            Student &tmp = a[subtask1[name]];
            printf("%d %d %d\n", tmp.grade, tmp.number, tmp.sex);
        }
        else {
            scanf("%d", &grade);
            for (int i = 0; i <G[grade].size(); ++i) {
                printf("%s\n", G[grade][i]);
            }
        }
    }
    return 0;
}

G题 贪吃的派蒙 (数学)

直接找到派蒙的位置,先撸一遍他前面的人的吃掉的东西的范围,如果能让派蒙刷盘子就好,不能的话就下一轮,这时候是除了派蒙的所有人都要撸一遍,然后就有点像那种怎么说呢,换种方法描述一下

现在有甲,派蒙两个人轮番吃东西,派蒙每次都吃固定的 \(a_x\) 个,而甲则是第一次会吃区间 \([L_1,R_1]\) 之内的东西,之后每次会吃 \([L_2,R_2]\) 之内的东西。那么我们就需要控制范围,使得尽量使派蒙无法吃到东西,从而刷盘子。

显然,\(L_1=x-1,L_2=n-1,R_1=\displaystyle\sum\limits_{i=1}^{x-1}a_i,R_2=\displaystyle\sum\limits_{i=1}^{n}a_i - a_x\)

我们求出他们的值,就可以转化成,能否解出一个不等式的特定解:\(L_1+cnt*L_2 \leq k-cnt*a_x \leq R_1+cnt*R2\)

移项转化,变为 \(\frac{k-R_1}{R_2+a_x} \leq cnt \leq \frac{k-L_1}{L_2+a_x}\)

保证 \(cnt\) 存在非负解

这个可以写一个函数特判一下,看看是否存在自然数 \(x\),使得 \(\frac{A}{B} \leq x \leq \frac{C}{D}\) (注意我的用词,自然数,这是判定解是否在定义域上面,虽然这题没有卡,但是有可能在你修改别的 \(bug\) 的时候莫名触犯到这条禁忌)

bool exists(long long A, long long B, long long C, long long D) {
    if (A * D > B * C) return false;
    if (A % B == 0 || C % D == 0) return true;
    return A / B < C / D;
}

好了,于是我们写出了代码,然后交上去

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
long long k, a[N];
int getX() {
    long long maxv = -1;
    for (int i = 1; i <= n; ++i)
        maxv = max(maxv, a[i]);
    for (int i = 1; i <= n; ++i)
        if (maxv == a[i]) return i;
    return -1;
}
bool exists(long long A, long long B, long long C, long long D) {
    if (A * D > B * C) return false;
    if (A % B == 0 || C % D == 0) return true;
    return A / B < C / D;
}
bool solve()
{
    scanf("%d%lld", &n, &k);
    for (int i = 1; i <= n; ++i)
        scanf("%lld", &a[i]);

    int x = getX();
    long long n0 = x - 1, S0 = 0, S1 = 0;
    for (int i = 1; i < x; ++i) S0 += a[i], S1 += a[i];
    for (int i = x + 1; i <= n; ++i) S1 += a[i];
    //if (n0 <= k && k <= S0) return true;
    return exists(k - S0, S1 + a[x], k - n0, a[x] + n - 1);
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) {
        puts(solve() ? "YES" : "NO");
    }
    return 0;
}

啪的一下就 \(WA\) 了,很快啊!

这个点我一直捉摸不透,知道看了官方题解,然后细细比对,发现了一个 \(bug\)

上面那个 \(exists\) 函数,由于负数运算,以及 \(C/C++\) 除法实现的原因(\(C/C++\) 里面的除法是向 \(0\) 取整而非向下取整),似乎对于 \(k \leq R_1\) 的情况不太适用,所以必须特判。另外,我们的 \(cnt\) 必须是一个自然数,所以对于 \(k < L_1\) 的情况必须输出 \(NO\),而不是管有没有解。

也就是在 \(exists\) 之前加上这么两行(在 \(WA\) 代码里面被注释掉了):

if (k < n0) return false;
if (n0 <= k && k <= S0) return true;

(实际上,我们写的这个函数,是可以对付 \(k<L_1\) 的情况的,虽然是阴差阳错)

H题 数羊 (数学)

好在 \(m\) 的取值只有 \(0,1,2\),那么我们照着递推一下就行了,高中找规律难度:

\(m=0\)的情况,题目已经给出了,直接用就完事了。(一定要记得\(A(1,0)=2\),我当时就是被这个卡了一会)

\(m=1\)的情况如下:\(A(n,1)=A(A(n-1,1),0)\)

\(A(0,1)=1\),所以 \(A(1,1)=A(1,0)=2\),那么\(A(2,1)=A(A(1,1),0)=2+2=4\),依此类推......

根据数学归纳法可得:\(A(n,1)=2n\)

\(m=2\)的情况如下:\(A(n,2)=A(A(n-1,2),1)=2A(n-1,2)\),又因为\(A(0,2)=1\),所以\(A(n,2)=2^n\)

然后照着写就好了(需要取模,所以要写一个快速幂

#include<bits/stdc++.h>
using namespace std;
const long long mod = 998244353;
long long quickpow(long long n) {
    if (n == 0) return 1;
    else if (n == 1) return 2;
    long long half = quickpow(n / 2);
    if (n % 2 == 0) return half * half % mod;
    else return half * half % mod * 2 % mod;
}
long long calc(long long n, long long m) {
    if (m == 0) return (n == 1) ? 2 : n + 2;
    if (m == 1) return n * 2 % mod;
    else return quickpow(n) % mod;
}
int main()
{
    int T;
    cin>>T;
    while (T--) {
        int n, m;
        cin>>n>>m;
        cout<<calc(n, m)<<endl;
    }
    return 0;
}

I题 买花 (数学)

假设第 \(1\) 天买 \(m\) 朵,分 \(k\) 天买完,那么可以买 \(\displaystyle\sum\limits_{i=1}^{k}{2^{i-1}m} = (2^k-1)m\)

\(2\)\(15\) 枚举 \(k\),如果 \(n\) 可以被 \(2^k-1\) 整除,就输出 \(YE5\)。如果找不到能整除的,就输出 \(N0\)。(老是在输出上面玩障眼法,累不累啊(所以一定要记得直接复制题目里面的字符串,尽量别自己打))

#include<bits/stdc++.h>
using namespace std;
long long p[20];
int main()
{
    for (int i = 1; i <= 15; ++i)
        p[i] = (1<<i) - 1;
    int T;
    cin>>T;
    while (T--) {
        long long n;
        cin>>n;
        bool flag = false;
        for (int i = 2; i <= 15; ++i) {
            if (n % p[i] == 0) {
                flag = true;
                break;
            }
        }
        if (flag) cout<<"YE5"<<endl;
        else cout<<"N0"<<endl;
    }
    return 0;
}

J题 这是一题简单的模拟 (图的遍历)

我看到这个题目,还以为这是个难题;结果没想到,这确实是一个简单的模拟......

直接照着模拟就完事了,好像除了手误,也没有碰到啥编码障碍

#include<bits/stdc++.h>
using namespace std;
const int N = 310;
int n, m, dis[N][N], x[N], vis[N];
long long solve()
{
    memset(vis, 0, sizeof(int) * (n + 1));
    int cnt;
    scanf("%d", &cnt);
    for (int i = 1; i <= cnt; ++i)
        scanf("%d", &x[i]);
    x[++cnt] = 0;
    int now = 0;
    int passed = 0;
    long long ans = 0;
    for (int i = 1; i <= cnt; ++i) {
        int to = x[i];
        if (dis[now][to] == 0x3f3f3f3f || vis[to]) return -1;
        ans += dis[now][to];
        if (to) ++passed, vis[to] = 1;
        now = to;
    }
    if (passed < n) return -1;
    return ans;
}
int main()
{
    memset(dis, 0x3f, sizeof(dis));
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; ++i) {
        int u, v, d;
        scanf("%d%d%d", &u, &v, &d);
        dis[u][v] = dis[v][u] = d;
    }
    int T;
    scanf("%d", &T);
    long long ans = 0x7fffffff;
    bool ans_exist = false;
    for (int i = 1; i <= T; ++i) {
        long long possible_ans = solve();
        if (possible_ans != -1)
            ans_exist = true, ans = min(ans, possible_ans);
    }
    if (ans_exist) printf("%lld", ans);
    else printf("-1");
    return 0;
}

K题 黑洞密码 (模拟)

看题面,看输入输出,估计也不是什么难题,照着模拟就完了。

这里有一个坑点,\(Z\) 后面是 \(b\) 不是 \(a\)\(z\) 后面是 \(B\) 不是 \(A\),这玩意必须记好了,不然估计得被数据折磨疯掉(难怪当时官方一再强调这题样例没出错,估计不少人都栽在这了)

别的没啥好说的,直接写就完事了,上代码:

#include<bits/stdc++.h>
using namespace std;
int main()
{
    char str[32];
    char s1[20], s2[20];
    scanf("%s", str);
    for (int i = 0, x = 0, y = 0; i < 32; ++i) {
        char c = str[i];
        if(c >= '0' && c <= '9')
            s2[++x] = c;
        else s1[++y] = c;
    }

    char ans[20];
    for (int n = 0; n < 4; ++n) {
        int l = 4 * n + 1, r = 4 * n + 4;
        for (int i = l; i <= r; ++i) {
            int dx = s2[i] - '0';
            if (s1[i] >= 'A' && s1[i] <= 'Z') {
                ans[i] = 'A' + (s1[i] - 'A' + dx);
                if (s1[i] - 'A' + dx >= 26)
                    ans[i] = 'b' + (s1[i] - 'A' + dx) % 26;
            }
            else {
                ans[i] = 'a' + (s1[i] - 'a' + dx);
                if (s1[i] - 'a' + dx >= 26)
                    ans[i] = 'B' + (s1[i] - 'a' + dx) % 26;
            }
        }
        reverse(ans + l, ans + r + 1);
    }
    for (int i = 1; i <= 16; ++i)
        putchar(ans[i]);
    return 0;
}

L题 建立火车站 (二分)

写过 NOIP2015 跳石头 的人,看到这题肯定很眼熟。

二分答案,对于每一个枚举的距离 \(d\),判断每两个相邻城市之间需要插入多少临时站点,最后统计一下,和 \(k\) 比较即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, k;
long long p[N];
bool check(long long d)
{
    int cnt = 0;
    for (int i = 1; i < n; ++i) {
        long long L = p[i + 1] - p[i];
        cnt += (L + d - 1) / d - 1;
    }
    return cnt <= k;
}
int main()
{
    //input
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i)
        scanf("%lld", &p[i]);
    sort(p + 1, p + n + 1);
    //solve
    long long l = 0, r = 1e12 + 10;
    while (l < r) {
        long long mid = (l + r) >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    //output
    printf("%d", l);
    return 0;
}
posted @ 2021-01-30 20:49  cyhforlight  阅读(137)  评论(0编辑  收藏  举报