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$的鏈,便覆蓋了整個圖。於是最小鏈覆蓋數小於等於最長反鏈長度。
證畢。
Moscow Pre-Finals Workshop 2016. Japanese School OI Team Selection. 套題詳細解題報告