文章目录
前言
蓝桥杯官网:蓝桥杯大赛——全国大学生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 R−L+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,
1≤N≤10000,
1
≤
P
i
≤
N
1≤P_i≤N
1≤Pi≤N
输入样例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
j−i+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
1≤i,j,k≤N
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,
1≤N≤105,
0
≤
A
i
,
B
i
,
C
i
≤
1
0
5
0≤A_i,B_i,C_i≤10^5
0≤Ai,Bi,Ci≤105
输入样例:
输出样例:
思路分析
本题有三种求法,对应三个不同的算法思维,分别为:
- 前缀和
- 二分
- 双指针
在本博客中,只介绍前两种代码,双指针的解法会在本蓝桥杯专栏的双指针一讲中进行讲解。
在说两个算法的具体实现之前,我们先来分析一些数据:首先是本题的数据范围:
1
≤
N
≤
1
0
5
,
1≤N≤10^5,
1≤N≤105, 故本题的算法设计必须使得时间复杂度为:
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
0≤Ai,Bi,Ci≤105,故当
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
1≤Ai,Bi,Ci≤105+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 2、0、1、9 的数字很感兴趣(不包括前导 0 0 0),在 1 1 1 到 40 40 40 中这样的数包括 1 、 2 、 9 、 10 1、2、9、10 1、2、9、10 至 32 、 39 32、39 32、39 和 40 40 40,共 28 28 28 个,他们的和是 574 574 574。
请问,在 1 1 1 到 n n n 中,所有这样的数的和是多少?
输入格式:
共一行,包含一个整数 n n n。
输出格式:
共一行,包含一个整数,表示满足条件的数的和。
数据范围:
1 ≤ n ≤ 10000 1≤n≤10000 1≤n≤10000
输入样例:
输出样例:
思路分析
直接暴力做即可,这里有一个常用的模板,求一个数的各位的数字,不熟悉的读者建议背一下:
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 1≤N≤100
输入样例:
输出样例:
思路分析
这个题的其实也十分的直白,我们直接对
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(1≤i≤8) 从左向右数的第 i i i 个数字和第 9 − i 9−i 9−i 个数字(即从右向左数的第 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;
}