hdu - 4338 - Simple Path - 割點 && 雙連通

敘說下題意:就是說給你一些詢問,問一個圖中,已知s、t,在你已知s、t的情況下,你可能在那些點停留(題目是問你不可能在哪裏停留)。
    首先比如說下圖:http://acm.hdu.edu.cn/showproblem.php?pid=4338

很簡單就是將如圖所示的環給縮成點,再隨便搞搞就好。

但是再一畫,會發現有些情況很麻煩,比如下圖:

突然覺得這樣很蛋疼,中間那個算那個連通集合呢。。

然後使用雙連通分量、割點建圖能很有效的解決這個問題。
如上圖的建圖:

//make_map
拿割點和點雙連通建樹後:

紅色的對應的就是割點,黑色的是點連通分量, 當求(1,7)時候,我們分別查看1,7對應所在樹中的集合,求出路徑上所有的和 sum = 2 + 1 + 3 + 1 + 3 + 1 + 2 = 13 ,由於割點被重複計算,計算的次數爲樹邊的數量,
len = 6 , ans = sum - len = 7 ,答案就是可能停留的有7個點。

然後就是如何實現如上問題。

如上圖,求1,2的最短距離即爲lca(1,2),而結果就是,定義樹根到某點路徑所有的長度爲sson[i],nb[i]爲集合中樹節點中元素個數
sum = sson[1]+sson[2]-2*sson[3]+nb[3]
len = deep[1]+deep[2]-2*deep[3]
如此,問題轉化爲求lca的問題了...順便記錄下如上所要用的信息。

代碼寫了我好久,比較挫。。。

具體見代碼:用C++交,不然會棧溢出。
#define sz 100005
struct node
{
    int s,t,nxt;
} e[sz*10];
int hd[sz],cnt;
void insert(int s,int t)
{
    e[cnt].s=s;
    e[cnt].t=t;
    e[cnt].nxt=hd[s];
    hd[s]=cnt++;
}

//   tarjan割點模板,順便求點連通分量
//   如果stack overflow的話,試試在代碼最前面加
//   #pragma comment(linker, "/STACK:102400000,102400000")
int dfn[sz],low[sz],iscut[sz],sta[sz];
int deep,top,bnum,root;
vector <int> block[sz];
void tarjan(int s,int pre)
{
    int flag=0;
    dfn[s]=low[s]=++deep;
    sta[top++]=s;
    for(int i=hd[s]; i!=-1; i=e[i].nxt)
    {
        int t=e[i].t;
        if(t==pre && !flag)//判父邊,再次出現爲重邊,認爲連通
        {
            flag=1;
            continue;
        }
        else if(dfn[t]==-1)
        {
            tarjan(t,s);
            low[s]=min(low[s],low[t]);
            if(low[t]>=dfn[s])
            {
                if(pre == -1)root++;
                else iscut[s]=1;
                int tp;
                do
                {
                    tp=sta[--top];
                    block[bnum].pb(tp);
                }
                while(tp!=t);
                block[bnum].pb(s);
                bnum++;
            }
        }
        else
        {
            low[s]=min(low[s],dfn[t]);
        }
    }
}
struct node1
{
    int s,t,v,nxt;
} e1[sz*10];
int hd1[sz],cnt1;
void insert1(int s,int t)
{
    e1[cnt1].s=s;
    e1[cnt1].t=t;
    e1[cnt1].nxt=hd1[s];
    hd1[s]=cnt1++;
}

int lab[sz]; //用於映射
int nb[sz];  //用於記錄點中割點數,


int dpmin[sz][20];
int dpminid[sz][20];
int r[sz*2],R[sz*2],lab1[sz*2],sson[sz*2],posid[sz*2];
void create_Dpmin(int n)
{
    for(int i = 1 ; i <= n ; i++ )
    {
        dpmin[i][0] = r[i];
        dpminid[i][0] = i;
    }
    for(int j = 1 ; j <= log((double)(n+1))/log(2.0) ; j++ )
        for(int i = 1 ; i+(1<<j)-1 <= n ; i++ )
            if(dpmin[i][j-1] < dpmin[i+(1<<(j-1))][j-1])
            {
                dpmin[i][j] = dpmin[i][j-1] ;
                dpminid[i][j] = dpminid[i][j-1];
            }
            else
            {
                dpmin[i][j] = dpmin[i+(1<<(j-1))][j-1];
                dpminid[i][j] = dpminid[i+(1<<(j-1))][j-1];
            }
}
int lca(int a,int b)
{
    if(R[a]>R[b])return lca(b,a);
    a=R[a];
    b=R[b];
    int k = (int)(log((double)(b-a+1))/log(2.0));
    return (dpmin[a][k]<dpmin[b-(1<<k)+1][k])?dpminid[a][k]:dpminid[b-(1<<k)+1][k];
}
void dfs(int s,int pos,int pre,int lb,int cnt)
{
    if(R[s]==-1)
    {
        R[s]=deep;
        lab1[s]=lb;
    }
    posid[deep]=s;
    r[deep++]=pos;
    sson[s]=cnt+nb[s];
    for(int i=hd1[s]; i!=-1; i=e1[i].nxt)
    {
        if(e1[i].t == pre)continue;
        dfs(e1[i].t,pos+1,s,lb,sson[s]);
        posid[deep]=s;
        r[deep++]=pos;
    }
}

