2016年1月24日 星期日

Codeforces #201 Div.1

比賽連結:http://codeforces.com/contest/346/

Problem A - Alice and Bob (by TreapKing)

看題目名稱就知道是一個Alice先手的遊戲了。題目會先給出一個數字都相異的數列,Alice先手,每一次行動是挑兩個數字x, y,然後把 | x - y | 加入數列,但如果 | x - y | 本來就存在就不能加,沒有行動可以做的人就輸了,問誰會贏。

題解:
先想只有兩個數字a, b的情況,那麼下一個操作就能做出a-b(正負先不理他xD),在下一次會做出b - a-b = 2b-a 或 a-b - a = -b之類的,思考一陣會發現如果是給你a跟b,那麼應該對於所有整數x, y,ax + by應該都做得出來,不過會滿足 | ax + by | <= max(a, b)之類的。然後就會發現所有出現的數字都會是gcd(a, b)的倍數。官方的tuorial是直接提到不管怎麼樣最後數列的長相都一樣,然後就直接令d = gcd{ x[i] },然後說最後的數列就會是{d, 2d, 3d, ... , max{x[i]} }。細節就看code吧。

code:
#include <bits/stdc++.h>
#define PB push_back
#define MP make_pair
#define F first
#define S second
#define SZ(x) ((int)(x).size())
#define ALL(x) (x).begin(),(x).end()
#ifdef _DEBUG_
 #define debug(...) printf(__VA_ARGS__)
#else
 #define debug(...) (void)0
#endif
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
typedef vector<int> VI;

int main() {
 int n;
 scanf("%d", &n);
 int x = 0, mx = 0;
 for(int i=0; i<n; i++) {
  int tmp;
  scanf("%d", &tmp);
  x = __gcd(x, tmp);
  mx = max(mx, tmp);
 }

 puts((mx/x-n)%2 ? "Alice" : "Bob");
    
    return 0;
}



Problem C - Number Transformation II (by Yanger)

給你兩個正整數a, b (a >= b),以及n個正整數Xi,今天每步有以下兩種選擇:
1. 在Xi裡面選一個數,讓a變成a - (a%Xi)
2. 使a--
問你最少要多少步可以讓a變成b。
n<= 10^5,所有數值都在10^9以內,但保證a-b <= 10^6。

題解:
首先有個greedy的想法,就是每一步都讓 a 變得越小越好。稍微想一下(其實想了蠻久)就發現很對,因為假設今天在某一步有兩個選擇a1和a2(a1 < a2),那無論選a2後的下一步有多棒,選a1以後都能用相同的Xi走到那一步。
接下來的問題就變成每次要怎麼找當前最大的步伐了。結論是直接枚舉Xi去試就好。乍看起來很像O(n * (a - b)),但我們可以考慮一種走法:選定一個Xi 之後,就只用它和減一交替來走 (後面的渣渣再用減一處理),然後發現這種走法裡面每兩步a就會減少Xi。所以其實只要維護好當前「還能用」(不會超過b)的Xi,每步步伐的量級就會是Max(Xi),當Xi 全部相異的時候這就相當於n。算一算複雜度就會是好的O(a-b)。
有點幹的是題目沒有說Xi會相異,所以要事先把它unique掉。



Problem D - Robot Control (by Yanger)

給一個點數、邊數最多都到10^6的有向圖,以及一個起點s和一個終點t。今天有個機器人要從起點走到終點。它有以下幾種性質:
1. 一踏到曾經踏過的點就會爆炸。
2. 沒路可走了(出度為0)就會爆炸。
3. 有不只一條路(出度大於1)的時候會隨機選一條走。
看起來很GG,但所幸聰明的你可以在它每走到一個有叉路的點的時候給它下指令,叫它走某個特定的邊,它在那個點就不會隨機了。
問你,最少需要幾個指令,可以保證機器人安全從s走到t?

題解:
從起點往下走前途茫茫,於是就想想從終點往回走。首先,一個指向終點的點,如果出度為一,則可以直接視為另一個安全的「終點」。把這些點都找完以後,如果另外有一個點的所有出邊都指向那些終點,那它也可以視為終點。照這樣遞迴找完所有終點以後,就代表,其他指向它們的點都需要一個指令才能安全到達。然後又可以接著做跟剛剛類似的事......
想想才發現這其實就是在反向圖上面做dijstra。而且因為需要的指令數只會在每個「階段」增加1,所以連priority queue都不需要。這樣複雜度就只是把所有邊遍歷一次而已,O(|E|)。詳細實作看code吧。

code: http://codeforces.com/contest/346/submission/15576591


Problem E - Doodle Jump (by Yanger)

給定整數a, n, p,定義數列Ai = a * i % p (0 <= i <= n)。
問Ai排序過後,相鄰值的差是否皆小於等於h ?
其中
1 <= a, p <= 10^9 且a, p互質
1 <= n < p
0 <= h <= 10^9

