jlu(2023)数据结构第三次上机考

第一题 重复计数

在一个有限的正整数序列中,有些数会多次重复出现。请你统计每个数的出现次数,然后按数字在序列中第一次出现的位置顺序输出数及其次数。

输入格式:

第1行,1个整数N,表示整数的个数,(1≤N≤50000)。

第2行,N个正整数,每个整数x 都满足 1 ≤ x ≤2000000000。

输出格式:

若干行,每行两个用一个空格隔开的数,第一个是数列中出现的数,第二个是该数在序列中出现的次数。

输入样例:

在这里给出一组输入。例如:

12
8 2 8 2 2 11 1 1 8 1 13 13

输出样例:

在这里给出相应的输出。例如:

8 3
2 3
11 1
1 3
13 2

 解题思路(暴力)

 第一种思路,也是我本人最直接的想法,直接暴力求解。暴力思路很简单,首先开一个足够大的数组,这个数组用来存放出现的数字,同时也存放数据的顺序,同时这个数组必须满足里面数字不重复。我们在读入数字的同时,就开始比较,代码如下


#include<iostream>
#include<algorithm>
int arr[50001];
int coun[50001];
using namespace std;
int main() {
	int n, i, num;
	int j, top = 0;//top是已经进入单数字的数组
	cin >> n;
	for (i = 0; i < n; i++) {
		cin >> num;
		for (j = 0; j <= top; j++) {
			if (arr[j] == num) {
				coun[j]++;
				break;
			}
		}
		if (j > top) {
			arr[++top] = num;
			coun[top]++;
		}//如果这个数字第一次出现,就加入arr数组,并且次数加一
	}
	for (i = 1; i <= top; i++) {
		cout << arr[i] << " " << coun[i];
		if (i != top)
			cout << endl;
	}
	return 0;
}

	


 第二种思路(map-参考大佬)

map相当于一个映射,与数组相似又稍有区别,事实上这道题,我们在不考虑太多的情况下,肯定可以用数组来写(读数字100,arr[100]++),但在实际题目中,如果出现两个数字10000000和1,那么数组就必须要开这么大,显然浪费很多空间,而map数组就是一一对应。代码如下

#include<iostream>
#include<map>
using namespace std;
map<int, int>first, second;
int read() {
	int num = 0; char ch = getchar();
	while (ch < '0' || ch>'9')ch = getchar();
	while (ch >= '0' && ch <= '9') {
		num = num * 10 + ch - '0';
	}
	return num;
}
int main() {
	int n;
	cin >> n;
	int arr[100000];
	for (int i = 1; i <= n; i++) {
		cin >> arr[i];
		if (first[arr[i]] == 0)second[arr[i]] = i;
		first[arr[i]]++;
	}
	for (int i = 1; i <= n; i++) {
		if (second[arr[i]] == i) {
			cout << arr[i] << " " << first[arr[i]] << endl;
		}
	}
	return 0;
}

反思

 从本题中学习到了map的使用,以及快速读入

int read(){
	int t=0; char ch=getchar();
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') t=(t<<1)+(t<<3)+(ch^48),ch=getchar(); 
	return t;
} 

第二题二叉树加权距离

二叉树结点间的一种加权距离定义为:上行方向的边数×3 +下行方向的边数×2 。上行方向是指由结点向根的方向,下行方向是指与由根向叶结点方向。
给定一棵二叉树T及两个结点u和v,试求u到v的加权距离。

输入格式:

第1行,1个整数N,表示二叉树的结点数,(1≤N≤100000)。

随后若干行,每行两个整数a和b,用空格分隔,表示结点a到结点b有一条边,a、b是结点的编号,1≤a、b≤N;根结点编号为1,边从根向叶结点方向。

最后1行,两个整数u和v,用空格分隔,表示所查询的两个结点的编号,1≤u、v≤N。

输出格式:

1行,1个整数,表示查询的加权距离。

输入样例:

在这里给出一组输入。例如:

5
1 2
2 3
1 4
4 5
3 4


输出样例:

在这里给出相应的输出。例如:

8


 解题思路

 本题事实上最重要的是理解题意,通过画出样例,我们可以知道,这题事实上是要我们求解公共的祖先问题。在本题中,一开始建树我用了左子树和右子树,想要建立一颗完整的树,但后面发现,这题本质上只要求各个节点的高度,以及共同祖先的高度就可以了,所以我们只需要建一颗father树,用一个递归,求出两个节点的高度,再用一个循环,求出公共祖先的高度,循环的结束条件为,两个节点相同,代码如下


