NOIP学习小站
西安交通大学附属中学航天学校

二维数组和多维数组

前面介绍的都是一维数组,一维数组就像“一排容器”,能够存储若干相同数据类型的数据。对应的还有二维数组、三维数组……多维数组相当于一片容器的矩阵。

一、二维数组

前面介绍了一维数组就像“一排容器”,可以描述为一种“线性”的结构,那么二维数组就像一个有行有列的阵列(矩阵)。

1.二维数组的定义

定义二维数组的方法很简单:数组类型 数组变量名[第一维长度][第二维长度];,例如:

int a[3][5];
long long b[4][100],c[10000][100000];
double d[1001];
char strs[1001][101];

int a[3][5];为例,二维数组中的元素往往描述成有行有列的矩阵(虽然在内存中仍然是线性存储的),每个元素按照矩阵方式排列如下:

第0列第1列第2列第3列第4列
第0行a[0][0]a[0][1]a[0][2]a[0][3]a[0][4]
第1行a[1][0]a[1][1]a[1][2]a[1][3]a[1][4]
第2行a[2][0]a[2][1]a[2][2]a[2][3]a[2][4]

由此可知,定义二维数组时数组类型 数组变量名[第一维长度][第二维长度];,第一维长度对于矩阵的行,第二维长度对应矩阵的列。二维数组元素的使用仍然是使用下标,不过有两个[],两个[]中内容依次是第一维下标和第二维下标。

2.二维数组元素的使用

二维数组中的元素和一维数组的元素一样,可以理解成二维数组中的特殊变量,除了书写时比较特殊外,使用方法和普通变量一致:

int a[10][5];

a[0][0] = 100;
cin>>a[0][1];
a[0][2] = a[0][0]*a[0][1];
cout<<a[0][1]<<endl;
cout<<a[0][1]+a[0][2]<<endl;

//使用双重循环输入数据到数组中
for(int i=0;i<10;i++){
	for(int j=0;j<5;j++){
		cin>>a[i][j];
	}
}

//使用双重循环按照行列形式输出数组中的每个元素
for(int i=0;i<10;i++){
	for(int j=0;j<5;j++){
		cout<<a[i][j]<<" ";
	}
	cout<<endl;
}

二、二维数组例题

1. 鞍点的数量

分析:用二维数组来存储矩阵,使用双重循环遍历二维数组的每个元素,判断元素是否是鞍点(是所在行的最大值,也是所在列的最大值)并计数。

那么对于第\(i\)行\(j\)列的元素,如何判断是否是鞍点呢?可以扫描第\(i\)行的所有元素,看看能否找到比它大的元素,如果没有,那这个元素就是所在行的最大值。同理扫描第\(j\)列的所有元素,看看能否找到比它大的元素,如果没有,那这个元素就是所在列的最大值。

#include<iostream>
using namespace std;
int a[1010][1010];
int main()
{
	int N,M;

	cin>>N>>M;
	for(int i=1;i<=N;i++){
		for(int j=1;j<=M;j++){
			cin>>a[i][j];
		}
	}

	int ans = 0;
	//使用双重循环遍历二维数组,依次判断每个元素是否是鞍点
	for(int i=1;i<=N;i++){
		for(int j=1;j<=M;j++){
			//判断a[i][j]是第i行最大值
			bool f1 = true;
			for(int k=1;k<=M;k++){
				if(a[i][j]<a[i][k]){
					f1 = false;
					break;
				}
			}
			//判断a[i][j]是第j列最大值
			bool f2 = true;
			for(int k=1;k<=N;k++){
				if(a[i][j]<a[k][j]){
					f2 = false;
					break;
				}
			}
			//a[i][j]是鞍点
			if(f1==true && f2==true){
				ans++;
			}
		}
	}
	cout<<ans<<endl;
	return 0;
}

