0
点赞
收藏
分享

微信扫一扫

蓝桥杯第八讲--枚举与模拟【例题】

文章目录

前言

蓝桥杯官网:蓝桥杯大赛——全国大学生TMT行业赛事
✨本博客讲解 蓝桥杯C/C++ 备赛所涉及算法知识,此博客为第八讲:枚举与模拟【例题】

本篇博客所包含习题有:
👊连号区间数
👊递增三元组
👊特别数的和
👊错误票据
👊回文日期

枚举与模拟【习题】见博客:蓝桥杯第八讲–枚举与模拟【习题】

博客内容以题代讲,通过讲解题目的做法来帮助读者快速理解算法内容,需要注意:学习算法不能光过脑,更要实践,请读者务必自己敲写一遍本博客相关代码!!!


连号区间数

题目要求

题目描述:

小明这些天一直在思考这样一个奇怪而有趣的问题:

1 1 1 N N N 的某个排列中有多少个连号区间呢?

这里所说的连号区间的定义是:

如果区间 [ L , R ] [L,R] [L,R] 里的所有元素(即此排列的第 L L L 个到第 R R R 个元素)递增排序后能得到一个长度为 R − L + 1 R−L+1 RL+1 的“连续”数列,则称这个区间连号区间。

N N N 很小的时候,小明可以很快地算出答案,但是当 N N N 变大的时候,问题就不是那么简单了,现在小明需要你的帮助。

输入格式:

第一行是一个正整数 N N N,表示排列的规模。

第二行是 N N N 个不同的数字 P i P_i Pi,表示这 N N N 个数字的某一排列。

输出格式:

输出一个整数,表示不同连号区间的数目。

数据范围:

1 ≤ N ≤ 10000 , 1≤N≤10000, 1N10000,
1 ≤ P i ≤ N 1≤P_i≤N 1PiN

输入样例1:

输出样例1:

输入样例2:

输出样例2:

样例解释:

第一个用例中,有 7 7 7 个连号区间分别是: [ 1 , 1 ] , [ 1 , 2 ] , [ 1 , 3 ] , [ 1 , 4 ] , [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 4 ] [1,1],[1,2],[1,3],[1,4],[2,2],[3,3],[4,4] [1,1],[1,2],[1,3],[1,4],[2,2],[3,3],[4,4]
第二个用例中,有 9 9 9 个连号区间分别是: [ 1 , 1 ] , [ 1 , 2 ] , [ 1 , 3 ] , [ 1 , 4 ] , [ 1 , 5 ] , [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 4 ] , [ 5 , 5 ] [1,1],[1,2],[1,3],[1,4],[1,5],[2,2],[3,3],[4,4],[5,5] [1,1],[1,2],[1,3],[1,4],[1,5],[2,2],[3,3],[4,4],[5,5]

思路分析

题干中有一句话十分的关键: 1 1 1 N N N 的某个排列,这就意味着, 1 1 1 ~ N N N 中的每个数都会出现一次且仅会出现一次,所以对一个连号区间而言,必然有:区间内最大值 m a x v maxv maxv 与区间内最小值 m i n v minv minv 的差值等于区间内的元素个数 − 1 -1 1,我们用 i i i 0 0 0 开始枚举至 n n n 表示区间的左端点, j j j i i i 开始枚举至 n n n 表示区间的右端点,则区间内元素个数为: j − i + 1 j - i +1 ji+1,即如果对于一个区间内有:maxv-minv==j-i,则我们需要让 res++

代码

#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 10010;

int a[N];

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    
    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int maxv = -N, minv = N;
        for (int j = i; j < n; j ++ )
        {
            maxv = max(maxv, a[j]);
            minv = min(minv, a[j]);
            
            if (maxv - minv == j - i) res ++;
        }
    }
    
    printf("%d\n", res);
    
    return 0;
}

递增三元组

题目要求

题目描述:

给定三个整数数组

A = [ A 1 , A 2 , … A N ] , A=[A_1,A_2,…A_N], A=[A1,A2,AN],
B = [ B 1 , B 2 , … B N ] , B=[B_1,B_2,…B_N], B=[B1,B2,BN],
C = [ C 1 , C 2 , … C N ] , C=[C_1,C_2,…C_N], C=[C1,C2,CN],

请你统计有多少个三元组 ( i , j , k ) (i,j,k) (i,j,k) 满足:

