【題解在下面】node
早上5:50,Gekoo同窗來到機房並表態:「打暴力,打暴力就對了,打出來我就贏了。」ios
我:深覺得然。數組
(這是個伏筆)數據結構
聽說hzoi的人還差兩次考試【如今是一次了】就要從新分配機房,不知道咱們幾個的安排是什麼樣的,瑟瑟發抖。各類緣由做用,心情有些微妙地一遍瞎畫一邊等着7:10考試開始。ide
不怎麼適合塗鴉的本,不怎麼適合塗鴉的筆,不怎麼適合塗鴉的心情。考試開始,我摔筆看題。函數
T1上來感受東西不少很麻煩,過了一遍題發現大概要耐下心來去仔細推一推性質,因而沒細想先跳過。而後看T2,受到上一次考試第一題的荼毒,一眼線段樹,仔細想一想感受動態開點線段樹能寫,天然平衡樹也能寫,可是前者我不太熟悉後者容易寫炸。因而再思索幾秒,承接上一次考試第一題被pass掉的分塊思路,毅然決然分塊騙分。想到這裏先去看一眼T3,一眼過去大約是徹底懵逼…感受T3是最難的,也沒細想,回來先搞T2。測試
四十分鐘打完分塊檢查了一遍,而後又五十分鐘搞完T1的暴力,一個半小時過去,剩下的時間開始一邊摸魚一邊推T3……優化
考完發現最後結果竟然還不錯【咦】。此次的題目出得很良心,數據點分得很細。題解更良心,最後一題針對每一個測試點各個分數段都寫了題解,每道題的特殊性質都解釋得很清楚(與某份題解造成鮮明對比)。spa
習慣性上來說,每次考試我都算是盡力,雖然最近這幾天不知道是由於累以及身體緣由仍是其它緣故,狀態並很差。然而如今週日早上的懶覺時間被ban了…能不能好起來仍是會惡化到什麼程度,大約聽天由命吧。code
T1【P3938 斐波那契】:
考試的時候不知道是沒有細想仍是想難了,選擇處理出每隻兔子的父親,暴力跳父親直到LCA。用掃一遍斐波那契數組f的方式預處理出fa數組,最後獲得70分。其實對f數組的利用,以及對f數組前綴和也是斐波那契數的性質的發現,我都已經接近正解了。只差最後臨門一腳…就是這最簡單的一腳,生生釘在了門外。
好,來講一下正解。其實題解已經說得很詳細了。咱們列出每月份新產生的兔子數,1,1,1,2,3,5,8,13...發現是斐波那契數列。而後把他們1,2,3,4,5,6,7按這個數列排序一下,即寫成:
1
2
3
4 5
6 7 8
9 10 11 12 13
14 15 16 17 18 19 20 21
這就是每月新產生的兔子的編號。在旁邊列出它們的父親,發現它在這一行裏排第幾個,它的父親就是幾。例如14在這一行是第一個,那麼它的父親就是1。性質很顯然,由於同一個月出生的兔子的編號是按父親的編號由小到大編好的,並且每月所生的兔子是一段連續的序列。
那麼咱們只要能找出所詢問的兩個兔子在它們所處的那行中分別是第幾個,就能知道對應的父親。同理對於父親也能求出它的父親…就像LCA過程,發現每一次父親的編號都會縮小至少一半,那麼其實跳不了多少次。而且其實斐波那契數沒處理多少層【六十層左右】就超過1e12了。
接下來,怎麼求兔子在某一行中的位置呢?
我企圖計算一個前綴和,而後快速查找位置。而後在計算前綴和的過程當中意外發現…設si爲到上面表中第i層的前綴和,那麼s1=1,s2=2,s3=3,s4=5,s5=8,s6=13……
哦豁。斐波那契。
我發現這一點了,而後我沒再往下想,去寫暴力了…
其實發現這一點之後就很好辦了,既然前綴和也是一個斐波那契,f爲斐波那契數列數組。設所詢問的兔子編號爲x,那麼必定有f[i]<x<=f[i-1]。咱們找出這個f[i],x-f[i]就是這隻兔子在這一行的位置了。
斐波那契數列是單調的,直接二分查找。因而就這麼AC了…
稍微聊一下這個前綴和。咱們發現兔子數列的前三行都是1,湊出一個3,與第四項的2結合正好是f[4]+f[5],得出f[6]。而後這個前綴和f[6]再與f[5]相加就是f[7]。因此當前這一行的前綴和永遠在斐波那契數列上是這一行個數的後兩項。
#include<iostream> #include<cstdio> using namespace std; long long m,a[1500010],num,fa[1500010],cnt; long long x,y; long long get(long long x){ long long l=1,r=num; long long ans=0; while(l<=r){ int mid=(l+r)/2; if(a[mid]<x){ ans=mid; l=mid+1; } else r=mid-1; } return x-a[ans]; } int main() { scanf("%lld",&m); a[1]=1,a[2]=1,a[3]=1,num=3; for(int i=4;;i++){ a[i]=a[i-1]+a[i-2]; num++; if(a[i]>1e12)break; } while(m--){ scanf("%lld%lld",&x,&y); while(x!=y){ if(x>y)x=get(x); else y=get(y); } printf("%lld\n",x); } return 0; }
這道題告訴我一件很重要的事情,永遠拼命地去想,去思考本身發現的東西怎麼用,不要想到某個地步就不自信地悄悄離開了。若是思想懈怠了,差那臨門一腳,就真的給本身一腳。
T2【P3939 數顏色】:
這是我第一道動鍵盤的題。很快肯定了分塊的思路,其實再想一想說不定真的能想到vector裏面存每一個顏色的位置,而後二分查找邊界。
這題的作法真的是八仙過海各顯神通…官方給的題解就出現了多種解法。vector,動態開點線段樹,平衡樹,主席樹,還有不徹底得分的作法帶修莫隊,樹套樹【結果這些數據結構作法都被出題人嘲諷數據結構學傻】。以及玄學分塊…理論上是能過的,可是全機房沒人寫出來。
後來去聽AK神仙TKJ所說這三題在洛谷都有原題,因而興致勃勃去翻了T2的題解區。震驚地發現題解區更可怕,除了上面的作法,還有CDQ分治,淳樸排序枚舉,還有優化把分塊卡過去的代碼…
恕鄙人才疏學淺一個都沒寫出來.jpg
最後仍是寫了好寫的vector作法。開顏色個數個vector【vector是動態的不用擔憂空間】,讀入序列的時候在對應顏色的vector裏存入位置。由於一邊讀入一遍扔進vector,因此vector裏的位置是有序了,知足lower_bound和upper_bound的使用條件。那麼對於詢問,lower_bound【查大於等於詢問元素的第一個】找左邊界,upper_bound【查大於詢問元素的第一個】正好卡在右邊界之外的位置,後者減去前者就是所求的元素數量。
對於修改操做,直接交換x和x+1所在vector裏相應位置的信息,而後交換兩個元素自己的信息。須要注意的是,若是x和x+1的顏色是同樣的,要跳過這個操做。交換不一樣顏色的信息是不會改變某個顏色vector裏位置的相對關係的,而若是交換同種顏色就不知足單調性了。我在這個地方卡75分卡了一小會纔不肯定地加了這個判斷,後來聽人一說才明白是怎麼回事【感謝HEOI-動動】。
#include<iostream> #include<cstdio> #include<vector> #include<cstring> #include<algorithm> using namespace std; int n,m,opt; struct node{ int c,pos; }a[300010]; vector<int>col[300010]; int main() { scanf("%d%d",&n,&m); for(int i=1,x;i<=n;i++){ scanf("%d",&x); col[x].push_back(i); a[i].c=x,a[i].pos=col[x].size()-1; } while(m--){ scanf("%d",&opt); if(opt==1){ int l,r,cc; scanf("%d%d%d",&l,&r,&cc); if(!col[cc].size()){ printf("0\n"); continue; } int x1=lower_bound(col[cc].begin(),col[cc].end(),l)-col[cc].begin(); int x2=upper_bound(col[cc].begin(),col[cc].end(),r)-col[cc].begin(); printf("%d\n",x2-x1); } else{ int x; scanf("%d",&x); if(a[x].c==a[x+1].c)continue; col[a[x].c][a[x].pos]=x+1; col[a[x+1].c][a[x+1].pos]=x; swap(a[x],a[x+1]); } } return 0; }
沒想到作法仍是由於對STL不熟悉…因而我以後去查了一些和vector有關的東西【以後再說其餘STL_(:з」∠)_】,以及lower_bound和upper_bound。
1 c.begin()傳回迭代器中的第一個數據地址。 2 c.clear()移除容器中全部數據。 3 c.empty()判斷容器是否爲空。 4 c.end() 指向迭代器中末端元素的下一個,指向一個不存在元素。 5 c.erase(pos)刪除pos位置的數據,傳回下一個數據的位置。 6 c.erase(beg,end)刪除[beg,end)區間的數據,傳回下一個數據的位置。 7 c.front()傳回第一個數據。 8 c.pop_back()刪除最後一個數據。 9 c.push_back()在尾部加入一個數據。 10 c.size()返回容器中實際數據的個數。
函數lower_bound()在first和last的前閉後開區間進行二分查找,返回大於或等於val的第一個元素位置。若是全部元素都小於val,則返回last的位置. 若是全部元素都小於val,則返回last的位置,可能會數組越界。 函數upper_bound()返回的是在前閉後開區間中查找的關鍵字的上界,返回大於val的第一個元素位置 一樣,若是全部元素都不知足查找條件也返回last的位置,也可能越界。
lower_bound是大於等於,upper_bound是大於,若是查不到值返回的東西均可能越界。
T3【P3940 分組】:
考試的時候勤勤懇懇【一邊摸魚一邊】騙分。首先是對於直接輸出1的那一個點不加贅述,而後考慮k=1而且答案惟一的點好像掃一下就能獲得答案。對於k=1或k=2,而且ai<=2的狀況好像記錄一下2的個數也能騙到分。
因而碼了一下,最後騙到24分。
考完試對於正解仍然是束手無策…去翻了全部能找到的題解和博客,而後去理解了一下同場考試其餘大佬的思路。
正解分紅k=1和k=2來分別考慮。顯然對於這兩種狀況,所用的作法是不同的。
對於k=1,分組所要知足的條件是任意一組中元素之間沒有互相沖突,即相加爲平方數的。那麼每一次分組,咱們能夠考慮當前元素和上一個分組點以後的全部元素是否產生衝突,若是衝突就考慮進行分組。那麼顯然掃一遍以前的元素就能判斷是否衝突。
可是n有1e5+的級別…最壞的可能性這一掃就要爆炸。那麼有什麼快速判斷當前元素是否合法的方式嗎?發現若是咱們只用考慮當前元素是否合法,那麼前面的元素徹底能夠只記錄值而不記錄位置。判斷兩個數相加是否爲一個平方數,能夠循環每一個數,固然也能夠只記錄值而循環平方數。
ai的上限實際上是一個提示。131072*2=262144=512²。咱們只須要枚舉1-512,判斷枚舉到的數的平方減去當前元素,所獲得的值是否出現過,就能夠判斷是否合法。
可是上面這幾句都創建在咱們只用考慮當前元素是否合法這一前提下。實際上,還有一個重要的限制——字典序。若是從前日後枚舉,咱們須要記錄與當前元素衝突的值的位置,由於把這個位置做爲分組點顯然比把當前元素的前一位做爲分組點要優。舉個栗子,對於序列1,2,3,在1和2之間分組顯然比在2和3之間分組要優,可是這兩種分組都合法。
那麼咱們能不能讓判斷產生了衝突的位置成爲答案呢——把序列倒過來枚舉就能夠了。
官方題解也給出了說法,倒過來枚舉的衝突點,也就是正序中可能的最靠前的分界點位置,必定更優。由於分界點若是靠後,不只對於這個分組操做來講不優,而且對於上一個組,它的段長變大,段內產生衝突的可能性變大,分組變多的可能性也隨之上升。
因而最後k=1的作法就是,倒過來枚舉序列並判斷當前元素是否與以前產生衝突,記錄衝突點。注意一個一個清空記錄存在過的值的vis數組,若是直接memset會增長沒必要要的複雜度。
對於k=2的狀況,思路就要更爲複雜一點。首先繼承k=1的思路中整體上的倒敘枚舉以及枚舉平方數的思路。
很顯然的是,對於每一個分組中的元素,若是根據衝突關係把它們黑白染色,那麼只要分組中的元素能組成一張二分圖,這個分組就是合法的。根據這一點,枚舉序列的時候每次暴力判斷二分圖其實就是一種高分作法,而且彷佛是能夠經過所有數據的…
而正常去考慮,每次都跑一次二分圖的複雜度顯然太高。咱們依舊須要能快速判斷當前元素是否合法的方法。發現對於交錯複雜的敵對關係,即衝突,咱們彷佛在哪裏見到過。
P1525關押罪犯。這道題裏咱們用並查集的擴展域或者帶權並查集維護了交織的敵對關係,並最後判斷哪一組不得不產生衝突。這與這道題如今考慮到的這一部分很類似。
其實沒有作過這道題也能考慮到並查集,畢竟咱們要維護一堆衝突關係,而且判斷何時不能再把衝突的兩個數字分別放在兩組中,而是不管怎麼放都會產生衝突。
想到這裏,嘗試用並查集來處理這道題。和關押罪犯同樣開一個擴展域,並查集中1-maxx【最大數值】爲1-maxx自己的集合,而maxx+1-maxx*2是1-maxx的敵人集合。掃到一個元素,並判斷以前有衝突的數值出現的時候,就查看二者並查集維護的信息。若是兩個數值自己不在一個集合裏,那麼它們就能被分別放在兩組中。而後維護敵對關係,讓二者的敵人域分別和對方的本域合併在一塊兒。
可是還有須要特殊考慮的狀況!咱們並查集維護的是每個數值的信息,而序列中若是出現了一樣的數值,不可能再把一個並查集分裂出去也無法維護信息。仔細想一想,兩個相等的數值只要不會相加產生一個平方數,那麼它們就能夠分在同一組中,毫無影響。因而對於有相同元素出現的時候,判斷它的二倍是否是平方數,若是是的話,再考慮能不能分在同一組。兩個這樣的相同元素能分在同一組的條件是,它們出現且僅出現兩次,沒有其它敵對元素。若是有其它敵對元素,那麼這三個元素必定不能安定地分紅兩組。因而對相同元素進行特判處理。
最後清空等操做和k=1時是差很少的。
#include<iostream> #include<cstdio> using namespace std; int n,k,maxx; int a[150010],vis[150010],flag,lst,tag[300010],fa[300010]; int ans[150010],sum; int get(int x){ if(fa[x]==x)return x; else return fa[x]=get(fa[x]); } void solve1(){ for(int i=n;i>=1;i--){ flag=0; for(int j=1;j<=512;j++){ if(j*j<=a[i])continue; if(j*j-a[i]>maxx)break; if(vis[j*j-a[i]]){ flag=1; break; } } if(flag){ ans[++sum]=i; for(int j=i+1;j<=lst;j++){ vis[a[j]]=0; } lst=i; } vis[a[i]]=1; } } void solve2(){ for(int i=1;i<=maxx;i++)fa[i]=i,fa[i+maxx]=i+maxx; for(int i=1;i*i<=maxx*2;i++)tag[i*i]=1; for(int i=n;i>=1;i--){ flag=0; if(vis[a[i]]){ if(tag[a[i]*2]){//一樣的數字出現了兩次,而且它的二倍是平方數 for(int j=1;j<=512;j++){ if(vis[a[i]]==2||fa[a[i]+maxx]!=a[i]+maxx){flag=1;break;} //這裏能夠用判斷敵人域的表明元素是否是它自己的方法來得知有沒有敵對元素,是由於我每次並查集合並都是讓敵人域合向本域 if(j*j<a[i])continue; if(j*j-a[i]>maxx)break; if(vis[j*j-a[i]]&&j*j-a[i]!=a[i]){flag=1;break;} } if(flag){ ans[++sum]=i; for(int j=i;j<=lst;j++){ fa[a[j]]=a[j],fa[a[j]+maxx]=a[j]+maxx; vis[a[j]]=0; } lst=i; } } } else{ for(int j=1;j<=512;j++){ if(j*j<a[i])continue; if(j*j-a[i]>maxx)break; if(vis[j*j-a[i]]){ if(tag[2*(j*j-a[i])]&&vis[j*j-a[i]]==2){ flag=1; break; } int x1=get(a[i]),x2=get(a[i]+maxx),y1=get(j*j-a[i]),y2=get(j*j-a[i]+maxx); if(x1==y1){ flag=1; break; } else{ fa[y2]=x1; fa[x2]=y1; } } } if(flag){ ans[++sum]=i; for(int j=i;j<=lst;j++){ fa[a[j]]=a[j],fa[a[j]+maxx]=a[j]+maxx; vis[a[j]]=0; } lst=i; } } vis[a[i]]++; } } int main() { scanf("%d%d",&n,&k); lst=n; for(int i=1;i<=n;i++)scanf("%d",&a[i]),maxx=max(maxx,a[i]); if(k==1)solve1(); else solve2(); printf("%d\n",sum+1); for(int i=sum;i>=1;i--){ printf("%d ",ans[i]); } return 0; }
要注意的判斷細節有點多,在各類細節上WA了好幾回,包括由於清空不正確,自信地認爲拿到了前四十分,其實只拿到一半…
思路不是很好想,這題一上來給的東西不少,對於我這種思路混亂的容易搞成一團亂麻。應該條理地觀察總結性質,逐一思考獲取最後的思路。
此次考試的難度感受挺接近noip【而後我考炸了】,題目和題解也很良心,整個考試和改題過程都感受挺好。但願能多作這樣的模擬題吧XD
伏筆回收,咱們的Gekoo同窗立下了打暴力的豪言壯志【何】,而後T3敲了五行代碼輸出1果斷跑路。