2020复旦大学计算机考研机试题详解

写在前面

复旦巨喜欢考动规,建议好好准备


A、斗牛

给定五个 0~9 范围内的整数 a1, a2, a3, a4, a5。如果能从五个整数中选出三个并且这三个整数的和为10 的倍数(包括 0),那么这五个整数的权值即为剩下两个没被选出来的整数的和对 10 取余的结果,显然如果有多个三元组满⾜和是 10 的倍数,剩下两个数之和对 10 取余的结果都是相同的;如果
选不出这样三个整数,则这五个整数的权值为 -1。
现在给定 T 组数据,每组数据包含五个 0~9 范围内的整数,分别求这 T 组数据中五个整数的权值。

【输⼊格式】 第⼀⾏⼀个整数 T (1<=T<=1000),表⽰数据组数。 接下来 T ⾏,每⾏ 5 个 0~9 的整数,表⽰⼀组数据。

【输出格式】输出 T ⾏,每⾏⼀个整数,表⽰每组数据中五个整数的权值。

【样例输⼊】
4
1 0 0 1 0
1 0 0 8 6
3 4 5 6 7
4 5 6 7 8

【样例输出】
2
-1
-1
0

【解释】
在第⼀组(1 0 0 1 0)中,三元组 0 0 0 的和为 0,是 10 的倍数,剩余的 1 1 之和为 2,对 10 取余为2。
在第⼆组中,不存在任何⼀个三元祖只和为 10 的倍数。
在第四组中,三元组 5 7 8 的和为 20,是 10 的倍数,剩余的 4 6 只和为 10,对 10取余为 0。
在第五组中,三元组 0 3 7 和三元组 0 4 6 的和都是 10,是 10 的倍数,但是根据简单的数论可知,如果存在多个三元组满⾜情况,那么剩余数字的结果之和对 10 取余是相等的,在本例中和为 10,对 10取余为 0。

答案解析

基本思路:

其实就是判断是否有三数之和为10的倍数,而且确定总共只有五个数,那么可以用fou循环来解决,但是为了代码美观而且看起来牛掰点,还是使用了回溯法,二者的时间复杂度和空间复杂度是相同的。最后如果三数之和为10的倍数,那么只要五数之和减去三数之和就可以了。
时间复杂度:O(n)
空间复杂度:O(1)

回溯法:使用visited数组来记录每个元素的是否被访问过,每次确定一个元素,然后递归确定其他元素

代码:

#include 
#include 
#include 
using namespace std;

//回溯法得到其中三数之和,若没有10的倍数,返回-1
int judge(int sum,vector<int>& visited,vector<int>D,int num) {
	if (num > 3)	return -1;
	if (num == 3 && sum % 10 == 0)	return sum;
	for (int i = 0; i < D.size(); i++) {
		if (visited[i] == 0) {
			visited[i] = 1;
			int f = judge(sum + D[i], visited, D, num + 1);
			if (f != -1)	return f;
			visited[i] = 0;
		}
	}
	return -1;
}

int main()
{
	int T;
	cin >> T;
	int N = 5;
	vector<vector<int>>data(T, vector<int>(N));
	for (int i = 0; i < T; i++) {
		for (int j = 0; j < N; j++) {
			cin >> data[i][j];
		}
	}
	vector<int>result;
	vector<int> visited(N, 0);	//记录已被访问过的数
	for (int i = 0; i < T; i++) {
		int sum = 0, num = 0;
		fill(visited.begin(), visited.end(), 0);
		int flag = judge(sum, visited, data[i], num);
		if (flag != -1) {	//存在三数之和为10的倍数
			int S = 0;
			for (int j = 0; j < N; j++)	S += data[i][j];
			result.push_back((S - flag) % 10);	//总和减去三数之和就是剩余两数之和
		}
		else  result.push_back(flag);
	}
	for (int i = 0; i < result.size(); i++)	cout << result[i] << endl;
}



打地鼠

给定 n 个整数 a1, a2, …, an 和⼀个 d,你需要选出若⼲个整数,使得将这些整数从⼩到⼤排好序之后,任意两个相邻的数之差都不⼩于给定的 d,问最多能选多少个数出来。

【输⼊格式】 第⼀⾏两个整数 n,d (1<=n<=10^5, 0<=d<=10^9),分别表⽰整数个数和相邻整数差的下界。 第⼆⾏ n
个整数 a1, a2, …, an (1<=ai<=10^9, 1<=i<=n),表⽰给定的 n 个整数。

【输出格式】 仅⼀⾏⼀个整数,表⽰答案。

【样例输⼊】
6 2
1 4 2 8 5 7

【样例输出】
3

