本文为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做的,肯定就直接建数组了,也就没有后面的问题了。
这道题的讲解在数据结构课程里有。
因为是早期作品,所以代码长得比较尴尬,懒得改了,我想睡觉。
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 9 struct Node 10 { 11 int address; 12 int data; 13 int next; 14 }; 15 16 int main(int argc, char const *argv[]) 17 { 18 int first, n, k; 19 std::cin >> first >> n >> k; 20 std::vector data(n); 21 for (int i = 0; i != n; ++i) 22 std::cin >> data[i].address >> data[i].data >> data[i].next; 23 24 decltype(data) list; 25 int address = first; 26 for (int i = 0; i != n; ++i) 27 { 28 auto pos = std::find_if(std::begin(data), std::end(data), [address](const Node& node) { 29 return node.address == address; 30 }); 31 list.emplace_back(*pos); 32 address = pos->next; 33 if (address == -1) 34 break; 35 } 36 n = list.size(); 37 38 auto begin = list.begin(); 39 auto end = begin + (list.end() - begin) / k * k; 40 for (; begin != end; begin += k) 41 { 42 std::stack stack; 43 for (auto iter = begin; iter != begin + k; ++iter) 44 stack.push(*iter); 45 for (auto iter = begin; iter != begin + k; ++iter) 46 { 47 *iter = stack.top(); 48 stack.pop(); 49 } 50 } 51 for (int i = 0; i != n - 1; ++i) 52 list[i].next = list[i + 1].address; 53 list[n - 1].next = -1; 54 55 std::cout << std::setfill('0'); 56 for (auto& node : list) 57 { 58 std::cout << std::setw(5) << node.address << ' '; 59 std::cout << std::setw(0) << node.data << ' '; 60 if (node.next == -1) 61 { 62 std::cout << "-1"; 63 break; 64 } 65 std::cout << std::setw(5) << node.next << std::endl; 66 } 67 68 return 0; 69 }
1098:
它的兄弟题目1089在数据结构课程中讲过,包括这道题中也用到的判断插入排序的方法。
1 #include2 #include 3 #include 4 5 int main() 6 { 7 int size; 8 std::cin >> size; 9 std::vector<int> original(size); 10 std::vector<int> partial(size); 11 for (auto& i : original) 12 std::cin >> i; 13 for (auto& i : partial) 14 std::cin >> i; 15 16 int cur = 1; 17 for (; cur != size && partial[cur] >= partial[cur - 1]; ++cur) 18 ; 19 bool insertion = true; 20 for (auto i = cur; i != size; ++i) 21 if (partial[i] != original[i]) 22 { 23 insertion = false; 24 break; 25 } 26 27 if (insertion) 28 { 29 std::cout << "Insertion Sort" << std::endl; 30 int insert = partial[cur]; 31 for (--cur; cur >= 0; --cur) 32 if (partial[cur] > insert) 33 partial[cur + 1] = partial[cur]; 34 else 35 break; 36 partial[cur + 1] = insert; 37 } 38 else 39 { 40 std::cout << "Heap Sort" << std::endl; 41 int cur = size - 1; 42 auto top = partial[0]; 43 for (; cur >= 0; --cur) 44 if (partial[cur] <= top) 45 break; 46 if (cur >= 0) 47 { 48 std::pop_heap(partial.begin(), partial.begin() + cur + 1); 49 partial[cur] = top; 50 } 51 } 52 auto end = partial.end() - 1; 53 for (auto iter = partial.begin(); iter != end; ++iter) 54 std::cout << *iter << ' '; 55 std::cout << *end; 56 }
其实这道还要稍微简单一点,因为那道题要自己写merge,这道题的heap可以用标准库。
BTW,建堆的操作是O(N)的,证明起来挺组合数学的。
还有呢,我感觉这两道题贼尴尬。
1051:
判断一个序列是不是一个stack中pop出来的序列。
1 #include2 #include 3 #include 4 5 class Stack : public std::stack<int> 6 { 7 using super = std::stack<int>; 8 public: 9 explicit Stack(int _cap) 10 : capacity_(_cap) 11 { 12 ; 13 } 14 void push(const int& _data) 15 { 16 if (size() == capacity_) 17 throw 0; 18 super::push(_data); 19 } 20 private: 21 int capacity_; 22 }; 23 24 void check(int _cap, const std::vector<int>& _data) 25 { 26 Stack stack(_cap); 27 int pushed = 0; 28 for (auto i : _data) 29 { 30 if (stack.empty()) 31 { 32 stack.push(++pushed); 33 } 34 if (stack.top() < i) 35 { 36 for (int j = pushed + 1; j <= i; ++j) 37 stack.push(j); 38 pushed = i; 39 } 40 if (stack.top() == i) 41 { 42 stack.pop(); 43 continue; 44 } 45 if (stack.top() > i) 46 throw 0; 47 } 48 } 49 50 int main() 51 { 52 int m, n, k; 53 std::cin >> m >> n >> k; 54 std::vector<int> data(n); 55 for (int i = 0; i != k; ++i) 56 try 57 { 58 for (int j = 0; j != n; ++j) 59 std::cin >> data[j]; 60 check(m, data); 61 std::cout << "YES" << std::endl; 62 } 63 catch(...) 64 { 65 std::cout << "NO" << std::endl; 66 } 67 68 return 0; 69 }
题目不难,这里把代码贴出来,是想作个错误示范。算法本身是正确的,能AC,但是设计是不好的:标准库中的容器类不是用来作基类的,因为其析构函数非虚,我写的 Stack 类直接继承 std::stack<int> 是不好的,尽管在这个类设计中,子类的subclass不需要析构,而且整个程序没有作什么派生类到基类的指针转换。我的本意是想实现adapter,为了偷懒就直接公有继承,理应在 Stack 类中包装一个 std::stack<int> 实例。在工程中,公有继承标准库容器就是错误的。
1057:
这次不按题号讲,就是因为这道题要压轴。
初看,不就一个stack和中位数吗,两个我都会写,放到一起我也会写。然后我就写了第一个版本,做了一个 std::stack<int> 的adapter,提供了push、pop、median操作,其中median是把stack拷贝、排序、寻址中位数。感觉很好,样例也过了,想着一遍AC。
1 #include2 #include <string> 3 #include 4 #include 5 6 class Stack 7 { 8 public: 9 Stack() = default; 10 void push(int i) 11 { 12 stack.push_back(i); 13 } 14 void pop() 15 { 16 if (stack.empty()) 17 std::cout << "Invalid"; 18 else 19 { 20 std::cout << stack.back(); 21 stack.pop_back(); 22 } 23 std::cout << std::endl; 24 } 25 void median() 26 { 27 if (stack.empty()) 28 std::cout << "Invalid"; 29 else 30 { 31 auto temp = stack; 32 std::sort(temp.begin(), temp.end()); 33 int m = 0; 34 if (temp.size() % 2) 35 m = temp[(temp.size() - 1) / 2]; 36 else 37 m = temp[temp.size() / 2 - 1]; 38 std::cout << m; 39 } 40 std::cout << std::endl; 41 } 42 private: 43 std::vector<int> stack; 44 }; 45 46 int main() 47 { 48 int count; 49 std::cin >> count; 50 Stack stack; 51 for (int i = 0; i != count; ++i) 52 { 53 std::string instr; 54 std::cin >> instr; 55 if (instr == "Push") 56 { 57 int i; 58 std::cin >> i; 59 stack.push(i); 60 } 61 else if (instr == "Pop") 62 { 63 stack.pop(); 64 } 65 else 66 { 67 stack.median(); 68 } 69 } 70 }
然而并没有。5个case,3个超时。
是 std::cin 读取字符串太慢了吗?换成C的输入输出,还有2个case超时。
然后我想到输入数据有个比较小的范围是有用的,就建了个数组,写了个乱七八糟的访问控制来防止每次遍历都把每个元素访问一遍。没用,超时。
我意识到线性结构不能解决这个问题,不然也对不起这30分了。于是我就想到用 std::map 来存储。先放代码:
1 #include2 #include <string> 3 #include 4 #include
这个算法挺复杂的。客户维护一个 Stack 实例,它维护一个 Median 实例,两个都是我自己写的类。Median 中包含两个 std::map<int, int> 实例,分别储存比中位数小的和比中位数大的数。核心算法是 Median::adjust() ,它通过调整两边内容,维护中位数的正确性。不想细说了,看代码吧,只涉及到 std::map 的一些基本操作。
因为两次调整之间只有一个操作,所以可以保证调整一次就好了。
值得吐槽的一点是,C++的输入输出在这道题中比C慢了100ms。然而,算法不对,输入输出再快,也要超时。PAT好像没什么卡输入输出时间的题,毕竟世上除了C和C++还有好多编程语言,要考虑它们的感受。
写完了发现其他博客里没有用这么烦的方法,好像用了什么树状数组,我不会。
1 try 2 { 3 learn_queue.push("树状数组"); 4 } 5 catch(...) 6 { 7 std::cout << "快去睡觉" << std::endl; 8 } 9 10 -------------------------------------------------------------------------------- 11 快去睡觉 12 Program exited with code 0...
正好提到树了,下一篇就写树吧。