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

本文介绍了五个编程题目,涉及模拟算法、贪心策略、动态规划以及期望DP,展示了在不同问题场景下的解决方案,包括计数、字符串操作、最长上升子序列等。

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.构造数字

题意

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

分析

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

#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≤1052\leq n\leq 10^52n105,看到这题,先去搜了下10510^5105范围内的质数数量,发现只有10310^3103级别个,所以直接考虑枚举。
先枚举所有质数,再去枚举子数组的右端点,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.合并石子加强版

题意

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

分析

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

#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问题

题意

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

分析

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

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

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

  • 能否在LISLISLIS前面插入:判断sta[1]=0sta[1]=0sta[1]=0
  • 能否在LISLISLIS后面插入:判断pos[len]=npos[len]=npos[len]=n即最大元素是否为数组的最后一位
  • 能否在LISLISLIS中间插入:遍历单调栈,如果有相邻的两个值满足,差大于2(sta[i+1]−sta[i]>2sta[i+1]-sta[i]>2sta[i+1]sta[i]>2),并且在原数组中的位置不是紧挨着的(pos[i+1]−pos[i]>1pos[i+1]-pos[i]>1pos[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

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

#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,结合逆元等知识点来做,等我看懂了再更。

### 回答1: 思路: - 对于一个子数组 A[l...r],它的异或值可以通过前缀异或值的异或运算得到:xor[l-1] xor xor[r]。它的乘积可以通过前缀积的除法运算得到:prod[r] / prod[l-1]。 - 因为异或运算和除法运算都是可逆的,所以 A[l...r] 的乘积等于异或值的充分必要条件是 xor[l-1] xor xor[r] 等于 prod[r] / prod[l-1]。 - 我们可以对于每个 r 计算符合条件的 l 的数量,然后将其加起来即可。 - 对于每个 r,我们需要找到最大的 l,使得 A[l...r] 的乘积等于 A[l...r] 的异或值。我们可以从左往右遍历数组,维护一个乘积 prod 和一个异或值 xor,当 prod 等于 xor 时,我们就找到了一个符合条件的子数组。 - 我们可以使用两个哈希表来优化查找最大的 l 的过程。一个哈希表存储每个前缀的最小的 r,使得 A[i...r] 的乘积等于 A[i...r] 的异或值;另一个哈希表存储每个前缀的最大的 r,使得 A[i...r] 的乘积等于 A[i...r] 的异或值。这两个哈希表可以使用单调栈来维护。 - 时间复杂度为 O(n)。 代码实现: ```go func solve(A []int) int { n := len(A) xor := make([]int, n+1) prod := make([]int, n+1) for i := 1; i <= n; i++ { xor[i] = xor[i-1] ^ A[i-1] prod[i] = prod[i-1] * A[i-1] } cnt := 0 minR := make(map[int]int) maxR := make(map[int]int) stack := make([]int, 0, n) for r := 0; r < n; r++ { for len(stack) > 0 && A[stack[len(stack)-1]] > A[r] { i := stack[len(stack)-1] stack = stack[:len(stack)-1] if len(stack) > 0 { j := stack[len(stack)-1] if prod[r] == prod[j]*A[i] && xor[r] == xor[j] { cnt += r - minR[j] } } if prod[r] == A[i] && xor[r] == 0 { cnt += r - minR[i] } if prod[r] == 0 && xor[r] == xor[i] { cnt += maxR[i] - minR[i] + 1 } delete(minR, i) } stack = append(stack, r) if prod[r] == 0 && xor[r] == 0 { cnt++ } if _, ok := minR[xor[r]]; !ok { minR[xor[r]] = r } maxR[xor[r]] = r } return cnt } ``` 参考文献: - [Codeforces 1426F2 Subsequences (hard version)](https://codeforces.com/problemset/problem/1426/F2) ### 回答2: 假设给定的正整数数组A的长度为n。 要求满足条件的子数组,即满足乘积等于异或的子数组。 首先观察到异或操作具有可逆性,即对任意正整数a和b,满足 a^b=b^a。 在一个子数组内,所有数字异或的结果等于该子数组中所有数字的乘积。因此,我们可以得出结论:一个子数组满足条件,表示该子数组中的数字两两异或的结果等于该子数组中的所有数字的乘积。 假设子数组开始的下标是i,结束的下标是j,则可以表示为 A[i] ^ A[i+1] ^ ... ^ A[j-1] ^ A[j] = A[i] * A[i+1] * ... * A[j-1] * A[j]。 进一步化简得:A[i] * A[i+1] * ... * A[j-1] * A[j] = (A[i] ^ A[i+1] ^ ... ^ A[j-1]) ^ A[j]。 通过上述公式,我们可以利用前缀异或数组prefixXOR[i]表示A[0] ^ A[1] ^ ... ^ A[i]的结果。 因此,我们可以将问题转化为:求出所有满足prefixXOR[i-1] ^ prefixXOR[j] = A[j]的子数组的个数。 具体的解法如下: 1. 初始化一个map,用于存储每个前缀异或结果(prefixXOR[i-1])出现的次数。 2. 初始化计数器count,用于记录满足条件的子数组的个数。 3. 遍历数组A,对于每个位置j,计算prefixXOR[j],然后查找map中是否存在prefixXOR[j] ^ A[j]的值。 - 如果存在,说明存在满足条件的起始位置i,此时将map中对应的值累加到count中。 - 将prefixXOR[j]的值存入map,并将值+1。 4. 返回count,即为满足条件的子数组的个数。 以下为使用go语言编写的实现代码: ```go func numSubarrayProductXOR(A []int) int { prefixXOR := make([]int, len(A)) prefixXOR[0] = A[0] for i := 1; i < len(A); i++ { prefixXOR[i] = prefixXOR[i-1] ^ A[i] } count := 0 prefixMap := make(map[int]int) prefixMap[0] = 1 // 初始化map for j := 0; j < len(A); j++ { if prefixXOR[j] == A[j] { count++ } if val, ok := prefixMap[prefixXOR[j] ^ A[j]]; ok { count += val } prefixMap[prefixXOR[j]]++ } return count } ``` 该算法的时间复杂度为O(n),其中n为数组A的长度。 ### 回答3: 要求时间复杂度小于O(n^2),可以使用滑动窗口的方法来解决这个问题。 首先,定义两个指针left和right,分别指向子数组的起始位置和结束位置。初始化left和right都指向数组的起始位置。 同时,定义一个变量count,用来记录满足要求的子数组的数量。 然后,进入循环,循环条件是right小于数组的长度。在每一次循环中,判断当前子数组的乘积是否等于异或结果。 如果相等,则将count加1,并将right指针向后移动一位。 如果不等,则将left指针向后移动一位。 循环结束后,count的值就是满足要求的子数组的数量。 以下是具体的Go语言代码实现: ```go func countSubArray(arr []int) int { n := len(arr) count := 0 left := 0 right := 0 xor := 0 prod := 1 for right < n { prod *= arr[right] xor ^= arr[right] for left < right && prod != xor { prod /= arr[left] xor ^= arr[left] left++ } if prod == xor { count++ } right++ } return count } ``` 以上代码中,prod表示当前子数组的乘积,xor表示当前子数组的异或结果。每次通过改变left和right的位置来更新prod和xor的值,并在满足条件时将count加1。 该算法的时间复杂度为O(n),符合题目要求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值