P2416 泡芙 题解

前置知识

  • 您需要知道如何求桥,否则请移步 P1656 炸铁路 的 Tarjan 做法。
  • 您需要知道边双连通分量的基本性质。

思路

提示:接下来所有形似“\(u\to v\) 路径的点权和”的描述中,点权和都包括 \(u,v\) 的点权。

当火星猫走过一条路之后,这条路就不能再走了

从这句话我们可以想出来,如果走过的这条路是桥,那么火星猫就会再也无法走回去。

那么我们可以先求一遍边双并缩点。如果您不会求边双,您可以看这个云剪贴板

求边双的同时我们可以标记该边双内是否有有泡芙的边。

求出边双缩点后再建图,由边双连通分量缩点的性质我们知道缩出来的一定是棵树,而且树上的边都是桥。所以问题转变为:

一棵树上每个点有点权(点权即该边双内是否有泡芙),每个边有边权(边权即该桥上是否有泡芙),问树上 \(u\to v\) 的一条路径上点权和或边权和是否大于等于 \(1\)

我们可以从根向下进行前缀和。每个点都有两个值要存储:一个是从根到这个点的路径的点权和,一个是从根到这里的路径的边权和。

我们再求出 \((u,v)\) 的 LCA。如果您不会 LCA 请移步 P3379 最近公共祖先

接下来对于 \(u\to v\) 路径的点权和和边权和分别推式子:

最后只需要检查边权和和点权和的值即可。

代码

您需要知道我的宏定义都是什么意思,您可以在这里查看我的缺省源。

这里省略了求桥部分,您可以移步 P1656 炸铁路的 Tarjan 做法学习如何求桥。

3.1 求边双连通分量

DFS 不走桥边法求边双:

void dfs(int x){
	eccnum[x]=tnt;
	efor(i,x){
		int y=edge[i].toe;
		if(eccnum[x]==eccnum[y]){//可能存在重边而不同权值的情况
			if(edge[i].val==1){
				has[tnt]=1;
			}
			ctn;//避免无限递归
		}
		if(bridge[i]){//不走桥
			ctn;
		}
		if(edge[i].val==1){//边的权值表示是否有泡芙
			has[tnt]=1;
		}
		dfs(y);
	}
}

\(has\) 数组所表示的是该边双中是否有泡芙,\(tnt\) 是当前边双连通分量的编号。

主函数中调用它的方式如下:

fr1(i,1,n){//图里面不一定只有一个边双
	if(!eccnum[i]){
		tnt++;
		dfs(i);
	}
}

只有这样才能保证每个点都有了对应的边双。

3.2 建边双缩点后的树

这里只需要注意链式前向星的细节问题就行了:

fr1(i,2,edgecnt-1){
	int x=edge[i^1].toe,y=edge[i].toe;
	if(eccnum[x]!=eccnum[y]){
		p[eccnum[x]].pb(mp(eccnum[y],edge[i].val));
	}
}

另附上我的链式前向星函数方便理解上面的代码:

int edgecnt=2;
struct Edge{
	int toe,val,nex;
} edge[N+N];
int head[N+N];
void add(int x,int y,int w){
	edge[edgecnt].toe=y;
	edge[edgecnt].val=w;
	edge[edgecnt].nex=head[x];
	head[x]=edgecnt;
	edgecnt++;
}

3.3 初始化 LCA 和前缀和

从这里开始,我们默认 \(1\) 为树根,将这棵树变为有根树。

注意,前缀和点权前要将pointvalue[1]=has[1],即要将树根处的点权前缀和初始化为树根边双是否有泡芙。

接下来就可以从树根开始往下 DFS,同时做边权前缀和与点权前缀和。此外,我们可以顺便完成 LCA 的初始化。

void dfs2(int x,int fa){
	deep[x]=deep[fa]+1;
	f[x][0]=fa;//同时初始化算 LCA 要用的几个数组 
	fv(i,p[x]){
		if(p[x][i].fi!=fa){//避免无限递归
			tsum[p[x][i].fi]=tsum[x]+p[x][i].se;
			psum[p[x][i].fi]=psum[x]+has[p[x][i].fi];//垒两个前缀和
			dfs2(p[x][i].fi,x);
		}
	}
}

\(psum\) 是点权前缀和,\(tsum\) 是边权前缀和。主函数中应该如下调用:

psum[1]=has[1];//不可缺失的初始化!!!
dfs2(1,1);

3.4 LCA

倍增 LCA 的部分您可以移步 P3379 最近公共祖先

注意 LCA 的递推部分,不要把循环写反。只要心中牢记 \(f\) 数组的含义,就永远不会发生问题。

下面给出一种参考写法:

num=log2(n);
fr1(j,1,num){
	fr1(i,1,n){
		f[i][j]=f[f[i][j-1]][j-1];
	}
}

求 LCA 的部分不给出参考代码。

3.5 处理询问

cin>>q;
while(q--){
	int x,y;
	cin>>x>>y;
	x=eccnum[x];
	y=eccnum[y];
	int lcaa=lca(x,y);
	if((psum[x]+psum[y]-psum[lcaa]-psum[(lcaa==1?0:f[lcaa][0])])||(tsum[x]+tsum[y]-tsum[lcaa]-tsum[lcaa])){
		puts("YES");
	}
	else{
		puts("NO");
	}
}

注意输入的并不是边双的编号,我们要自己转化一下。

求出 LCA 后就可以套用思路里面推出的公式了。注意我们在计算点权贡献时要判断 \(lcaa\) 是否是 \(1\),如果是的话就不能减去psum[f[lcaa][0]](因为 LCA 预处理时我们使用了f[1][0]=1),而是要减去 \(0\)

AC 记录

AC 记录

posted @ 2022-01-26 16:34  Shunpower  阅读(57)  评论(0编辑  收藏  举报