题目来自 2018-2019 ACM-ICPC, NEERC, Southern Subregional Contest,
Qualification Stage。link
完成率
9/12。怎么说呢,我觉得能够做得更好。还是要吸取一个教训:不要被题目的通过人数所吓到;有时候通过数较少并不一定意味着自己想不出来。另外,当卡在一个题目过久时,要及时止损开另一道题。
这次训练我们队内的交流很少,基本上是在自己打自己的。的确,用英文交流对我来说是一个挑战,有时候真的想不起来某个术语对应的英文说法。但交流起到的作用究竟大不大呢?还是接下来慢慢探索吧。
This article provides a feasible solution to a
certain competitive programming problem. If you have better solutions,
feel free to share with me!
Coffee Break
简单贪心。很自然的想到用 set 实现。
Gilder
实际上上升气流之间的间隔就是选择经过某个上升气流的代价。飞行轨迹一定是一段连续的区间,可以观察到这就是一个换皮滑动窗口问题。双指针轻松解决。
这个问题有个坑:当高度为 0
时,即使有上升气流也不能继续向前飞行。队友在这里卡了很久;我后来补题的时候也踩了这个坑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <bits/stdc++.h> using namespace std;const int MAX_N = 2e5 + 10 ;struct seg { int x, y; } a[MAX_N]; int main () { int n, h; cin >> n >> h; for (int i = 1 ; i <= n; ++i) { cin >> a[i].x >> a[i].y; } int l = 1 , r = 1 ; int ans = 0 , cur = a[1 ].y - a[1 ].x, cost = h; while (l <= n) { while (r < n && cost > a[r + 1 ].x - a[r].y) { cost -= a[r + 1 ].x - a[r].y; ++r; cur += a[r].y - a[r - 1 ].y; } ans = max (ans, cur + cost); if (l < n) { cur -= a[l + 1 ].x - a[l].x; cost += a[l + 1 ].x - a[l].y; } ++l; } cout << ans << endl; return 0 ; }
Masquerade strikes back
思路其实比较好想,但我硬是没找到一种优雅的实现方法。最后看了标程发现它的实现居然比较暴力,理论上的时间复杂度竟然是
\(O(n\sqrt{c})\)
的。但其实这个时间复杂度在 2s 内是可以接受的。
具体来说,使用 \(O(\sqrt{c})\)
的算法暴力枚举某数的因数,并用一个数组进行记忆。当再次遇到该数时从记忆数组中储存的位置开始枚举因数。此外还有一个小小的优化:在输出
\(\{a,b\}\) 后下次一定输出 \(\{b,a\}\) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <iostream> #include <algorithm> #include <cmath> using namespace std;const int MAX_C = 1e7 + 10 ;const int MAX_N = 2e5 + 10 ;int d[MAX_C], p[MAX_N];int a[MAX_N];int ans1[MAX_N], ans2[MAX_N];bool vis[MAX_C];int main () { int n; cin >> n; for (int i = 1 ; i <= n; ++i) cin >> a[i]; for (int i = 1 ; i <= n; ++i) { if (vis[a[i]]) { vis[a[i]] = false ; ans1[i] = a[i] / p[a[i]], ans2[i] = p[a[i]]; continue ; } bool flag = false ; for (int j = p[a[i]] + 1 ; j <= sqrt (a[i]); ++j) { if (a[i] % j == 0 ) { flag = true ; p[a[i]] = j; if (j * j != a[i]) vis[a[i]] = true ; break ; } } if (!flag) { cout << "NO" << endl; return 0 ; } ans1[i] = p[a[i]], ans2[i] = a[i] / p[a[i]]; } cout << "YES" << endl; for (int i = 1 ; i <= n; ++i) cout << ans1[i] << " " << ans2[i] << endl; return 0 ; }
Painting the fence
有点难。这个真的没想出来怎么做。
首先发现一个性质:每个颜色最多只能被操作一次。在首次操作过后,之后对该颜色的操作都是无意义的。
可以把操作想象成一个缩点的过程,即首次操作某颜色 \(i\) 后,我们将 \(i\) 所对应的区间 \([l,r]\) 视作一个颜色为 \(i\) 的单独点
(具体实现中我们留下两个点,标记缩点对应区间的范围)。一次有效的缩点会令点的个数减少
\(\geq1\) ,既然如此,有效操作所需的总时间是不会超过
\(O(n)\) 的。
那么问题转化为,如何快速的找到被操作的区间的位置:即,高效地找到颜色
\(i\) 所对应的 \(l,r\) 。观察到颜色不超过 \(3\cdot 10^5\) 种,我们不妨开 \(3\cdot10^5\) 个 set
储存每种颜色对应栅栏的位置。
当对颜色 \(i\)
执行操作时,我们:
若 \(i\) 已经被操作过
(vis[i]=true
) 或该颜色的栅栏不足两个
(set[i].size()<2
),本次操作无效。
访问 set[i]
,获得颜色 \(i\) 对应的左边界 \(l\) 与右边界 \(r\) 。
删除所有 \([l+1,r-1]\)
中的元素;注意处理区间内含有缩点的情况。
将 vis[i]
设为 true
,标记颜色 \(i\) 所对应的区间已经成为了缩点。
最后根据 set 中存储的信息对所有栅栏进行最终染色。时间复杂度 \(O(m\log n+n)\) ,确实很巧妙。
看到有别的队用 splay 过的,orz...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <iostream> #include <set> using namespace std;const int MAX_N = 3e5 + 10 ;int n, m;int a[MAX_N];bool vis[MAX_N];set<int > pos[MAX_N]; int main () { cin >> n; int maxi = 0 ; for (int i = 1 ; i <= n; ++i) { cin >> a[i]; maxi = max (maxi, a[i]); pos[a[i]].insert (i); } cin >> m; for (int i = 1 ; i <= m; ++i) { int col; cin >> col; if (vis[col] || pos[col].size () < 2 ) continue ; int p = *(pos[col].begin ()) + 1 , r = *(pos[col].rbegin ()) - 1 ; while (p <= r) { pos[a[p]].erase (p); if (vis[a[p]] && !pos[a[p]].empty ()) { p = *(pos[a[p]].begin ()); pos[a[p]].erase (p); } p++; } vis[col] = true ; } for (int i = 1 ; i <= maxi; ++i) { if (pos[i].size () == 2 && vis[i]) { int l = *pos[i].begin (), r = *pos[i].rbegin (); for (int p = l; p <= r; ++p) { a[p] = i; } } } for (int i = 1 ; i <= n; ++i) cout << a[i] << " " ; return 0 ; }
Tickets
将询问离线下来排序处理即可。比较简单。
Tree Reconstruction
训练时看到过的人不多于是没有开题;实际上之后自己稍微想了下就想出来了。
首先观察到节点 \(n\)
一定会出现在所有数对中,不妨将节点 \(n\) 作为根进行构建。\(\{n,a\}\) 出现 \(x\) 次代表节点 \(a\) 在以 \(n\) 为根的树中深度为 \(x\) ;贪心的将未被约束的点挪过来凑深度即可。
如果未被约束的点中有大于 \(a\)
的,那么可以断定该树不存在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <bits/stdc++.h> using namespace std;const int MAX_N = 1010 ;int n;int dep[MAX_N];pair<int , int > ans[MAX_N]; struct dt { int x, y; bool operator < (const dt& a) { return y > a.y; } } e[MAX_N]; int main () { cin >> n; bool flag = false ; for (int i = 1 ; i < n; ++i) { cin >> e[i].x >> e[i].y; if (e[i].x < e[i].y) swap (e[i].x, e[i].y); if (e[i].x != n) flag = true ; } if (flag) { cout << "NO" << endl; return 0 ; } sort (e + 1 , e + n); int l = 1 , r = 2 , sum = 0 ; while (l < n) { while (r < n && e[r].y == e[l].y) ++r; int depth = r - l; dep[e[l].y] = depth; sum += depth - 1 ; l = r; } stack<int > internal; for (int i = 1 ; i < n; ++i) { if (!dep[i]) internal.push (i); } if (internal.size () < sum) { cout << "NO" << endl; return 0 ; } int ansCnt = 0 ; for (int i = n - 1 ; i >= 1 ; --i) { int lst = n; while (dep[i] >= 2 ) { if (internal.top () > i) { cout << "NO" << endl; return 0 ; } ans[++ansCnt] = make_pair (lst, internal.top ()); lst = internal.top (); internal.pop (); dep[i]--; } if (dep[i]) ans[++ansCnt] = make_pair (lst, i); } cout << "YES" << endl; for (int i = 1 ; i <= ansCnt; ++i) cout << ans[i].first << " " << ans[i].second << endl; return 0 ; }
Theater Square
普及组第一题水平。冇野讲。
简单规律。用 \(\geq m\)
的连续区间去补 \(<m\)
连续区间,余下的 \(\geq m\)
区间全部切成长度为 1 的小块使得答案最大化。按这个算法得到的答案一定是
\(\geq m\) 数的个数减去 \(< m\) 数的个数。
Ray in the tube
整场中通过率最低的题,但实际上我觉得比 E
要简单一点。但也的确是一个非常有趣的题!
首先根据 common sense 来讲,步长越小,反射越密集,可能覆盖到的
sensors 就越多,很自然的考虑步长为 \(1\) 的情况。
很显然,步长为 \(1\)
的轨迹一定不劣于其他步长为奇数的轨迹。但它却有可能覆盖不到步长为偶数的轨迹。于是我们试图寻找步长为偶数的轨迹中是否存在某个最优轨迹。
可以发现,任何步长为 \(2^n\times a\)
(\(a\) 为奇数) 的轨迹都能被步长为 \(2^n\)
的轨迹覆盖。(画个图就能明白,从下边界到上边界一定要经过奇数个步长)
因此,偶数步长是由 \(\{2,
2^2,...,2^n\}\) 这一步长集合 capture 的。
综上所述,我们只需要关注 \(\{2^0,2^1,...,2^n\}\)
这一系列步长,暴力求出它们对应的答案后取最大值即可。时间复杂度 \(O(n\log{n})\) 。将答案初始化为 \(2\) 防止两个点垂直排列的情况 (即步长为
\(0\) ) 产生影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <bits/stdc++.h> using namespace std; const int MAX_N = 1e5 + 10 ;const int MAX_M = 1e9 ; int a[MAX_N], b[MAX_N]; map<int , int > cnt; int main () { int n, y1, m, y2; cin >> n >> y1; for (int i = 1 ; i <= n; ++i) cin >> a[i]; cin >> m >> y2; for (int i = 1 ; i <= m; ++i) cin >> b[i]; int finalAns = 2 ; for (int step = 2 ; step <= MAX_M; step <<= 1 ) { cnt.clear (); int ans = 0 ; for (int i = 1 ; i <= m; ++i) cnt[(b[i] + step / 2 ) % step]++, ans = max (ans, cnt[(b[i] + step / 2 ) % step]); for (int i = 1 ; i <= n; ++i) cnt[a[i] % step]++, ans = max (ans, cnt[a[i] % step]); finalAns = max (finalAns, ans); } cout << finalAns << endl; return 0 ; }