20200615 仙人掌、圆方树、支配树、跳舞链作业

[APIO2018] Duathlon 铁人两项

无向图,选三个不同的点 s , c , f s,c,f s,c,f,使得存在一条 s → c → f s\to c\to f scf的简单路径,求方案数。

题解:
圆方树,缩点双。两个圆点树上路径中经过的方点对应点双中的点都可以作为 c c c
方点权值为点双大小,圆点权值为-1,那么可选的 c c c的数量就是路径点权和。统计一个点被多少圆点路径经过即可,简单树上DP。

Code:

#include<bits/stdc++.h>
#define maxn 200005
#define maxm 400005
using namespace std;
int n,m,tsz,sz,val[maxn];
long long ans;
namespace RST{
	int siz[maxn],fir[maxn],nxt[maxm],to[maxm],tot;
	inline void line(int x,int y){nxt[++tot]=fir[x],fir[x]=tot,to[tot]=y;}
	void dfs(int u){
		siz[u]=val[u]?0:(val[u]=-1,1);
		for(int i=fir[u],v;i;i=nxt[i]) 
			dfs(v=to[i]),ans+=1ll*siz[u]*siz[v]*val[u],siz[u]+=siz[v];
		ans+=1ll*(tsz-siz[u])*siz[u]*val[u];
	}
}
int dfn[maxn],low[maxn],tim,stk[maxn],top;
int fir[maxn],nxt[maxm],to[maxm],tot;
inline void line(int x,int y){nxt[++tot]=fir[x],fir[x]=tot,to[tot]=y;}
void tarjan(int u){
	dfn[u]=low[u]=++tim,stk[++top]=u,tsz++;
	for(int i=fir[u],v;i;i=nxt[i])
		if(!dfn[v=to[i]]){
			tarjan(v),low[u]=min(low[u],low[v]);
			if(dfn[u]<=low[v]){
				RST::line(u,++sz),val[sz]++;
				do RST::line(sz,stk[top]),val[sz]++; while(stk[top--]!=v);	
			}
		}
		else low[u]=min(low[u],dfn[v]);
}
int main()
{
	scanf("%d%d",&n,&m),sz=n;
	for(int i=1,x,y;i<=m;i++) scanf("%d%d",&x,&y),line(x,y),line(y,x);
	for(int i=1;i<=n;i++) if(!dfn[i]) tsz=0,tarjan(i),RST::dfs(i);
	printf("%lld\n",ans<<1);
}

「POI2011 R2 Day1」垃圾运输 Garbage

经过两次可以转化为不经过,两个小环形成一个大环。
所以就是求若干简单环覆盖图的方案。同欧拉回路,若有点度数为奇数无解。
简单环点不重复,所以写法有些奇特,每次只找一条边,找到环之后退栈存环,然后将第一个点入栈继续找。
当前弧优化以及给边打标记保证了复杂度是 O ( m ) O(m) O(m)的。

Code:

#include<bits/stdc++.h>
#define maxn 100005
#define maxm 2000005
using namespace std;
int n,m,d[maxn];
int fir[maxn],nxt[maxm],to[maxm],tot=1;
void line(int x,int y){nxt[++tot]=fir[x],fir[x]=tot,to[tot]=y;}
vector<int>cir[maxn];
int cnt,S[maxn],tp; bool inq[maxn],vis[maxm];
void dfs(int u){
	if(inq[u]){
		cir[++cnt].push_back(u); int v;
		do inq[v=S[tp--]]=0,d[v]-=2,cir[cnt].push_back(v); while(v!=u);
	}
	S[++tp]=u,inq[u]=1;
	for(int &i=fir[u];i;i=nxt[i]) if(!vis[i]){
		vis[i]=vis[i^1]=1,dfs(to[i]); return;
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1,x,y,s,t;i<=m;i++){
		scanf("%d%d%d%d",&x,&y,&s,&t);
		if(s!=t) line(x,y),line(y,x),d[x]++,d[y]++;
	}
	for(int i=1;i<=n;i++) if(d[i]&1) return puts("NIE"),0;
	for(int i=1;i<=n;i++) while(d[i]) dfs(i);
	printf("%d\n",cnt);
	for(int i=1,lim;i<=cnt;i++){
		printf("%d",lim=cir[i].size()-1);
		for(int j=0;j<=lim;j++) printf(" %d",cir[i][j]); putchar('\n');
	}
}

CF231E Cactus

