Tarjan
割点
low[x]
:x能到达的cnt最小的节点(包括x自身节点)
对于x节点,存在dfs树上x的子节点y,low[y]>=low[x]
,即y能到达的最小编号节点是以x为根节点的子树,删除x之后,y与x的父节点不连通(特判根节点)
void tarjan(int x){
low[x]=dfn[x]=++tarcnt;
int flag=0;
for(int i=0;i<mp[x].size();++i){
int y=mp[x][i];
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]){
flag++;
if(x^root||flag>1)cut[x]=1;
}
}else low[x]=min(low[x],dfn[y]);
}
}
对于一个点x,删除x和与x相连的边后,所有满足low[y]>=dfn[x]的点的子树构成连通块,剩下的节点共同构成一个连通块。
割边
low[x]
:x能到达的cnt最小的节点(不包括x自身节点)
对于x节点,存在dfs树上x的子节点y,low[y]>low[x]
,即y能到达的最小编号节点是以x为根节点的子树,删除x之后,y与x的父节点不连通(不用特判根节点)
void tarjan(int x,int f){
low[x]=dfn[x]=++tarcnt;
for(int i=0;i<mp[x].size();++i){
int y=mp[x][i];
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>dfn[x]){
bridge[i]=1;
}//x-y的边就是桥,可以把标记放在节点y上。
}else if(y^f)low[x]=min(low[x],dfn[y]);
}
}
给定无向连通图,q次操作,每次加一条边,求前i次操作后桥的数量。
把桥标记放在子节点上,(x,y)之间加边,就是把dfs树上x与y路径上的所有桥删除。
可以暴力,每次询问O(N),优化:将删除后的图缩成一个点。
struct Greap{
struct E{int y,nt;}e[MAXN<<2];
int head[MAXN],cnt;
void add(int x,int y){
e[++cnt].y=y;
e[cnt].nt=head[x];
head[x]=cnt;
}
void init(){
memset(head,0,sizeof(head));
cnt=0;
}
}g;
int n,m;
struct Tarjan{
int dfn[MAXN],cnt,low[MAXN],fat[MAXN],ans;
bool isb[MAXN];
void tarjan(int x,int f){
low[x]=dfn[x]=++cnt;
fat[x]=f;
for(int i=g.head[x];i;i=g.e[i].nt){
int y=g.e[i].y;
if(!dfn[y]){
tarjan(y,x);
low[x]=min(low[x],low[y]);
if(low[y]>dfn[x])isb[y]=1,ans++;
}else if(y^f)low[x]=min(low[x],dfn[y]);
}
}
int tp;
void lca(int x,int y){
tp=0;
while(x^y){
//类似树剖,x,y往上跳的时候记录路径
//把路径上的所有点的fat数组直接指向最后的一个节点,即指向dfs树上的lca(x,y)
if(dfn[x]<dfn[y])swap(x,y);
low[tp++]=x;
ans-=isb[x];
isb[x]=0,x=fat[x];
}
if(!tp)return;
int ff=low[--tp];
for(int i=0;i<tp;++i)fat[low[i]]=ff;
}
void init(){
cnt=ans=0;
memset(dfn,0,sizeof(dfn));
memset(isb,0,sizeof(isb));
}
}tj;
int main() {
int cas=1;
while(1){
read(n,m);
if(n==0&&m==0)return 0;
if(cas^1)putchar('\n');
printf("Case %d:\n",cas++);
g.init();
for(int i=0,x,y;i<m;++i){
read(x,y);
g.add(x,y),g.add(y,x);
}
tj.init();
tj.tarjan(1,0);
int q;read(q);
for(int i=0,x,y;i<q;++i){
read(x,y);
tj.lca(x,y);
printf("%d\n",tj.ans);
}
}
return 0;
}
连通分量
有向图强联通分量(SCC)
有向图 G强连通是指,G 中任意两个结点相互可达。
在dfs树中,如果遍历到某个强联通分量的某个节点x,该强连通分量所有节点都在x的dfs子树中。
dfs到x节点时,x入栈,如果回溯时low[x]==dfn[x]
说明栈x后的节点构成一个强联通分量。
int scc[MAXN];//scc[i]:节点i所在scc编号
int siz[MAXN];//siz[i]:强联通i的大小
void tarjan(int x){
low[x]=dfn[x]=++cnt;
sta[++tp]=x;//入栈
for(int i=g.head[x];i;i=g.e[i].nt){
int y=g.e[i].y;
if(!dfn[y])tarjan(y,x),low[x]=min(low[x],low[y]);
else if(!scc[y])low[x]=min(low[x],dfn[y]);
//y还在栈中
}
if(low[x]==dfn[x]){
sc++;int y;
do{
y=sta[tp--];
siz[sc]++;
scc[y]=sc;
}while(y^x)
}
scc缩点
遍历每条边,如果x->y边x,y属于不同的scc
for(int x=1;x<=n;++x){
for(int i=head[x];i;i=e[i].nt){
int y=g.e[i].y;
if(scc[x]^scc[y])add_c(scc[x],scc[y]);
}
}
poi-1236-Network of Schools
给定n个学校,a支援b即a的软件b可以使用
-
求一个新软件至少要给多少个学校
-
至少添加几个支援关系使得软件给任意一个学校,其他所有学校都能获得
scc缩点后,每个scc内部节点都相互可达,所以,ans1就是缩点后零入度节点数量
添加若干有向边,使得一个有向图变成一个强联通图。
对任意节点v,如果v出度为零,则必须添加一条从v开始的有向边。
对任意节点v,如果v入度为零,则必须添加一条到达v的有向边。
若图中共有p个入度为零的节点,q个出度为零的节点,至少添加max(p,q)
条有向边。
struct G{;
struct E{int y,nt;}e[MAXN*100];
int head[MAXN],cnt;
void add(int x,int y){...}
}g,gg;
int n;
int dfn[MAXN],tarcnt,low[MAXN];
int scc[MAXN],scccnt,stk[MAXN],tp;
void tarjan(int x){
dfn[x]=low[x]=++tarcnt;
stk[++tp]=x;
for(int i=g.head[x];i;i=g.e[i].nt){
int y=g.e[i].y;
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}else if(!scc[y])low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
int y;++scccnt;
do y=stk[tp--],scc[y]=scccnt;while(y^x);
}
}
int in[MAXN],out[MAXN];
int main() {
...
for(int i=1;i<=n;++i)if(!dfn[i])tarjan(i);
for(int x=1;x<=n;++x){
for(int i=g.head[x];i;i=g.e[i].nt){
int y=g.e[i].y;
if(scc[x]^scc[y]){
gg.add(scc[x],scc[y]);
in[scc[y]]++,out[scc[x]]++;
}
}
}
int ans1=0,ans2=0;
for(int i=1;i<=scccnt;++i)ans1+=(in[i]==0),ans2+=(out[i]==0);
printf("%d\n%d",ans1,scccnt==1?0:max(ans1,ans2));
return 0;
}
边双联通分量(e-dcc)
图g是边双联通图,当且仅当图中任意一条边都包含在至少一个简单环中,即不存在桥。
e-dcc求法:求出所有桥后,删除桥,原图分为若干个联通分量,每个联通分量都是一个边双联通分量。
//先tarjan求出桥
int dcc[MAXN],dc;//dcc[i]:节点i所属的dcc编号
void dfs(int x){
dcc[x]=dc;
for(int i=head[x];i;i=e[i].nt){
int y=e[i].y;
if(dcc[y]||bridge[i])continue;
dfs(y);
}
}
//在main里加入。
for(int i=1;i<=n;++i){
if(dcc[i])continue;
++dc;
dfs(i);
}
e-DCC缩点
将每个e-dcc缩成一个点,新点的编号是dcc编号(也可以是原图节点的编号)
//在求出dcc[]基础上
for(int x=1;x<=n;++x){
for(int i=head[x];i;i=e[i].nt){
int y=e[i].y;
if(bridge[i])add(dcc[x],dcc[y]);
}
}
点双联通分量(v-dcc)
图g是点双联通图,当且仅当图的顶点数为2或者图中任意两个顶点都包含在至少一个简单环中,即不存在割点。
注意,某个割点可以在多个v-dcc中
求v-dcc:
-
当一个节点第一次被访问时,入栈该节点。
-
当
low[y]>=dfn[x]
时,无论x是否为根,都要:-
出栈,直至y出栈。
-
出栈的所有节点与x节点构成一个v-dcc。
-
vector<int>dcc[MAXN];//dcc[i]:保存第i个v-dcc的节点
int dc,stk[MAXN],tp;//栈
void tarjan(int x){
low[x]=dfn[x]=++cnt;
stk[++tp]=x;
int flag=0;
if(x==root&&head[x]==0){//特判孤立节点
dcc[++dc].clear();
dcc[dc].push_back(stk[tp--]);
return;
}
for(int i=g.head[x];i;i=g.e[i].nt){
int y=g.e[i].y;
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]){
dcc[++dc].clear();int z;
do dcc[dc].push_back(z=sta[tp--]);while(z^y);
dcc[dc].push_back(x);
flag++;
if(x^root||flag>1)cut[x]=1;
}
}
else low[x]=min(low[x],dfn[y]);
}
}
v-DCC缩点
一个割点可能属于多个v-dcc,设图中共有p个割点,t个v-dcc,缩点后建立p+t 个节点的新图。
int belong[MAXN];//belong[i]:节点i所属的v-dcc编号
int main(){
num=dc;//每个割点新编号,从dc+1开始
for(int i=1;i<=n;++i)
if(cut[i])new_id[i]=++num;
for(int i=1;i<=dc;++i){
for(int j=0;j<dcc[i].size();++j){
int y=dcc[i][j];
if(cut[y]){
add_c(i,new_id[y]);
add_c(new_id[y],i);
}else c[x]=i;//除了割点,每个节点仅属于一个v-dcc
}
}
}
Knights of the Round Table
n个骑士,m个关系,a,b之间有有矛盾<=>a,b不能邻座,最终选择奇数(至少3个人)个满足条件的人,求不可能被选上的人数。
建立补图,只有a,b有边才能邻座。被选上的x个人在补图中组成奇环,即求补图中不经任意一个奇环的点数。
一个点双联通分量如果存在奇环,那么分量中的所有点都至少在一个奇环里。
对一个v-dcc进行染色,如果不是二分图,那么必然存在奇环。
#define Init(arr,val) memset(arr,val,sizeof(arr))
bool mp[MAXN][MAXN];
int dfn[MAXN],tarcnt,low[MAXN];
vector<int>dcc[MAXN];//储存e-dcc的点
int dc,stk[MAXN],tp,rt;
void tarjan(int x) {
low[x]=dfn[x]=++tarcnt;
stk[++tp]=x;
if(x==rt&&head[x]==0) {//孤立节点。
dcc[++dc].push_back(stk[tp--]);
return;
}
for(int i=head[x]; i; i=e[i].nt) {
int y=e[i].y;
if(!dfn[y]) {
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]) {
++dc;int z;
do dcc[dc].push_back(z=stk[tp--]);while(z^y);
dcc[dc].push_back(x);
}
} else low[x]=min(low[x],dfn[y]);
}
}
int V[MAXN],col[MAXN],ok[MAXN];//ok[i]:点i符合条件
bool dfs(int x,int color) {//dfs染色
col[x]=color;
for(int i=head[x]; i; i=e[i].nt) {
int y=e[i].y;
if(V[x]^V[y])continue;
if(col[y]==color)return 0;
if(!col[y]&&!dfs(y,3-color))return 0;
}return 1;
}
int solve(int n) {
int ans=n;
for(int i=1; i<=n; ++i)if(!dfn[i])tarjan(rt=i);//补图可能不连通
while(dc) {//遍历每个v-dcc
int siz=dcc[dc].size();
for(int i=0; i<siz; ++i)V[dcc[dc][i]]=dc;
if(!dfs(dcc[dc][0],1))for(int i=0; i<siz; ++i)ok[dcc[dc][i]]=1;
for(int i=0; i<siz; ++i)col[dcc[dc][i]]=0;
dc--;
}
for(int i=1; i<=n; ++i)ans-=ok[i];
return ans;
}
int main() {
while(1) {
read(n,m);
if(!n)return 0;
dc=tarcnt=e_cnt=0;
Init(dfn,0);Init(col,0);Init(ok,0);Init(head,0),Init(V,0);Init(mp,0);
for(int i=0;i<=n;++i)dcc[i].clear();
for(int i=0,x,y; i<m; ++i) {
read(x,y);
mp[x][y]=1,mp[y][x]=1;
}
for(int i=1; i<=n; ++i)for(int j=i+1; j<=n; ++j)
if(!mp[i][j])add(i,j),add(j,i);
printf("%d\n",solve(n));
}
}