本文为PAT甲级分类汇编系列文章。

理论这一类,是让我觉得特别尴尬的题,纯粹是为了考数据结构而考数据结构。看那Author一栏清一色的某老师,就知道教数据结构的老师的思路就是和别人不一样。

题号 标题 分数 大意 Author
1051 Pop Sequence 25 判断一个序列是否是pop序列 CHEN, Yue
1052 Linked List Sorting 25 链表排序 CHEN, Yue
1057 Stack 30 一个有中位数功能的stack CHEN, Yue
1074 Reversing Linked List 25 分段逆转链表 CHEN, Yue
1089 Insert or Merge 25 判断插入排序或归并排序 CHEN, Yue
1097 Deduplication on a Linked List 25 链表去重 CHEN, Yue
1098 Insertion or Heap Sort 25 判断插入排序或堆排序 CHEN, Yue

好几道题在上MOOC的时候就在数据结构题集里面做过,有1051、1074和1089。还有1098,是之前做merge那道的时候想一并做的,但后来因为merge花了太多时间就跳过了。

这次讲1074、1098、1051和1057 4道题。没错,不按题号顺序。

1074:

这可能是我做的第一道PAT题,也可能是第一道卡住的题。其实,所有用5位整数来模拟链表的题,好像都必须创建100000大小的数组,直接寻址才能跑进时间限制, std::map 是不行的。不过呢,如果我一开始是用C做的,肯定就直接建数组了,也就没有后面的问题了。

这道题的讲解在数据结构课程里有。

因为是早期作品,所以代码长得比较尴尬,懒得改了,我想睡觉。

 #include <iostream>
#include <vector>
#include <utility>
#include <algorithm>
#include <type_traits>
#include <stack>
#include <iomanip> struct Node
{
int address;
int data;
int next;
}; int main(int argc, char const *argv[])
{
int first, n, k;
std::cin >> first >> n >> k;
std::vector<Node> data(n);
for (int i = ; i != n; ++i)
std::cin >> data[i].address >> data[i].data >> data[i].next; decltype(data) list;
int address = first;
for (int i = ; i != n; ++i)
{
auto pos = std::find_if(std::begin(data), std::end(data), [address](const Node& node) {
return node.address == address;
});
list.emplace_back(*pos);
address = pos->next;
if (address == -)
break;
}
n = list.size(); auto begin = list.begin();
auto end = begin + (list.end() - begin) / k * k;
for (; begin != end; begin += k)
{
std::stack<Node> stack;
for (auto iter = begin; iter != begin + k; ++iter)
stack.push(*iter);
for (auto iter = begin; iter != begin + k; ++iter)
{
*iter = stack.top();
stack.pop();
}
}
for (int i = ; i != n - ; ++i)
list[i].next = list[i + ].address;
list[n - ].next = -; std::cout << std::setfill('');
for (auto& node : list)
{
std::cout << std::setw() << node.address << ' ';
std::cout << std::setw() << node.data << ' ';
if (node.next == -)
{
std::cout << "-1";
break;
}
std::cout << std::setw() << node.next << std::endl;
} return ;
}

1098:

它的兄弟题目1089在数据结构课程中讲过,包括这道题中也用到的判断插入排序的方法。

 #include <iostream>
#include <vector>
#include <algorithm> int main()
{
int size;
std::cin >> size;
std::vector<int> original(size);
std::vector<int> partial(size);
for (auto& i : original)
std::cin >> i;
for (auto& i : partial)
std::cin >> i; int cur = ;
for (; cur != size && partial[cur] >= partial[cur - ]; ++cur)
;
bool insertion = true;
for (auto i = cur; i != size; ++i)
if (partial[i] != original[i])
{
insertion = false;
break;
} if (insertion)
{
std::cout << "Insertion Sort" << std::endl;
int insert = partial[cur];
for (--cur; cur >= ; --cur)
if (partial[cur] > insert)
partial[cur + ] = partial[cur];
else
break;
partial[cur + ] = insert;
}
else
{
std::cout << "Heap Sort" << std::endl;
int cur = size - ;
auto top = partial[];
for (; cur >= ; --cur)
if (partial[cur] <= top)
break;
if (cur >= )
{
std::pop_heap(partial.begin(), partial.begin() + cur + );
partial[cur] = top;
}
}
auto end = partial.end() - ;
for (auto iter = partial.begin(); iter != end; ++iter)
std::cout << *iter << ' ';
std::cout << *end;
}

