NOIP 模拟八
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 }
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 }