zoukankan      html  css  js  c++  java
  • 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;
    }
    

  • 相关阅读:
    Leetcode 191.位1的个数 By Python
    反向传播的推导
    Leetcode 268.缺失数字 By Python
    Leetcode 326.3的幂 By Python
    Leetcode 28.实现strStr() By Python
    Leetcode 7.反转整数 By Python
    Leetcode 125.验证回文串 By Python
    Leetcode 1.两数之和 By Python
    Hdoj 1008.Elevator 题解
    TZOJ 车辆拥挤相互往里走
  • 原文地址:https://www.cnblogs.com/Troverld/p/14621483.html
Copyright © 2011-2022 走看看