分析上面程序,可知算法时间复杂度是\(O(N*M*(N+M))\)。还有更高效的做法。可以计算出每行、每列的最大值存放到一维数组中,那么对于第\(i\)行\(j\)列的元素,如果值与第\(i\)行最大值相等并且值与第\(j\)列最大值相等,那么它就是一个鞍点。这样可以将算法时间复杂度优化到\(O(N*M)\)。

#include<iostream>
using namespace std;
int a[1010][1010];
//rowm记录每行最大值,colm记录每列最大值
int rowm[1010],colm[1010];
int main()
{
	int N,M;
	cin>>N>>M;
	for(int i=1;i<=N;i++){
		for(int j=1;j<=M;j++){
			cin>>a[i][j];
		}
	}
	//计算每行最大值(行优先遍历)
	for(int i=1;i<=N;i++){
		rowm[i] = a[i][1];
		for(int j=2;j<=M;j++){
			if(a[i][j]>rowm[i]) rowm[i] = a[i][j];
		}
	}
	//计算每列最大值(列优先遍历)
	for(int j=1;j<=M;j++){
		colm[j] = a[1][j];
		for(int i=2;i<=N;i++){
			if(a[i][j]>colm[j]) colm[j] = a[i][j];
		}
	}
	//使用双重循环遍历二维数组,依次判断每个元素是否是鞍点
	int ans = 0;
	for(int i=1;i<=N;i++){
		for(int j=1;j<=M;j++){
			if(a[i][j]==rowm[i] && a[i][j]==colm[j]){
				ans++;
			}
		}
	}
	cout<<ans<<endl;
	return 0;
}

2.买钢笔

分析:要买尽量多的铅笔,那么应该优先考虑多买 4 元钱的钢笔,剩余的钱数可能是0,1,2,3,可以使用多分支语句或者switch...case语句来进行分类讨论:

#include<iostream>
using namespace std;
int main()
{
    int x,a,b,c;
    cin>>x;
    c = x/4;
    a = b = 0;
    int mod = x%4;
    if(mod == 1){
    	b++;c--;
	}else if(mod == 2){
    	a++,c--;
	}else if(mod == 3){
    	a++,b++,c-=2;
	}
    cout<<a<<" "<<b<<" "<<c<<endl;
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
	int x,a,b,c;
	cin>>x;
	c = x/4;
	a = b = 0;
	switch(x%4){
		case 1:b++;c--;break;
		case 2:a++,c--;break;
		case 3:a++;b++;c-=2;break;
	}
	cout<<a<<" "<<b<<" "<<c<<endl;
	return 0;
} 

这里也可以使用数组来解决问题,此时甚至可以不使用分支结构(大家自行分析下面的程序):

#include<iostream>
using namespace std;
int main()
{
	int x;
	cin>>x;
	int ans[3] = {0,0,x/4};
	int change[4][3] = {
		{0,0,0},
		{0,1,-1},
		{1,0,-1},
		{1,1,-2},
	};
	int mod = x%4;
	for(int i=0;i<3;i++)
		cout<<ans[i]+change[mod][i]<<" ";
	cout<<endl;
	return 0;
} 

3.阵列方格

来看一个例题:给定正整数\(n,m,i,j\)(\(1 \le n,m \le 1000\),\(1 \le i \le n\),\(1 \le j \le m\)),在\(n\)行\(m\)列的方格阵列中,找出与第\(i\)行第\(j\)列方格(记为\((i,j)\),行列序号均从1开始计数)同行、同列、同对角线的所有方格(不包括\((i,j)\)自身),这样的方格很多,要求按照行列的顺序输出。

如左侧图解,\(n=9,m=7\),图中标识出了与\((3,6)\)这个方格同行、同列、同对角线的所有方格,那么输出的内容应该是:\((1,4)\)、\((1,6)\)、\((2,5)\)、\((2,6)\)、…、\((8,1)\)、\((8,6)\)、\((9,6)\)。

