2023YNU“迎新杯”程序设计大赛の题解

移步我的小站,食用更佳 =》传送门

A:Welcome to Yunnan!

【知识点】 数学、思维题

【idea】

假如 n n n k k k 的倍数,直接每个碟子放1个即可;

否则,由于每个碟子至少放一个,我们先在每个碟子内放一个

然后找到比n大的第一个k的倍数,把相差的这么多个均分到 n n n 个碟子

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int main()
{
    ll n, k;
    cin >> n >> k;
    if(n % k == 0) {
        cout << 1 << endl;
    } else {
        ll sum = (n + k - 1) / k * k; //找到比n大的第一个k的倍数
        ll r = sum - n;
        cout << (r+n-1)/n + 1 << endl;
    }
    return 0;
}

B:Welcome come to YNU!

【知识点】递推

【idea】

ynuer出现的次数,可通过每一个r之前出现的ynue的个数累加求得;

同理,yune出现的次数…

其实本题也可以改为用一个数组来递推,但由于ynuer比较短,用五个变量即可达到目的;假如要求计数的字符串长度很长,也可以照样通过数组来递推。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
typedef long long ll;
const int mod = 1e9+7;
int main()
{
    string s;
    cin >> s;
    ll y = 0; //y出现的个数
    ll yn = 0; //yn出现的个数
    ll ynu = 0; //ynu出现的个数
    ll ynue = 0; //ynue出现的个数
    ll res = 0; //ynuer出现的个数
    for(char c : s) {
        if(c == 'y') y ++;
        if(c == 'n') yn = (yn + y) % mod;
        if(c == 'u') ynu = (ynu + yn) % mod;
        if(c == 'e') ynue = (ynue + ynu) % mod;
        if(c == 'r') res = (res + ynue) % mod;
    }
    cout << res << endl;
    return 0;
}

C:云上的气球派对

【知识点】桶思想

考察对一维数组的理解,与桶思想类似,典

【idea】

设立一个元素数组、一个地址数组

交换的时候两个数组的值都要改变。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
typedef long long ll;

int main()
{
    int n, q;
    cin >> n >> q;
    vector<int> a(n+5), p(n+5);
    //a[i]元素数组:代表第i个气球上的数字
    //p[i]地址数组:代表写有i的气球所在位置编号
    for(int i=1; i<=n; i++)
        a[i] = p[i] = i;
    while(q --)
    {
        int x;
        cin >> x;
        int p1 = p[x], p2; //p1记录x的原位置,p2记录x将前往的位置
        if(p1 != n) p2 = p1+1;
        else p2 = p1-1;
        swap(a[p1], a[p2]);
        p[a[p1]] = p1;
        p[a[p2]] = p2;
    }
    for(int i=1; i<=n; i++)
        cout << a[i] << " ";
    return 0;
}

D:穿越之我是最伟大的卡牌大师!

【知识点】贪心算法

【idea】

本题改编自某一年ICPC亚洲区域赛(南京)的签到题

首先,要使p/q越大,q-1的次数越多越好。

我们先尽可能多的把0都当做-1使,如果走到后面发现前面q加的不够,再来矫正。

这里有个技巧,用一个tot变量来记录此前0->-1的数量,就可以用O(1)实现矫正。

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
typedef long long ll;
inline void solve()
{
    int n;
    scanf("%d", &n);
    
    vector<int> a(n+5);
    for(int i=1; i<=n; i++)
        scanf("%d", &a[i]);
    int p = 1, q = 1;
    int tot = 0;
    for(int i=1; i<=n; i++)
    {
        if(a[i] == 1) {
            p ++, q ++;
        }
        if(a[i] == 0) {
            if(q > 1) { //q还大于1,就把0当-1
                q --;
                tot ++;
            } else {
                p ++;
                q ++;
            }
        }
        if(a[i] == -1) {
            q --;
            if(q == 0) {
                if(tot > 0) { //纠正一个0
                    q += 2;
                    p ++;
                    tot --;
                    continue;
                }
                puts("-1");
                return;
            }
            
        }
    }
    int g = __gcd(p, q);
    printf("%d %d\n", p/g, q/g);
}
int main()
{
    int t;
    t = 1;
    while(t--)
        solve();
    return 0;
}