点仙人掌(每个点属于一个简单环),求 x x x y y y不经过重复边的路径条数

如果用圆方树的话需要考虑 x , y , l c a x,y,lca x,y,lca的儿子及 l c a lca lca的父亲是不是方点。(因为是不经过重复边,所以 x x x可以到它的点双中转一圈再往上)

而实际上,点仙人掌是可以直接缩的(因为一个点只在一个环里面,可以当成边双来缩点),然后就只需要看 s c c [ x ] scc[x] scc[x] s c c [ y ] scc[y] scc[y]的路径上缩点之前大小>1的点的个数就可以了。做个树上前缀和+LCA轻松解决。

Code:

#include<bits/stdc++.h>
#define maxn 100005
#define pb(x) push_back(x)
#define rep(v,G) for(int i=G.size()-1,v;i>=0?(v=G[i],1):0;i--)
using namespace std;
const int mod = 1e9+7;
int n,m,scnt,scc[maxn],siz[maxn],dfn[maxn],low[maxn],tim,stk[maxn],top;
vector<int>G[maxn],id[maxn],E[maxn];
void tarjan(int u,int ff){
	dfn[u]=low[u]=++tim,stk[++top]=u;
	rep(v,G[u]) if(id[u][i]!=ff){
		if(!dfn[v]) tarjan(v,id[u][i]),low[u]=min(low[u],low[v]);
		else low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		++scnt;
		do scc[stk[top]]=scnt,siz[scnt]++; while(stk[top--]!=u);
	}
}
int dep[maxn],fa[maxn],s[maxn],f[17][maxn],pw[maxn];
void dfs(int u,int ff){
	dep[u]=dep[f[0][u]=ff]+1,s[u]=s[ff]+(siz[u]>1);
	for(int i=1;1<<i<dep[u];i++) f[i][u]=f[i-1][f[i-1][u]];
	rep(v,E[u]) if(v!=ff) dfs(v,u);
}
int LCA(int u,int v){
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=0,d=dep[u]-dep[v];d;d>>=1,i++) if(d&1) u=f[i][u];
	if(u==v) return u;
	for(int i=16;i>=0;i--) if(f[i][u]!=f[i][v]) u=f[i][u],v=f[i][v];
	return f[0][u];
}
int main()
{
	scanf("%d%d",&n,&m); int x,y;
	for(int i=1;i<=m;i++) scanf("%d%d",&x,&y),G[x].pb(y),G[y].pb(x),id[x].pb(i),id[y].pb(i);
	tarjan(1,0);
	for(int u=1;u<=n;u++) rep(v,G[u]) if((x=scc[u])!=(y=scc[v])) E[x].pb(y);
	dfs(1,0);
	int Q;scanf("%d",&Q);
	for(int i=pw[0]=1;i<=n;i++) pw[i]=pw[i-1]*2%mod;
	for(int z;Q--;) scanf("%d%d",&x,&y),z=LCA(x=scc[x],y=scc[y]),printf("%d\n",pw[s[x]+s[y]-s[z]-s[f[0][z]]]);
}

LOJ#6240. 仙人掌

这概率计算好tm恶心。。

仙人掌随机点分治,求期望复杂度。
可以先看看树随机点分治怎么做。
根据 E ( ∑ x i ) = ∑ E ( x i ) E(\sum x_i)=\sum E(x_i) E(xi)=E(xi),可以分开算每个点的贡献,就是它在点分树中的深度。设 P i , j P_{i,j} Pi,j 表示当删除点 j j j i , j i,j i,j 在前一时刻保持连通的概率,那么点 i i i 的贡献等价于 ∑ j P i , j \sum_{j}P_{i,j} jPi,j

