1. 程式人生 > 其它 >NOIP 模擬8

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];
11 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 }
View Code

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