1 ≤ i , j , k ≤ N 1≤i,j,k≤N 1i,j,kN
A i < B j < C k A_i<B_j<C_k Ai<Bj<Ck

输入格式:

第一行包含一个整数 N N N

第二行包含 N N N 个整数 A 1 , A 2 , … A N A_1,A_2,…A_N A1,A2,AN

第三行包含 N N N 个整数 B 1 , B 2 , … B N B_1,B_2,…B_N B1,B2,BN

第四行包含 N N N 个整数 C 1 , C 2 , … C N C_1,C_2,…C_N C1,C2,CN

输出格式:

一个整数表示答案。

数据范围:

1 ≤ N ≤ 1 0 5 , 1≤N≤10^5, 1N105,
0 ≤ A i , B i , C i ≤ 1 0 5 0≤A_i,B_i,C_i≤10^5 0Ai,Bi,Ci105

输入样例:

输出样例:

思路分析

本题有三种求法,对应三个不同的算法思维,分别为:

  • 前缀和
  • 二分
  • 双指针

在本博客中,只介绍前两种代码,双指针的解法会在本蓝桥杯专栏的双指针一讲中进行讲解。

在说两个算法的具体实现之前,我们先来分析一些数据:首先是本题的数据范围: 1 ≤ N ≤ 1 0 5 , 1≤N≤10^5, 1N105, 故本题的算法设计必须使得时间复杂度为: O ( n ) O(n) O(n) 或者是 O ( n l o g n ) O(nlogn) O(nlogn),所以我们最多只能枚举一个数组,由此我们只能去枚举 B B B数组,因为 B B B数组起到了连接 A , C A,C A,C数组的作用。
我们根据数据范围还可以推断出,本题的最多方案数为: N × N = 1 0 10 N\times N=10^{10} N×N=1010,即本题会爆 i n t int int,所以本题的 r e s res res 需要用 l o n g long long l o n g long long 去存。

前缀和

前缀和的具体知识概念讲解见博客:蓝桥杯第四讲–前缀和【例题】
前缀和【习题】详见博客:蓝桥杯第四讲–前缀和【习题】
前缀和算法模板详见博客:前缀和算法模板

下面来对前缀和进行讲解:

如果对于每一个 b [ i ] b[i] b[i] ,我们都能知道有多少个 a [ i ] a[i] a[i] 比它小,有多少个 c [ i ] c[i] c[i] 比它大,那么我们可以很轻易的求出 r e s res res,设我们有数组 a s [ i ] as[i] as[i] 表示在 a a a 中有多少个元素小于 b [ i ] b[i] b[i] c [ i ] c[i] c[i] 表示在 c c c 中有多少个元素大于 b [ i ] b[i] b[i],那么有 res += (LL)as[i] * cs[i]

a s as as数组:我们用 c n t [ i ] cnt[i] cnt[i] 表示在数组 a a a 中,数字 i i i 出现的次数,举个栗子,a[] = {1, 7, 3, 3},那么此时的 c n t cnt cnt数组中:cnt[1] = 1, cnt[3] = 3, cnt[7] = 1,这是一种典型的 空间换时间 的方法, s s s 即为前缀和数组, s [ i ] s[i] s[i] 表示的含义为:在数组 a a a 中, 1 1 1 ~ i i i 出现的总次数,那么对于每一个 b [ i ] b[i] b[i],在 a a a 中有多少个元素小于 b [ i ] b[i] b[i] 其实就是 s [ b [ i ] − 1 ] s[b[i] - 1] s[b[i]1],由于涉及 b [ i ] − 1 b[i]-1 b[i]1,且 b b b 中元素的范围是: 0 ≤ A i , B i , C i ≤ 1 0 5 0≤A_i,B_i,C_i≤10^5 0Ai,Bi,Ci105,故当 b [ i ] = 0 b[i]=0 b[i]=0 的时候会发生数组越界,一个取巧的方法就是在输入的过程中令 a[i] ++; b[i] ++; c[i] ++;,即我们人为的把数据范围变成: 1 ≤ A i , B i , C i ≤ 1 0 5 + 1 1≤A_i,B_i,C_i≤10^5+1 1Ai,Bi,Ci105+1.