如果在树中, P i , j = 1 n u m b e r   o f   p o i n t s   o n   p a t h ( i , j ) P_{i,j}=\frac {1}{number~of~points ~on~path(i,j)} Pi,j=number of points on path(i,j)1,即点 j j j 是路径上第一个删除的点。
在仙人掌上,由于一个点双有两条路径,所以可以断掉在某一半的点, i , j i,j i,j仍然是连通的。
这时候 P i , j P_{i,j} Pi,j 的计算方式大概有两种思路:

  • 方法一:

    c n t cnt cnt表示 i → j i\to j ij经过的点双大小之和(包括单点)。
    求出 d p [ j ] [ x ] dp[j][x] dp[j][x]表示在这 c n t cnt cnt个点中选择 x x x个点删去后 i , j i,j i,j仍然连通的方案数。
    求答案就枚举 j j j在路径上是第几个被删去的点,假设有 x x x个点在它之前删去,那么概率是 1 c n t ∗ 1 c n t − 1 . . . 1 c n t − x ∗ x ! = ( c n t − x − 1 ) ! x ! c n t ! \frac {1}{cnt}*\frac {1}{cnt-1}...\frac 1{cnt-x}*x!=\frac {(cnt-x-1)!x!}{cnt!} cnt1cnt11...cntx1x!=cnt!(cntx1)!x!

    答案即为 ∑ j = 1 n ∑ x = 0 c n t − 2 d p [ j ] [ x ] ∗ ( c n t − x − 1 ) ! x ! c n t ! \sum\limits_{j=1}^n\sum\limits_{x=0}^{cnt-2}dp[j][x]*\frac {(cnt-x-1)!x!}{cnt!} j=1nx=0cnt2dp[j][x]cnt!(cntx1)!x!

    d p dp dp 值的方法也有两种:

    • 转移就是枚举当前点双选某一边删去 k k k个点, d p [ u ] [ i ] → d p [ v ] [ i + k ] dp[u][i]\to dp[v][i+k] dp[u][i]dp[v][i+k],但是枚举需要预处理优化复杂度,同时预处理之后转移时需要去掉同时从 d p [ u ] dp[u] dp[u]转移来的部分,详见cz_xuyixuan‘s blog
    • 更改定义, d p dp dp值相当于是在求在一些集合里面选出一些点,可以求出集合的总大小,再在其中选点,每个位置需要加上上一轮的值*-1,减去重复选点的方案。详见chasedeath’s blog

  • 方法二:

    d p [ j ] [ x ] dp[j][x] dp[j][x] 表示保留 x x x 个点使其连通(不能删)的方案数(即每个点双的两边强制一边保留、另一边任意),删掉 x x x 个点中的任意一个都会使其不连通,所以在删 j j j 时保持连通的概率就是 1 x + 1 \frac 1{x+1} x+11。( j j j 第一个被删)。转移就枚举哪一边保留,于是答案中计算了强制左边保留和强制右边保留的概率之和,所以两边都连通的概率被重复计算了,需要令 d p [ v ] [ x + c i r c l e − 1 ] − = d p [ u ] [ x ] dp[v][x+circle-1]-=dp[u][x] dp[v][x+circle1]=dp[u][x]
    (略迷)

Code(方法二):

#include<bits/stdc++.h>
#define maxn 805
#define pb(x) push_back(x)
#define rep(v,G) for(int i=G.size()-1,v;i>=0&&(v=G[i]);i--)
using namespace std;
const int mod = 998244353;
int n,m,dfn[maxn],low[maxn],tim,stk[maxn],top,sz,pos[maxn][maxn],siz[maxn];
int inv[maxn],f[maxn][maxn],ans;
vector<int>G[maxn],E[maxn];
void tarjan(int u){
	dfn[u]=low[u]=++tim,stk[++top]=u;
	rep(v,G[u])
		if(!dfn[v]){
			tarjan(v),low[u]=min(low[u],low[v]);
			if(dfn[u]<=low[v]){
				E[++sz].pb(u),E[u].pb(sz); int x,d=0;
				do x=stk[top],E[x].pb(sz),E[sz].pb(x),pos[sz][x]=++d; while(stk[top--]!=v);
				siz[sz]=d+1;
			}
		}
		else low[u]=min(low[u],dfn[v]);
}
void dfs(int u,int ff,int cnt){
	for(int i=1;i<=cnt;i++) if(f[u][i]) ans=(ans+1ll*f[u][i]*inv[i])%mod;
	rep(k,E[u]) if(k!=ff)
		rep(v,E[k]) if(v!=u){
			int d=abs(pos[k][v]-pos[k][u]),s=siz[k];
			for(int j=1;j<=cnt;j++) if(f[u][j]){
				(f[v][j+d]+=f[u][j])%=mod,
				(f[v][j+s-d]+=f[u][j])%=mod,
				(f[v][j+s-1]-=f[u][j])%=mod;
			}
			dfs(v,k,cnt+s-1);
		}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1,x,y;i<=m;i++) scanf("%d%d",&x,&y),G[x].pb(y),G[y].pb(x);
	sz=n,tarjan(1);
	inv[0]=inv[1]=1; for(int i=2;i<=n;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
	for(int i=1;i<=n;i++) memset(f,0,sizeof f),f[i][1]=1,dfs(i,0,1);
	printf("%d\n",(ans+mod)%mod);
}