void solve(int n)
{
    //求割點,求雙連通
    rep(i,n)block[i].clear();
    memset(iscut,0,sizeof(iscut));
    memset(dfn,-1,sizeof(dfn));
    deep=top=bnum=0;
    rep(i,n)
        if(dfn[i]==-1)
        {
            root=0;
            tarjan(i,-1);
            iscut[i]=(root>1);
        }
    memset(lab,-1,sizeof(lab));
    int k=0;
    rep(i,n)
        if(iscut[i])
        {
            lab[i]=k;
            nb[k]=1;
            k++;
        }
    memset(hd1,-1,sizeof(hd1));
    cnt1=0;
    rep(i,bnum)
    {
        rep(j,block[i].size())
        {
            int l=block[i][j];
            if(iscut[l])
            {
                insert1(lab[l],k);
                insert1(k,lab[l]);
            }
            else
            {
                lab[l]=k;
            }
        }
        nb[k]=int(block[i].size());
        k++;
    }
    deep=1;
    memset(R,-1,sizeof(R));
    rep(i,k)
        if(R[i]==-1)
            dfs(i,0,-1,i,0);
    create_Dpmin(deep-1);
    int q,s,t,u,v,fa;
    scanf("%d",&q);
    rep(i,q)
    {
        scanf("%d%d",&s,&t);
        if(s == t) //相同
        {
            printf("%d\n",n-1);
            continue;
        }
        if(lab[s]==-1 || lab[t]==-1) // lab爲-1時候,表示孤立的點,既不是割點也不是塊
        {
            printf("%d\n",n);
            continue;
        }
        u = lab[s]; //對應映射過去的點
        v = lab[t];

        if(lab1[u]!=lab1[v]) // 對塊形成的樹進行連通判斷,不在同集合
        {
            printf("%d\n",n);
            continue;
        }
        fa = lca(u,v); // 返回dfs後lca的對應在r串所在位置,r串是lca要用到的深度
        fa = posid[fa];// posid對應的是r串對應的節點
        int len;
        len = r[R[u]]+r[R[v]]-2*r[R[fa]]; //路徑長度
        int ans;
        ans = sson[u]+sson[v]-2*sson[fa]+nb[fa];
        ans -= len;
        printf("%d\n",n-ans);
    }
}

