[APIO2018] Duathlon 铁人两项
无向图,选三个不同的点 s , c , f s,c,f s,c,f,使得存在一条 s → c → f s\to c\to f s→c→f的简单路径,求方案数。
题解:
圆方树,缩点双。两个圆点树上路径中经过的方点对应点双中的点都可以作为
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 i→j经过的点双大小之和(包括单点)。
求出 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!} cnt1∗cnt−11...cnt−x1∗x!=cnt!(cnt−x−1)!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=1∑nx=0∑cnt−2dp[j][x]∗cnt!(cnt−x−1)!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+circle−1]−=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);
}