如果问题要求是先输出所用同行、然后输出同列,然后输出同对角线的方格,其实是可以使用先后的四个循环依次输出这些方格。但是题目要求的是按照行列的顺序输出,也就是四类方格是混在一起的,直接使用循环暂时没有思路,可以试试使用二维数组来处理。

我们可以使用二维数组int a[1001][1001];中的元素a[x][y]来标记方格阵列中第\(x\)行第\(y\)列方格\((x,y)\)是否满足条件。开数组的时候直接使用了最大数据规模,并且考虑了要使用的下标与自然序列一致(第一维长度、第二维长度都比数据规模多开1)。

最开始的时候将数组a每个元素都赋值为0,表示每个方格都不符合条件,然后将与方格\((i,j)\)同行、同列、同对角线的方格对应的数组元素都标记成1。最后使用双重循环按照行列的顺序依次筛选输出值为1的数组元素对应的方格坐标即可。

同行同列的方格容易找出来,对角线上的方格如何找出来呢?

思路一:分别查找标记方格\((i,j)\)左侧对角线和右侧对角线上的方格:

#include<iostream>
using namespace std;
int a[1001][1001]; //全局int类型数组,默认每个元素值都是0,正好表示筛选前都不符合条件 
int main()
{
    int n,m,i,j;
    cin>>n>>m>>i>>j;
    
    //标记同行方格满足条件
    for(int y=1;y<=m;y++) a[i][y] = 1;
    
    //标记同列方格满足条件
    for(int x=1;x<=n;x++) a[x][j] = 1;
    
    //左侧对角线上的方格(从第j-1列依次到第1列)
	for(int y=j-1;y>=1;y--){
		int d = j-y;
		int x = i+d;	//第【y】列对角线【左下方位】的方格x坐标 
		if(x>=1 && x<=n) a[x][y] = 1;
        x = i-d;		//第【y】列对角线【左上方位】的方格x坐标 
        if(x>=1 && x<=n) a[x][y] = 1;
	}

    //右侧对角线上的方格(从第j+1列依次到第m列)
	for(int y=j+1;y<=m;y++){
		int d = y-j;
		int x = i+d;	//第【y】列对角线【右下方位】的方格x坐标 
		if(x>=1 && x<=n) a[x][y] = 1;
        x = i-d;		//第【y】列对角线【右上方位】的方格x坐标
        if(x>=1 && x<=n) a[x][y] = 1;
	}
    
    a[i][j] = 0;     //本身不输出,排除掉(前面被标记过)
    
    //查找数组值为1的元素,输出对应方格坐标 
    for(int x=1;x<=n;x++){
        for(int y=1;y<=m;y++){
            if(a[x][y]==1) cout<<"("<<x<<","<<y<<")"<<" ";
        }
        cout<<endl;
    } 
    return 0;
}

思路二:从第一列开始依次查找标记每列上对角线上的方格:

#include<iostream>
using namespace std;
int a[1001][1001]; //全局int类型数组,默认每个元素值都是0,正好表示筛选前都不符合条件 
int main()
{
    int n,m,i,j;
    cin>>n>>m>>i>>j;
    
    //标记同行方格满足条件
    for(int y=1;y<=m;y++) a[i][y] = 1;
    
    //标记同列方格满足条件
    for(int x=1;x<=n;x++) a[x][j] = 1;
    
    //从第1列开始查找每一列上满足条件的方格(在对角线上) 
    for(int y=1;y<=m;y++){
    	int d = y-j;
    	int x = i+d; 
    	if(x>=1 && x<=n) a[x][y] = 1;
    	x = i-d;
    	if(x>=1 && x<=n) a[x][y] = 1;
	}
    
    a[i][j] = 0;     //本身不输出,排除掉(前面被标记过)
    
    //查找数组值为1的元素,输出对应方格坐标 
    for(int x=1;x<=n;x++){
        for(int y=1;y<=m;y++){
            if(a[x][y]==1) cout<<"("<<x<<","<<y<<")"<<" ";
        }
        cout<<endl;
    } 
    return 0;
}