【解释】
注意,选出的数在排序后,相邻两数之差不⼩于给定值。
B. 打地⿏
⽐如,对于给定值 2,[1 4 7] 是⼀个满⾜条件的选择⽅案,但是[1 4 5] 却不是,因为 5 - 4 = 1 < 2。
在本样例中,[1 4 7],[1 4 8],[1 5 7],[1 5 8],[2 4 7],[2 4 8] 都是满⾜要求的选择⽅案,但是⽆论如何都没有办法得到⼀个选出 4 个数且满⾜条件的⽅案,所以本样例的答案为 3。

答案解析

基本思路:

有两种方法,一种是动规,一种是贪心。这里我使用的是贪心
首先递增排序,data[i]代表第 i 个数,d代表差,那么:
1、如果data[j]-data[i]>=d,由于数据增序排列,那么data[k] (j 2、如果data[j]-data[i] 时间复杂度:O(nlogn)
空间复杂度:O(1)

代码

#include 
#include 
#include 
using namespace std;
int main()
{
	int n, d;
	cin >> n >> d;
	vector<int>data(n);
	for (int i = 0; i < data.size(); i++)	cin >> data[i];
	sort(data.begin(), data.end());	//从小到大排序
	int num = 0;
	if (n < 2) {	//只有一个整数
		cout << num << endl;
		return 0;
	}

	for (int i = 0, j = 1; j < data.size();) {
		if (data[j] - data[i] >= d) {
			num++;
			i = j;
			j++;
		}
		else j++;
	}
	if (num == 0)	cout << num << endl;	//没有符合条件的数
	else  cout << num + 1 << endl;	//由于每次只将较小的数加入结果集,因此最后还需将最后的大数加入结果集
	return 0;
}



C、排队打饭

下课了,有 n 位同学陆续赶到⻝堂进⾏排队打饭,其中第 i 位同学的到达时间为 ai,打饭耗时为 ti,等待时间上限为 bi,即如果其在第 ai+bi 秒的时刻仍然没有轮到他开始打饭,那么他将离开打饭队列,另寻吃饭的地⽅。问每位同学的开始打饭时间,或者指出其提前离开了队伍(如果这样则输出 -1)。

【输⼊格式】 第⼀⾏⼀个整数 n (1<=n<=10^5),表⽰来打饭的同学数量。 接下来 n ⾏,每⾏三个整数 ai,ti,bi (1<=ai,ti,bi<=10^9, 1<=i<=n),分别表⽰每位同学的到达时间、打 饭耗时、等待时间上限。 保证 a1

【输出格式】 ⼀⾏ n 个整数,表⽰每位同学的开始打饭时间或者 -1(如果该同学提前离开了队伍)。

【样例输⼊】
4
1 3 3
2 2 2
3 9 1
4 3 2

【样例输出】
1 4 -1 6

【解释】
C. 排队打饭
第⼀个同学在 1 时刻到达队列,需要 3 个单位时间才能打好饭(也就是说如果在 1 时刻开始打饭,那么将在 1 + 3 = 4 时刻打好饭离开),最⻓等待时间为 3 个单位时间(也就说如果在到达队列之后的 3单位时间后还没开始给他打饭,他就忍耐不了离开了)。
在本样例中,
1 时刻:第⼀个同学在 1 时刻到达队列,同时开始了打饭操作(对应输出的第⼀个值为 1)。
2 时刻:在 2 时刻,第⼆个同学加⼊了队列,给第⼆个同学打饭需要 2 个单位时间,但是如果在等待了 2 个单位时间没给第⼆个同学打饭的话,第⼆个同学将离开。
3 时刻:在 3 时刻,第三个同学加⼊了队列,给第三个同学打饭需要 9 个单位时间,但是如果在等待了 1 个单位时间没给第三个同学打饭的话,第三个同学将离开,换句话说,如果在 3 (到达时刻) + 1(可等待时间⻓度)= 4 时刻还没给第三个同学打饭,那么第三个同学将离开。
4 时刻:第⼀个同学在时刻 4 打完饭离开,同时队列⾥的第⼆个同学开始打饭(对应输出的第⼆个值为 4),此时第三个同学没有达到饭,所以第三个同学就在时刻 4 离开了队伍(对应输出的第三个值为 -1)。同时,在时刻 4,第四个同学也加⼊了队列,第四个同学最⻓等待到 4(到达时刻)+ 2 (可等待时间⻓度)= 6 时刻。
5 时刻:5 时刻还在给第⼆个同学打饭,第四个同学还在队列⾥⾯排队。
6 时刻:6 时刻,第⼆个同学打饭完成,同时第四个同学开始打饭(对应输出的第四个值为 6)。
根据上⾯描述的过程,输出为 1 4 -1 6。

答案解析

主要思路

使用endtime来记录上一个学生的打饭的结束时间,那么当前学生的开始时间如果大于等于endtime,就可以直接开始;否则的话,当前学生的开始时间为endtime,同时计算等待时间,判断是否大于最长等待时限。
时间复杂度:由于默认到达时间递增,所以只需按到达时间遍历一遍就够了,位O(n);
空间复杂度:只需要一个用来记录上一位学生的打饭结束时间的变量,为O(1)

代码

#include 
#include 
#include 
using namespace std;
int main()
{
	int n;
	cin >> n;
	vector<vector<long long int>>data(n, vector<long long int>(3));
	for (int i = 0; i < data.size(); i++){
		for (int j = 0; j < data[i].size(); j++) {
			cin >> data[i][j];
		}
	}
	long long int endtime = 0;	//记录上一个学生打饭结束的时间
	vector<long long int>result;
	for (int i = 0; i < data.size(); i++) {
		if (data[i][0] >= endtime) {	//第i位学生达到时无人打饭,可以直接开始打饭
			result.push_back(data[i][0]);
			endtime = data[i][0] + data[i][1];
		}
		else {	//第i位学生正好有人打饭,那么第i位学生的等待时间=上一位学生的结束时间-第i位学生的开始时间
			if (endtime - data[i][0] > data[i][2])	result.push_back(-1);	
			else {
				result.push_back(endtime);
				endtime = endtime + data[i][1];
			}
		}
	}
	for (int i = 0; i < result.size(); i++)	cout << result[i] << ' ';
	return 0;
}



D、二叉搜索树

给定⼀个 1~n 的排列 P,即⻓度为 n,且 1~n 中所有数字都恰好出现⼀次的序列。现在按顺序将排列中的元素⼀⼀插⼊到初始为空的⼆叉搜索树中(左小右大),问最后每个节点的⽗亲节点的元素是什么。特别地,根节点的⽗亲节点元素视为 0。

【输⼊格式】
第⼀⾏⼀个整数 n (1<=n<=10^5),表⽰排列 P 中的元素个数。
第⼆⾏ n 个整数 p1, p2, …, pn (1<=pi<=n, 1<=i<=n),表⽰给定的排列。

【输出格式】 ⼀⾏ n 个整数,其中第 i 个整数 ai 表⽰元素 i 对应节点的⽗亲节点的元素。特别地,根节点的⽗亲节 点元素视为 0。

【样例输⼊】
5
2 3 5 1 4

【样例输出】
2 0 2 5 3

【样例解释】
最后建出来的⼆叉搜索树如下:
2020复旦大学计算机考研机试题详解_第1张图片
1 的⽗亲为 2,2 为根结点,所以⽗亲为 0,3 的⽗亲为 2,4 的⽗亲为 5,5 的⽗亲为 3。

答案解析

主要思路

这是最难的一题了,做是能做出来,但是基本都超时,这里介绍两种方法:

第一种:最容易想到的方法,不就是求搜索二叉树的父节点嘛,只要建一棵树出来不就得了。
时间复杂度:O(n2)
空间复杂度:O(n)

代码

#include 
#include 
#include 
#include 
using namespace std;
typedef struct node {
	int v;
	node* left;
	node* right;
}node;
int main()
{
	//边建树边查找,n2
	int n;
	cin >> n;
	vector<int>data(n + 1);
	for (int i = 1; i < data.size(); i++)	cin >> data[i];
	vector<int>result(n + 1);
	node* T = new node;
	T->v = 0; T->left = NULL; T->right = NULL;
	//边遍历边构造二叉搜索树,在找到插入位置时,同时保存父节点
	for (int i = 1; i < data.size(); i++) {
		node* p = T;
		while (true) {
			if (data[i] > p->v) {
				if (p->right)	p = p->right;
				else {
					node* q = new node;
					q->v = data[i]; q->left = NULL; q->right = NULL;
					p->right = q;
					break;
				}
			}
			else {
				if (p->left)	p = p->left;
				else {
					node* q = new node;
					q->v = data[i]; q->left = NULL; q->right = NULL;
					p->left = q;
					break;
				}
			}
		}
		result[data[i]] = p->v;
	}
	for (int i = 1; i < result.size(); i++)	cout << result[i] << ' ';
	return 0;
}

第二种:
       使用一个 f 数组来记录每一个节点的父节点,使用一个map来记录每一个节点的层次(初始化0节点的层次为0),使用mx来记录当前树中的最大值:
        1、新加入的节点的权值若大于mx,那么该节点的父节点必定是mx,并且更新mx,同时将该节点的层次加入map;
       2、新加入的节点的权值若小于mx,查找map中第一个大于该节点权值的键next以及最后一个小于该节点的键pre(因为map会自己按键值排列,且next和pre比相邻,因此用二分查找)
       由于二叉搜索树的中序遍历必定是增序的,因此该节点的中序遍历位置必定pre和next之间。
       因此如果next的层次大于pre(即next是pre的右子树的最左下节点),若想中序递增,当前节点必定是next的左孩子,因此将当前节点加入map,其层次为next的层次加1;
       如果next的层次小于pre(pre是next的左子树的最右下节点),若想中序递增,那么当前节点必是pre的右孩子,因此当前的节点的层次为pre的层次加1;


时间复杂度:查找带插入位置使用二分查找法,因此为O(nlogn);
空间复杂度:O(n)

代码

#include 
#include 
#include 
#include 
using namespace std;
int main(){
int n;
	cin >> n;
	vector<int>father(n + 1);
	map<int, int>dict;
	dict[0] = 0;
	int maxn = 0;
	for (int i = 0; i < n; i++) {
		int t;
		cin >> t;
		if (t > maxn) {
			father[t] = maxn;
			dict[t] = dict[maxn] + 1;
			maxn = t;
		}
		else {
			map<int, int>::iterator small = dict.upper_bound(t);
			map<int, int>::iterator big = small--;
			if (big->second > small->second) {
				father[t] = big->first;
				dict[t] = big->second + 1;
			}
			else {
				father[t] = small->first;
				dict[t] = small->second + 1;
			}
		}
	}
	for (int i = 1; i < father.size(); i++)	cout << father[i] << ' ';
}



E、序列

给定⼀个⻓为 n 的序列 A,其中序列中的元素都是 0~9 之间的整数,对于⼀个⻓度同样为 n 整数序列B,定义其权值为 |A_i-B_i| (1<=i<=n) 之和加上 (B_j-B_j+1)^2 (1<=j

【输⼊格式】 第⼀⾏⼀个整数 n (1<=n<=10^5),表⽰序列 A 的⻓度。 第⼆⾏ n 个整数 a1, a2, …, an (0<=ai<=9, 1<=i<=n),表⽰序列 A 中的元素。

【输出格式】仅⼀⾏⼀个整数,表⽰答案。

【样例输⼊】
6
1 4 2 8 5 7

【样例输出】
11

【解释】
A 数组是 [1 4 2 8 5 7]
B 数组可以是 [3 4 4 5 5 6]。
权值为 |A_i - B_i| (1<=i<=n) 之和加上 (B_j - B_j+1)^2 (1<= j 权值第⼀部分|A_i - B_i| (1<=i<=n)之和为:
|1 - 3| + |4 - 4| + |2 - 4| + |8 - 5| + |5 - 5| + |7 - 6| = 2 + 0 + 2 + 3 + 0 + 1 = 8
权值第⼆部分(B_j - B_j+1)^2 (1<= j (3 - 4)^2 + (4 - 4)^2 + (4 - 5)^2 + (5 - 5)^2 + (5 - 6)^2 = 1 + 0 + 1 + 0 + 1 = 3
所以总权值为 8 + 3 = 11。

主要思路

二维动规法:dp[i][j]代表处理到第i个数时填j 的当前最小权值和
for (int i = 1; i < A.size(); i++) {
     for (int j = 0; j < N; j++) {
          for (int k = 0; k < N; k++) {
              dp[i][j] = min(dp[i][j], dp[i - 1][k] + abs(A[i] - j) + (j - k)*(j - k));
          }
     }
}

dp[i][j] = min(dp[i][j], dp[i - 1][k] + abs(A[i] - j) + (j - k)(j -k)); dp[i][j] 代表当前在B[i]填j所得到的当前的最小权值,初始化为INT_MAX;
dp[i - 1][k] + abs(A[i] - j) + (j - k)
(j - k):遍历B[i-1]填k(k为0到9)的各个情况,计算当前B[i]填j时与各个k计算的权值和


时间复杂度:O(n) 空间复杂度:O(n)

代码

#include 
#include 
#include 
#include 
#include 
using namespace std;

#define N 10

int main() {
	int n;
	cin >> n;
	vector<int>A(n);
	for (int i = 0; i < A.size(); i++)	cin >> A[i];
	vector<vector<int>>dp(n, vector<int>(N, INT_MAX));
	for (int i = 0; i < N; i++)	dp[0][i] = abs(i - A[0]);
	for (int i = 1; i < A.size(); i++) {
		for (int j = 0; j < N; j++) {
			for (int k = 0; k < N; k++) {
				dp[i][j] = min(dp[i][j], dp[i - 1][k] + abs(A[i] - j) + (j - k)*(j - k));
			}
		}
	}
	int result = INT_MAX;
	for (int i = 0; i < N; i++)	result = min(result, dp[n - 1][i]);
	cout << result;
	return 0;
}

如有其他相关问题,请联系:[email protected]

你可能感兴趣的:(2020复旦大学计算机考研机试题详解)