其实这道还要稍微简单一点,因为那道题要自己写merge,这道题的heap可以用标准库。<algorithm> 提供了 std::pop_heap 等函数用于堆操作。至于查看堆顶元素,把起始迭代器解引用就好,标准库没有给。

BTW,建堆的操作是O(N)的,证明起来挺组合数学的。

还有呢,我感觉这两道题贼尴尬。

1051:

判断一个序列是不是一个stack中pop出来的序列。

 #include <iostream>
#include <vector>
#include <stack> class Stack : public std::stack<int>
{
using super = std::stack<int>;
public:
explicit Stack(int _cap)
: capacity_(_cap)
{
;
}
void push(const int& _data)
{
if (size() == capacity_)
throw ;
super::push(_data);
}
private:
int capacity_;
}; void check(int _cap, const std::vector<int>& _data)
{
Stack stack(_cap);
int pushed = ;
for (auto i : _data)
{
if (stack.empty())
{
stack.push(++pushed);
}
if (stack.top() < i)
{
for (int j = pushed + ; j <= i; ++j)
stack.push(j);
pushed = i;
}
if (stack.top() == i)
{
stack.pop();
continue;
}
if (stack.top() > i)
throw ;
}
} int main()
{
int m, n, k;
std::cin >> m >> n >> k;
std::vector<int> data(n);
for (int i = ; i != k; ++i)
try
{
for (int j = ; j != n; ++j)
std::cin >> data[j];
check(m, data);
std::cout << "YES" << std::endl;
}
catch(...)
{
std::cout << "NO" << std::endl;
} return ;
}

题目不难,这里把代码贴出来,是想作个错误示范。算法本身是正确的,能AC,但是设计是不好的:标准库中的容器类不是用来作基类的,因为其析构函数非虚,我写的 Stack 类直接继承 std::stack<int> 是不好的,尽管在这个类设计中,子类的subclass不需要析构,而且整个程序没有作什么派生类到基类的指针转换。我的本意是想实现adapter,为了偷懒就直接公有继承,理应在 Stack 类中包装一个 std::stack<int> 实例。在工程中,公有继承标准库容器就是错误的。

1057:

这次不按题号讲,就是因为这道题要压轴。

初看,不就一个stack和中位数吗,两个我都会写,放到一起我也会写。然后我就写了第一个版本,做了一个 std::stack<int> 的adapter,提供了push、pop、median操作,其中median是把stack拷贝、排序、寻址中位数。感觉很好,样例也过了,想着一遍AC。

 #include <iostream>
#include <string>
#include <vector>
#include <algorithm> class Stack
{
public:
Stack() = default;
void push(int i)
{
stack.push_back(i);
}
void pop()
{
if (stack.empty())
std::cout << "Invalid";
else
{
std::cout << stack.back();
stack.pop_back();
}
std::cout << std::endl;
}
void median()
{
if (stack.empty())
std::cout << "Invalid";
else
{
auto temp = stack;
std::sort(temp.begin(), temp.end());
int m = ;
if (temp.size() % )
m = temp[(temp.size() - ) / ];
else
m = temp[temp.size() / - ];
std::cout << m;
}
std::cout << std::endl;
}
private:
std::vector<int> stack;
}; int main()
{
int count;
std::cin >> count;
Stack stack;
for (int i = ; i != count; ++i)
{
std::string instr;
std::cin >> instr;
if (instr == "Push")
{
int i;
std::cin >> i;
stack.push(i);
}
else if (instr == "Pop")
{
stack.pop();
}
else
{
stack.median();
}
}
}

然而并没有。5个case,3个超时。

是 std::cin 读取字符串太慢了吗?换成C的输入输出,还有2个case超时。

然后我想到输入数据有个比较小的范围是有用的,就建了个数组,写了个乱七八糟的访问控制来防止每次遍历都把每个元素访问一遍。没用,超时。

我意识到线性结构不能解决这个问题,不然也对不起这30分了。于是我就想到用 std::map 来存储。先放代码:

 #include <iostream>
