算法笔记(五)
5.1 一维数组
例题一 小鱼比可爱(洛谷P1428) 。人比人,气死人;鱼比鱼,难死鱼。小鱼最近参加了一个“比可爱”比赛,比的是每只鱼的可爱程度。参赛的鱼被从左到右排成一排,头都朝向左边,然后每只鱼会得到一个整数数值,表示这只鱼的可爱程度,很显然整数越大,表示这只鱼越可爱,而且任意两只鱼的可爱程度可能一样。由于所有的鱼头都朝向左边,所以每只鱼只能看见在它左边的鱼的可爱程度,它们心里都在计算,在自己的眼力范围内有多少只鱼不如自己可爱呢。请你帮这些可爱但是鱼脑不够用的小鱼们计算一下。
输入格式
第一行输入一个正整数 n n n,表示鱼的数目。
第二行内输入 n n n 个正整数,用空格间隔,依次表示从左到右每只小鱼的可爱程度 a i a_i ai。
输出格式
一行,输出 n n n 个整数,用空格间隔,依次表示每只小鱼眼中有多少只鱼不如自己可爱。
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 100 1 \leq n\leq 100 1≤n≤100, 0 ≤ a i ≤ 10 0 \leq a_i \leq 10 0≤ai≤10。
#include<iostream>
using namespace std;
int main (){
int a[110], n;
cin >> n;
for(int i = 0; i < n; i++){
cin >> a[i];
}
for(int i = 0; i < n; i++){
int cnt = 0;
for(int j = 0; j < i; j++)
if(a[j] < a[i])
cnt++;
cout << cnt << ' ';
}
return 0;
}
定义一维数组的方式是“数组类型 数组变量名称[ 元素个数 ];”。
例题二 小鱼的数字游戏(洛谷P1427)。小鱼最近被要求参加一个数字游戏,要求它把看到的一串数字 a i a_i ai(长度不一定,以 0 0 0 结束),记住了然后反着念出来(表示结束的数字 0 0 0 就不要念出来了)。这对小鱼的那点记忆力来说实在是太难了,你也不想想小鱼的整个脑袋才多大,其中一部分还是好吃的肉!所以请你帮小鱼编程解决这个问题。
输入格式
一行内输入一串整数,以 0 0 0 结束,以空格间隔。
输出格式
一行内倒着输出这一串整数,以空格间隔。
对于 100 % 100\% 100% 的数据,保证 0 ≤ a i ≤ 2 31 − 1 0 \leq a_i \leq 2^{31} - 1 0≤ai≤231−1,数字个数不超过 100 100 100。
#include<iostream>
using namespace std;
int main (){
int a[110], n;
int i;
for(i = 0; ; i++){
cin >> n;
if(!n) break;
a[i] = n;
}
for(int j = i - 1; j >= 0; j--){
cout << a[j] << ' ';
}
return 0;
}
解法2
#include<iostream>
using namespace std;
int main (){
int n = 0, tmp, a[110];
do{
cin >> tmp;
a[n] = tmp;
n++; // 本行可以替代成 cin >> a[n++];
}while(tmp != 0); // 结束循环时,n 多完成了一次自增
n--; // 或者 --n;
while(n--) // 不能写成 while(--n)
cout << a[n] << ' ';
return 0;
}
在while 语句中,首先判断 n 是否为 0,然后对 n 减 1,接着依次输出 a[n] 的值。由于开始时 a[n] 是最后读入的 0,不需要输出,所以可以先自减,然后输出 a[n]。当判断 n 是 0 的时候,说明 a[0] 上一轮循环已经输出过了,这时就可以退出循环,结束程序。
例题三 冰雹猜想(洛谷P5727)。给出一个正整数 n n n,然后对这个数字一直进行下面的操作:如果这个数字是奇数,那么将其乘 3 3 3 再加 1 1 1,否则除以 2 2 2。经过若干次循环后,最终都会回到 1 1 1。经过验证很大的数字( 7 × 1 0 11 7\times10^{11} 7×1011)都可以按照这样的方式比变成 1 1 1,所以被称为“冰雹猜想”。例如当 n n n 是 20 20 20,变化的过程是 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1 20\to 10\to 5\to 16\to 8\to 4\to 2\to 1 20→10→5→16→8→4→2→1。根据给定的数字,验证这个猜想,并从最后的 1 1 1 开始,倒序输出整个变化序列。已知变化次数不会超过 200 次。
输入格式
输入一个正整数 n n n。
输出格式
输出若干个由空格隔开的正整数,表示从最后的 1 1 1 开始倒序的变化数列。
数据保证, 1 ≤ n ≤ 100 1 \le n\le 100 1≤n≤100。
解法1
#include<iostream>
using namespace std;
int main (){
int n, a[210];
cin >> n;
int i = 0;
a[i] = n;
while(n != 1){
if(n % 2 == 0){
n /= 2;
a[++i] = n;
} else{
n = 3 * n + 1;
a[++i] = n;
}
}
i++; // 此时的a[i] = 1; 而 i++ 是为了抵消下面循环条件 i--
while(i--)
cout << a[i] << ' ';
return 0;
}
解法2
分析:按照题目的要求计算数字,并像上一个例子一样依次将计算结果存入数组,直到计算到了 1 为止。最后使用 for 循环倒序输出。while 语句和 do-while 语句只是在判定循环条件的位置不一样,是可以相互转换的。
#include<iostream>
using namespace std;
#define MAXN 205
int main (){
int n, num = 0, a[MAXN];
cin >> n;
while(n != 1){
a[num++] = n;
if(n % 2 == 0) n /= 2;
else n = 3 * n + 1;
}
a[num] = 1; //将最后的 1 加入数组
for(int i = num; i >= 0; i--) //倒序输出
cout << a[i] << ' ';
return 0;
}
在本例中使用了宏定义,将 MAXN 定义为 205。这就意味着 a[MAXN] 的元素个数为 205 个。这样在要定义多个个数相同的数组时,如果需要调整数组大小,只要调整 MAXN 定义的数字即可,而不需要逐一调整。
在定义数组时,数组中所含元素的个数应当是一个确定的正整数,这个正整数可以是直接数字的形式(如 a[205])、表达式(如 a[200+5])、宏定义或者 const 整数常量(a[MAXN])。简而言之,定义个数不可以是一个变量,比如定义成 a[n] 是不可以的,因为 n 不是固定的常量。
例题四 校门外的树(洛谷P1047,NOIP2005 普及组)。
某校大门外长度为 l l l 的马路上有一排树,每两棵相邻的树之间的间隔都是 1 1 1 米。我们可以把马路看成一个数轴,马路的一端在数轴 0 0 0 的位置,另一端在 l l l 的位置;数轴上的每个整数点,即 0 , 1 , 2 , … , l 0,1,2,\dots,l 0,1,2,…,l,都种有一棵树。
由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树。
输入格式
第一行有两个整数,分别表示马路的长度 l l l 和区域的数目 m m m。
接下来 m m m 行,每行两个整数 u , v u, v u,v,表示一个区域的起始点和终止点的坐标。
输出格式
输出一行一个整数,表示将这些树都移走后,马路上剩余的树木数量。
对于 20 % 20\% 20% 的数据,保证区域之间没有重合的部分。
对于 100 % 100\% 100% 的数据,保证 1 ≤ l ≤ 1 0 4 1 \leq l \leq 10^4 1≤l≤104, 1 ≤ m ≤ 100 1 \leq m \leq 100 1≤m≤100, 0 ≤ u ≤ v ≤ l 0 \leq u \leq v \leq l 0≤u≤v≤l。
#include<iostream>
// #include<cstring>
using namespace std;
int main (){
int l, m, tree[10010] = {0}, a, b, s = 0;
cin >> l >> m;
// memset(tree, 0, sizeof(tree));
for(int i = 0; i < m; i++){
cin >> a >> b;
for(int j = a; j <= b; j++)
tree[j] = 1;
}
for(int i = 0; i <= l; i++)
if(tree[i] == 0)
s++;
cout << s << endl;
return 0;
}
数组的初始化
在定义数组后面加上 ={0} 就可以将整个数组初始化为 0;但如果是 int a[10010] = {1},就只是将 a[0] 赋值成 1,后面剩余的都初始化为 0;同样的 int a[10010] = {5,2,1},就只是将 a[0]、a[1]、a[2] 分别就是 5、2、1,后面剩余的还是 0。
除了在定义数组时可以初始化,数组还可以随时使用 memset 函数(需要 cstring 头文件),将数组内的全部“填充”成 0,以达成初始化的效果。方法是 memset(数组名称, 0, sizeof(数组名称))。值得注意的是,如果数组是 int 类型,那么中间的一项只有是 0 或者 -1 的时候,数组的每一项值才会被变成 0 或者 -1。
5.2 多维数组
例题一 旗鼓相当的对手(洛谷P5728)。现有 N N N 名同学参加了期末考试,并且获得了每名同学的信息:语文、数学、英语成绩(均为不超过 150 150 150 的自然数)。如果某对学生 ⟨ i , j ⟩ \lang i,j\rang ⟨i,j⟩ 的每一科成绩的分差都不大于 5 5 5,且总分分差不大于 10 10 10,那么这对学生就是“旗鼓相当的对手”。现在想知道这些同学中,有几对“旗鼓相当的对手”?同样一个人可能会和其他好几名同学结对。
输入格式
第一行一个正整数 N N N。
接下来 N N N 行,每行三个整数,其中第 i i i 行表示第 i i i 名同学的语文、数学、英语成绩。最先读入的同学编号为 1 1 1。
输出格式
输出一个整数,表示“旗鼓相当的对手”的对数。
数据保证, 2 ≤ N ≤ 1000 2 \le N\le 1000 2≤N≤1000 且每科成绩为不超过 150 150 150 的自然数。
#include<iostream>
#include<cmath>
using namespace std;
int main (){
int s[1010][3], N, ans = 0;
cin >> N;
for(int i = 0; i < N; i++)
for(int j = 0; j < 3; j++)
cin >> s[i][j];
for(int i = 0; i < N; i++)
for(int j = i + 1; j < N; j++)
if((abs(s[i][0] - s[j][0]) <= 5)&&(abs(s[i][1] - s[j][1]) <= 5)&&(abs(s[i][2] - s[j][2]) <= 5)&&(abs(s[i][0] - s[j][0] + s[i][1] - s[j][1] + s[i][2] - s[j][2]) <= 10))
ans++;
cout << ans;
return 0;
}
例题二 地毯(洛谷P3397,By 阮行止)。在 n × n n\times n n×n 的格子上有 m m m 个地毯( n ≤ 50 n\le 50 n≤50, m ≤ 100 m\le 100 m≤100)。给出这些地毯的信息,问每个点被多少个地毯覆盖。
输入格式
第一行,两个正整数 n , m n,m n,m。意义如题所述。
接下来 m m m 行,每行两个坐标 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 和 ( x 2 , y 2 ) (x_2,y_2) (x2,y2),代表一块地毯,左上角是 ( x 1 , y 1 ) (x_1,y_1) (x1,y1),右下角是 ( x 2 , y 2 ) (x_2,y_2) (x2,y2)。
输出格式
输出 n n n 行,每行 n n n 个正整数。
第 i i i 行第 j j j 列的正整数表示 ( i , j ) (i,j) (i,j) 这个格子被多少个地毯覆盖。
#include<iostream>
#include<cstdio>
#include<cstring>
#define MAXN 55
using namespace std;
int main (){
int n, m, a[MAXN][MAXN];
memset(a, 0, sizeof(a));
cin >> n >> m;
for(int i = 1; i <=m; i++){
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
for(int j = x1; j <= x2; j++)
for(int k = y1; k <= y2; k++)
a[j][k]++;
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
cout << a[i][j] << (j == n ? '\n' : ' ');
return 0;
}
例题三 工艺品制作(洛谷P5729)。现有一个长宽高分别为 w , x , h w,x,h w,x,h 组成的实心玻璃立方体,可以认为是由 1 × 1 × 1 1\times1\times1 1×1×1 的数个小方块组成的,每个小方块都有一个坐标 $ ( i,j,k ) $。现在需要进行 q q q 次切割。每次切割给出 ( x 1 , y 1 , z 1 ) , ( x 2 , y 2 , z 2 ) (x_1,y_1,z_1),(x_2,y_2,z_2) (x1,y1,z1),(x2,y2,z2) 这 6 个参数,保证 x 1 ≤ x 2 x_1\le x_2 x1≤x2, y 1 ≤ y 2 y_1\le y_2 y1≤y2, z 1 ≤ z 2 z_1\le z_2 z1≤z2;每次切割时,使用激光工具切出一个立方体空洞,空洞的壁平行于立方体的面,空洞的对角点就是给出的切割参数的两个点。换句话说,所有满足 x 1 ≤ i ≤ x 2 x_1\le i\le x_2 x1≤i≤x2,$y_1\le j \le y_2 , , ,z_1\le k\le z_2$ 的小方块 ( i , j , k ) (i,j,k) (i,j,k) 的点都会被激光蒸发。例如有一个 4 × 4 × 4 4\times4\times 4 4×4×4 的大方块,其体积为 64 64 64;给出参数 ( 1 , 1 , 1 ) , ( 2 , 2 , 2 ) (1,1,1),(2,2,2) (1,1,1),(2,2,2) 时,中间的 8 8 8 块小方块就会被蒸发,剩下 56 56 56 个小方块。现在想知道经过所有切割操作后,剩下的工艺品还剩下多少格小方块的体积?
输入格式
第一行三个正整数 w , x , h w,x,h w,x,h。
第二行一个正整数 q q q。
接下来 q q q 行,每行六个整数 ( x 1 , y 1 , z 1 ) , ( x 2 , y 2 , z 2 ) (x_1,y_1,z_1),(x_2,y_2,z_2) (x1,y1,z1),(x2,y2,z2)。
输出格式
输出一个整数表示答案。
数据保证, 1 ≤ w , x , h ≤ 20 1\le w,x,h\le 20 1≤w,x,h≤20, 1 ≤ q ≤ 100 1 \leq q\le 100 1≤q≤100。 1 ≤ x 1 ≤ x 2 ≤ w 1 \leq x_1 \leq x_2 \leq w 1≤x1≤x2≤w, 1 ≤ y 1 ≤ y 2 ≤ x 1 \leq y_1\leq y_2 \leq x 1≤y1≤y2≤x, 1 ≤ z 1 ≤ z 2 ≤ h 1 \leq z_1 \leq z_2 \leq h 1≤z1≤z2≤h。
#include<iostream>
#include<cstring>
#define MAXN 25
using namespace std;
int main (){
int w, x, h, q, ans = 0, a[MAXN][MAXN][MAXN];
memset(a, 0, sizeof(a));
cin >> w >> x >> h >> q;
for(int i = 1; i <=q; i++){
int x1, y1, z1, x2, y2, z2;
cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;
for(int j = x1; j <= x2; j++)
for(int k = y1; k <= y2; k++)
for(int l = z1; l <= z2; l++)
a[j][k][l]++;
}
for(int i = 1; i <= w; i++)
for(int j = 1; j <= x; j++)
for(int k = 1; k <= h; k++)
if(a[i][j][k] == 0)
ans++;
cout << ans;
return 0;
}
5.3 数组应用案例
例题一 彩票摇号(洛谷P2550,安徽省队选拔 2001)。为了丰富人民群众的生活、支持某些社会公益事业,北塔市设置了一项彩票。该彩票的规则是:
- 每张彩票上印有 7 7 7 个各不相同的号码,且这些号码的取值范围为 1 ∼ 33 1\sim33 1∼33。
- 每次在兑奖前都会公布一个由七个各不相同的号码构成的中奖号码。
- 共设置 7 7 7 个奖项,特等奖和一等奖至六等奖。
兑奖规则如下:
- 特等奖:要求彩票上 7 7 7 个号码都出现在中奖号码中。
- 一等奖:要求彩票上有 6 6 6 个号码出现在中奖号码中。
- 二等奖:要求彩票上有 5 5 5 个号码出现在中奖号码中。
- 三等奖:要求彩票上有 4 4 4 个号码出现在中奖号码中。
- 四等奖:要求彩票上有 3 3 3 个号码出现在中奖号码中。
- 五等奖:要求彩票上有 2 2 2 个号码出现在中奖号码中。
- 六等奖:要求彩票上有 1 1 1 个号码出现在中奖号码中。
注:兑奖时并不考虑彩票上的号码和中奖号码中的各个号码出现的位置。例如,中奖号码为 23 31 1 14 19 17 18 23\ 31\ 1\ 14\ 19\ 17\ 18 23 31 1 14 19 17 18,则彩票 12 8 9 23 1 16 7 12\ 8\ 9\ 23\ 1\ 16\ 7 12 8 9 23 1 16 7 由于其中有两个号码( 23 23 23 和 1 1 1)出现在中奖号码中,所以该彩票中了五等奖。
现已知中奖号码和小明买的若干张彩票的号码,请你写一个程序帮助小明判断他买的彩票的中奖情况。
输入格式
输入的第一行只有一个自然数 n n n,表示小明买的彩票张数;
第二行存放了 7 7 7 个介于 1 1 1 和 33 33 33 之间的自然数,表示中奖号码;
在随后的 n n n 行中每行都有 7 7 7 个介于 1 1 1 和 33 33 33 之间的自然数,分别表示小明所买的 n n n 张彩票。
输出格式
依次输出小明所买的彩票的中奖情况(中奖的张数),首先输出特等奖的中奖张数,然后依次输出一等奖至六等奖的中奖张数。
对于 100 % 100\% 100% 的数据,保证 1 ≤ n < 1000 1 \leq n\lt1000 1≤n<1000。
解法1:先存储所有数据再比较
#include<iostream>
using namespace std;
int main (){
int n, c[1005][10], grade[8] = {0}, num = 0;
cin >> n;
for(int i = 0; i <= n; i++)
cin >> c[i][1] >> c[i][2] >> c[i][3] >> c[i][4] >> c[i][5] >> c[i][6] >> c[i][7];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= 7; j++)
for(int k = 1; k <= 7; k++)
if(c[i][j] == c[0][k])
num++;
grade[num]++;
num = 0;
}
for(int i = 7; i >= 1; i--)
cout << grade[i] << ' ';
return 0;
}
解法2:只存储 7 个中奖号码,购买的号码依次读入与七个中奖号码比较
#include<iostream>
using namespace std;
int main (){
int n, a[10], num[10] = {0};
cin >> n;
for(int i = 1; i <= 7; i++)
cin >> a[i]; // 创建一个数组存下中奖号码
while(n--){
int ans = 0;
for(int i = 1; i <= 7; i++){
int x;
cin >> x;
for(int j = 1; j <= 7; j++) // 每次比较每个号码是否为中奖号码
if(a[j] == x)
ans++;
}
num[ans]++;
}
for(int i = 7; i; i--)
cout << num[i] << (i == 1 ? '\n' : ' '); /* 输出答案,注意最后一个要加换行而不是空格 */
return 0;
}
例题二 神奇的幻方(洛谷P2615,NOIP2015 提高组)。幻方是一种很神奇的 N × N N\times N N×N 矩阵:它由数字 1 , 2 , 3 , ⋯ ⋯ , N × N 1,2,3,\cdots \cdots ,N \times N 1,2,3,⋯⋯,N×N 构成,且每行、每列及两条对角线上的数字之和都相同。
当 N N N 为奇数时,我们可以通过下方法构建一个幻方:
首先将 1 1 1 写在第一行的中间。
之后,按如下方式从小到大依次填写每个数 K ( K = 2 , 3 , ⋯ , N × N ) K \ (K=2,3,\cdots,N \times N) K (K=2,3,⋯,N×N) :
- 若 ( K − 1 ) (K-1) (K−1) 在第一行但不在最后一列,则将 K K K 填在最后一行, ( K − 1 ) (K-1) (K−1) 所在列的右一列;
- 若 ( K − 1 ) (K-1) (K−1) 在最后一列但不在第一行,则将 K K K 填在第一列, ( K − 1 ) (K-1) (K−1) 所在行的上一行;
- 若 ( K − 1 ) (K-1) (K−1) 在第一行最后一列,则将 K K K 填在 ( K − 1 ) (K-1) (K−1) 的正下方;
- 若 ( K − 1 ) (K-1) (K−1) 既不在第一行,也不在最后一列,如果 ( K − 1 ) (K-1) (K−1) 的右上方还未填数,则将 K K K 填在 ( K − 1 ) (K-1) (K−1) 的右上方,否则将 K K K 填在 ( K − 1 ) (K-1) (K−1) 的正下方。
现给定 N N N ,请按上述方法构造 N × N N \times N N×N 的幻方。
输入格式
一个正整数 N N N,即幻方的大小。
输出格式
共 N N N 行,每行 N N N 个整数,即按上述方法构造出的 N × N N \times N N×N 的幻方,相邻两个整数之间用单空格隔开。
对于 100 % 100\% 100% 的数据,对于全部数据, 1 ≤ N ≤ 39 1 \leq N \leq 39 1≤N≤39 且 N N N 为奇数。
#include<iostream>
using namespace std;
int main (){
int N, a[40][40] = {0};
cin >> N;
a[1][(N+1) / 2] = 1;
int m = 1, n = (N + 1) / 2;
for(int i = 2; i <= N * N; i++){
if(m == 1 && n != N){
m = N;
n++;
a[m][n] = i;
}
else if(m != 1 && n == N){
m--;
n = 1;
a[m][n] = i;
}
else if(m == 1 && n == N){
m++;
a[m][n] = i;
}
else if(m != 1 && n != N){
if(a[m-1][n+1] == 0){
m--;
n++;
a[m][n] = i;
}else{
m++;
a[m][n] = i;
}
}
}
int cnt = 1;
for(int i = 1; i <= N; i++)
for(int j = 1; j <= N; j++){
cout << a[i][j] << (cnt % N == 0 ? '\n' : ' ' );
cnt++;
}
return 0;
}