• LOJ#3077. 「2019 集训队互测 Day 4」绝目编诗


    VI.LOJ#3077. 「2019 集训队互测 Day 4」绝目编诗

    神题。

    乍一看好像和虚树半毛钱关系都没有呀?没关系,过亿会就有了。

    我们不妨先从暴力开始想起。

    暴力怎么写?暴力怎么写?加边加边加边,搜就完事了。

    没错,这里的暴力就是爆搜——搜出所有环来,然后判断是否有两个环长度相等即可。

    但是这里的暴力也需要一些艺术——因为我们要不重不漏地统计所有的环。我们得加上这几条限制:

    1. 爆搜时,只搜从一个点出发再回到该点本身的简单环,不包括 \(\rho\) 型甚至更诡异形状的。

    2. 既然是简单环,我们就不允许经过同一个点两次。

    3. 为了避免将长度为 \(2\) 的简单环也计入,我们还要记录环上前一个节点,避免走反边回到上一个节点。

    这样,我们就保证可以搜出所有简单环。但是,一个简单环可能被重复搜出。更具体地说,对于一个长度为 \(x\) 的简单环,其会旋转 \(x\) 次,翻转 \(1\) 次,总计被搜出 \(2x\) 次。因此,当我们发现某一个长度的环出现了至少 \(2x+1\) 次,才可以判出现了长度相同的环。

    暴力的复杂度是指数级别的,期望得分 \(60\%\)

    代码:

    //Version: Completely brute solution
    #include<bits/stdc++.h>
    using namespace std;
    int n,m,sz[10100],dep[10100];
    vector<int>v[10100];
    void dfs(int x,int fa){
    	dep[x]=dep[fa]+1;
    	for(auto y:v[x])if(y!=fa){
    		if(dep[y]){
    			if(dep[y]!=1)continue;
    			sz[dep[x]]++;
    			if(sz[dep[x]]>2*dep[x]){puts("Yes");exit(0);}
    			continue;
    		}
    		dfs(y,x);
    	}
    	dep[x]=0;
    }
    int main(){
    	scanf("%d%d",&n,&m);
    	if(m>=2*n){puts("Yes");return 0;}
    	for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    	for(int i=1;i<=n;i++)dfs(i,0);
    	puts("No");
    	return 0;
    }
    

    考虑优化。

    首先,众所周知,一个 \(n\) 个点的简单图,其最大简单环的长度最多只能为 \(n\)。又因为不存在长度为 \(1,2\) 的简单环,所以若一个简单图不存在长度相等的简单环,其所拥有的简单环数量最多是 \(3\sim n\) 各一个,共 \(n-2\) 个。

    考虑求出该图的任意一极大生成森林,其边数最多是 \(n-1\) 条。考虑往上加任意一条边,环数都必定增加至少 \(1\)。故添加至多 \(n-2\) 条边后,\(3\sim n\) 所有长度的环都出现了。再加任意一条边,都会出现长度相同的环。因此,不存在长度相同环的图,其 \(m\) 最大只能是 \((n-1)+(n-2)=2n-3\),超过此界直接判 Yes 即可。

    于是 \(m\) 就被压缩到了和 \(n\) 同级。

    下面我们加入第二个优化。因为 No 的最多情况下,环的总长是 \(3\sim n\) 各出现一次,共 \(n^2\) 级别。因此我们考虑仅访问环边,复杂度就是 \(n^3\) 的(因为每个环都会被访问 \(2x\) 次)。如何做到呢?很简单,只需在每次到达一个新节点时,\(O(n+m)\)dfs 一下看能不能回到起点,不能就直接剪枝剪掉即可。这样子,复杂度就被优化到了严格 \(O(n^4)\),再也不是什么玄学的指数复杂度了。

    期望得分 \(80\%\)

    代码:

    //Version: improved brute solution (n^4)
    #include<bits/stdc++.h>
    using namespace std;
    int n,m,sz[10100],dep[10100],vis[10100],id;
    vector<int>v[10100];
    bool reach(int x,int fa,int O){
    	vis[x]=O;
    	for(auto y:v[x])if(y!=fa){
    		if(dep[y]==1)return true;
    		if(dep[y]||vis[y]==O)continue;
    		if(reach(y,x,O))return true;
    	}
    	return false;
    }
    void dfs(int x,int fa){
    	dep[x]=dep[fa]+1;
    	if(!reach(x,fa,++id)){dep[x]=0;return;}
    	for(auto y:v[x])if(y!=fa){
    		if(dep[y]){
    			if(dep[y]!=1)continue;
    			sz[dep[x]]++;
    			if(sz[dep[x]]>2*dep[x]){puts("Yes");exit(0);}
    			continue;
    		}
    		dfs(y,x);
    	}
    	dep[x]=0;
    }
    int main(){
    	scanf("%d%d",&n,&m);
    	if(m>=2*n){puts("Yes");return 0;}
    	for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    	for(int i=1;i<=n;i++)dfs(i,0);
    	puts("No");
    	return 0;
    }
    

    下面我们祭出最终优化。

    明显,\(m\leq2n-3\) 这个限制是极松的。能否找到更紧的限制?

    我们来考虑任何一张无长度相等的环的图。考虑极端情况,此时 \(3\sim n\) 的环各出现一次。

    我们此时来删掉一些边。考虑对于每条边,都有 \(\dfrac{1}{\sqrt{n}}\) 的概率将其删掉。则因为 \(m,n\) 同级,所以期望删掉的边的条数是 \(O\Big(\dfrac{m}{\sqrt{n}}\big)=O(\sqrt{n})\) 级别的。

    现在,考虑一长度为 \(l\) 的环。在删边结束后,该环幸存的概率是 \(\Big(1-\dfrac{1}{\sqrt{n}}\Big)^l\)。显然,每个环剩余的概率都是独立的,故全图剩余环的期望就是 \(\sum\limits_{l=3}^n\Big(1-\dfrac{1}{\sqrt{n}}\Big)^l\),是等比数列求和的形式。又因为公比 \(q=1-\dfrac{1}{\sqrt{n}}\),故 \(q\in(0,1)\),所以该式 \(<\dfrac{1}{1-q}=\sqrt{n}\)

    因此,我们在删掉期望 \(\sqrt{n}\) 条边后,期望剩余 \(\sqrt{n}\) 个环。又因为期望涵盖了一切可能,所以定存在一种删边方案,使得删边后剩余环数量不超过 \(\sqrt{n}\)。此时,再删去至多 \(\sqrt{n}\) 条边即可消去剩余的 \(\sqrt{n}\) 个环。两者结合起来,即对于任一无长度相等的环的图,删去至多 \(2\sqrt{n}\) 条边即可消去一切环,换句话说,得到一个边数至多为 \(n-1\) 的森林。正因如此,我们就证明了无长度相等的环的图具有的边数至多是 \(n+2\sqrt{n}\) 级别的。这样,对于 \(m>n+2\sqrt{n}\),也可以直接判 Yes 了。

    (事实上,这个界也不是实的,但对于本题来说够用了)

    考虑求出原图的任意生成森林。这时,非树边至多就只有 \(2\sqrt{n}\) 条,所牵涉到的点最多只有 \(4\sqrt{n}\) 个。对这 \(4\sqrt{n}\) 个点建出虚树,然后虚树上的边便带了权。再把非树边加入虚树,便得到了一张点数至多 \(4\sqrt{n}\) 的图。同时,因为虚树是一棵树,边数是 \(\sqrt{n}\) 级别的,再加入 \(\sqrt{n}\) 级别的非树边,总边数仍是 \(\sqrt{n}\) 级别的。这样,我们便得到了一张点、边均为 \(\sqrt{n}\) 级别的带权图。明显我们上述 \(n^4\) 的算法对带权图仍然适用,因此复杂度便被优化到了 \(n^2\)

    需要注意的是,在加入非树边后,该虚树便可能出现重边。这时不能简单记录环上上一个节点,而应记录环上上一条边。

    同时,因为边带了权,长度为 \(x\) 的环上不一定就有恰好 \(x\) 个点,也就不一定出现恰 \(2x\) 次了。为了避免这种情况,我们记录当前长度为 \(x\) 的环上点数,若搜出的新环的长度与之不同,明显是两个不同的环,可以直接判 Yes 了;否则,因为知晓了点数,就和之前一样统计即可。

    时间复杂度 \(O(n^2)\)。因为压根跑不满,再加上LOJ逆天的评测姬,轻松跑过。期望得分 \(100\%\)

    .代码:

    //Version: correct solution (n^2)
    #include<bits/stdc++.h>
    using namespace std;
    int n,m;
    namespace OriginalGraph{
    	int dsu[10100];
    	int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
    	bool merge(int x,int y){
    		x=find(x),y=find(y);
    		if(x==y)return false;
    		dsu[x]=y;return true;
    	}
    }
    using namespace OriginalGraph;
    namespace ImprovedGraph{
    	int tms[10100],sz[10100],dep[310],ped[310],vis[310],id,head[310],cnt;
    	struct node{int to,next,val;}edge[40100];
    	void ae(int u,int v,int w){
    //		printf("%d %d %d\n",u,v,w);
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
    	}
    	bool reach(int x,int fe,int O){
    		vis[x]=O;
    		for(int i=head[x],y;i!=-1;i=edge[i].next)if((i^fe)!=1){
    			y=edge[i].to;
    			if(!dep[y])return true;
    			if(dep[y]!=-1||vis[y]==O)continue;
    			if(reach(y,i,O))return true;
    		}
    		return false;
    	}
    	void circ(int x,int fe){
    		if(!reach(x,fe,++id))return;
    		for(int i=head[x],y;i!=-1;i=edge[i].next)if((i^fe)!=1){
    			y=edge[i].to;
    			if(dep[y]!=-1){
    				if(dep[y])continue;
    				if(sz[dep[x]+edge[i].val]){
    					if(ped[x]+1!=sz[dep[x]+edge[i].val]){puts("Yes");exit(0);}
    					tms[dep[x]+edge[i].val]++;
    					if(tms[dep[x]+edge[i].val]>2*sz[dep[x]+edge[i].val]){puts("Yes");exit(0);}
    				}else sz[dep[x]+edge[i].val]=ped[x]+1,tms[dep[x]+edge[i].val]++;
    				continue;
    			}
    			dep[y]=dep[x]+edge[i].val,ped[y]=ped[x]+1,circ(y,i),dep[y]=-1,ped[y]=0;
    		}
    	}	
    }
    using namespace ImprovedGraph;
    namespace SpanningTree{
    	vector<int>v[10100];
    	vector<pair<int,int> >e;
    	int dis[10100],num[10100],tot,las[10100];
    	void dfs(int x,int fa){
    //		printf("%d\n",x);
    		dis[x]=dis[fa]+1;
    		if(las[x])num[x]=++tot;
    		for(auto y:v[x]){
    			if(y==fa)continue;
    			dfs(y,x);
    			if(!las[y])continue;
    			if(las[x]){
    				if(las[x]==x)ae(num[x],num[las[y]],dis[las[y]]-dis[x]);
    				else num[x]=++tot,ae(num[x],num[las[x]],dis[las[x]]-dis[x]),ae(num[x],num[las[y]],dis[las[y]]-dis[x]),las[x]=x;
    			}else las[x]=las[y];
    		}
    	}
    }
    using namespace SpanningTree;
    int main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
    	if(m>=2*sqrt(n)+n){puts("Yes");return 0;}
    	for(int i=1;i<=n;i++)dsu[i]=i;
    	for(int i=1,x,y;i<=m;i++){
    		scanf("%d%d",&x,&y);
    		if(merge(x,y))v[x].push_back(y),v[y].push_back(x);
    		else e.push_back(make_pair(x,y)),las[x]=x,las[y]=y;
    	}
    	for(int i=1;i<=n;i++)if(!dis[i])dfs(i,0);
    	for(auto i:e)ae(num[i.first],num[i.second],1);
    	memset(dep,-1,sizeof(dep));
    	for(int i=1;i<=tot;i++)dep[i]=0,circ(i,-1),dep[i]=-1;
    	puts("No");
    	return 0;
    }
    

  • 相关阅读:
    【费用流】【CODEVS】1227 方格取数2
    【CODEVS】1034 家园
    【BZOJ】1066: [SCOI2007]蜥蜴
    【最大流】【CODEVS】1993 草地排水
    【HDU】2191 多重背包问题
    【TYVJ】1520 树的直径
    【BZOJ】1984 月下“毛景树”
    【BZOJ】1588: [HNOI2002]营业额统计
    【NOI】2004 郁闷的出纳员
    【POJ】2892 Tunnel Warfare
  • 原文地址:https://www.cnblogs.com/Troverld/p/14621483.html
Copyright © 2020-2023  润新知