c s cs cs数组:大致思维和求 a s as as数组是一致的,这里需要注意我们在求 c s cs cs数组之前需要把 s s s数组和 c n t cnt cnt数组置为 0 0 0,与求 a s as as数组的不同之处在于,因为我们的 s [ i ] s[i] s[i] 表示的含义为:在数组 c c c 中, 1 1 1 ~ i i i 出现的总次数,,然后 c s cs cs数组是要求:在 c c c 中有多少个元素大于 b [ i ] b[i] b[i],故我们在计算 c s cs cs数组的时候,代码为:cs[i] = s[N - 1] - s[b[i]];

二分

二分的具体知识概念讲解见博客:蓝桥杯第三讲–二分【例题】
二分【习题】详见博客:蓝桥杯第三讲–二分【习题】
二分的模板详细见博客:二分算法模板

下面来对二分进行讲解:

二分代码的思路其实就要清晰很多,我们可以先对 数组 A , B , C A,B,C A,B,C 中的元素从大到小进行排序,然后对于每一个 b [ i ] b[i] b[i],我们用二分的方法找到在 A A A数组中小于 b [ i ] b[i] b[i] 的最大的数和在 C C C数组中大于 b [ i ] b[i] b[i] 的最小的数,比如在找 a a a 数组中我们二分的最后停在了 l l l 的位置,如果这个 l l l 不是数组 a a a 的第一个点,即 l != 0,那么证明在 0 0 0 ~ l l l 上的所有的数都小于 b [ i ] b[i] b[i],即返回 l + 1 l+1 l+1,如果 l == 0,那么需要分类讨论,因为导致 l == 0 有两种可能的情况,第一种为正好只有一个元素小于 b [ i ] b[i] b[i],那么返回 1 1 1,第二种为如果 A A A数组中所有的元素都要大于 b [ i ] b[i] b[i],那么也会使得最终二分到 l == 0,这个时候需要返回 0 0 0

在计算和 C C C数组相关的计算,思路和求 A A A 是一样的,同时也需要特判一下边界,这里不再进行赘述,读者可以比较着下述的二分代码进行理解,如果堆 C C C 的分析有些懵逼,可以在评论区留言,博主看到后会立刻进行回复。

代码(前缀和)

#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long LL;

const int N = 100010;

int a[N], b[N], c[N];
int s[N], cnt[N];
int as[N]; // as[i]表示在a中有多少个元素小于b[i]
int cs[N]; // cs[i]表示在c中有多少个元素大于b[i]

int main()
{
    int n;
    scanf("%d", &n);
    
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]), a[i] ++;
    for (int i = 0; i < n; i ++ ) scanf("%d", &b[i]), b[i] ++;
    for (int i = 0; i < n; i ++ ) scanf("%d", &c[i]), c[i] ++;
    // 求as
    for (int i = 0; i < n; i ++ ) cnt[a[i]] ++;
    for (int i = 1; i < N; i ++ ) s[i] = s[i - 1] + cnt[i];
    for (int i = 0; i < n; i ++ ) as[i] = s[b[i] - 1];
    
    memset(cnt, 0, sizeof cnt);
    memset(s, 0, sizeof s);
    // 求cs
    for (int i = 0; i < n; i ++ ) cnt[c[i]] ++;
    for (int i = 1; i < N; i ++ ) s[i] = s[i - 1] + cnt[i];
    for (int i = 0; i < n; i ++ ) cs[i] = s[N - 1] - s[b[i]];
    
    LL res = 0;
    for (int i = 0; i < n; i ++ ) res += (LL)as[i] * cs[i];
    
    printf("%lld\n", res);
    
    return 0;
}

代码(二分)

#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int a[N], b[N], c[N];

int finda(int u)
{
    int l = 0, r = n - 1;
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (a[mid] >= b[u]) r = mid - 1;
        else l = mid;
    }
    
    if (l == 0 && a[l] < b[u]) return 1;
    else if (l == 0 && a[l] >= b[u]) return 0;
    return l + 1;
}

int findc(int u)
{
    int l = 0, r = n - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (c[mid] <= b[u]) l = mid + 1;
        else r = mid;
    }
    
    if (l == n - 1 && c[l] > b[u]) return 1;
    else if (l == n - 1 && c[l] <= b[u]) return 0;
    return (n - 1 - l + 1);
}

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    for (int i = 0; i < n; i ++ ) scanf("%d", &b[i]);
    for (int i = 0; i < n; i ++ ) scanf("%d", &c[i]);
    
    sort(a, a + n);
    sort(b, b + n);
    sort(c, c + n);
    
    LL res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int resa = finda(i);
        int resc = findc(i);
        res += (LL)resa * resc;
    }
    
    printf("%lld\n", res);
    
    return 0;
}