E:穿越之我要玩魔兽争霸!

【知识点】思维题

【idea】

首先,数据摆在那里了,这么大的数据,就不用考虑暴力模拟了。

这道题的过程虽然看着复杂,但是我们以宏观的视角去看这个问题,其实就没那么复杂。

需要重点关注的性质: x + y ≤ n x+y≤n x+yn

我们可以分两类情况讨论:

x ≤ y x≤y xy 则一定每秒都可以召唤水元素,即可召唤m个;

x > y x>y x>y 时,一定不会超过魔法值上限,所以 m m m秒的总魔法值为 n + ( m − 1 ) ∗ y n+(m-1)*y n+(m1)y(因为最后一秒回复的魔法值是在召唤后所以用不到,故 m − 1 m-1 m1),召唤一个水元素消耗 x x x点魔法,故召唤水元素为 m i n min min { m , ( n + ( m − 1 ) ∗ y ) / x m, (n+(m-1)*y)/x m,(n+(m1)y)/x}。(之所以要取min,是因为当n很大时,得出的值可能大于m)

综合可得, a n s = m i n ( m , ( n + ( m − 1 ) ∗ y ) / x ) ans=min(m,(n+(m-1)*y)/x) ans=min(m,(n+(m1)y)/x)

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
typedef long long ll;

int main()
{
    ll n, m, x, y;
    cin >> n >> m >> x >> y;
    cout << min(m, ((m-1)*y+n)/x) << endl;
    return 0;
}

Fun Fact:出这题的时候出题人不再善良@_@没有让暴力做法过。

我们知道普通cpu在1s时限内可以做 1 0 9 10^9 109次运算,但一般做算法题的时候我们都需要把程序的最大操作数控制在 1 0 8 10^8 108以内,甚至有些题是 1 0 7 10^7 107以内。

这是因为:一方面,一个算法都是会有常数的,在实际运行过程中,算法的常数会使实际操作数增大一个或多个数量级;另一方面测评机运行和本地运行不太一样,申请地址空间、调用编译好的文件啊都是需要费时的。所以说上限不是1e9。

但是验题的时候,octal发现O(1e9)的数据在C++20开O2优化的情况下竟然可以只用700ms,所以之后就更改了时限。

F:云山探险记

【知识点】背包 / dp+二分

【idea】

两种做法:一种是当做前缀背包的板子题来做;一种是做一个思维的转化,转为一道二分答案来写

第一种做法有点超纲,算是铜牌算法了,目前不要求掌握。这里提供第二种解法。

其实很简单,假如一个物品是 “必要” 的,那么所有勇气值比它大的物品也一定是必要的。

证明 :若 x x x 是必要的,则存在集合 S S S 使得 S S S 去掉 x x x < K <K K,加上 x x x ≥ K ≥K K。那么对于 S S S 中的任意一个大于 x x x 的数,显然也一定是必要的。对于集合外任意一个大于 x x x 的数 y y y ,也一定有 S − x + y S-x+y Sx+y 满足条件。

所以,我们二分查找这个边界即可。

c h e c k check check 函数需要用到 DP,这个过程其实就等价于货币面值那个模型。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
int main()
{
    int n, k;
    cin >> n >> k;
    vector<int> a(n+5);
    for(int i=1; i<=n; i++)
        scanf("%d", &a[i]);
    sort(a.begin()+1, a.begin()+1+n);
    
    auto check = [&](int x) {
        vector<int> dp(k+5);
        dp[0] = 1;
        for(int i=1; i<=n; i++)
        if(i != x) {
            for(int j=k; j>=a[i]; j--)
                dp[j] |= dp[j-a[i]];
        }
        for(int i=max(k-a[x], 0); i<=k-1; i++)
        if(dp[i]) return true;
        return false;
    };
    int l = 1, r = n+1;
    while(l < r)
    {
        int mid = l+r >> 1;
        if(check(mid)) r = mid;
        else l = mid+1;
    }
    cout << n-l+1 << endl;
    return 0;
}