CF980F Cactus to Tree

点仙人掌,对每个点独立算出:将仙人掌删成一棵树后离它最远的点的距离的最小值。

题解:
稍作思考,实际上求的是点仙人掌上最短路的最大值。(贪心,如果从 x x x进入环,那么删的一定是环上与 x x x相对的那条边,每个点的最短路都能取到)

先把第一个环作为根拿出来考虑。
先BFS求出每个点往下的最远距离,此时第一个环的 L L L 就已知了。
记第一个环上的点经过桥边走到的最远距离为 L [ x ] L[x] L[x],那么对于环上的点 u u u,它的答案就是 max ⁡ L [ v ] + d i s t ( u , v ) \max L[v]+dist(u,v) maxL[v]+dist(u,v)
d i s t ( u , v ) dist(u,v) dist(u,v) 是环上 u , v u,v u,v 的最短距离,记每个点的答案为 f [ x ] f[x] f[x],环上的 f f f 就可以通过单调队列顺时针逆时针分别转一圈半算出。

然后要转移到与根相连的环,假设相接的点是 u , v u,v u,v,那么要用 max ⁡ ( f [ u ] + 1 , l + 1 ) \max (f[u]+1,l+1) max(f[u]+1,l+1) 去更新 L [ p ] L[p] L[p] l l l 是根据 L [ p ] + 1 L[p]+1 L[p]+1 是否等于 L [ u ] L[u] L[u] 来决定是否取次大值。

Code:

#include<bits/stdc++.h>
#define maxn 500005
#define pb(x) push_back(x)
#define rep(i,j,k) for(int i=(j),LIM=(k);i<=LIM;i++)
using namespace std;
const int mod = 998244353;
int n,m,dfn[maxn],low[maxn],tim,stk[maxn],top,scnt,scc[maxn];
vector<int>G[maxn],cir[maxn];
void tarjan(int u,int ff){
	dfn[u]=low[u]=++tim,stk[++top]=u;
	for(int v:G[u]) if(v!=ff){
		if(!dfn[v]) tarjan(v,u),low[u]=min(low[u],low[v]);
		else low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		++scnt;
		for(int x=-1;x!=u;) scc[x=stk[top--]]=scnt,cir[scnt].pb(x);
	}
}
int d[maxn],mxd[maxn];
void BFS(){
	queue<int>q; memset(d,-1,sizeof d),d[1]=0,q.push(1);
	while(!q.empty()) {int u=q.front();q.pop(); for(int v:G[u]) if(d[v]==-1) d[v]=d[u]+1,q.push(v);}
}
bool vis[maxn];
void DFS(int u){
	vis[u]=1,mxd[u]=d[u];
	for(int v:G[u]) if(!vis[v]) DFS(v),mxd[u]=max(mxd[u],mxd[v]);
}
int L[maxn],CL[maxn],f[maxn];
void dfs(int u,int ff,int fL){
	int o=scc[u];
	for(int u:cir[o]) for(int v:G[u]) if(scc[v]!=o){
		int now = scc[u]>scc[v] ? mxd[v]-d[u] : fL;
		if(now>L[u]) CL[u]=L[u],L[u]=now;
		else CL[u]=max(CL[u],now);
	}
	static pair<int,int>q[maxn<<1];
	int len=cir[o].size(),lim=len/2,l,r;
	#define work \
	q[l=r=1]=make_pair(L[cir[o][0]],0);\
	rep(i,1,len-1+lim){\
		while(i-q[l].second>lim) l++;\
		int u=cir[o][i%len]; f[u]=max(f[u],i+q[l].first);\
		while(l<=r&&q[r].first<=L[u]-i) r--;\
		q[++r]=make_pair(L[u]-i,i);\
	}
	work
	reverse(cir[o].begin(),cir[o].end());
	work
	for(int u:cir[o]) for(int v:G[u]) if(scc[v]!=o&&v!=ff)
		dfs(v,u,max(f[u],mxd[v]-d[u]==L[u]?CL[u]:L[u])+1);
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1,x,y;i<=m;i++) scanf("%d%d",&x,&y),G[x].pb(y),G[y].pb(x);
	tarjan(1,0);
	BFS(),DFS(1);
	dfs(1,0,0);
	rep(i,1,n) printf("%d%c",max(f[i],L[i]),i==n?10:32);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值