POJ 3694 Network ★(边双连通分量+并查集缩点+LCA)

[题意]一个无向图可以有重边,下面q个操作,每次在两个点间连接一条有向边,每次连接后整个无向图还剩下多少桥(每次回答是在上一次连边的基础之上) [分析]好题,做完后涨了很多姿势~ 普通做法当然就是每加一条边重新算一次桥,但这样复杂度将达到O(q*M),显然要超时。。所以我们需要“动态地”在原图的基础上求桥~ 我们可以先把图求一次边双连通分量(BCC)然后缩点,因为同一双连通分量中没有桥,加边没有影响。一个很重要的性质就是 一个图求一次边双连通分量缩点后将变成一颗树或者森林,并且树中的每条边都是桥}。因为此题中说所有的点都有边连着,所以这里缩点后是一棵树。 显然,树中每添加一条边,就会形成一个环,而这个环中的边将不再是桥,并且他们构成新的边双连通分量,所以我们每次在桥中减去这些边,并把他们缩成一个点。 第一, 怎么求在树中加边(u,v)后形成的环? 我们可以求出u,v的LCA,然后环就是v->LCA(u,v)->u>(u,v)->v. 第二, 怎么缩点? 以前一直做的是“静态缩点”,就是求一遍边BCC后图的结构就不变了,此时我们可以在求出每个点所属的BCC(bcc[i])后以BCC的标号来代替缩后的点。如果我们动态地加边,则每次都要修改原BCC中所有的点成新BCC,所以直接这样改行不通。这里我们是不是发现它很像……并查集?对!就是用并查集维护bcc[]!这是我做这道题学到的最重要的姿势 用并查集维护动态加边的缩点。 当然此题中我们没有用到bcc[],因为在求LCA时我们用的朴素的方法:{用level[]表示每个节点的深度,两个点同时向根爬,深度深的节点先爬(LCA(u,v) = LCA(u, father[v]) ),深度相同时两个点一起爬(LCA(u, v) = LCA(father[u], father[v]) ),直到两个点相同。}这种方法的好处是可以一边求LCA一边缩点。那么我们就需要一个father[]来维护节点的父节点。这样我们就不需要bcc[]了,直接用并查集维护father[],并用它表示缩点及所属BCC。  
#include 
 
   
    
  
#include 
  
    
      #include 
     
       #include 
      
        #include 
       
         #include 
        
          #include 
         
           #include 
           #include 
           
             #include 
            
              #include 
             
               #define MID(x,y) ((x+y)/2) #define mem(a,b) memset(a,b,sizeof(a)) using namespace std; const int MAXV = 100005; const int MAXE = 400005; struct node{ int u, v; int next; int opp; //把一个无向边拆成两个有向边,对应反向边标号 }arc[MAXE]; int cnt, head[MAXV]; void init(){ cnt = 0; mem(head, -1); return ; } void add(int u, int v){ arc[cnt].u = u; arc[cnt].v = v; arc[cnt].next = head[u]; arc[cnt].opp = cnt + 1; head[u] = cnt ++; arc[cnt].u = v; arc[cnt].v = u; arc[cnt].next = head[v]; arc[cnt].opp = cnt - 1; head[v] = cnt ++; return ; } int id, dfn[MAXV], low[MAXV]; int level[MAXV]; int father[MAXV]; //点的父节点,便于爬山坡(向根节点走),也用于并查集缩点 int bridge_num, bridge[MAXV]; //标记该边是不是桥,这里用桥的子节点表示 bool vis_arc[MAXE]; //一条边无向边(两个有向边)只访问一次, int find(int u){ //并查集 + 路径压缩处理缩点, 缩点合并边时把子节点的父亲设为父节点 if(father[u] != u) return father[u] = find(father[u]); else return u; } void tarjan(int u, int l){ dfn[u] = low[u] = ++id; level[u] = l; for (int i = head[u]; i != -1; i = arc[i].next){ int v = arc[i].v; if (vis_arc[i]) continue; vis_arc[i] = vis_arc[arc[i].opp] = 1; if (!dfn[v]){ father[v] = u; tarjan(v, l+1); low[u] = min(low[u], low[v]); } else{ low[u] = min(low[u], dfn[v]); } if (dfn[u] < low[v]){ bridge[v] = 1; bridge_num ++; } else{ int x = find(u); int y = find(v); if (x != y) father[y] = x; } } } void solve(int n){ id = bridge_num = 0; mem(dfn, 0); mem(low, 0); mem(level, 0); mem(bridge, 0); mem(vis_arc, 0); for (int i = 0; i <= n; i ++) father[i] = i; for (int i = 1; i <= n; i ++){ if (!dfn[i]) tarjan(1, 1); } return ; } vector 
              
                path; int lca(int u, int v){ path.clear(); if (level[u] > level[v]) swap(u, v); while(u != v){ while(level[u] != level[v]){ if(level[v] > level[u]){ if (bridge[v]){ bridge[v] = 0; bridge_num --; } path.push_back(v); v = father[v]; } else{ if (bridge[u]){ bridge[u] = 0; bridge_num --; } path.push_back(u); u = father[u]; } } while(u != v){ if (bridge[v]){ bridge[v] = 0; bridge_num --; } if (bridge[u]){ bridge[u] = 0; bridge_num --; } path.push_back(u); path.push_back(v); v = father[v]; u = father[u]; } } int lc = u; //把加边后的环用并查集缩成一个点 for (int i = 0; i < (int)path.size(); i ++){ int u = path[i]; father[u] = lc; } return bridge_num; } int main(){ int n, m, t = 1; while(scanf("%d %d", &n, &m) != EOF){ if (n + m == 0) break; init(); for (int i = 0; i < m; i ++){ int u, v; scanf("%d %d", &u, &v); add(u, v); } solve(n); int q; scanf("%d", &q); printf("Case %d:\n", t); for (int i = 0; i < q; i ++){ int u, v; scanf("%d %d", &u, &v); printf("%d\n", lca(u, v)); } puts(""); t ++; } return 0; } 
               
              
             
            
          
         
        
       
      
    
 
   

你可能感兴趣的:(NetWork)