G:黑白手

【知识点】博弈论、思维题

【idea】

n × n n × n n×n 的棋盘是中心对称的,所以后手的最优策略就是模仿先手;而先手的最优策略是尽可能多地走过棋盘的格子。

在双方的相互施法下,最终一定是能将棋盘的每个格子都走过。

那么,棋盘的格子总数为奇数,就是先手获胜;反之,则后手胜。

#include<bits/stdc++.h>
using namespace std;

int main()
{
    int n;
    cin >> n;
    if(n % 2) puts("J");
    else puts("L");
    return 0;
}

H:幸福在哪里

【知识点】暴力模拟 / 数学

作为签到题,本题数据范围很小,没有卡暴力做法

#include<bits/stdc++.h>
using namespace std;
#define int long long

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);cout.tie(nullptr);
    int n,k;
    cin>>n>>k;
    string str="";
    for(int i=0;i<26;i++){
        for(int j=0;j<n;j++){
            str+='A'+i;
        }
    }
    cout<<str[k-1]<<"\n";
    return 0;
}

当然,除了暴力模拟,也可以直接得到答案

(赛场上遇到这种范围的题,建议暴力做,肯定不会出错)

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
typedef long long ll;

int main()
{
    int n, k;
    cin >> n >> k;
    cout << (char)((k+n-1)/n + 'A' - 1) << endl;
    return 0;
}

I:幸福终将降临在我们头上

【知识点】前缀数组+双指针算法 / 线段树 + 二分

【idea】

先想清楚一个结论:在一个序列中,假如要交换两数的位置,且只允许用类似冒泡排序交换方式,最少交换次数为两数之间的距离。

举例来说,我要交换 a [ i ] a[i] a[i] a [ j ] a[j] a[j] ,最少需要 ∣ i − j ∣ |i-j| ij 次相邻交换。

接下来,我们再来想这道题要怎么做。

①第一步,先建立数学模型

我想让交换之后严格大于交换之前,是不是只要在 [ k + 1 , n ] [k+1, n] [k+1,n] 之中找一个数 x x x ,再在 [ 1 , k ] [1, k] [1,k] 中找一个数 y y y ,满足 x > y x > y x>y,然后交换两数即可。

那么问题可以转化为,找一个区间 [ L , R ] [L, R] [L,R],使得区间包含 k k k,并且 [ k + 1 , R ] [k+1,R] [k+1,R] 中 存在 x x x , [ L , k ] [L, k] [L,k] 中存在 y y y,满足 x > y x > y x>y。求对于所有满足条件的区间,区间长度的最小值。

这种模型,一看就应该想到经典二分,可以二分区间长度然后 c h e c k check check 一下,关于如何找 x , y x, y x,y ,可以考虑使用线段树。

当然,对于这道题来说,二分还是属于暴力的做法,但不失为一个好方法。

接下来介绍另一种转化,运用了前缀数组的思想。

因为这道题重要的是 k k k 之前和 k k k 之后的大小关系,但并不是所有的数都是重要的。在k前只有大数重要,在k后只有小数重要。

我们可以用前缀最值(后缀最值)来维护这些数。

我们用前缀最值(后缀最值)可以把目光集中在对两边数组的大小关系起决定性变化的几个数(也就是我们维护的后缀min前缀max)而忽视掉不大不小的那些无关紧要的数。

然后就是双指针算法的应用了。

我们要求最小值嘛,外循环来移左边界,不断地去缩小区间长度。

