本文为PAT甲级分类汇编系列文章。
线性类,指线性时间复杂度可以完成的题。在1051到1100中,有7道:
题号 | 标题 | 分数 | 大意 | 时间 |
1054 | The Dominant Color | 20 | 寻找出现最多的数 | 200ms |
1061 | Dating | 20 | 寻找字符串中相同字符 | 200ms |
1071 | Speech Patterns | 25 | 寻找出现最多的单词 | 300ms |
1077 | Kuchiguse | 20 | 字符串共同后缀 | 150ms |
1082 | Read Number in Chinese | 25 | 中文读数 | 400ms |
1084 | Broken Keyboard | 20 | 比较两序列的差异 | 200ms |
1095 | Cars on Campus | 30 | 模拟车辆进出 | 300ms |
可以看到线性题一般分数不高,一般只有模拟事件的题会出30分,但也不难。
这种题一般一看就会做(最大子列和除外),难度一般在细节处理(所有PAT题都是)和时间常数上。
关于细节处理,分数低的题,本来就简单,做的时候容易想起一些细节问题,在第一次提交之前就处理好了。
关于时间限制,其他题时间限制一般都是400ms,但线性类时间更严格的比较多,这时候就要小心常数了。
仔细阅读了一下7道题,决定做1054、1071、1082和1095一共4道。
放在线性分类下,就要用线性方法做。用 std::map 做起来一看就很简单,为了挑战自己,换一种方法。
大致思路是每个维度(颜色)建立一个散列表,表中存放指向下一个维度的散列表的指针,最后一个维度存放数据。
代码如下:
1 #include <iostream> 2 #include <vector> 3 #include <memory> 4 5 template <typename T> 6 using Pointer_vector = std::shared_ptr<std::vector<T>>; 7 8 int main() 9 { 10 int m, n, total; 11 std::cin >> m >> n; 12 total = m * n; 13 using Blue = int; 14 using Green = Pointer_vector<Blue>; 15 using Red = Pointer_vector<Green>; 16 std::vector<Red> data(256); 17 for (int cnt = 0; cnt != total; ++cnt) 18 { 19 int color; 20 std::cin >> color; 21 int red = color >> 16; 22 int green = (color >> 8) % 256; 23 int blue = color % 256; 24 auto& red_data = data[red]; 25 if (!red_data) 26 red_data = Red(new std::vector<Green>(256)); 27 auto& green_data = (*red_data)[green]; 28 if (!green_data) 29 green_data = Green(new std::vector<Blue>(256)); 30 auto& blue_data = (*green_data)[blue]; 31 ++blue_data; 32 } 33 try 34 { 35 for (int r = 0; r != 256; ++r) 36 if (data[r]) 37 for (int g = 0; g != 256; ++g) 38 if ((*data[r])[g]) 39 for (int b = 0; b != 256; ++b) 40 if ((*(*data[r])[g])[b] > total / 2) 41 throw (r << 16) + (g << 8) + b; 42 } 43 catch (int res) 44 { 45 std::cout << res; 46 } 47 }
对于多重循环要 break 的算法我一般用异常来做流控。这是一种备受争议的做法,因此我又想着用输出和 return 替换掉原来的 throw 语句,没想到竟然超时了!加了优化也没用。
不是说 try block会导致性能下降的吗?为什么加了异常处理以后性能反而提高?
想不通。我又用 std::map 实现了一个算法:
1 #include <iostream> 2 #include <map> 3 4 #pragma GCC optimize(O3) 5 6 int main() 7 { 8 int m, n, total; 9 std::cin >> m >> n; 10 total = m * n; 11 std::map<int, int> map; 12 for (int cnt = 0; cnt != total; ++cnt) 13 { 14 int color; 15 std::cin >> color; 16 ++map[color]; 17 } 18 for (const auto& pair : map) 19 if (pair.second > total / 2) 20 { 21 std::cout << pair.first; 22 return 0; 23 } 24 return 0; 25 }
一开始当然是没有 #pragma 那一行的,但是超时了,后来加上了才AC。
这道题告诉我们,线性题对时间要求真的很高。或许用 scanf 代替 std::cin 能让原本超时的AC吧,我没试过。
字符串的线性处理、std::map 的使用,这道题难度就这么多了。代码如下:
1 #include <iostream> 2 #include <string> 3 #include <map> 4 #include <cctype> 5 6 int main() 7 { 8 std::map<std::string, int> words; 9 std::string string; 10 while (1) 11 { 12 char c = std::cin.get(); 13 if (std::isalnum(c)) 14 string.push_back(std::tolower(c)); 15 else 16 if (!string.empty()) 17 { 18 ++words[string]; 19 string.clear(); 20 } 21 if (c == ' ') 22 break; 23 } 24 auto max = words.cbegin(); 25 for (auto iter = words.cbegin(); iter != words.cend(); ++iter) 26 if (iter->second > max->second) 27 max = iter; 28 std::cout << max->first << ' ' << max->second << std::endl; 29 }
值得杠一杠的是这道题是不是线性。
首先,输入是O(n),输出是O(1),都在线性范围内。
假设一共有a种单词,对 std::map 的操作是O(a·loga);a种单词连起来的最小长度是O(a·loga),n一定大于这个长度,因此这道题是线性的。
呵,这道题就是在考数学。但我没有把它放到数学那一类中,因为这是小学数学。
一开始觉得很难,但只要把10000以内的数字怎么读搞清楚了,这道题就差不多了。代码如下:
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 5 std::vector<std::string> data; 6 7 const char pinyin[10][5] = 8 { 9 "ling", 10 "yi", 11 "er", 12 "san", 13 "si", 14 "wu", 15 "liu", 16 "qi", 17 "ba", 18 "jiu" 19 }; 20 21 void chinese(int num) 22 { 23 int qian = num / 1000; 24 num %= 1000; 25 int bai = num / 100; 26 num %= 100; 27 int shi = num / 10; 28 int ge = num % 10; 29 int digit = 0; 30 if (qian) 31 { 32 digit = 3; 33 data.push_back(pinyin[qian]); 34 data.push_back("Qian"); 35 } 36 if (bai) 37 { 38 digit = 2; 39 data.push_back(pinyin[bai]); 40 data.push_back("Bai"); 41 } 42 if (shi) 43 { 44 if (digit > 2) 45 data.push_back(pinyin[0]); 46 digit = 1; 47 data.push_back(pinyin[shi]); 48 data.push_back("Shi"); 49 } 50 if (ge) 51 { 52 if (digit > 1) 53 data.push_back(pinyin[0]); 54 data.push_back(pinyin[ge]); 55 } 56 } 57 58 int main() 59 { 60 int num; 61 std::cin >> num; 62 if (num == 0) 63 { 64 std::cout << pinyin[0]; 65 return 0; 66 } 67 if (num < 0) 68 { 69 num = -num; 70 data.push_back("Fu"); 71 } 72 int yi = num / 100000000; 73 int wan = num % 100000000 / 10000; 74 int ge = num % 10000; 75 int seg = 0; 76 if (yi) 77 { 78 seg = 2; 79 data.push_back(pinyin[yi]); 80 data.push_back("Yi"); 81 } 82 if (wan) 83 { 84 if (wan < 1000 && seg > 1) 85 data.push_back(pinyin[0]); 86 seg = 1; 87 chinese(wan); 88 data.push_back("Wan"); 89 } 90 if (ge) 91 { 92 if (ge < 1000 && seg > 0) 93 data.push_back(pinyin[0]); 94 chinese(ge); 95 } 96 int end = data.size() - 1; 97 for (int i = 0; i != end; ++i) 98 std::cout << data[i] << ' '; 99 std::cout << data[end]; 100 }
这是我第一次用拼音来命名变量。我也不想这样,但谁让这道题中文背景这么明显呢?
这道题要求模拟车辆进出校园的过程,这类模拟一个过程的题目,我称之为模拟类题。
模拟类题的一个通用方法就是以时间为变量循环,但是这道题写着写着就用上另一种方法了,就是以对象为变量循环,这里的对象就是车辆进出记录。
题目逻辑比较复杂,大致可以分为3个步骤:
第一步,读取所有记录,并按时间顺序排序,然后按记录类型配对;
第二步,读取查询的时间,并模拟车辆进出的过程,这是模拟类题的核心(但在本题中占的比例不大);
第三步,找出最长的停车时间,并输出对应的车牌号(老司机开车!)。
梳理完这堆逻辑以后就直接上代码吧:
1 #include <iostream> 2 #include <iomanip> 3 #include <string> 4 #include <vector> 5 #include <map> 6 #include <algorithm> 7 8 int read_time() 9 { 10 int res, temp; 11 std::cin >> temp; 12 res = temp * 3600; 13 std::cin.get(); 14 std::cin >> temp; 15 res += temp * 60; 16 std::cin.get(); 17 std::cin >> temp; 18 res += temp; 19 return res; 20 } 21 22 void print_time(int time) 23 { 24 std::cout << std::setfill('0'); 25 std::cout << std::setw(2) << time / 3600 << ':'; 26 time %= 3600; 27 std::cout << std::setw(2) << time / 60 << ':'; 28 std::cout << std::setw(2) << time % 60; 29 } 30 31 enum class Status 32 { 33 in, out 34 }; 35 36 struct Record 37 { 38 std::string plate; 39 int time; 40 Status status; 41 bool paired = false; 42 int parking; 43 }; 44 45 int main() 46 { 47 int n, k; 48 std::cin >> n >> k; 49 std::vector<Record> records(n); 50 for (auto& r : records) 51 { 52 std::cin >> r.plate; 53 r.time = read_time(); 54 std::string str; 55 std::cin >> str; 56 r.status = str == "in" ? Status::in : Status::out; 57 } 58 std::sort(records.begin(), records.end(), [](const Record& _lhs, const Record& _rhs) { 59 return _lhs.time < _rhs.time; 60 }); 61 for (auto rec = records.begin(); rec != records.end(); ++rec) 62 if (rec->status == Status::in) 63 for (auto iter = rec + 1; iter != records.end(); ++iter) 64 if (iter->plate == rec->plate && iter->status == Status::in) 65 break; 66 else if (iter->plate == rec->plate && iter->status == Status::out) 67 { 68 rec->paired = iter->paired = true; 69 rec->parking = iter->time - rec->time; 70 break; 71 } 72 73 auto iter = records.begin(); 74 int count = 0; 75 for (int cnt = 0; cnt != k; ++cnt) 76 { 77 int time = read_time(); 78 for (; iter != records.end() && iter->time <= time; ++iter) 79 if (iter->paired && iter->status == Status::in) 80 ++count; 81 else if (iter->paired && iter->status == Status::out) 82 --count; 83 std::cout << count << std::endl; 84 } 85 86 std::map<std::string, int> parking; 87 for (const auto& rec : records) 88 if (rec.paired && rec.status == Status::in) 89 parking[rec.plate] += rec.parking; 90 int longest = 0; 91 for (const auto& car : parking) 92 if (car.second > longest) 93 longest = car.second; 94 for (const auto& car : parking) 95 if (car.second == longest) 96 std::cout << car.first << ' '; 97 print_time(longest); 98 }
这道题放在线性类,是因为配对和模拟的算法都是线性的(其实是因为我看到它是模拟类就把它分给线性类了)。
总之,线性类题目难度不高,但坑不少,不仅有各种边界数据,还有卡时间常数的。