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\)。