蓝桥算法小白双周赛第一场题解2023.12.9

1.蘑菇炸弹

纯模拟, O ( n ) O(n) O(n)遍历统计即可
代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+10;
int arr[maxn];
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int ans=0,n;cin>>n;
    for(int i=1;i<=n;i++) cin>>arr[i];
    for(int i=2;i<=n-1;i++){
        if(arr[i]>=arr[i-1]+arr[i+1]) ans++;
    }
    cout<<ans<<endl;
    //system("pause");
    return 0;
}

2.构造数字

题意

给定 N N N, M M M,求出十进制下各个位的和为 M M M的最大 N N N位数

分析

看到数据范围 N , M ≤ 1 0 6 N,M\leq 10^6 N,M106,明显要用字符串,总的是 N N N位数,所以可以先初始化为 N N N 0 0 0。然后要让各个位数的和为 M M M且最大,显然贪心,令这个数的高位尽可能大,也就是前几位应该尽可能多地填 9 9 9,所以 O ( N ) O(N) O(N)地遍历这个答案字符串,最高位填 9 9 9的同时让 M M M减去9,一直到 M ≤ 9 M\leq 9 M9时填 M M M
代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int n,m;cin>>n>>m;
    string str;
    for(int i=1;i<=n;i++) str+='0';
    for(int i=0;i<n;i++){
        int tem=min(m,9);
        str[i]+=tem;
        m-=tem;
        if(m==0) break;
    }
    cout<<str<<endl;
    //system("pause");
    return 0;
}

3.小蓝的金牌梦

题意

给定一个数组 a [ N ] a[N] a[N],求该数组的长度为质数的最大子数组

分析

数据范围 2 ≤ n ≤ 1 0 5 2\leq n\leq 10^5 2n105,看到这题,先去搜了下 1 0 5 10^5 105范围内的质数数量,发现只有 1 0 3 10^3 103级别个,所以直接考虑枚举。
先枚举所有质数,再去枚举子数组的右端点, O ( 1 ) O(1) O(1)计算出左端点,考虑这个子数组和,所以可以再去用一个前缀和优化一下,知道左右端点后再 O ( 1 ) O(1) O(1)的计算出子数组和更新答案。
代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+100;
int cnt=0,vis[maxn],prime[maxn];//cnt为素数的个数,maxn是要找的素数的上界
int arr[maxn],sum[maxn];
void find_prime(){
    for(int i=2;i<maxn;i++){
        if(vis[i]==0)
            vis[i]=1,prime[++cnt]=i;
        for(int j=1;j<=cnt&&i*prime[j]<maxn;j++){
            vis[i*prime[j]]=1;
            if(i%prime[j]==0)break;
        }
    }
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    find_prime();
    int n;cin>>n;
    for(int i=1;i<=n;i++) cin>>arr[i],sum[i]=sum[i-1]+arr[i];
    int ans=-inf;
    for(int i=1;prime[i]<=n;i++){
        int x=prime[i];
        for(int j=x;j<=n;j++)
            ans=max(ans,sum[j]-sum[j-x]);
    }
    cout<<ans<<endl;
    //system("pause");
    return 0;
}

4.合并石子加强版

题意

n n n堆石子围成一个环,第 i i i堆石子的数量为 a [ i ] a[i] a[i],每次合并两个相邻石子,代价为两堆石子的数量乘积,问合并的最小代价。

分析

