HDU 4338 Simple Path [双联通分量+RMQ(LCA)]
题目大意是说给你一个起点和终点,一个人要从起点走到终点,它不能经过一个点两次,问他不可能经过哪些点。
显然转化成能经过哪些点要好想一些,用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++交了。
1 #pragma comment(linker, "/STACK:102400000,102400000") 2 #include <string.h> 3 #include <stdio.h> 4 #include <math.h> 5 #include <algorithm> 6 #define MAXN 100005 7 struct edge{ 8 int u,v,n; 9 }e1[MAXN*4],e2[MAXN*4]; 10 int f1[MAXN],f2[MAXN*2],es1,es2; 11 int n,m,q,tu,tv; 12 void addedge1(int u,int v){ 13 e1[es1].u=u,e1[es1].v=v,e1[es1].n=f1[u],f1[u]=es1++; 14 } 15 void addedge2(int u,int v){ 16 e2[es2].u=u,e2[es2].v=v,e2[es2].n=f2[u],f2[u]=es2++; 17 } 18 //===并查集=== 19 int p[MAXN*2]; 20 int find(int x){return x==p[x]?x:p[x]=find(p[x]);} 21 void merge(int x,int y){p[find(x)]=find(y);} 22 //===DP&RMQ=== 23 int sum[MAXN*2],tsum[MAXN*2],dis[MAXN]; 24 int lca_f[MAXN*4],lca_b[MAXN*4],lca_p[MAXN*2],rid; 25 int dminv[MAXN*4][20],dminid[MAXN*4][20]; 26 void dp(int u,int f,int dd,int tot){ 27 // printf("%d :%d %d\n",u,dd,sum[u]); 28 dis[u]=dd,tsum[u]=tot+sum[u]; 29 lca_f[++rid]=u,lca_b[rid]=dd,lca_p[u]=rid; 30 for(int i=f2[u];i!=-1;i=e2[i].n){ 31 int v=e2[i].v; 32 if(v==f)continue; 33 dp(v,u,dd+1,tot+sum[u]); 34 lca_f[++rid]=u,lca_b[rid]=dd; 35 } 36 } 37 void makermq(){ 38 rid=0; 39 dp(0,-1,0,0); 40 for(int i=1;i<=rid;i++)dminv[i][0]=lca_b[i],dminid[i][0]=i; 41 int maxj=(int)(log(rid+1.0)/log(2.0)); 42 for(int j=1;j<=maxj;j++){ 43 int maxi=rid+1-(1<<j); 44 for(int i=1;i<=maxi;i++){ 45 if(dminv[i][j-1]<dminv[i+(1<<(j-1))][j-1]){ 46 dminv[i][j]=dminv[i][j-1]; 47 dminid[i][j]=dminid[i][j-1]; 48 }else{ 49 dminv[i][j]=dminv[i+(1<<(j-1))][j-1]; 50 dminid[i][j]=dminid[i+(1<<(j-1))][j-1]; 51 } 52 } 53 } 54 } 55 int lca(int x,int y){ 56 if(lca_p[x]>lca_p[y])std::swap(x,y); 57 x=lca_p[x],y=lca_p[y]; 58 int k=(int)(log(y-x+1.0)/log(2.0)); 59 int xx=dminv[x][k]<dminv[y+1-(1<<k)][k]?dminid[x][k]:dminid[y+1-(1<<k)][k]; 60 return lca_f[xx]; 61 } 62 //===Tarjan=== 63 int dfn[MAXN],low[MAXN],cid[MAXN],stk[MAXN],col[MAXN],top,ind,cls,tmp; 64 int cal[MAXN*2]; 65 //为割点的条件是根节点能搜到两个分支或者low[v]>=dfn[u],找到割点并给割点标号 66 void dfs_cutpnt(int u,int f,int root){ 67 dfn[u]=low[u]=++ind; 68 int cnt=0; 69 int flag=0; 70 for(int i=f1[u];i!=-1;i=e1[i].n){ 71 int v=e1[i].v; 72 if(v==f&&!flag){flag=1;continue;} 73 if(!dfn[v]){ 74 cnt++; 75 dfs_cutpnt(v,u,root); 76 if(low[v]<low[u])low[u]=low[v]; 77 if(u==root&&cnt>1&&cid[u]==0)cid[u]=++cls,sum[cls]=1; 78 else if(u!=root&&low[v]>=dfn[u]&&cid[u]==0)cid[u]=++cls,sum[cls]=1; 79 }else if(dfn[v]<low[u])low[u]=dfn[v]; 80 } 81 } 82 //找双联通分量并给双联通分量标号,当这个双联通分量中包含某个割点时,连一条边 83 void dfs_tarjan(int u,int f){ 84 low[u]=dfn[u]=++ind; 85 stk[++top]=u; 86 int flag=0; 87 for(int i=f1[u];i!=-1;i=e1[i].n){ 88 int v=e1[i].v; 89 if(v==f&&!flag){flag=1;continue;} 90 if(!dfn[v]){ 91 dfs_tarjan(v,u); 92 if(low[v]<low[u])low[u]=low[v]; 93 if(low[v]>=dfn[u]){ 94 sum[++cls]=1,col[u]=cls; 95 do{ 96 tmp=stk[top--],col[tmp]=cls,++sum[cls]; 97 if(cid[tmp]){addedge2(cid[tmp],cls);addedge2(cls,cid[tmp]);merge(cid[tmp],cls);} 98 }while(tmp!=v); 99 if(cid[u]){addedge2(cid[u],cls);addedge2(cls,cid[u]);merge(cid[u],cls);} 100 } 101 }else if(dfn[v]<low[u])low[u]=dfn[v]; 102 } 103 } 104 int size; 105 void makegraph(){ 106 //找割点 107 memset(dfn,0,sizeof dfn); 108 memset(low,0,sizeof low); 109 memset(cid,0,sizeof cid); 110 cls=ind=0; 111 //找双联通分量并建图 112 for(int i=0;i<n;i++)dfs_cutpnt(i,-1,i); 113 memset(dfn,0,sizeof dfn); 114 memset(low,0,sizeof low); 115 memset(col,0,sizeof col); 116 top=ind=0; 117 for(int i=0;i<n;i++)dfs_tarjan(i,-1); 118 //将森林补成树,便于dp以及查询 119 memset(cal,0,sizeof cal); 120 for(int i=1;i<=cls;i++){ 121 if(cal[find(i)]==0){ 122 cal[find(i)]=1; 123 addedge2(0,i); 124 } 125 } 126 } 127 int main(){ 128 //freopen("test.in","r",stdin); 129 int cas=1; 130 while(scanf("%d%d",&n,&m)!=EOF){ 131 memset(f1,-1,sizeof f1); 132 memset(f2,-1,sizeof f2); 133 for(int i=0;i<=2*n;i++)p[i]=i; 134 es1=es2=0; 135 136 for(int i=0;i<m;i++){ 137 scanf("%d%d",&tu,&tv); 138 addedge1(tu,tv); 139 addedge1(tv,tu); 140 } 141 142 //转化成双联通与割点相邻的图 143 makegraph(); 144 //lca转化成rmq 145 makermq(); 146 147 printf("Case #%d:\n",cas++); 148 scanf("%d",&q); 149 while(q--){ 150 scanf("%d%d",&tu,&tv); 151 //起点和终点重合 152 if(tu==tv)printf("%d\n",n-1); 153 else{ 154 //如果是割点的话就一定要用割点对应的点,因为割点会被染成不同的颜色! 155 tu=cid[tu]?cid[tu]:col[tu]; 156 tv=cid[tv]?cid[tv]:col[tv]; 157 //孤立点或者不在同一个联通块中 158 if(tu==0||tv==0||find(tu)!=find(tv)){ 159 printf("%d\n",n); 160 }else{ 161 int fa=lca(tu,tv); 162 int ans=tsum[tu]+tsum[tv]-2*tsum[fa]+sum[fa]; 163 ans-=(dis[tu]+dis[tv]-2*dis[fa]); 164 printf("%d\n",n-ans); 165 } 166 } 167 } 168 printf("\n"); 169 } 170 return 0; 171 }