为了更好地理解双指针的过程,我们需要明白一个这个前缀数组(后缀数组)的特性:

k前面是单调递增的,k后面也是单调递增的。

O(n)

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
typedef long long ll;

int main()
{
    int n, k;
    cin >> n >> k;
    vector<int> a(n+5);
    for(int i=1; i<=n; i++)
        cin >> a[i];
    for(int i=k-1; i>=1; i--)
        a[i] = min(a[i+1], a[i]); //前面找一个小的
    for(int i=k+2; i<=n; i++)
        a[i] = max(a[i-1], a[i]); //后面找一个大的
    int res = 1e9;
    for(int i=1, j=k+1; i<=k; i++)
    {
        while(j <= n && a[j] <= a[i])
            j ++;
        if(j <= n)
            res = min(res, j - i);
    }
    if(res < 1e9) cout << res << endl;
    else cout << -1 << endl;
    return 0;
}

Lihg的本意是想考上面这个方法,但我验题的时候发现搓一个线段树也能过。

想着这道题能想到第一步转化,并且用线段树来实现也算很不错了,所以没有也并没有让出题人加强数据。

不过线段树方法属于暴力做法,时间复杂度还是很危险的,一不小心就会超时。

O(nlogn*logn)

#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define pdd pair<double, double>
typedef long long ll;
const int N = 4e5+5;
int a[N];
struct node {
    int maxv;
    int minv;
} tr[N * 4];
void update(int id)
{
    tr[id].maxv = max(tr[id*2].maxv, tr[id*2+1].maxv);
    tr[id].minv = min(tr[id*2].minv, tr[id*2+1].minv);
}
void build(int id, int l ,int r)
{
    if(l == r) 
        tr[id].maxv = tr[id].minv = a[l];
    else {
        int mid = l+r >> 1;
        build(id * 2, l, mid);
        build(id * 2 + 1, mid + 1, r);
        update(id);
    }
}
//查询的区间为ql和qr max
int query1(int id, int l, int r, int ql, int qr)
{
    if(l == ql && r == qr) return tr[id].maxv;
    int mid = l+r >> 1;
    if(qr <= mid) return query1(id*2, l, mid, ql, qr);
    else if(ql > mid) return query1(id*2+1, mid+1, r, ql, qr);
    else return max(query1(id*2, l, mid, ql, mid),
    query1(id*2+1, mid+1, r, mid+1, qr));
}
//查询的区间为ql和qr min
int query2(int id, int l, int r, int ql, int qr)
{
    if(l == ql && r == qr) return tr[id].minv;
    int mid = l+r >> 1;
    if(qr <= mid) return query2(id*2, l, mid, ql, qr);
    else if(ql > mid) return query2(id*2+1, mid+1, r, ql, qr);
    else return min(query2(id*2, l, mid, ql, mid),
    query2(id*2+1, mid+1, r, mid+1, qr));
}
int main()
{
    int n, k;
    cin >> n >> k;
    priority_queue<int> hp;
    for(int i=1; i<=n; i++) {
        cin >> a[i];
        hp.push(a[i]);
    }
    build(1, 1, n);
    set<int> b, c;
    for(int i=1; i<=k; i++) {
        b.insert(a[i]);
        c.insert(hp.top());
        hp.pop();
    }
    if(b == c) {
        puts("-1");
        return 0;
    }
    auto check = [&](int x) {
        for(int i=k+1; i<=k+x; i++)
        {
            int R = min(n, i);
            int L = max(1, i-x);
            int minv = query2(1, 1, n, L, k);
            int maxv = query1(1, 1, n, k+1, R);
            
            if(maxv > minv) 
                return true; 
                
        }
        return false;
    };
    int l = 1, r = n;
    while(l < r) {
        int mid = l+r >> 1;
        if(check(mid)) r = mid;
        else l = mid + 1;
    }
    cout << l << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值