看数据范围 n ≤ 3 ∗ 1 0 4 n\leq 3*10^4 n3104,一开始感觉可能是个 O ( n 2 ) O(n^2) O(n2)的算法,但是这种合并又像是区间 d p dp dp,需要 O ( n 3 ) O(n^3) O(n3),所以先想着去贪心一下。
贪心的话无非就两种选择,要么每次合并最小的两堆,要么每次就合并最大的两堆,然后手写一个例子算一下,发现两种合并方法最终的答案一样,于是直接猜结论总花费跟合并次序无关(要证明应该也可以证明,就是直接设一堆字母,拿去合并,发现最后总答案不变,但是算法竞赛,能过就行,不用那么严谨)
于是直接 O ( N ) O(N) O(N)遍历,模拟从 1 1 1 n n n按顺序合并,并统计总花费。不过值得注意的是,这里乘积太大,会爆long long,所以我用的 i n t 128 int128 int128(不知道的小白可以去了解一下, i n t 128 int128 int128就像是long long中的long long,可以在大多数情况下替代高精度,缺点就是基本只能加减乘除取余,不能cin,cout,得去抄一份输入输出 i n t 128 int128 int128的板子)
代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=3e4+100;
int arr[maxn];
inline void printint(__int128 x){
    if (x < 0) putchar('-'),x = -x;
    if(x>=10) printint(x/10);
    putchar('0'^(x%10));
}
signed main(){
    int n;cin>>n;
    for(int i=1;i<=n;i++) cin>>arr[i];
    __int128_t ans=0,sum=0;
    for(int i=1;i<=n;i++){
        ans+=sum*arr[i];
        sum+=arr[i];
    }
    printint(ans);
    //system("pause");
    return 0;
}

5.简单的LIS问题

题意

给一个数组,在可以把一个任意位置的数 a r r [ i ] arr[i] arr[i]改为从 0 到 1 0 100 0到10^{100} 010100的任何数的前提下,求数组的最长上升子序列,数据范围 n ≤ 5 ∗ 1 0 3 n\leq 5*10^3 n5103,可以 O ( n 2 ) O(n^2) O(n2)

分析

首先容易想到的是,答案要么是原数组的最长上升子序列长度 l e n len len,要么是 l e n + 1 len+1 len+1。因为可以改变一个数的大小,那么肯定会去在原来的最长上升子序列的基础上,争取改变一个数,使得其插入原来的 L I S LIS LIS,那么答案就是 l e n + 1 len+1 len+1
那么考虑一下什么时候不行,有以下几种情况,一是原来的 L I S LIS LIS的开头已经是 0 0 0时,因为修改数字的范围是非负数,此时不能在 L I S LIS LIS的开头前面插数。二是当 L I S LIS LIS的最后一个数也是数组的最后一个数时,无法在 L I S LIS LIS后面插一个数。三是 L I S LIS LIS内部,如果是一个贴一个,或者相邻的数字之间只差1,都无法在内部再插入一个数。
于是有了第一种做法,分类讨论

做法一、插入位置分类讨论

这个做法我是从原题的题解区学到的,赛时也有想过,但是感觉不太对劲,没想到能过,但是我也证明不来,这里就介绍一下这种做法吧。
原理是经典的 L I S LIS LIS的单调栈优化做法 O ( N l o g N ) O(NlogN) O(NlogN),然后可以在单调栈中 s t a [ l e n ] sta[len] sta[len]储存有潜力的上升子序列,长度跟 L I S LIS LIS的长度是对的,但是具体的值不一定对应。
这种做法就是跑一遍这个单调栈优化,并且记录此时栈内每个值对应数组的下标 p o s [ l e n ] pos[len] pos[len],然后按照分析中的3种插入可能性判断,即

  • 能否在 L I S LIS LIS前面插入:判断 s t a [ 1 ] = 0 sta[1]=0 sta[1]=0
  • 能否在 L I S LIS LIS后面插入:判断 p o s [ l e n ] = n pos[len]=n pos[len]=n即最大元素是否为数组的最后一位
  • 能否在 L I S LIS LIS中间插入:遍历单调栈,如果有相邻的两个值满足,差大于2( s t a [ i + 1 ] − s t a [ i ] > 2 sta[i+1]-sta[i]>2 sta[i+1]sta[i]>2),并且在原数组中的位置不是紧挨着的( p o s [ i + 1 ] − p o s [ i ] > 1 pos[i+1]-pos[i]>1 pos[i+1]pos[i]>1),就可以在这两个数之间再插入一个数
    代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e3+10;