其实思路一仍然是依次查找了每列对角线上的方格,只不过分成了左侧和右侧两部分来分别查找。思路二则将两部分整合,直接从第一列依次查找每列对角线上的方格。可以先看右侧对角线第\(y\)列的两个坐标点:

上面的计算方法其实对于左边对角线上的点同样适用。

可以通过右侧的图解分析可知:

设列标为\(y\),记\(d = y-j\),那么可知第\(y\)列\((i,j)\)对角线上的两个方格的行坐标应该是\(i+d\)和\(i-d\),还需要考虑行坐标是否在\([1,n]\)范围内。

这个时候我们猛然发现,每一列上满足条件的方格其实是可以计算出来的!同样地,每一行上满足条件的方格也能计算出来。

通过上面的分析可知这个问题不用数组也能解决,通过查找规律可以借助循环一行一行地找出每行符合条件的方格:

#include<iostream>
using namespace std;
int main()
{
	int n,m,i,j;
	cin>>n>>m>>i>>j;
	
	//依次查找每一行符合条件的方格 
	for(int x=1;x<=n;x++){         //x是行标 
		if(x!=i){				  //不是第i行 
			int d = i-x;
			if(d<0) d = -d;		//d是第i行与第x行的距离
			//通过分析查找规律可知第x行在(x,j)左侧且在(i,j)对角线上的方格是(x,j-d)
			//第x行在(x,j)右侧且在(i,j)对角线上的方格是(x,j+d)

			//考查对角线上的方格(x,j-d)是否在阵列范围内(没有越界) 
			if(j-d>=1) cout<<"("<<x<<","<<j-d<<") ";
			
			cout<<"("<<x<<","<<j<<") ";    //(x,j)与(i,j)同列 
			
			//考查对角线上的方格(x,j+d)是否在阵列范围内(没有越界)
			if(j+d<=m) cout<<"("<<x<<","<<j+d<<") "; 
		}else{					//第i行
			//除了(x,j)都输出:因为x==i,(x,j)就是(i,j) 
			for(int y=1;y<=m;y++){
				if(y!=j) cout<<"("<<x<<","<<y<<") "; 
			}
		}
		cout<<endl;
	}
    return 0;
}

4.洛谷P5728 旗鼓相当的对手

分析:使用二维数组int a[1000][4];保存每位同学三门科目的考试成绩和总成绩,每位同学的成绩占二维数组一行。同学两两组合考虑是否是旗鼓相当的对手,为了避免重复考虑,对于第0~\(N-2\)的第\(i\)位同学,考虑并统计他与第\(i+1\)~\(N-1\)的每位同学是否是旗鼓相当的对手即可。

#include<iostream>
#include<cmath>
using namespace std;
int a[1000][4]; 
int main()
{
    int N,s1,s2,ans = 0;
    cin>>N;
    for(int i=0;i<N;i++){
    	cin>>a[i][0]>>a[i][1]>>a[i][2];
    	a[i][3] = a[i][0]+a[i][1]+a[i][2];	//计算总分 
	}
	for(int i=0;i<N-1;i++){
		for(int j=i+1;j<N;j++){
			bool f = abs(a[i][3]-a[j][3])<=10;       //总分之差不大于10
			for(int k=0;f && k<3;k++){               //借助循环判断3门学科
				f = f && (abs(a[i][k]-a[j][k])<=5);    //每门学科成绩之差不大于5
			}
			if(f) ans++;
		}
	}
	cout<<ans;
    return 0;
}

代码中判断“旗鼓相当”的语句大家可以仔细琢磨:

bool f = abs(a[i][3]-a[j][3])<=10;           //总分之差不大于10
for(int k=0;f && k<3;k++){                   //借助循环判断3门学科
	f = f && abs(a[i][k]-a[j][k])<=5;        //每门学科成绩之差不大于5
}
if(f) ans++;