#include<iostream>
using namespace std;
int father[100001];
int dfsfather(int n) {
	int num = 0;
	while (father[n] != 0) {
		n = father[n];
		num++;
	}
	return num;
}
int main()
{
	int n;
	cin >> n;
	for (int i = 1; i < n; i++)
	{
		int u, v;
		cin >> u >> v;
		father[v] = u;
	}
	father[1] = 0;
	int a, b;
	cin >> a >> b;
	int high_a = dfsfather(a);
	int high_b = dfsfather(b);
	int origin_a = high_a;
	int origin_b = high_b;
	//cout << high_a << " " << high_b << " " << a << b;;
	 while (a != b) {
        if (high_a >= high_b) {
            high_a--;
            a = father[a];
        }
        else {
            high_b--;
            b = father[b];
        }
    }
	int num = 0;
	num= (origin_b - high_b) * 2 + (origin_a - high_a) * 3;
	cout << num;
	return 0;
}

 


第三题 发红包

新年到了,公司要给员工发红包。员工们会比较获得的红包,有些员工会有钱数的要求,例如,c1的红包钱数要比c2的多。每个员工的红包钱数至少要发888元,这是一个幸运数字。

公司想满足所有员工的要求,同时也要花钱最少,请你帮助计算。

输入格式:

第1行,两个整数n和m(n<=10000,m<=20000),用空格分隔,分别代表员工数和要求数。

接下来m行,每行两个整数c1和c2,用空格分隔,表示员工c1的红包钱数要比c2多,员工的编号1~n 。

输出格式:

一个整数,表示公司发的最少钱数。如果公司不能满足所有员工的需求,输出-1.

输入样例:

在这里给出一组输入。例如:

 2 1 
 1 2

输出样例:

在这里给出相应的输出。例如:

1777

 解题思路

 本题读完题后,我们可以想到,每个人都有至少一个前置条件来决定他的红包钱数,首先,公司要满足所有人红包至少888元,而员工之间又有大小红包的要求,这事实上就是我们学的拓扑路径问题,如果有一个人叫a,他要满足比b,c,d的红包钱都多,那么我们就不能直接决定这个人红包钱多少,必须把bcd的红包钱都算出来,再用最大的钱+1才是a的钱。所以,我们每次都找出入度为0的点,更新他周围的点,入度减少,如果有入度为0的点就入栈,如果在读完所有点之前,栈就空了,那么说明此时路径存在环,输出-1,代码如下

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
vector<int>apex[10001];
queue<int>tp;
int ru[10001];
int big(int x, int y) {
	if (x > y)return x;return y;
}
int main() {
	int n, m, i, money[10001]; \
		cin >> n >> m;
	for (i = 1; i <= m; i++) {
		int u, v;
		cin >> u >>v;
		apex[v].push_back(u);
		ru[u]++;
	}
	for (i = 1; i <= n;i++)
		money[i] = 0;
	for (i = 1; i <= n; i++) {
		if (ru[i] == 0)
			tp.push(i);
	}
	
	for (i = 1; i <n; i++) {
		if (tp.empty()) {
			cout << -1;
			return 0;
		}
		int k = tp.front();
		tp.pop();
		for (int j = 0; j < apex[k].size(); j++) {
			int seer = apex[k][j];
			ru[seer]--;
			money[seer] = big(money[seer], money[k] + 1);
			if (ru[seer] == 0)
				tp.push(seer);
		}
	}
	int num = 0;
	for (i = 1; i <= n; i++) {
		num = num + 888 + money[i];
	}
	cout << num;
	return 0;
}

7-4 数据结构设计I

小唐正在学习数据结构。他尝试应用数据结构理论处理数据。最近,他接到一个任务,要求维护一个动态数据表,并支持如下操作:

  1. 插入操作(I):从表的一端插入一个整数。
  2. 删除操作(D):从表的另一端删除一个整数。
  3. 取反操作(R):把当前表中的所有整数都变成相反数。
  4. 取最大值操作(M):取当前表中的最大值。 如何高效实现这个动态数据结构呢?

输入格式:

第1行,包含1个整数M,代表操作的个数, 2≤M≤1000000。

第2到M+1行,每行包含1个操作。每个操作以一个字符开头,可以是I、D、R、M。如果是I操作,格式如下:I x, x代表插入的整数,-10000000≤x≤10000000。

输出格式:

若干行,每行1个整数,对应M操作的返回值。如果M和D操作时队列为空,忽略对应操作。

输入样例:

在这里给出一组输入。例如:

6
I 6
R
I 2
M
D
M

输出样例:

在这里给出相应的输出。例如:

2
2

 解题思路

 本题,事实上本人在上机考时候并没有太好的思路,掌握的stl有限而且并不是太熟悉,所以最后选择了尽量拿一点分的做法(可以看看),就是人工创建链表,设置左右指针来进行插入和删除,并且这个链表是一个双向链表,不然不能删除或者插入,当然这种做法肯定会超时,因为你每次取反,都要遍历链表一个一个取反,如果你想在插入数字的时候就保存最大值也是不行的,因为有取反操作和删除操作,你必须遍历一遍链表,所以这个方法是显而易见的超时方法,代码如下