int arr[maxn],sta[maxn],pos[maxn];
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int n;cin>>n;
    for(int i=1;i<=n;i++) cin>>arr[i];
    int len=1;sta[len]=arr[1];pos[len]=1;
    for(int i=2;i<=n;i++){
        if(arr[i]>sta[len]){
            sta[++len]=arr[i];
            pos[len]=i;
        }
        else{
            int tem=lower_bound(sta+1,sta+1+len,arr[i])-sta;
            sta[tem]=arr[i];
            pos[tem]=i;
        }
    }
    if(len==1){
        cout<<min(2,n)<<endl;
        return 0;
    }
    int flag=0;
    for(int i=1;i<=len-1;i++){
        if(sta[i+1]-sta[i]>1&&pos[i+1]-pos[i]>1){
            flag=1;
            break;
        }
    }
    if(sta[1]!=0&&pos[1]!=1) flag=1;
    if(pos[len]!=n) flag=1;
    cout<<len+flag<<endl;
    //system("pause");
    return 0;
}

PS:这种做法我也不知道为什么替换不会导致原本可能插入中间的位置被替换后反而插入不了,望大佬评论区教一下。

做法二、前后缀DP

这个做法是排行榜里面看别人代码看懂的,也是我感觉最容易理解的一种做法,充分利用了数据范围来简化难度。
开两个数组, p r e [ i ] pre[i] pre[i]表示从 1 到 i 的 L I S 的长度 1到i的LIS的长度 1iLIS的长度 s u f [ i ] suf[i] suf[i]表示从 i i i n n n L I S LIS LIS的长度。
这两个数组很容易用双重 f o r 循环 for循环 for循环,一个正着,一个逆着, O ( n 2 ) O(n^2) O(n2)预处理出来。即 p r e [ i ] = m a x ( p r e [ i ] , p r e [ j ] + 1 ) , ∨ 1 ≤ j < i pre[i]=max(pre[i],pre[j]+1),\vee_{1\leq j<i} pre[i]=max(pre[i],pre[j]+1),1j<i,同理有 s u f [ i ] = m a x ( s u f [ i ] , s u f [ j ] + 1 ) , ∨ i < j ≤ n suf[i]=max(suf[i],suf[j]+1),\vee_{i<j\leq n} suf[i]=max(suf[i],suf[j]+1),i<jn
因为对于一个有插入数的 L I S LIS LIS,可以把插入的位置作为分界线,分为左右两侧,左侧的长度就是从 1 1 1到分界线左侧对应下标的 L I S LIS LIS,右侧长度是从分界线右侧到 n n n L I S LIS LIS长度,所以会去这样处理前后缀 D P DP DP,然后两层 f o r for for循环枚举分界线的左右两侧。
具体来说,一层 f o r for for循环 i i i,即分界线的左侧元素下标,然后第二层 f o r for for循环 j j j i + 2 i+2 i+2开始,中间至少要有1个空位才能插,然后要是满足能插入,还需要原数组中 a r r [ i ] + 1 < a r r [ j ] arr[i]+1<arr[j] arr[i]+1<arr[j],因为如果分界线左右两侧的数值原本就差1,中间也没法插入中间值。
这样两层 f o r for for循环跑一遍取最大值即为答案。
代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5e3+10;
int pre[maxn],suf[maxn],arr[maxn];
signed main(){
    int n;cin>>n;
    for(int i=1;i<=n;i++){
        cin>>arr[i];
        pre[i]=suf[i]=1;
    }
    for(int i=1;i<=n;i++){
        for(int j=i+1;j<=n;j++)
            if(arr[j]>arr[i])
                pre[j]=max(pre[j],pre[i]+1);
    }
    for(int i=n;i;i--){
        for(int j=i-1;j;j--)
            if(arr[j]<arr[i])
                suf[j]=max(suf[j],suf[i]+1);
    }
    int ans=1;
    for(int i=1;i<=n;i++){
        if(i<n) ans=max(ans,pre[i]+1);
        else ans=max(ans,pre[i]);
        if(i>1&&arr[i]) ans=max(ans,suf[i]+1);
        else ans=max(ans,suf[i]);
        for(int j=i+2;j<=n;j++)
            if(arr[j]-arr[i]>1)
                ans=max(ans,pre[i]+suf[j]+1);
    }
    cout<<ans<<endl;
    return 0;
}

6.期望次数

还没看懂题解,不过大方面是期望dp,结合逆元等知识点来做,等我看懂了再更。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值