特别数的和

题目要求

题目描述:

小明对数位中含有 2 、 0 、 1 、 9 2、0、1、9 2019 的数字很感兴趣(不包括前导 0 0 0),在 1 1 1 40 40 40 中这样的数包括 1 、 2 、 9 、 10 1、2、9、10 12910 32 、 39 32、39 3239 40 40 40,共 28 28 28 个,他们的和是 574 574 574

请问,在 1 1 1 n n n 中,所有这样的数的和是多少?

输入格式:

共一行,包含一个整数 n n n

输出格式:

共一行,包含一个整数,表示满足条件的数的和。

数据范围:

1 ≤ n ≤ 10000 1≤n≤10000 1n10000

输入样例:

输出样例:

思路分析

直接暴力做即可,这里有一个常用的模板,求一个数的各位的数字,不熟悉的读者建议背一下:

while (x)
{
   int t = x % 10;    //取个位
   x /= 10;           //删个位
}

代码

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

int main()
{
    int n;
    cin >> n;
    
    int res = 0;
    for (int i = 1; i <= n; i ++ ) 
    {
        int x = i;
        while (x)
        {
            int t = x % 10;    //取个位
            x /= 10;           //删个位
            if (t == 2 || t == 0 || t == 1 || t == 9)
            {
                res += i;
                break;
            }
        }
    }
    
    cout << res << endl;
    
    return 0;
}

错误票据

题目要求

题目描述:

某涉密单位下发了某种票据,并要在年终全部收回。

每张票据有唯一的 I D ID ID号。

全年所有票据的ID号是连续的,但 I D ID ID的开始数码是随机选定的。

因为工作人员疏忽,在录入 I D ID ID号的时候发生了一处错误,造成了某个 I D ID ID断号,另外一个 I D ID ID重号。

你的任务是通过编程,找出断号的 I D ID ID和重号的 I D ID ID

假设断号不可能发生在最大和最小号。

输入格式:

第一行包含整数 N N N,表示后面共有 N N N 行数据。

接下来 N N N 行,每行包含空格分开的若干个(不大于 100 100 100个)正整数(不大于 100000 100000 100000),每个整数代表一个 I D ID ID号。

输出格式:

要求程序输出 1 1 1行,含两个整数 m , n m,n m,n,用空格分隔。

其中, m m m表示断号 I D ID ID n n n表示重号 I D ID ID

数据范围:

1 ≤ N ≤ 100 1≤N≤100 1N100

输入样例:

输出样例:

思路分析

这个题的其实也十分的直白,我们直接对 I D ID ID 进行排序,如果相邻的两个 I D ID ID 相同那么就是重号 I D ID ID,如果相邻两个 I D ID ID 之间的数值之差大于 1 1 1(或者大于等于 2 2 2,注;其实本题的断号 I D ID ID 就是两个 I D ID ID 之间相差为 2 2 2),那么就输出相差的那个 I D ID ID,本题的恶心之处在于读入十分的恶心,我们可以使用头文件 #include <sstream> 对数据进行读入,具体的读入见下述代码,需要强调, g e t l i n e getline getline 会读入换行,故我们最开始需要用一个 g e t c h a r ( ) getchar() getchar() 去把输入完几行数据后的那个换行符给读掉。

代码

#include <iostream>
#include <sstream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 100010;

int cnt, n;
int a[N];
string l;

int main()
{
    cin >> cnt;
    
    getchar();    //读掉换行符
    while (cnt -- )
    {
        getline(cin, l);
        stringstream ssin(l);
        
        while (ssin >> a[n]) n ++;
    }
    
    sort(a, a + n);
    
    int res1, res2;
    for (int i = 1; i < n; i ++ )
        if (a[i] == a[i - 1]) res2 = a[i];
        else if (a[i] >= a[i - 1] + 2) res1 = a[i] - 1;
        
    cout << res1 << ' ' << res2 << endl;
    
    return 0;
}

回文日期

题目要求

题目描述:

在日常生活中,通过年、月、日这三个要素可以表示出一个唯一确定的日期。

牛牛习惯用 8 8 8 位数字表示一个日期,其中,前 4 4 4 位代表年份,接下来 2 2 2 位代表月份,最后 2 2 2 位代表日期。