題解:
學官方舉個例,若a = 5, p = 23,則Ai會長得像這樣(這邊先不考慮n到多少,把全部都先列出來):
0 5 10 15 20
2 7 12 17 22
4 9 14 19
1 6 11 16 21
3 8 13 18
換行的意思應該很明顯,列出來後會發現我們可以把Ai依照mod 5分成5 群。
有幾個觀察
1. 如果n不足以跑完第一群,最大間隔就直接是5。
2. 否則,第一群相當於把[0, 23) 隔成了幾個長度相等的子區間([0, 5], [5, 10], etc.)和後面的一些渣渣,而後面的數我們可以想像成去「填補」那些子區間。
3. 每一群恰好會填在每個子區間內的同一個相對位置,所以我們只要考慮一個子區間和每群中的一個數就好了。

整體就等同以2的步長在分割 [0, 5],我們發現它其實是規模變小的原題 (a' = a - p % a, p' = a) 。所以我們可以一直遞迴下去,直到出現1.的情況再直接看答案。這邊其實都沒有考慮到2.中所謂的渣渣,不過經過一些證明可以知道它們根本不重要。

最後看複雜度,當a = p - 1的時候很明顯會慘掉,但因為取a = p - a 走出來其實一模一樣,所以我們每次只要取a' = min(a - p % a, p % a),就能確保每遞迴一次a就至少會減半。這樣複雜度就會是好好的log了!!

code: http://codeforces.com/contest/346/submission/15824994

2016年1月19日 星期二

Codeforces #200 Div.1

比賽連結:http://codeforces.com/contest/343

Problem A - Rational Resistance (by TreapKing)

你有很多很多個電阻為\(1\)的電阻器,你可以反覆做這樣的操作:
1. 把一個電阻為\(1\)的電阻器拿在手上。
2. 拿一個電阻為\(1\)的電阻器跟你手上的電阻器串聯。
3. 拿一個電阻為\(1\)的電阻器跟你手上的電阻器並聯。
給你兩整數\(a, b\),問你要湊出一個電阻為\(\frac{a}{b}\)的電阻器最少要用幾個電阻為\(1\)的電阻器?
保證輸入有解。
註:兩個電阻分別為\(x, y\)的電阻,串連後的電阻是\(x+y\),並連後電阻是\(\frac{1}{ \frac{1}{x} + \frac{1}{y}}\)。

題解:
可以發現一個電阻為\(\frac{a}{b}\)的電阻器,加上一個電阻器之後的電阻會變成\(\frac{a+b}{a}\)或\(\frac{a}{a+b}\),手畫了幾步之後感覺每一組\(a, b\)都只會出現一次,也就是每一組\(a, b\)都只存在唯一一個湊法,不用考慮用最少個電阻器的問題。所以就得到了一個做法:一直把小的數字當作\(a\),大的數字當作\(a+b\),然後把\(\frac{a}{a+b}\)變成\(\frac{a}{b}\)之類的,然後會發現這件事情長的跟輾轉相除法很像,於是就產生了下面的code。
這樣做的複雜度就跟輾轉相除法一樣,我只知道至少是\(O(lgN)\)啦。

code:http://codeforces.com/contest/343/submission/15429853

Problem B - Alternating Current (by TreapKing)

給你兩個兩端都黏死的電線的每個交叉點是哪一條在上面,問你可不可以在不拆掉左右黏死的部分,把纏繞的部分拆掉。
像這張圖,輸入就會是-++-,只要把中間的兩個紅色在上的翻好,再把兩個藍色在上的翻好就拆完了(覺得自己翻的很爛....可以考慮自己去讀原題QQ)。

題解:
總覺得這題有強烈的既視感,好像是以前在ioicamp寫過。
思考一陣後會發現,兩個相鄰的++或--就可以把他拆開來,也就是直接消掉一個++或--。如果這樣可以把整個序列消完就是Yes,消不完就是No。
然後會發現這個東西其實跟括弧匹配蠻像的,所以可以用stack做。
這樣做的複雜度是\(O(N)\)。


Problem C - Read Time (by TreapKing)

可以想成有\(N\)個人分別站在\(h_1, h_2, ... , h_N\)的位子上,有\(M\)個地方要探查(探查不用花時間,其實就是被人踩過),分別是\(p_1, p_2, .., p_M\),每個人一單位時間可以走一單位長度而且每個人的移動互相獨立,問你最少要花多少時間才能探查到所有需要探查的點?也就是所有需要被踩到的點都被至少一個人踩到要花多少時間。

題解:
看到Note寫的東西就覺得可以二分搜答案,經過思考後會發現如果a人在左邊b人在右邊,那麼讓a負責的部分完全在b負責的部分的左邊一定不會比較糟。所以二分搜答案後,可以強迫讓最左邊的人負責最左邊的幾個點,並把他負責的部分盡量往右推,下一個人就從還沒被探查過的點開始探查,安排每個人負責的區域的細節就直接看code吧。這樣做的複雜度會是\(O((N+M)lgC)\),\(C\)是數值範圍。

code:http://codeforces.com/contest/343/submission/15437104

Problem D - Water Tree (by TreapKing)

給你一棵樹,一開始每個點的點權都是\(0\),支援以下三種操作:
1. 把一個點\(v\)以及他的子樹的點權改成\(1\)。
2. 把一個點\(v\)以及他的祖先的點權改成\(0\)。
3. 詢問一個點的點權。