甚至还可以进一步让该“旗鼓相当”的判断更加工整:

#include<iostream>
#include<cmath>
using namespace std;
int a[1000][4];
int b[4] = {5,5,5,10};     //记录每门学科最大差值和总分最大差值
int main()
{
    int N,s1,s2,ans = 0;
    cin>>N;
    for(int i=0;i<N;i++){
    	cin>>a[i][0]>>a[i][1]>>a[i][2];
    	a[i][3] = a[i][0]+a[i][1]+a[i][2];	//计算总分 
	}
	for(int i=0;i<N-1;i++){
		for(int j=i+1;j<N;j++){
			bool f = true;
			for(int k=0;f && k<4;k++){
				f = f && (abs(a[i][k]-a[j][k])<=b[k]);
			}
			if(f) ans++;
		}
	}
	cout<<ans;
    return 0;
}

如果不易理解,其实上面判断“旗鼓相当”的代码也可以写成下面虽然繁琐但简单的代码:

if(abs(a[i][0]-a[j][0])<=5 && abs(a[i][1]-a[j][1])<=5 && abs(a[i][2]-a[j][2])<=5 && abs(a[i][3]-a[j][3])<=10) ans++;

三、多维数组

回顾一维数组、二维数组的定义和元素的使用方法,不难得出更高维数组的定义和元素的使用方法。

一维数组的使用举例

int a[11];
a[1] = 123;
cin>>a[2];
a[3] = a[1]*a[2];
cout<<a[3];
cout<<a[3]*a[1];

//使用循环输入数据到数组中
for(int i=1;i<=10;i++){
	cin>>a[i];
}

//使用循环输出数组中的每个元素
for(int i=1;i<=10;i++){
	cout<<a[i]<<" ";
}

二维数组的使用举例

int a[10][5];

a[0][0] = 100;
cin>>a[0][1];
a[0][2] = a[0][0]*a[0][1];
cout<<a[0][1]<<endl;
cout<<a[0][1]+a[0][2]<<endl;

//使用双重循环输入数据到数组中
for(int i=0;i<10;i++){
    for(int j=0;j<5;j++){
        cin>>a[i][j];
    }
}

//使用双重循环按照行列形式输出数组中的每个元素
for(int i=0;i<10;i++){
    for(int j=0;j<5;j++){
        cout<<a[i][j]<<" ";
    }
    cout<<endl;
}

通过一个例子来看三维数组的使用:洛谷P5729 工艺品制作

分析:使用三维数组记录每个小方块是否被蒸发(最开始都没有被蒸发,元素都标记为0),对于每一次操作,相当于把一个小的立方体中的每个小方块蒸发掉(标记为1)。所有操作完成后,统计三维数组中值为0的元素的个数即可。

#include<iostream>
#include<cmath>
using namespace std;
//三维全局数组v,元素v[i][j][k]记录坐标点(i,j,k)小方块是否被蒸发掉
//全局数组所有元素默认值为0,正好表示开始时都没有被蒸发
int v[21][21][21]; 
int main()
{
	int w,x,h,q,ans = 0;
	int x1,y1,z1,x2,y2,z2;
	cin>>w>>x>>h;
	cin>>q;
	while(q--){		//q次重复操作 
		cin>>x1>>y1>>z1>>x2>>y2>>z2;
		//坐标范围描述的小立方体中的每个小方块都被蒸发掉,使用三重循环处理
		for(int i=x1;i<=x2;i++){
			for(int j=y1;j<=y2;j++){
				for(int k=z1;k<=z2;k++){
					v[i][j][k] = 1;     //标记(i,j,k)小方块被蒸发掉 
				}
			}
		}
	}
	for(int i=1;i<=w;i++){
		for(int j=1;j<=x;j++){
			for(int k=1;k<=h;k++){
				ans += 1-v[i][j][k];    //统计未蒸发小方块数量 
			}
		}
	}
	cout<<ans;
    return 0;
}