int main()
{
    int n,m,s,t;
    int cas=1;
    while(~scanf("%d%d",&n,&m))
    {
        memset(hd,-1,sizeof(hd));
        cnt=0;
        rep(i,m)
        {
            scanf("%d%d",&s,&t);
            insert(s,t);
            insert(t,s);
        }
        printf("Case #%d:\n",cas++);
        solve(n);
        printf("\n");
    }
}


 題目大意是說給你一個起點和終點,一個人要從起點走到終點,它不能經過一個點兩次,問他不可能經過哪些點。

  顯然轉化成能經過哪些點要好想一些,用N減去能經過的點就可以得到答案。下面的討論都是基於求他可能經過的點有多少個。

  很容易想到用雙聯通分量,但是建圖確實比較麻煩。如下圖,一個人想要從1走到3,那它可能在的點就是1,2,3。因爲2是一個割點,它如果從2走到了4,想要到達3就必須再經過2,所以可以用割點和雙聯通建圖。

  用割點和雙聯通可以建成雙聯通與割點相鄰的圖,若右圖所示。可以證明這是一棵樹,因爲如果存在環,這個環必然可以縮成一個點,樹中任意兩點有且僅有一條路徑,於是只要統計這條路徑上有一共多少個點即可。但是有一個問題就是割點會被重複統計,每一個割點會被它左邊以及右邊的雙連通分量各多統計一次,因此只要將點數減去路徑上的邊數即可,例如求1-3,答案就是2+1+2-2=3。

 

  

  

       

  快速計算路徑上的點數總和要用到LCA,先DP處理出每個節點到根節點的距離dis[u],以及到根節點的節點總數Tsum[u],每個節點的包含的節點數記爲sum[u],則這條路徑上的點數爲ans=(Tsum[u]+Tsum[v]-2*Tsum[lca(u,v)]+sum[lca(u,v)])-(dis[u]+dis[v]-2*dis[lca(u,v)])。LCA可以轉化成RMQ以便在線查詢,每次查詢複雜度logN。

  還要注意幾個小問題,在建樹的時候用並查集進行合併,這樣就可以快速判斷兩個點是否在同一個連通分量中;另外在可以虛擬一個父親節點,到所有的連通塊中各連一條邊,這樣可以使dp方便很多。查詢時,對點進行映射,如果這個點是割點,一定要將其映射到割點所對應的節點,因爲割點在tarjan中是會被染成不同的顏色的。

  有幾個小trick,起點與終點可能不聯通,也可能在同一個點,需要特判。

  比較坑的是DP會爆棧,但是不用DP將LCA轉RMQ會比較麻煩,只能手動擴棧(加上第一行),然後用C++交了。

