Moscow Pre-Finals Workshop 2016. Japanese School OI Team Selection. 套题详细解题报告
写在前面
谨以此篇题解致敬出题人!
真的期盼国内也能多出现一些这样质量的比赛啊。9道题中,没有一道凑数的题目,更没有码农题,任何一题拿出来都是为数不多的好题。可以说是这一年打过的题目质量最棒的五场比赛之一了!(其中G、I和D题简直是好题中的好题!)
由于网上没有任何这套题的题解,我每道题都绞尽脑汁想了好久(尤其是D题、I题和H题证明),在此认认真真的写一篇博客,也真心希望好题能被更多的人发现和赞美。
题目概述
题目 | 思维难度 | 推荐指数 | 考点 |
A | 3 | ☆☆☆☆ | 最长上升子序列 |
B | 暂留坑,>7 | ||
C | 3 | ☆☆☆☆ | 树状数组 |
D | 6 | ☆☆☆☆☆ | 图论,拓扑排序 |
E | 未做 | ☆☆ | |
F | 暂留坑,>7 | ||
G | 4 | ☆☆☆☆☆ | 图论,连通分量 |
H | 4 | ☆☆☆ | 图论,最短路径 |
I | 5 | ☆☆☆☆☆ | 贪心,栈,线段树 |
Problem A: Matryoshka
题目大意
有$n(n\le 200000)$个圆柱,每个有一个半径$r_i$和高$h_i$。一个圆柱能$i$放下另一个$j$当且仅当$r_i>r_j$且$h_i>h_j$。圆柱可以嵌套,但一个圆柱内最多直接放一个圆柱。现在给定$q(q\le 200000)$个询问,每次问$r_i\ge r,h_i\le h$的所有圆柱采用最优的嵌套方法,最少几个圆柱放在最外层?
Sample Input:
7 3 9 5 3 7 10 6 5 10 2 6 10 10 4 1 10 5 3 5 3 9
Sample Output:
0 1 2
详细题解
首先可以将二维平面上的点转为一维平面序列。方法是$r$轴作为下标,$h$作为值。特殊的,若$r$相同,$h$越大的应放在越靠前。这样问题转化为最少能用多少个最长上升子序列覆盖,它等于最长不上升子序列个数。由最小链覆盖等于最长反链可证明(详见附录)。
考虑如何处理询问。事实上我们可以将询问也看成一个点,求出从该点开始的最长不上升子序列即可;只不过这个点不会更新dp数组,只是求出答案来回答询问。
最后要注意一个细节:若询问和输入共点,要把询问排在输入后面,因为是最长不上升子序列(可以相等)。
时间复杂度$O((n+q)\log (n+q))$
AC代码
1 #include<cstdio> 2 #include<algorithm> 3 using namespace std; 4 struct Node{ 5 int x, y; 6 int id; 7 bool operator < (const Node& t)const{ 8 return x > t.x || (x == t.x && (y < t.y || (y == t.y && id < t.id))); 9 } 10 }a[400001]; 11 int ans[200001]; 12 int dp[400001]; 13 int main() 14 { 15 int n, q; 16 scanf("%d%d", &n, &q); 17 for (int i = 1; i <= n; i++){ 18 scanf("%d%d", &a[i].x, &a[i].y); 19 a[i].id = 0; 20 } 21 for (int i = 1; i <= q; i++){ 22 scanf("%d%d", &a[n + i].x, &a[n + i].y); 23 a[n + i].id = i; 24 } 25 sort(a + 1, a + n + q + 1); 26 int cnt = 0; 27 for (int i = 1; i <= n + q; i++){ 28 int pos = upper_bound(dp, dp + cnt, a[i].y) - dp; 29 if (a[i].id)ans[a[i].id] = pos; 30 else{ 31 dp[pos] = a[i].y; 32 if (pos == cnt)cnt++; 33 } 34 } 35 for (int i = 1; i <= q; i++) 36 printf("%d\n", ans[i]); 37 }
Problem C: Employment
题目大意(由个人改编)
有$n(n\le 200000)$个萝卜,每个初始长度已知。将它们左端对齐,从右端$L$处切下,那么长度大于等于$L$的萝卜会被切到。定义切痕段数为萝卜中连续被切到的段数。现在给定$q(q\le 200000)$次操作:
(1)1 $L$ 询问$L$时的切痕段数;
(2)2 $i$ $x$ 表示将第$i$个萝卜长度改为$x$。
Sample Input:
5 4 8 6 3 5 4 1 5 2 4 1 1 5 1 3
Sample Output:
2 1 2
详细题解
考虑维护每个询问的答案。我们让初始时萝卜长度全为0,那么所有询问答案均为0。现在考虑一次修改:
若当前萝卜$i$左右两个萝卜长度为$y$和$z(y\le z)$,且$i$长度从$x$增加到$x+1$。
情况1:若$x<y$,那么$x+1$对应的询问两段切痕将会合并;
情况2:若$y\ge x1<z$,那么任意位置询问不变;
情况3:若$y\ge z$,那么$x+1$对应询问多了一段切痕。
同样地,若$x$减少1,也对应相同的3种情况。
因此对于$x$的任意变化,对应于最多两段区间修改。因此将询问的$L$离散化后用树状数组进行区间加单点求值即可。
时间复杂度$O(n\log n+m\log m)$
AC代码
1 //使用树状数组模板 2 #include<algorithm> 3 using namespace std; 4 int a[200001], b[200002]; 5 int order[200001]; 6 struct Query{ 7 int op, x, y; 8 }q[200001]; 9 int getPos(int x){ 10 return lower_bound(order + 1, order + treeLen + 1, x) - order; 11 } 12 void add(int x, int y, int d){//[x,y) 13 int p1 = getPos(x), p2 = getPos(y); 14 add(p1, d); add(p2, -d); 15 } 16 void solve(int i, int x) 17 { 18 int y = b[i - 1], z = b[i + 1]; 19 if (y > z)swap(y, z); 20 if (b[i] < y)add(b[i] + 1, y + 1, -1); 21 else if (b[i] > z)add(z + 1, b[i] + 1, -1); 22 b[i] = x; 23 if (b[i] < y)add(b[i] + 1, y + 1, 1); 24 else if (b[i] > z)add(z + 1, b[i] + 1, 1); 25 } 26 int main() 27 { 28 int n, m; 29 scanf("%d%d", &n, &m); 30 for (int i = 1; i <= n; i++) 31 scanf("%d", &a[i]); 32 for (int i = 0; i < m; i++){ 33 scanf("%d%d", &q[i].op, &q[i].x); 34 if (q[i].op == 2)scanf("%d", &q[i].y); 35 else order[++treeLen] = q[i].x; 36 } 37 sort(order + 1, order + treeLen + 1); 38 for (int i = 1; i <= n; i++) 39 solve(i, a[i]); 40 for (int i = 0; i < m; i++){ 41 if (q[i].op == 1)printf("%d\n", sum(getPos(q[i].x))); 42 else solve(q[i].x, q[i].y); 43 } 44 }
Problem D: Sandwich
题目大意
有$2nm$个三明治摆成一个$n\times m(n,m\le 400)$的矩形,每个三明治是一个等腰直角三角形。每次可以拿走一个三明治,要求拿走的三明治满足以下条件之一:
(1) 这个三明治公共斜边的三明治已经拿走;
(2) 这个三明治公共直角边的2个三明治已经拿走。
问对于每个格子,拿走这个格子的三明治至少要拿走几个三明治。不能拿走输出-1。
Sample Input:
2 3 NZN ZZN
Sample Output:
10 8 2 8 6 4
详细题解
本题具有以下性质:三明治是成对拿走的。当拿走一个三明治后,其斜边另一侧三明治必然马上拿走。因为若不拿走,仅拿走单个三明治毫无意义,不能对拿走之后的三明治起到任何帮助。因此我们是一格一格的拿走三明治。
现在我们不用再考虑公共斜边,只需考虑公共直角边。我们建立一张有向图,三明治$(i,j)$建边:如果拿走$i$以及和$i$公共斜边的三明治后对拿走$j$有帮助(暴露了$j$的一条直角边)。
注意到如果该图是一般DAG,该问题最优解法也只能拓扑排序后用bitset优化达到平方除以32复杂度,这意味着必须使用本题中图的特殊性。注意到这是一个网格图,且边一定是相邻格子之间的,而且对于同一行的$2m$个三明治,其中$n$个具有依次向右连接的横向边,而另$n$个具有依次向左连接的横向边。因此,对每个格子$t$,我们可以整个求出一行中$m$个三明治哪些是到达$t$的前驱,而只用遍历一遍有向图而花费$O(nm)$的复杂度。方法如下:
不妨设一行中某$n$个三明治的指向关系为$1→2→…→m$,那么我们先从$m$出发遍历所有能到的三明治,将其答案加$m$;再从$m-1$出发遍历能到达且之前没到达的三明治,将其答案加$m-1$;……;直到最后从1出发遍历之前均没到达的三明治,将其答案加1。不难发现这样该行三明治对每个三明治的依赖数量就统计出来了,且每个结点最多遍历一遍。
最后还要注意,该图不一定是拓扑图。因此需要拓扑排序,找到所有能拓扑排序出的结点,其它结点必然直接在环上,或前驱在环上。
总时间复杂度$O(nm \min(n,m))$。
AC代码
1 #include<cstdio> 2 #include<vector> 3 #include<cstring> 4 #include<algorithm> 5 #define INF 0x3fffffff 6 using namespace std; 7 vector<int> v[1000001]; 8 int degree[1000001]; 9 int ans[1000001], layer[1000001]; 10 int toposort(int n) 11 { 12 int k = 0; 13 memset(degree, 0, sizeof(int)*n); 14 memset(layer, 0, sizeof(int)*n); 15 for (int i = 0; i < n; i++){ 16 for (unsigned int t = 0; t < v[i].size(); t++) 17 degree[v[i][t]]++; 18 } 19 for (int i = 0; i < n; i++){ 20 if (!degree[i])ans[k++] = i; 21 } 22 for (int j = 0; j < k; j++){ 23 for (unsigned int t = 0; t < v[ans[j]].size(); t++){ 24 int i = v[ans[j]][t]; 25 if (!--degree[i]){ 26 ans[k++] = i; 27 layer[i] = layer[ans[j]] + 1; 28 } 29 } 30 } 31 return k; 32 } 33 int n, m; 34 char s[401][401]; 35 bool used[320001]; 36 int num[320001]; 37 inline int mp(int i, int j, bool k){ return (i * m + j) * 2 + k; } 38 void dfs(int id, int x) 39 { 40 used[id] = true; num[id] += x; 41 for (int j : v[id]){ 42 if (!used[j])dfs(j, x); 43 } 44 } 45 int main() 46 { 47 scanf("%d%d", &n, &m); 48 for (int i = 0; i < n; i++) 49 scanf("%s", s[i]); 50 for (int i = 0, id = 0; i < n; i++){ 51 for (int j = 0; j < m; j++){ 52 for (int k = 0; k < 2; k++, id++){ 53 bool up = (s[i][j] == 'Z') ^ k; 54 if (up && i < n - 1)v[id].push_back(mp(i + 1, j, s[i + 1][j] == 'N')); 55 if (!up && i > 0)v[id].push_back(mp(i - 1, j, s[i - 1][j] == 'Z')); 56 if (j && k)v[id].push_back(mp(i, j - 1, k)); 57 if (j < m - 1 && !k)v[id].push_back(mp(i, j + 1, k)); 58 } 59 } 60 } 61 for (int i = 0; i < n; i++){ 62 memset(used, 0, sizeof(bool) * n * m * 2); 63 for (int j = 0; j < m; j++){ 64 int id = mp(i, j, 1); 65 if (!used[id])dfs(id, m - j); 66 } 67 memset(used, 0, sizeof(bool) * n * m * 2); 68 for (int j = m - 1; j >= 0; j--){ 69 int id = mp(i, j, 0); 70 if (!used[id])dfs(id, j + 1); 71 } 72 } 73 int cnt = toposort(m * n * 2); 74 memset(used, 0, sizeof(bool) * n * m * 2); 75 for (int i = 0; i < cnt; i++) 76 used[ans[i]] = true; 77 for (int i = 0, id = 0; i < n; i++){ 78 for (int j = 0; j < m; j++, id += 2){ 79 int res = INF; 80 if (used[id])res = min(res, num[id]); 81 if (used[id + 1])res = min(res, num[id + 1]); 82 printf("%d ", res == INF ? -1 : res * 2); 83 } 84 printf("\n"); 85 } 86 }
Problem G: Telegraph
题目大意
有$n(n\le 200000)$个电话,第$i$个电话有一条线连向第$a_i$个电话,只可以向该电话直接传递信息。对于第$i$条线,我们可以花$w_i$的代价改变它连向的电话。问最少花多少代价,可以使任意两个电话$(i,j)$,$i$都能直接或间接向$j$传递信息。
Sample Input:
4 2 2 1 4 1 3 3 1
Sample Output:
4
详细题解
问题等价于把线接成一个有向环。那么对于一个点,若它的入度小于1,必然要先断开到度等于1为止。
由于此题的特殊性,每一个弱连通块一定是一个环加外向树,其中树上边均指向这个环。
我们先将非环的点断开到入度为1,这可以贪心的断开边权小的边。对于环上的,还要确保最终环也被断开。因此贪心后若环没断开,还要枚举断开的环边来更改原来的选择。总之,这个贪心使用了最少代价使得所有弱连通块均无环且每个点入度小于等于1。那么整个图必由若干条链组成,因此将断开的边重新指定指向的点,必能形成一个大环。因此这个代价最小的方案是可行的。这就证明了算法的最优性和正确性。
实现时,考虑强连通分量求出所有环,先贪心选择,然后对每个环检查环上是否有边被断开,若没有再枚举环边即可。
时间复杂度$O(n+m)$。
AC代码
1 //引用强连通分量模板 2 int w[MAXN], best[MAXN], best2[MAXN]; 3 vector<int> s[MAXN]; 4 int main() 5 { 6 int n, x; 7 scanf("%d", &n); 8 long long ans = 0; 9 for (int i = 1; i <= n; i++){ 10 scanf("%d%d", &x, &w[i]); 11 e[i].push_back(x); 12 if (w[i] >= w[best[x]]){ best2[x] = best[x]; best[x] = i; } 13 else if (w[i] > w[best2[x]])best2[x] = i; 14 ans += w[i]; 15 } 16 for (int i = 1; i <= n; i++){ 17 if (!dfn[i])tarjan(i); 18 } 19 if (cnt == 1)printf("0"); 20 else{ 21 for (int i = 1; i <= n; i++) 22 ans -= w[best[i]]; 23 for (int i = 1; i <= n; i++) 24 s[res[i]].push_back(i); 25 for (int i = 1; i <= cnt; i++){ 26 bool flag = false; 27 int t = 0x3fffffff; 28 for (int j : s[i]){ 29 if (res[best[j]] != i){ flag = true; break; } 30 t = min(t, w[best[j]] - w[best2[j]]); 31 } 32 if (!flag)ans += t; 33 } 34 printf("%lld", ans); 35 } 36 }
Problem H: Dangerous Skating
题目大意
一个$n\times m$的网格$(n,m \le 2000)$,所有边界和一些内部有障碍物。要求从$(s_x,s_y)$出发到达$(t_x,t_y)$,按如下规则滑动,使得滑动次数最小:
设当前位置$(i,j)$。每次滑动选择一个方向,从$(i,j)$一直划到该方向下一格是障碍物为止,停住;同时先前格子$(i,j)$变为障碍物。
Sample Input:
5 5 ##### #...# #...# #...# ##### 2 2 3 3
Sample Output:
4
详细题解
考虑用最短路模型。我们对于每个格子,向其四个方向划到的位置建边,边权1;同时对于每个格子,还向相邻四格中非障碍格子建边,边权2。那么最短路即为答案。证明如下:
显然最短路一定是一种可行方案,因为边权为2的表示先滑到头在划回来。注意到最短路每个点不会经过多次,因此每次到的点不可能已经是障碍物了;同时其路径上都不可能有障碍物(不然不会是最短路)。这证明了最短路一定是可行方案。
其次证明存在一种最优方案是最短路方案。若不然,必然有若干次到达$(i,j)$后,之后利用了$(i,j)$是障碍的特性到达$(i,j)$旁边,且不是最短路中那种往返的情况。考虑最后一个这样的$(i,j)$,那么由于最短路仅需2步就能到达$(i,j)$旁边,因此将最优方案改成直接往返不会变差。这样不断的修改最优方案,最终一定能改成不存在刚才那样的$(i,j)$。
因此该算法是正确的。
时间复杂度$O(nm)$。
Problem I: Telegraph
题目大意
有$n(n\le 200000)$个人参加程序设计竞赛,比赛3小时和5小时分别记录了每个排名选手的国家以及得分。但是记录者可能会把国家记错(不会把得分记错)。现在问记录者最少记录错多少个国家。
具体来说,给定$(A_i,B_i)$和$(C_i,D_i)$表示3小时和5小时的每一名次国家和得分,满足$B_i,D_i$严格单调递减,问最少改变多少个$A_i$和$C_i$能使得存在3小时二元组到5小时二元组之间的一一映射,使得每对映射的$(i,j)$满足$B_i\le D_j$且$A_i=C_j$。$(1\le A_i,C_i\le n,0\le B_i,D_i\le 10^9)$
详细题解
我们将3小时和5小时的所有分数放到数轴上排序,并让3小时的点对应一个值1,5小时的点对应一个值-1。那么一个方案合法当且仅当数轴上任意位置点值前缀和大于等于0。我们需要尽可能配对最多的相同国家的点,使得将这些点删除后数轴上任意位置点值前缀和仍大于等于0。
于是我们预处理出数轴上每一段当前前缀和,那么删除2个点[x,y]就对应数轴[x,y)区间的前缀和减1。于是问题转化为一些可行的点对[xi,yi],取最多个数。我们对每种国家的点分别考虑,哪些点对一定不会选。
首先考虑一个性质,若$a\le b\le c\le d$满足$a,b$值为1,$c,d$值为-1,那么$[a,d][b,c]$配对任何前缀和情况都比$[a,c][b,d]$配对优。这意味着对于同一个国家的点,选择的配对区间不会相交。于是我们可贪心的处理出可能的候选区间:从左到右,对每个值为-1的点找到左边离它最近的值为1的点,并将这两个点删除。这显然可用栈维护。
最后考虑如何贪心的从候选区间中选区间。考虑对区间按右端点升序排序(右端点相同时左端点降序)。然后对每个区间,如果选择它,前缀和仍处处非负,那么就贪心的选,否则必然不选。基于贪心经典的区间覆盖问题同样的证明思路,不难证明本题这样的贪心是正确的。
最后,实现时要将一段区间减1,并判断一段区间每个元素是否均大于0。这需要用区间减1,区间查询最小值的线段树。
时间复杂度$O(n\log n)$。
AC代码
1 //使用区间增加区间最小值线段树模板 2 #include<vector> 3 #define MAXN 200001 4 struct Node{ 5 int c, s, f; 6 bool operator < (const Node& t)const{ 7 return s < t.s || (s == t.s && f > t.f); 8 } 9 }a[MAXN * 2]; 10 int sum[MAXN * 2]; 11 vector<int> c[MAXN]; 12 pair<int, int> seg[MAXN]; 13 int main() 14 { 15 int n; 16 scanf("%d", &n); 17 for (int i = 1; i <= n; i++){ 18 scanf("%d%d", &a[i].c, &a[i].s); 19 a[i].f = 1; 20 } 21 for (int i = n + 1; i <= 2 * n; i++){ 22 scanf("%d%d", &a[i].c, &a[i].s); 23 a[i].f = -1; 24 } 25 sort(a + 1, a + 2 * n + 1); 26 int cnt = 0, ans = 0; 27 for (int i = 1; i <= 2 * n; i++){ 28 sum[i] = sum[i - 1] + a[i].f; 29 if (a[i].f == 1)c[a[i].c].push_back(i); 30 else if (!c[a[i].c].empty()){ 31 int t = c[a[i].c].back(); 32 c[a[i].c].pop_back(); 33 seg[cnt++] = { t, i }; 34 } 35 } 36 sort(seg, seg + cnt); 37 init(1, 1, 2 * n, sum); 38 for (int i = cnt - 1; i >= 0; i--){ 39 int x = seg[i].first, y = seg[i].second; 40 if (queryMin(1, 1, 2 * n, x, y - 1) > 0){ 41 ans++; 42 addValue(1, 1, 2 * n, x, y - 1, -1); 43 } 44 } 45 printf("%d", n - ans); 46 }
附录:最小链覆盖等于最长反链的证明
定义一个链覆盖是DAG上的若干条链(可以相交),使得覆盖了所有点。我们证明,最长反链等于最小链覆盖。
显然最长反链小于等于最小链覆盖。
下证用归纳法证明最小链覆盖小于等于最长反链。
设点数小于$n$的图成立,考虑点数等于$n$的图。设图的点集$V$,最长反链集合$A$。所有能到达$A$中某个点的点集即为$S$,$A$中某个点能到达的点集记为$T$。
(1)若$A$中既存在有入度的点,又存在有出度的点,则易证:$B\neq V,C\neq V,B \cup C=V,B \cap C=A$。
设$B$和$C$对应子图的最小链覆盖分别为$Chain(B)$和$Chain(C)$。由于$|B|,|C|<|V|$,归纳假设,$Chain(B)$和$Chain(C)$链数分别等于子图$B$和$C$的最长反链大小。
考虑一条链$c \in Chain(B)$,$c$上必存在$A$中的一点$v$,若不然$B$的最小链覆盖会大于最长反链。同时$v$不能到$C$中除$v$的任意一点,所以$A$中的点都是$Chain(B)$中某条链的终点。
同理,$A$中的点都是$Chain(C)$中某条链的起点。
因此将$Chain(B)$和$Chain(C)$拼起来,得到的链覆盖数就等于最长反链长度了,因此最小链覆盖数小于等于最长反链长度。
(2)否则,不妨设$A$包含所有入度为0的点。任取$v \in A$,向下走直到出度为0为止(该点设为$u$),将$u$和$v$去掉,则最长反链必然至少少1。若不然,可以找到不选$u,v$的最长反链,于是利用(1)证明即可。
当最长反链必然至少少1后,利用归纳可以找到一条链覆盖。在加上$v$到$u$的链,便覆盖了整个图。于是最小链覆盖数小于等于最长反链长度。
证毕。