#include <string>
#include <stack>
#include <map> class Median
{
public:
Median() = default;
int get()
{
return median;
}
void insert(int i)
{
if (median_count == )
{
median = i;
median_count = ;
}
else if (i < median)
++small[i], ++small_count;
else if (i > median)
++large[i], ++large_count;
else
++median_count;
adjust();
}
void erase(int i)
{
if (i < median)
{
--small[i], --small_count;
if (small[i] == )
small.erase(i);
}
else if (i > median)
{
--large[i], --large_count;
if (large[i] == )
large.erase(i);
}
else
--median_count;
adjust();
}
private:
std::map<int, int, std::greater<int>> small;
int small_count = ;
std::map<int, int> large;
int large_count = ;
int median;
int median_count = ;
void small_to_median()
{
median = small.begin()->first;
median_count = small.begin()->second;
small.erase(small.begin());
small_count -= median_count;
}
void large_to_median()
{
median = large.begin()->first;
median_count = large.begin()->second;
large.erase(large.begin());
large_count -= median_count;
}
void median_to_small()
{
small[median] = median_count;
small_count += median_count;
}
void median_to_large()
{
large[median] = median_count;
large_count += median_count;
}
void adjust()
{
if (median_count == )
{
if (small_count < large_count)
large_to_median();
else if (small_count)
small_to_median();
}
else if (small_count + median_count < large_count)
{
median_to_small();
large_to_median();
}
else if (small_count >= median_count + large_count)
{
median_to_large();
small_to_median();
}
}
}; class Stack
{
public:
Stack() = default;
void push(int i)
{
stack_.push(i);
median_.insert(i);
}
void pop()
{
if (stack_.empty())
std::cout << "Invalid";
else
{
int i = stack_.top();
stack_.pop();
std::cout << i;
median_.erase(i);
}
std::cout << std::endl;
}
void median()
{
if (stack_.empty())
std::cout << "Invalid";
else
{
std::cout << median_.get();
}
std::cout << std::endl;
}
private:
std::stack<int> stack_;
Median median_;
}; int main()
{
int count;
std::cin >> count;
Stack stack;
for (int i = ; i != count; ++i)
{
std::string instr;
std::cin >> instr;
if (instr == "Push")
{
int i;
std::cin >> i;
stack.push(i);
}
else if (instr == "Pop")
stack.pop();
else
stack.median();
}
}

这个算法挺复杂的。客户维护一个 Stack 实例,它维护一个 Median 实例,两个都是我自己写的类。Median 中包含两个 std::map<int, int> 实例,分别储存比中位数小的和比中位数大的数。核心算法是 Median::adjust() ,它通过调整两边内容,维护中位数的正确性。不想细说了,看代码吧,只涉及到 std::map 的一些基本操作。

因为两次调整之间只有一个操作,所以可以保证调整一次就好了。

值得吐槽的一点是,C++的输入输出在这道题中比C慢了100ms。然而,算法不对,输入输出再快,也要超时。PAT好像没什么卡输入输出时间的题,毕竟世上除了C和C++还有好多编程语言,要考虑它们的感受。

写完了发现其他博客里没有用这么烦的方法,好像用了什么树状数组,我不会。

 try
{
learn_queue.push("树状数组");
}
catch(...)
{
std::cout << "快去睡觉" << std::endl;
} --------------------------------------------------------------------------------
快去睡觉
Program exited with code ...

正好提到树了,下一篇就写树吧。