#pragma comment(linker, "/STACK:102400000,102400000")
 #include <string.h>
 #include <stdio.h>
 #include <math.h>
 #include <algorithm>
 #define MAXN 100005
 struct edge{
     int u,v,n;
 }e1[MAXN*4],e2[MAXN*4];
 int f1[MAXN],f2[MAXN*2],es1,es2;
 int n,m,q,tu,tv;
 void addedge1(int u,int v){
     e1[es1].u=u,e1[es1].v=v,e1[es1].n=f1[u],f1[u]=es1++;
 }
 void addedge2(int u,int v){
     e2[es2].u=u,e2[es2].v=v,e2[es2].n=f2[u],f2[u]=es2++;
 }
 //===並查集===
 int p[MAXN*2];
 int find(int x){return x==p[x]?x:p[x]=find(p[x]);}
 void merge(int x,int y){p[find(x)]=find(y);}
 //===DP&RMQ===
 int sum[MAXN*2],tsum[MAXN*2],dis[MAXN];
 int lca_f[MAXN*4],lca_b[MAXN*4],lca_p[MAXN*2],rid;
 int dminv[MAXN*4][20],dminid[MAXN*4][20];
 void dp(int u,int f,int dd,int tot){
    // printf("%d :%d %d\n",u,dd,sum[u]);
     dis[u]=dd,tsum[u]=tot+sum[u];
     lca_f[++rid]=u,lca_b[rid]=dd,lca_p[u]=rid;
     for(int i=f2[u];i!=-1;i=e2[i].n){
         int v=e2[i].v;
         if(v==f)continue;
         dp(v,u,dd+1,tot+sum[u]);
         lca_f[++rid]=u,lca_b[rid]=dd;
     }
 }
 void makermq(){
     rid=0;
     dp(0,-1,0,0);
     for(int i=1;i<=rid;i++)dminv[i][0]=lca_b[i],dminid[i][0]=i;
     int maxj=(int)(log(rid+1.0)/log(2.0));
     for(int j=1;j<=maxj;j++){
         int maxi=rid+1-(1<<j);
         for(int i=1;i<=maxi;i++){
             if(dminv[i][j-1]<dminv[i+(1<<(j-1))][j-1]){
                 dminv[i][j]=dminv[i][j-1];
                 dminid[i][j]=dminid[i][j-1];
             }else{
                 dminv[i][j]=dminv[i+(1<<(j-1))][j-1];
                 dminid[i][j]=dminid[i+(1<<(j-1))][j-1];
             }
         }
     }
 }
 int lca(int x,int y){
     if(lca_p[x]>lca_p[y])std::swap(x,y);
     x=lca_p[x],y=lca_p[y];
     int k=(int)(log(y-x+1.0)/log(2.0));
     int xx=dminv[x][k]<dminv[y+1-(1<<k)][k]?dminid[x][k]:dminid[y+1-(1<<k)][k];
     return lca_f[xx];
 }
 //===Tarjan===
 int dfn[MAXN],low[MAXN],cid[MAXN],stk[MAXN],col[MAXN],top,ind,cls,tmp;
 int cal[MAXN*2];
 //爲割點的條件是根節點能搜到兩個分支或者low[v]>=dfn[u],找到割點並給割點標號
 void dfs_cutpnt(int u,int f,int root){
     dfn[u]=low[u]=++ind;
     int cnt=0;
     int flag=0;
     for(int i=f1[u];i!=-1;i=e1[i].n){
         int v=e1[i].v;
         if(v==f&&!flag){flag=1;continue;}
         if(!dfn[v]){
             cnt++;
             dfs_cutpnt(v,u,root);
             if(low[v]<low[u])low[u]=low[v];
             if(u==root&&cnt>1&&cid[u]==0)cid[u]=++cls,sum[cls]=1;
             else if(u!=root&&low[v]>=dfn[u]&&cid[u]==0)cid[u]=++cls,sum[cls]=1;
         }else if(dfn[v]<low[u])low[u]=dfn[v];
     }
 }
 //找雙聯通分量並給雙聯通分量標號,當這個雙聯通分量中包含某個割點時,連一條邊
 void dfs_tarjan(int u,int f){
     low[u]=dfn[u]=++ind;
     stk[++top]=u;
     int flag=0;
     for(int i=f1[u];i!=-1;i=e1[i].n){
         int v=e1[i].v;
         if(v==f&&!flag){flag=1;continue;}
         if(!dfn[v]){
             dfs_tarjan(v,u);
             if(low[v]<low[u])low[u]=low[v];
             if(low[v]>=dfn[u]){
                 sum[++cls]=1,col[u]=cls;
                 do{
                     tmp=stk[top--],col[tmp]=cls,++sum[cls];
                     if(cid[tmp]){addedge2(cid[tmp],cls);addedge2(cls,cid[tmp]);merge(cid[tmp],cls);}
                 }while(tmp!=v);
                 if(cid[u]){addedge2(cid[u],cls);addedge2(cls,cid[u]);merge(cid[u],cls);}
             }
         }else if(dfn[v]<low[u])low[u]=dfn[v];
     }
 }
 int size;
 void makegraph(){
     //找割點
     memset(dfn,0,sizeof dfn);
     memset(low,0,sizeof low);
     memset(cid,0,sizeof cid);
     cls=ind=0;
     //找雙聯通分量並建圖
     for(int i=0;i<n;i++)dfs_cutpnt(i,-1,i);
     memset(dfn,0,sizeof dfn);
     memset(low,0,sizeof low);
     memset(col,0,sizeof col);
     top=ind=0;
     for(int i=0;i<n;i++)dfs_tarjan(i,-1);
     //將森林補成樹,便於dp以及查詢
     memset(cal,0,sizeof cal);
     for(int i=1;i<=cls;i++){
         if(cal[find(i)]==0){
             cal[find(i)]=1;
             addedge2(0,i);
         }
     }
 }
 int main(){
     //freopen("test.in","r",stdin);
     int cas=1;
     while(scanf("%d%d",&n,&m)!=EOF){
         memset(f1,-1,sizeof f1);
         memset(f2,-1,sizeof f2);
         for(int i=0;i<=2*n;i++)p[i]=i;
         es1=es2=0;
 
         for(int i=0;i<m;i++){
             scanf("%d%d",&tu,&tv);
             addedge1(tu,tv);
             addedge1(tv,tu);
         }
 
         //轉化成雙聯通與割點相鄰的圖
         makegraph();
         //lca轉化成rmq
         makermq();
 
         printf("Case #%d:\n",cas++);
         scanf("%d",&q);
         while(q--){
             scanf("%d%d",&tu,&tv);
             //起點和終點重合
             if(tu==tv)printf("%d\n",n-1);
             else{
                 //如果是割點的話就一定要用割點對應的點,因爲割點會被染成不同的顏色!
                 tu=cid[tu]?cid[tu]:col[tu];
                 tv=cid[tv]?cid[tv]:col[tv];
                 //孤立點或者不在同一個聯通塊中
                 if(tu==0||tv==0||find(tu)!=find(tv)){
                     printf("%d\n",n);
                 }else{
                     int fa=lca(tu,tv);
                     int ans=tsum[tu]+tsum[tv]-2*tsum[fa]+sum[fa];
                     ans-=(dis[tu]+dis[tv]-2*dis[fa]);
                     printf("%d\n",n-ans);
                 }
             }
         }
         printf("\n");
     }
     return 0;
 }



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章