NOIP 模擬8
T1:星際旅行:
顯然根據題目要求,我們能想到該問題可以轉化為存在多少種情況,使得刪去兩條邊後存在一條路徑可以經過每條邊兩次
顯然有等價於存在一條路徑可以經過每條邊一次,那麼問題顯然就是判斷是否存在(無向圖)尤拉路
然而考場上考慮的卻是用Tarjan判斷上述條件,顯然錯誤,原因在於基礎不牢固並且對於演算法本質認識不清
不能在任一情況下應用合適的演算法解決問題。
返回正題:尤拉路判斷方法:
尤拉路徑:
無向圖:連通,點的度數全為偶數(此時所有點均可以為起點,因為相當於歐拉回路),或者只有兩個點度數為奇數(只有度數為奇數的點才可以為起點)
有向圖:出度和入度相等(所有點可以為起點),或者只有兩個點:其中一個入度+1=出度(起點),另一個出度+1=入度(終點)
歐拉回路
無向圖:連通,點的度數全為偶數(所有點可以為起點)。
有向圖:出度和入度相等(所有點可以為起點)。
圖論問題中首先要注意重邊與自環:重邊可利用成對變換判斷解決,而自環根據情況可以在輸入時continue,而本題顯然不能如此
也就是要考慮重邊影響,在明確尤拉路判斷方法後,此問題顯然轉化成了一個計數問題,也就是刪掉兩條邊後使得無向圖存在0個或2個節點度數為奇數
分別考慮刪去兩自環,一自環一邊,和兩邊,判斷存在多少種刪法滿足要求(注意首先要判斷圖的連通性,因為尤拉路的判斷是基於圖的連通性進行的)。
程式碼如下:
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define I long long 4 #define B bool 5 #define C char 6 #define RE register 7 #define V void 8 #define L inline 9 const I MAXN = 1e5 + 10; 10 I n,m,x,y,pd,res,num[MAXN],outit[MAXN];View Code11 B jud[MAXN]; 12 struct Disjoint_set{ 13 I father[MAXN],self[MAXN],tot[MAXN]; 14 V initial() { for(RE I i(1);i <= n; ++ i) father[i] = i; } 15 I find(I x) { return x == father[x] ? x : father[x] = find(father[x]); } 16 V merge(I x,I y){ 17 I fx = find(x); I fy = find(y);18 if(fx != fy) father[fy] = fx,self[fx] += self[fy]; 19 } 20 }Disjoint_set; 21 L I read(); L I fir(I); L I ope(I); 22 signed main(){ 23 n = read(); m = read(); 24 Disjoint_set.initial(); 25 for(RE I i(1);i <= m; ++ i){ 26 x = read(); y = read(); 27 if(x == y) { Disjoint_set.self[Disjoint_set.find(x)]++; continue; } 28 Disjoint_set.merge(x,y); 29 outit[x]++; outit[y]++; 30 } 31 for(RE I i(1);i <= n; ++ i) { 32 I anc = Disjoint_set.find(i); 33 num[anc] += outit[i]; Disjoint_set.tot[anc] += fir(outit[i]); 34 } 35 for(RE I i(1);i <= n; ++ i) { 36 if(num[i]) pd++; 37 if(pd > 1) { printf("0"); return 0; } 38 num[i] >>= 1; 39 } 40 for(RE I i(1);i <= n; ++ i) { 41 I anc = Disjoint_set.find(i); 42 if(!jud[anc]) jud[anc] = 1,res += ope(anc); 43 } 44 printf("%lld",res); 45 } 46 L I read(){RE I x(0);RE C z(getchar());while(!isdigit(z))z=getchar();while(isdigit(z))x=(x<<3)+(x<<1)+(z^48),z=getchar();return x;} 47 L I fir(I x) { return x * (x - 1) / 2 ; } 48 L I ope(I x){ 49 Disjoint_set.tot[x] += fir(Disjoint_set.self[x]); 50 Disjoint_set.tot[x] += Disjoint_set.self[x] * num[x]; 51 return Disjoint_set.tot[x]; 52 }
T2:砍樹:
首先反思做題思路:拿到一道題首先要觀察資料範圍,根據資料範圍判斷時空複雜度範圍進而確定大致的演算法型別,其次應該先判斷題目型別是否為數學題,
如若通過推導難以解決那麼嘗試用資料結構解決問題
進入正題:根據資料範圍我們可以考慮log型別複雜度,然而考慮資料做法的話無從下手
此時考慮數學解法,對於資訊類問題的數學解法首先要對問題進行數學抽象,該問題可以轉化為:
求一個最大的d,滿足:sigma(ceil(ai/d)*d-ai)<=k,令C=k+sigma(ai),移項得sigma(ceil(ai/d))*d<=C;
顯然右側是一個定值(分離包含多個變數的項,使公式中不同變數之間相互獨立的思想),再移項得sigma(ceil(ai/d))<=C/d;
分析發現右側是一個分段函式,迴歸問題我們要求滿足該不等式最大的d,我們發現對於分段函式上的每一段,右端點顯然是最優解,
這理解涉及如何去列舉,如果列舉分段函式縱座標來計算每一段的右端點,那麼時間複雜度為O(sigma),顯然會超時,分析發現如果我們直接列舉
右端點進行轉移會省去很多不必要的列舉降低複雜度(O(sqrt(k+max(ai)))),也就是說在列舉過程中,我們要儘量類似此題跳躍性列舉來減少不必要的操作,
當然這需要列舉變數間存在一定的遞推關係。
程式碼如下:
1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<cmath> 5 using namespace std; 6 #define I unsigned long long 7 #define B bool 8 #define C char 9 #define RE register 10 #define V void 11 #define L inline 12 I n,k,maxn,sigma,tall[105]; 13 L I read(); L I up(I,I); L I fir(I); 14 signed main(){ 15 n = read(); k = read(); sigma += k; maxn += k; 16 for(RE I i(1);i <= n; ++ i) tall[i] = read(),sigma += tall[i]; 17 maxn += *max_element(tall + 1,tall + n + 1); 18 for(RE I i(sigma / (sigma / maxn)); i ;i = sigma / (sigma / i + 1)) 19 if(fir(i) <= sigma / i) { printf("%lld",i);break; } 20 } 21 L I read(){RE I x(0);RE C z=getchar();while(!isdigit(z))z=getchar();while(isdigit(z))x=(x<<3)+(x<<1)+(z^48),z=getchar();return x;} 22 L I up(I x,I y) { return x % y ? x / y + 1 : x / y ; } 23 L I fir(I x){ I res(0); 24 for(RE I j(1);j <= n; ++ j) 25 res += up(tall[j],x); 26 return res; 27 }View Code