PAT甲级题分类汇编——理论的更多相关文章

  1. PAT甲级题分类汇编——杂项

    本文为PAT甲级分类汇编系列文章. 集合.散列.数学.算法,这几类的题目都比较少,放到一起讲. 题号 标题 分数 大意 类型 1063 Set Similarity 25 集合相似度 集合 1067 ...

  2. PAT甲级题分类汇编——图

    本文为PAT甲级分类汇编系列文章. 图,就是层序遍历和Dijkstra这一套,#include<queue> 是必须的. 题号 标题 分数 大意 时间 1072 Gas Station 3 ...

  3. PAT甲级题分类汇编——树

    本文为PAT甲级分类汇编系列文章. AVL树好难!(其实还好啦~) 我本来想着今天应该做不完树了,没想到电脑里有一份讲义,PPT和源代码都有,就一遍复习一遍抄码了一遍,更没想到的是编译一遍通过,再没想 ...

  4. PAT甲级题分类汇编——排序

    本文为PAT甲级分类汇编系列文章. 排序题,就是以排序算法为主的题.纯排序,用 std::sort 就能解决的那种,20分都算不上,只能放在乙级,甲级的排序题要么是排序的规则复杂,要么是排完序还要做点 ...

  5. PAT甲级题分类汇编——计算

    本文为PAT甲级分类汇编系列文章. 计算类,指以数学运算为主或为背景的题. 题号 标题 分数 大意 1058 A+B in Hogwarts 20 特殊进制加法 1059 Prime Factors ...

  6. PAT甲级题分类汇编——线性

    本文为PAT甲级分类汇编系列文章. 线性类,指线性时间复杂度可以完成的题.在1051到1100中,有7道: 题号 标题 分数 大意 时间 1054 The Dominant Color 20 寻找出现 ...

  7. PAT甲级题分类汇编——序言

    今天开个坑,分类整理PAT甲级题目(https://pintia.cn/problem-sets/994805342720868352/problems/type/7)中1051~1100部分.语言是 ...

  8. 【转载】【PAT】PAT甲级题型分类整理

    最短路径 Emergency (25)-PAT甲级真题(Dijkstra算法) Public Bike Management (30)-PAT甲级真题(Dijkstra + DFS) Travel P ...

  9. PAT甲级题解分类byZlc

    专题一  字符串处理 A1001 Format(20) #include<cstdio> int main () { ]; int a,b,sum; scanf ("%d %d& ...

随机推荐

  1. service mesh,linkerd,sidecar,apigateway

    对于大规模部署微服务(微服务数>1000).内部服务异构程度高(交互协议/开发语言类型>5)的场景,使用service mesh是合适的.但是,可能大部分开发者面临的微服务和内部架构异构复 ...

  2. 范仁义web前端介绍课程---1、课程意义

    范仁义web前端介绍课程---1.课程意义 一.总结 一句话总结: 提供的这一整套学习方法和资源,配合艾宾浩斯遗忘曲线等各种复习.学习算法和后续会有的娱乐化学习方式,能否真正做到让学过的东西不再忘记. ...

  3. JVM synchronized关键字所生成的字节码

    一.创建测试类 package com.example.jvm.bytecode; public class MyTest2 { String str = "Welcome"; p ...

  4. C++ atof 函数

    腾讯云:https://cloud.tencent.com/developer/article/1391966 atof函数: 功能:将字串转换成浮点型数 相关函数 atoi,atol,strtod, ...

  5. dubbo线程模型配置

    首先了解一下dubbo线程模型 如果事件处理的逻辑能迅速完成,并且不会发起新的IO请求,比如只是在内存中记个标识.则直接在IO线程上处理更快,因为减少了线程池调度. 但如果事件处理逻辑较慢,或者需要发 ...

  6. CentOS 安装抓包工具wireshark-tshark抓包工具

    原文出处:razeen -> https://razeen.me/post/how-to-install-tshark-on-centos.html 准备在服务器上用tshark抓包,分析一下数 ...

  7. win10: windows+E 改回打开我的电脑

    之前习惯使用windows+E来打开我的电脑,用了win10之后按windows+E打开的却是“快速访问”文件夹,很不习惯,可用下列办法改回: 1.打开“查看”选项卡,选择”选项“按钮. 2.在“常规 ...

  8. Flutter -------- 解析JSON数据

    SON序列化方法: 手动序列化和反序列化通过代码生成自动序列化和反序列化 手动JSON序列化是指使使用dart:convert中内置的JSON解码器.它将原始JSON字符串传递给JSON.decode ...

  9. flutter 权限申请

    添加依赖 permission_handler: ^3.2.2 androidmanifest.xml添加对应的用户权限 在flutter 中app入口申请权限: import 'package:fl ...

  10. flutter中使用shared_preferences的存储

    添加依赖 shared_preferences: ^+ 工具类 import 'dart:async'; import 'package:shared_preferences/shared_prefe ...