#include<iostream>
using namespace std;
int arr[1000001];
int top = 0;
struct node {
	int data;
	node* left;
	node* right;
};
int main() {
	int n,i;
	cin >> n;
	node* rootr = NULL;
	node* rootl = NULL;
	for (i = 1; i <= n; i++) {
		char ch;
		cin >> ch;
		if (ch == 'I') {
			int x; cin >> x;
			node* head = new node;
			head->data = x;
			if (rootr!=NULL) {
				rootr->right = head;
				head->left = rootr;
				head->right = NULL;
				rootr = head;
			}
			else {
				head->left = NULL;
				head->right = NULL;
				rootr = head;
				rootl = head;
			}
		}
		if (ch == 'D') {
			if (rootl == NULL)
				continue;
			rootl = rootl->right;
			if (rootl != NULL) {
				rootl->left = NULL;
			}
		}
		if (ch == 'R') {
			node* bianli = rootl;
			while (bianli != NULL) {
				bianli->data = -1 * bianli->data;
				bianli = bianli->right;
			}
		}
		if (ch == 'M') {
			if (rootl == NULL)
				continue;
			node* bianli = rootl;
			long long max = bianli->data;
			bianli = bianli->right;
			while (bianli != NULL) {
				if (max < bianli->data)
					max = bianli->data;
				bianli = bianli->right;
			}
			arr[++top] = max;
		}
	}
	for (i = 1; i <= top; i++) {
		cout << arr[i];
		if (i != top)
			cout << endl;
	}
	return 0;
}

十分奇怪,第三个点超时可以理解,但最后一个点答案错误,没有找出原因。

正解

在机考后我得知这题可以使用multiset和一个队列来做,并且要使用一个小技巧,用一个judge来判断此时有无取反,multiset可以存储相同的数字,而且可以删除一个指定数字,并且自动排序。现在我们假设有两个数字已经插入,然后取了一波反,judge标记为0,然而我们不对前面已经存储的数字取反(这样要遍历一遍,浪费时间)我们对后面插入的数字取反,这样相当于,正确的数字都是现在存储的数字的相反数,那么最大值就是最小值的相反数。当然你也可以不用multiset,用deque来写,双端队列可以直接插入和删除前端后端,但为了取最大值,你就必须设置两个单调队列,一个是最大单调队列,一个是最小单调队列,因为judge是不确定的,你有可能要取最小值,代码如下

#include<iostream>
#include<cstdio>
#include<set>
#include<queue>
using namespace std;
multiset<int> h;
queue<int>q;
int main() {
	int n;
	cin >> n;
	int judge = 1;//判断是否取反
	for (int i = 1; i <= n; i++) {
		char ch = getchar();
		while (ch != 'I' && ch != 'R' && ch != 'D' && ch != 'M') ch = getchar();
		if (ch == 'I') {
			int x;
			scanf("%d", &x);
			if (judge) {
				q.push(x);
				h.insert(x);
			}
			else
			{
				q.push(-x);
				h.insert(-x);
			}
		}
		if (ch == 'R') {
			judge ^= 1;
		}
		if (ch == 'D') {
			if (!q.empty()) {
				int x = q.front();
				q.pop();
				h.erase(h.find(x));
			}
		}
		if (ch == 'M') {
			if (q.empty())
				continue;
			if (judge == 1) {
				printf("%d\n", *h.rbegin());
			}
			if (judge == 0) {
				printf("%d\n", -1 * (*h.begin()));
			}
		}
	}
	return 0;
}

 第二种用deque的代码如下

#include<iostream>
#include<deque>
using namespace std;
const int MAX = 1e6 + 5;
deque<int>maxx, minn;
int flag=1;
int a[MAX];
int top, fr;
int main()
{
    int m;
    scanf("%d", &m);
    char ch;
    while (m--)
    {
        ch = getchar();
        while (ch != 'I' && ch != 'R' && ch != 'D' && ch != 'M') ch = getchar();
        if (ch== 'I')
        {
            top++;
            scanf("%d", &a[top]);
            if (!flag)
                a[top] = -a[top];
            int tmp = a[top];
            while (!maxx.empty() && a[maxx.back()] > tmp)maxx.pop_back();
            maxx.push_back(top);
            while (!minn.empty() && a[minn.back()] < tmp)minn.pop_back();
            minn.push_back(top);
        }
        else if (ch == 'R')
        {
            flag ^= 1;
        }
        else if (ch == 'D')
        {
            if (fr < top)
            {
                fr++;
                while (!maxx.empty() && maxx.front() <= fr)maxx.pop_front();
                while (!minn.empty() && minn.front() <= fr)minn.pop_front();
            }
        }
        else if (ch == 'M')
        {
            if (fr < top)
            {
                if (!flag)
                {
                    printf("%d\n", -a[maxx.front()]);
                }
                else
                {
                    printf("%d\n", a[minn.front()]);
                }
            }
        }
    }
    return 0;
}

反思

stl还是掌握的少了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值