題解:
跟子樹操作還有對祖先們的操作就應該想到樹壓平(壓平後,一個子樹會對應到一個區間,對一個點的祖先們的操作可以去問一個點的子樹)。對於每個點,他的點權決定於他最後一次被改到是被誰改的,如果最後一次動到它是操作1那它的點權就會是\(1\),反之就會是\(0\)。
所以我們可以開兩個線段樹分別對應兩種操作,第一顆線段樹支援區間改值跟單點詢問,第二顆線段樹支援單點修改跟區間詢問最大值,如果第\(i\)個操作是1號操作,就把那個子樹所對應的區間的值改成\(i\)(線段樹1),如果第\(i\)個操作是2號操作的話,就把樹壓平後的隨便一個\(v\)出現的位子的值改成\(i\)(線段樹2)。對於第3種操作,就看(\(v\)的子樹在線段樹1的值(最後一次把\(v\)改成\(1\)的操作))跟(\(v\)的子樹的最大值(最後一次把\(v\)改成\(0\)的操作))誰比較大,就知道\(v\)的權重是多少了。
這樣預處理\(O(N)\),每一次操作都是\(O(lgN)\),所以總複雜度是\(O(N+QlgN)\)。

code:http://codeforces.com/contest/343/submission/15441990

另解(By Yanger):
樹壓平之後,每次詢問其實就是看,那個節點所代表的區間內是否全部為1。於是我們可以開一棵線段樹,支援區間修改與查詢,直接維護一個區間AND起來的值。對於題目中的各種操作,
操作1 --> 把該點對應的區間改成1,在此因為
操作2 --> 把那個點在區間上隨便挑一個點改成0
操作3 --> 詢問該點對應的整個區間取 AND

複雜度:
樹壓平&線段樹初始化 O(N)、每次操作都是O(lgN),所以總複雜度為 O(N + QlgN) 。

Problem E - Pumping Stations (by TreapKing)

給你一個\(N\)個點的帶權無向圖,請你找一個\(1\)~\(N\)的排列,讓相鄰兩項的最大流的和盡量大。

題解:
這題想了一陣之後一點頭緒也沒有,就直接看tutorial惹。
這題用到了一個東西叫做Gomory-Hu Tree,這個樹就是對於任意一個帶權無向圖\(G\),都可以找到一個\(G\)的Gomory-Hu Tree \(T\),\(T\)的點集和\(G\)的點集一樣,而對於任意點對\((u, v)\),在\(G\)上\((u, v)\)的最大流等於在\(T\)上\((u, v)\)的最大流(也就是在\(T\)上,\(u\)~\(v\)路徑上的最小值),而且對於任意一個\(T\)上的邊\((u, v)\),\((u, v)\)在\(G\)上的最小割等於\((u, v)\)在\(T\)上的最小割。
先假設我們不知道為什麼變出了一顆對於輸入的Gomory-Hu Tree,那麼這題的答案就會是那顆Gomory-Hu Tree的邊權和,原因是對於\(T\)上邊權最小的邊(如果有多個就隨便選一個),只用這個邊一次一定不會比較糟,那麼我們可以把這個邊兩端的子樹的答案算完之後,再把兩邊所形成的序列直接串起來,這樣就只會用到這條邊一次了。一直遞迴下去,每個邊就會恰被用一次。
現在問題就在於要怎麼找Gomory-Hu Tree,甚至連它存不存在都是個問題,查了一陣資料後發現他一定存在,而且好像是只要在滿足它的性質(相鄰兩點切開來就是原圖最小割)的前提下,胡亂接一接好像都會是對的,但原理我其實不是那麼了解。不過在這個前提下就生出了一個做法:把點照\(0\)~\((n-1)\)的順序一個一個放到樹上,然後想辦法判斷下個點該接在哪個點上&接上去的邊權是多少,一開始先假設通通都接在\(0\)上面。\(0\)當然就直接放上去,\(1\)就接在\(0\)上,然後因為最小割的性質,剩下的點就會基於他在原圖上的\(0-1\)最小割的位子(看他在\(0\)的那一邊還是\(1\)的那一邊)被切成兩邊,所以有些點會不可能接在\(0\)上,他就應該接在\(1\)上,然後一直做下去就會生出一顆Gomory-Hu Tree了,我也不知道為什麼這樣是對的就是了.......,不過看懂做法之後,感受一陣就會覺得對對的xD,例如被\(0, 1\)切來的兩部分,如果\(u, v\)分屬不同邊,他們的最大流絕對不會超過\(0, 1\)的最大流之類的。
參考資料:
我看懂找Gomory-Hu的原理的地方:
我看懂實作的地方,往下拉一點點會有文字描述的實作方法跟pseudo-code:

後來的code很醜而且落落長(200行),順便推個kutengine大大的code,在status隨機戳的時候戳到他的code,找Gomory-Hu Tree的部分很短(雖然他是用Ford-Fulkerson),找答案序列的部分我也是參(ㄓㄠˋ)考(ㄔㄠ)他的code寫的。