第一题 重复计数
在一个有限的正整数序列中,有些数会多次重复出现。请你统计每个数的出现次数,然后按数字在序列中第一次出现的位置顺序输出数及其次数。
输入格式:
第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
小唐正在学习数据结构。他尝试应用数据结构理论处理数据。最近,他接到一个任务,要求维护一个动态数据表,并支持如下操作:
- 插入操作(I):从表的一端插入一个整数。
- 删除操作(D):从表的另一端删除一个整数。
- 取反操作(R):把当前表中的所有整数都变成相反数。
- 取最大值操作(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还是掌握的少了