显然:一个日期只有一种表示方法,而两个不同的日期的表示方法不会相同。

牛牛认为,一个日期是回文的,当且仅当表示这个日期的 8 8 8 位数字是回文的。

现在,牛牛想知道:在他指定的两个日期之间(包含这两个日期本身),有多少个真实存在的日期是回文的。

一个 8 8 8 位数字是回文的,当且仅当对于所有的 i ( 1 ≤ i ≤ 8 ) i(1≤i≤8) i(1i8) 从左向右数的第 i i i 个数字和第 9 − i 9−i 9i 个数字(即从右向左数的第 i i i 个数字)是相同的。

例如:

  • 对于 2016 2016 2016 11 11 11 19 19 19 日,用 8 8 8 位数字 20161119 20161119 20161119 表示,它不是回文的。
  • 对于 2010 2010 2010 1 1 1 2 2 2 日,用 8 8 8 位数字 20100102 20100102 20100102 表示,它是回文的。
  • 对于 2010 2010 2010 10 10 10 2 2 2 日,用 8 8 8 位数字 20101002 20101002 20101002 表示,它不是回文的。

输入格式:

输入包括两行,每行包括一个 8 8 8 位数字。

第一行表示牛牛指定的起始日期 d a t e 1 date_1 date1,第二行表示牛牛指定的终止日期 d a t e 2 date_2 date2。保证 d a t e 1 date_1 date1 d a t e 2 date_2 date2 都是真实存在的日期,且年份部分一定为 4 4 4 位数字,且首位数字不为 0 0 0

保证 d a t e 1 date_1 date1 一定不晚于 d a t e 2 date_2 date2

输出格式:

输出共一行,包含一个整数,表示在 d a t e 1 date_1 date1 d a t e 2 date_2 date2 之间,有多少个日期是回文的。

输入样例:

输出样例:

思路分析

日期类题目,首先判断是否为闰年:如果一年是闰年,要么满足可以被 400 400 400 整除,要么满足不能被 100 100 100 整除但可以被 4 4 4 整除。本题中,我们如果去枚举日期的话是十分麻烦的,所以我们换一个思路,我们去枚举回文串,本题的回文串其实本质上就是一个回文八位数,我们可以枚举前四位,即枚举年,我们从 1000 1000 1000 开始枚举,直到枚举到 9999 9999 9999,利用前四位去构造后四位的回文,代码如下:

for (int i = 1000; i < 10000; i ++ )
{
   int date = i, x = i;
   for (int j = 0; j < 4; j ++ )
   {
       date = date * 10 + x % 10;
       x/= 10;
   }
}  

然后对于构造出来的回文八位数,我们去判断是否在两个给定日期之间,判断方法直接进行数值大小的比较即可,如果在两个给定日期之间,我们进而进行判断是否合法,我们取出 年月日:

int year = date / 10000;
int month = date / 100 % 100;
int day = date % 100;

m o n mon mon数组存储的就是平年的 12 12 12 个月, m o n mon mon 数组在定义的时候定义长度为 13 13 13,第一个位置是空出来的,为的就是 m o n [ i ] mon[i] mon[i] 正好对应第 i i i 个月,判断月份是否合法即看月份是否满足:month >= 1 && month <= 12,在判断日是否合法的时候先忽略掉二月份进行判断,即看 d a y day day 是否满足:day >= 1 && day <= mon[month],最终判断是否为闰年,是闰年的话就让二月份的天数 + 1 +1 +1,再看 d a y day day 是否满足条件即可。

代码

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

int mon[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

bool is_right(int date)
{
    int year = date / 10000;
    int month = date / 100 % 100;
    int day = date % 100;
    
    if (month > 12 || month < 1) return false;
    if (month != 2 && day > mon[month]) return false;
    
    int leap = (!(year % 400) || (year % 100) && !(year % 4));
    if (month == 2 && day > mon[month] + 1) return false;
    
    return true;
}

int main()
{
    int day1, day2;
    cin >> day1 >> day2;
    
    int res = 0;
    for (int i = 1000; i < 10000; i ++ )
    {
        int date = i, x = i;
        for (int j = 0; j < 4; j ++ )
        {
            date = date * 10 + x % 10;
            x/= 10;
        }
        
        if (date <= day2 && date >= day1)
            if (is_right(date)) res ++;
    }
    
    cout << res << endl;
    
    return 0;
}

举报

相关推荐

0 条评论