本文为PAT甲级分类汇编系列文章。
图,就是层序遍历和Dijkstra这一套,#include
题号 | 标题 | 分数 | 大意 | 时间 |
1072 | Gas Station | 30 | 最短距离最大,距离不超限 | 200ms |
1076 | Forwards on Weibo | 30 | 一定层数的转发计数 | 3000ms |
1079 | Total Sales of Supply Chain | 25 | 计算供应链总销售额 | 250ms |
1087 | All Roads Lead to Rome | 30 | 复杂权重的最短路径问题 | 200ms |
1090 | Highest Price in Supply Chain | 25 | 供应链最高价格 | 200ms |
1091 | Acute Stroke | 30 | 超过阈值的空间体积之和 | 600ms |
图这一类题的分值都比较大,因为代码量大,同时时间要么短要么长,不是标准的400ms。至于3000ms的1076,是一道简单题,应该是出题老师一不小心多打了一个0。
30分题肯定要做,这次就只做4道30分题:1072、1076、1087和1091。
1072:
这道题的输入数据是很典型的,算法以Dijkstra为核心,之后再加上一点最小距离、范围、平均距离等简单的东西,不难。
不知是第几次写Dijkstra了,每次实现都不太一样。之前写过一篇Dijkstra算法与堆,现在看来,算法都不是很好。这一次经过深思熟虑后,我将下面这种写法固定下来:
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 8 struct Road 9 { 10 Road(int dest, int dist) 11 : dest(dest), dist(dist) { } 12 int dest; 13 int dist; 14 }; 15 16 struct Node 17 { 18 int dist = std::numeric_limits<int>::max(); 19 std::vector roads; 20 }; 21 22 struct Coll 23 { 24 Coll(int index, int dist) 25 : index(index), dist(dist) { } 26 int index; 27 int dist; 28 }; 29 30 struct Comp 31 { 32 static void init(std::vector & _nodes) 33 { 34 nodes = &_nodes; 35 } 36 static std::vector * nodes; 37 bool operator()(Coll i, Coll j) 38 { 39 return (*nodes)[i.index].dist > (*nodes)[j.index].dist; 40 } 41 }; 42 43 std::vector * Comp::nodes = nullptr; 44 45 int get_node(int _offset) 46 { 47 std::string s; 48 std::stringstream ss; 49 std::cin >> s; 50 ss << s; 51 if (s[0] == 'G') 52 { 53 ss.get(); 54 int i; 55 ss >> i; 56 --i; 57 i += _offset; 58 return i; 59 } 60 else 61 { 62 int i; 63 ss >> i; 64 --i; 65 return i; 66 } 67 } 68 69 int main() 70 { 71 int num_house, num_station, num_road, range; 72 std::cin >> num_house >> num_station >> num_road >> range; 73 std::vector nodes(num_house + num_station); 74 Comp::init(nodes); 75 for (int i = 0; i != num_road; ++i) 76 { 77 auto n0 = get_node(num_house), n1 = get_node(num_house); 78 int dist; 79 std::cin >> dist; 80 nodes[n0].roads.emplace_back(n1, dist); 81 nodes[n1].roads.emplace_back(n0, dist); 82 } 83 int best = -1; 84 int minimum = 0; 85 int total = std::numeric_limits<int>::max(); 86 auto house_begin = nodes.begin(); 87 auto house_end = nodes.begin() + num_house; 88 auto station_begin = nodes.begin() + num_house; 89 auto station_end = nodes.end(); 90 for (auto station = station_begin; station != station_end; ++station) 91 { 92 std::priority_queue , Comp> queue; 93 for (auto& node : nodes) 94 node.dist = std::numeric_limits<int>::max(); 95 station->dist = 0; 96 queue.emplace(station - nodes.begin(), 0); 97 while (!queue.empty()) 98 { 99 auto coll = queue.top(); 100 queue.pop(); 101 auto& node = nodes[coll.index]; 102 if (node.dist != coll.dist) 103 continue; 104 for (const auto& r : node.roads) 105 { 106 if (node.dist + r.dist < nodes[r.dest].dist) 107 { 108 nodes[r.dest].dist = node.dist + r.dist; 109 queue.emplace(r.dest, nodes[r.dest].dist); 110 } 111 } 112 } 113 int m = std::numeric_limits<int>::max(); 114 int t = 0; 115 bool bad = false; 116 for (auto house = house_begin; house != house_end; ++house) 117 { 118 if (house->dist > range) 119 bad = true; 120 t += house->dist; 121 if (house->dist < m) 122 m = house->dist; 123 } 124 if (bad) 125 continue; 126 if (m > minimum) 127 { 128 minimum = m; 129 best = station - nodes.begin(); 130 total = t; 131 } 132 else if (m == minimum && t < total) 133 { 134 best = station - nodes.begin(); 135 total = t; 136 } 137 } 138 if (best == -1) 139 { 140 std::cout << "No Solution"; 141 return 0; 142 } 143 best = best - num_house + 1; 144 std::cout << 'G' << best << std::endl; 145 std::cout.setf(std::ios::fixed); 146 std::cout << std::setprecision(1); 147 std::cout << (double)minimum << ' ' << (double)total / num_house; 148 }
Dijkstra算法的时间复杂度关键取决于“找到距离已收集顶点最近的顶点”这步操作的复杂度。线性肯定是不好的,用优先队列是更好的方法。然而,当你把一个顶点加入到队列中以后,在它弹出之前,这个顶点的距离可能变得更近,即会改变它在堆中应该放的位置,为此需要重排,这样时间复杂度就不划算了。我的新方法是,在优先队列中存储结构体,包括顶点下标与插入优先队列时它的距离。弹出时,将这个距离与顶点对象中维护的距离相比较,如果不相等,说明这个弹出已经过时了,只是之前受限于结构无法删除,但现在可以忽略。由于优先队列以结构体中的距离为键值,此顶点在之前肯定已经被处理过了,所以可以名正言顺地忽略它。这样就可以不需要排序、查找等乱七八糟操作,同时顶点对象中也不再需要表示是否已收集的变量了。
1087:
在Dijkstra算法的基础上,这道题主要增加了对考虑相等情况的要求。在cost同为最小的情况下,看第二键值happiness,在后者同为最大的情况下,选择更大的平均happiness,也就是看路径长度哪个更短。由于最优的选择条件依赖于整条路径,不能由已收集的部分得出,因此必须维护所有可能的路径,跟之前PBMC的题类似,我记得那道题卡了我好久。
维护路径的过程中,如果出现相等长度的情况(这道题中是cost),路径要合并;如果有更短路径,就不要之前维护的路径,而从当前处理的节点的路径生成。
1 #include2 #include <string> 3 #include 4 #include 5 #include
第一次提交的时候,一个case出现浮点错误。我没有用过浮点,那就是除以0的错误。这个case肯定是一种边界情况,但是我不怎么清楚路径长度是怎么为0的。也许是出发点就是终点吧。
1076:
3000ms真的吓到我了,啥题需要3000ms呢?怀着忐忑的心情我做完了这道题,跑出来140ms,而且我用的是C++的输入输出。因此我有理由相信这道题本应是300ms。
这里的图是没有权重的,因此就是一个简单的层序遍历,只是要维护层数并判断。把层数放入对象,像Dijkstra算法中维护距离一样,在处理一个对象时把后继节点的层数设为此对象的层数加1就可以了。
1 #include2 #include 3 #include 4 5 struct User 6 { 7 std::vector<int> fans; 8 int layer; 9 bool forward; 10 }; 11 12 int main() 13 { 14 int num_user, layer; 15 std::cin >> num_user >> layer; 16 std::vector users(num_user); 17 for (int i = 0; i != num_user; ++i) 18 { 19 auto& u = users[i]; 20 int count; 21 std::cin >> count; 22 for (int j = 0; j != count; ++j) 23 { 24 int t; 25 std::cin >> t; 26 --t; 27 users[t].fans.push_back(i); 28 } 29 } 30 int num_query; 31 std::cin >> num_query; 32 for (int i = 0; i != num_query; ++i) 33 { 34 int t; 35 std::cin >> t; 36 --t; 37 for (auto& u : users) 38 u.layer = 0, u.forward = false; 39 users[t].forward = true; 40 int count = 0; 41 std::queue<int> queue; 42 queue.push(t); 43 while (!queue.empty()) 44 { 45 auto index = queue.front(); 46 auto& user = users[index]; 47 queue.pop(); 48 for (int i : user.fans) 49 { 50 auto& fan = users[i]; 51 if (!fan.forward) 52 { 53 //std::cout << i << ' '; 54 fan.forward = true; 55 ++count; 56 fan.layer = user.layer + 1; 57 if (fan.layer < layer) 58 queue.push(i); 59 } 60 } 61 } 62 std::cout << count << std::endl; 63 } 64 }
算法很正确,但是样例就错了。把转发的用户打印出来看,我发现最初发微博的用户作为自己的粉丝的粉丝……的粉丝,把自己的微博转发了一下。于是我就在初始条件里面把这个用户设置为已转发,但不计数。要是样例输入不挖这个坑,我可能要很久才能发现这个问题。
1091:
给定立体图像,计算每一块的体积并选择达到阈值的进行累加。
对于一个有效单位,可以用像层序遍历一样的算法把与之连通的单位都计算进来,同时将它们都标记为以收集。碰到下一个未收集的有效单位,再用同样的方法计算,直到遍历完。
实现中还用到了昨天提到的内部迭代器,用统一的接口包装输入和计算两个看似毫不相关的操作。
1 #include2 #include 3 #include 4 5 struct Unit 6 { 7 bool bad; 8 bool collected = false; 9 }; 10 11 struct Point 12 { 13 Point() 14 : Point(0, 0, 0) { } 15 Point(int x, int y, int z) 16 : x(x), y(y), z(z) { } 17 int x; 18 int y; 19 int z; 20 }; 21 22 class Brain 23 { 24 public: 25 Brain(Point p) 26 : size(p) 27 { 28 data = new Unit[size.x * size.y * size.z]; 29 } 30 ~Brain() 31 { 32 delete[] data; 33 } 34 Brain(const Brain&) = delete; 35 Brain(Brain&&) = delete; 36 Brain& operator=(const Brain&) = delete; 37 Brain& operator=(Brain&&) = delete; 38 Unit& operator[](const Point& p) 39 { 40 return data[point_to_int(p)]; 41 } 42 template 43 void for_each(F _functor) 44 { 45 Point p; 46 for (p.x = 0; p.x != size.x; ++p.x) 47 for (p.y = 0; p.y != size.y; ++p.y) 48 for (p.z = 0; p.z != size.z; ++p.z) 49 _functor(p, operator[](p)); 50 } 51 Point size; 52 private: 53 Unit* data; 54 int point_to_int(const Point& p) 55 { 56 return (p.x * size.y + p.y) * size.z + p.z; 57 } 58 }; 59 60 void push_if_valid(std::queue & queue, Brain& brain, const Point& point) 61 { 62 if (point.x >= 0 && point.x < brain.size.x && 63 point.y >= 0 && point.y < brain.size.y && 64 point.z >= 0 && point.z < brain.size.z && 65 brain[point].bad && 66 !brain[point].collected) 67 queue.emplace(point); 68 } 69 70 void push_if_valid(std::queue & queue, Brain& brain, int x, int y, int z) 71 { 72 if (x >= 0 && x < brain.size.x && 73 y >= 0 && y < brain.size.y && 74 z >= 0 && z < brain.size.z) 75 { 76 Point p(x, y, z); 77 auto& unit = brain[p]; 78 if (unit.bad && !unit.collected) 79 queue.emplace(std::move(p)); 80 } 81 82 } 83 84 int main() 85 { 86 int size_x, size_y, size_z, threshold; 87 std::cin >> size_y >> size_z >> size_x >> threshold; 88 Point size(size_x, size_y, size_z); 89 Brain brain(size); 90 brain.for_each([](const Point& p, Unit& u) { 91 //std::cin >> u.bad; 92 int t; 93 std::scanf("%d", &t); 94 u.bad = t; 95 }); 96 int total = 0; 97 brain.for_each([=, &brain, &total](const Point& p, Unit& u) { 98 if (!u.bad || u.collected) 99 return; 100 int size = 0; 101 std::queue queue; 102 queue.push(p); 103 while (!queue.empty()) 104 { 105 auto p = queue.front(); 106 queue.pop(); 107 auto& unit = brain[p]; 108 if (!unit.bad || unit.collected) 109 continue; 110 unit.collected = true; 111 ++size; 112 push_if_valid(queue, brain, p.x - 1, p.y, p.z); 113 push_if_valid(queue, brain, p.x + 1, p.y, p.z); 114 push_if_valid(queue, brain, p.x, p.y - 1, p.z); 115 push_if_valid(queue, brain, p.x, p.y + 1, p.z); 116 push_if_valid(queue, brain, p.x, p.y, p.z - 1); 117 push_if_valid(queue, brain, p.x, p.y, p.z + 1); 118 } 119 if (size >= threshold) 120 total += size; 121 }); 122 std::cout << total; 123 }
然而,这道题有个坑,在于输入数据第一行中3个维度的长度不是x、y、z的顺序,而是y、z、x的顺序。如果顺序不对,那么后面判断连通也会错误。如果按x、y、z来处理,样例数据的输出会是28而不是26,除了那大片区域外的4个点连通起来了。