树的定义:连通无回路的无向图是一棵树。
有关树的问题:
1、最小生成树。
2、次小生成树。
3、有向图的最小树形图。
4、LCA(树上两点的最近公共祖先)。
5、树的最小支配集、最小点覆盖、最大独立集。
一、最小生成树
解决的问题是:求无向图中边权值之和最小的生成树。
算法有Kruskal和Prim。
Kruskal使用前向星和并查集实现,可以存储重边(平行边),时间复杂度是O(m log m + m),m是边的数量。
Prim使用邻接矩阵建图,不可以存储重边(平行边),如果出现重边,存储的是权值最小的那一条,时间复杂度为O(n*n), n是顶点的数量。使用邻接表建图可能会提高效率。
一般情况下,题目都是比较裸的。难度为易。
模版:建好图以后,直接调用,输出。
prim
1 const int maxn=101,INF=0x3f3f3f3f; 2 int dist[maxn],Map[maxn][maxn],pre[maxn]; 3 //向外延伸的最短边长,记录图信息,记录连接信息 4 bool p[maxn];//1表示点已经在树的,0表示点在树外 5 bool f[maxn][maxn]; 6 int Prim(int n) 7 { 8 int i,j,k,Min,ans=0; 9 for(i=2;i<=n;i++) 10 { 11 p[i]=0; 12 dist[i]=Map[1][i]; 13 pre[i]=1; 14 } 15 dist[1]=0; 16 p[1]=1; 17 for(i=1;i<n;i++) 18 { 19 Min=INF; 20 k=0; 21 for(j=1;j<=n;j++) 22 { 23 if(!p[j]&&dist[j]<Min) 24 { 25 Min=dist[j]; 26 k=j; 27 } 28 } 29 if(k==0) return -1;//G不连通 30 ans+=Min; 31 p[k]=1; 32 for(j=1;j<=n;j++) 33 { 34 if(!p[j]&&Map[k][j]!=INF&&dist[j]>Map[k][j]) 35 { 36 dist[j]=Map[k][j]; 37 pre[j]=k; 38 } 39 } 40 } 41 return ans; 42 }
Kruskal
1 const int N=1010,M=100010; 2 int f[N],r[N]; 3 struct node 4 { 5 int x, y, len; 6 }e[M]; 7 int cmp(const node &a,const node &b) 8 { 9 return a.len<b.len; 10 } 11 void Make_Set(int n) 12 { 13 for(int i=0;i<=n;i++) 14 { 15 f[i]=i; 16 r[i]=1; 17 } 18 } 19 void Link(int a, int b) 20 { 21 if (r[a] > r[b]) {f[b] = a; r[a]+=r[b];} 22 else {f[a] = b; r[b]+=r[a];} 23 } 24 int Find_Set(int a) 25 { 26 if (a == f[a]) return a; 27 else return f[a] = Find_Set(f[a]); 28 } 29 int Kruskal(int n,int m) 30 { 31 int i, j, k, s=0,cn=0; 32 Make_Set(n); 33 sort(e,e+m,cmp); 34 for(i=0;i<m;i++) 35 { 36 j=Find_Set(e[i].x); 37 k=Find_Set(e[i].y); 38 if(j!=k) { 39 Link(j,k); 40 s+=e[i].len; 41 cn++; 42 } 43 if(cn==n-1) break; 44 } 45 if(cn<n-1) return -1; 46 return s; 47 }
二、次小生成树
1、最朴素的办法就是暴力枚举。求一遍最小生成树以后,依次删除最小生成树上的边,求此时的最小生成树,值最小的就是我们所求的。总共要运行 n次最小生成树算法,时间复杂度为n倍最小生成树算法的复杂度。有些题目能AC。暴力枚举在没有好方法时,也是一种非常好的方法。
2、我们知道,添加任意一条不在最小生成树上的边以后一定会形成一个环。找到环上除了(u,v)以外的权值最大的边,把它删掉,那么此时生成树就可能是次小生成树。基于这种思想:首先求出原图最小生成树,权值之和为MST。在执行最小最小生成树算法的时候,记录任意两点(u,v)路径之间的最大权值,maxe[u][v]。注意,不是记录以u与v为起点和终点的边的最大值,是两点之间的路径,可能有多个点在它们之间。然后枚举每一条边,如果该边不是原本最小生成树上的边,就用MST - maxe[u][v] + Map[u][v],统计结果最小的。
需要知道的是次小生成树的权值和可以与最小生成树的权值和相等。
模版:
prim(直接贴poj1679的代码了)
1 #include<iostream> 2 #include<cstring> 3 #include<cstdio> 4 #include<algorithm> 5 using namespace std; 6 7 const int maxn=1100,INF=0x3f3f3f3f; 8 int dist[maxn],Map[maxn][maxn],pre[maxn],maxe[maxn][maxn]; 9 //向外延伸的最短边长,记录图信息,记录连接信息,最长边 10 bool vis[maxn];//1表示点已经在树的,0表示点在树外 11 bool f[maxn][maxn];//存在边为1,用过为0 12 int n,m;//顶点数目 13 int Prim() 14 { 15 int i,j,k,Min,ans=0; 16 memset(vis,0,sizeof(vis)); 17 memset(f,0,sizeof(f)); 18 memset(maxe,0,sizeof(maxe)); 19 for(i=2;i<=n;i++) 20 { 21 dist[i]=Map[1][i]; 22 pre[i]=1; 23 } 24 dist[1]=0; 25 vis[1]=1; 26 pre[1]=0; 27 for(i=1;i<n;i++) 28 { 29 Min=INF; 30 k=0; 31 for(j=1;j<=n;j++) 32 { 33 if(!vis[j]&&dist[j]<Min) 34 { 35 Min=dist[j]; 36 k=j; 37 } 38 } 39 if(Min==INF) return -1;//G不连通 40 vis[k]=1; 41 ans+=Min; 42 f[pre[k]][k] = f[k][pre[k]]=1; 43 for(j=1;j<=n;j++) 44 { 45 if(vis[j]) maxe[k][j]=maxe[j][k]=max(maxe[j][pre[k]],dist[k]); 46 if(!vis[j]&&dist[j]>Map[k][j]) 47 { 48 dist[j]=Map[k][j]; 49 pre[j]=k; 50 } 51 } 52 } 53 return ans; 54 } 55 void init() 56 { 57 for(int i=0;i<=n;i++) 58 for(int j=0;j<=n;j++) 59 { 60 if(i==j)Map[i][j]=0; 61 else Map[i][j]=INF; 62 } 63 } 64 int main() 65 { 66 //freopen("test.txt","r",stdin); 67 int i,j,k,a,b,c,ca; 68 scanf("%d",&ca); 69 while(ca--) 70 { 71 scanf("%d%d",&n,&m); 72 init(); 73 for(i=0;i<m;i++) 74 { 75 scanf("%d%d%d",&a,&b,&c); 76 Map[a][b]=Map[b][a]=c; 77 } 78 int ans=Prim(); 79 if(ans==-1) {printf("Not Unique!\n");continue;} 80 int res=INF; 81 for(i=1;i<=n;i++) 82 { 83 for(j=i+1;j<=n;j++) 84 { 85 if(!f[i][j]&&Map[i][j]!=INF) 86 { 87 res=(res,ans+Map[i][j]-maxe[i][j]); 88 } 89 } 90 } 91 if(res==ans) printf("Not Unique!\n"); 92 else printf("%d\n",ans); 93 } 94 return 0; 95 }
kruskal
1 //O(mlogm+n^2) 2 const int N=1010,M=100010,INF=0x3f3f3f3f; 3 int f[N],r[N]; 4 int Find(int x) 5 { 6 if(x==f[x]) return x; 7 return f[x]=Find(f[x]); 8 } 9 void Link(int x,int y) 10 { 11 int a=Find(x), b=Find(y); 12 if(a!=b) 13 { 14 f[b]=a; 15 r[a]+=r[b]; 16 } 17 } 18 struct node 19 { 20 int u,v,w; 21 bool select; 22 }edge[M]; 23 bool cmp(const node &a, const node &b) 24 { 25 if(a.w!=b.w) return a.w<b.w; 26 if(a.u!=b.u) return a.u<b.u; 27 return a.v<b.v; 28 } 29 struct node1 30 { 31 int to,next; 32 }e[N]; 33 int cnt,tot,head[N],en[N],maxe[N][N]; 34 //e的边数、头结点、尾结点、两点在mst中的最大边 35 void Kruskal(int n,int m) 36 { 37 int k=0, i,j,t; 38 for(cnt=0;cnt<n;cnt++) 39 { 40 e[cnt].to=cnt+1; 41 e[cnt].next=head[cnt+1]; 42 en[cnt+1]=cnt; 43 head[cnt+1]=cnt; 44 } 45 sort(edge,edge+m,cmp); 46 for(i=0;i<m;i++) 47 { 48 if(k==n-1) break; 49 if(edge[i].w<0) continue; 50 int a=Find(edge[i].u), b=Find(edge[i].v); 51 if(a!=b) 52 { 53 for(j=head[a];j!=-1;j=e[j].next){ 54 for(t=head[b];t!=-1;t=e[t].next){ 55 int u=e[j].to, v=e[t].to; 56 maxe[u][v]=maxe[v][u]=edge[i].w; 57 } 58 } 59 e[en[b]].next=head[a]; 60 en[b]=en[a]; 61 Link(a,b); 62 k++; 63 edge[i].select=1; 64 } 65 } 66 } 67 void addedge(int u,int v,int w) 68 { 69 edge[tot].u=u;edge[tot].v=v;edge[tot].w=w;tot++; 70 } 71 void init() 72 { 73 tot=0; 74 memset(head,-1,sizeof(head)); 75 memset(en,-1,sizeof(en)); 76 for(int i=0;i<N;i++){ 77 edge[i].select=0; 78 f[i]=i; 79 r[i]=1; 80 } 81 } 82 int main() 83 { 84 //freopen("test.txt","r",stdin); 85 int mst,secmst; 86 Kruskal(n,m); 87 mst=0; 88 for(i=0;i<m;i++){ 89 if(edge[i].select) mst+=edge[i].w; 90 } 91 secmst=INF; 92 for(i=0;i<m;i++){ 93 if(!edge[i].select) 94 secmst=min(secmst,mst+edge[i].w-maxe[edge[i].u][edge[i].u]]); 95 } 96 97 return 0; 98 }
算法的不同,当然也继承了该算法的特点。Prim不能存重边。这是需要注意的。
题目:Hdu2489 poj1679
三、有向图的最小树形图
解决的问题:一水源给菜地供水,水只能从高处向低处流,修水渠需要一定的花费,问怎样才能使得花费最低。
边是有方向的,与最小生成树不同,这可以理解为有向图的最小生成树。
算法是朱刘算法,难得的以国人名字命名的算法。
邻接矩阵建图,时间复杂度为O( n*n*n )。 hdu4009用这种方法超时了。
前向星建图,时间复杂度为O( n*m )。
朱刘算法的两种实现可以看我的博客 http://www.cnblogs.com/Potato-lover/p/3942321.html
题目:hdu4009 、 hdu2121
四、LCA(树上两点的最近公共祖先)
解决的问题:求树上的任意两个点之间的最短距离。
用最短路的方法是行不通的,复杂度太高。所以才有解决LCA的专门的算法。
我只会离线的Tarjan(图拉)算法。 时间复杂度为O(m+q),m是边数,q是问题的个数。
我的博客里已经有相关的介绍了。http://www.cnblogs.com/Potato-lover/category/613545.html
题目:poj1330 (hdu2586)
五、树的最小支配集、最小点覆盖、最大独立集
注意:是树上的,图中一定不能有圈。
都是基于深度优先搜索。
最小支配集:对于图G=(V,E),从V取尽量少的点组成一个集合,使得V中剩余的点都与取出来的点有边相连。
最小点覆盖:对于图G=(V,E),从V取尽量少的点组成一个集合,使得E中所有的边都与取出来的点相连。
最大独立集:对于图G=(V,E),从V取尽量多的点组成一个集合,使得这些点之间没有边相连。
时间复杂度都是O(n)。
这方面的题目似乎不曾出现,但是作为图论的学习者,有必须会。
实现的算法很相似。
顶点下标从1开始的。
1 /*newpos[i]表示深度优先遍历序列的第i个点是哪个点, 2 now表示当前深度优先遍历序列已经有多少个点了。select[]用于 3 深度优先遍历的判重,p[i]表示点i的父节点的编号。对于greedy(), 4 s[i]为1表示第i个点被覆盖。se[i]表示点i属于要求的点集*/ 5 6 const int N = 1010; 7 struct node 8 { 9 int to, w, next; 10 }e[N*N]; 11 int head[N],tot,now; 12 int p[N], newpos[N] ; 13 bool select[N]; 14 bool s[N],se[N]; 15 //深度优先遍历,得到深度优先遍历序列 16 void dfs(int x) 17 { 18 newpos[now++] = x; 19 int k; 20 for(k=head[x]; k!=-1; k=e[k].next) 21 { 22 if(!select[e[k].to]) 23 { 24 select[e[k].to]= 1; 25 p[e[k].to]=x; 26 dfs(e[k].to); 27 } 28 } 29 } 30 //最小支配集 31 int greedy1() 32 { 33 memset(s,0,sizeof(s)); 34 memset(se,0,sizeof(se)); 35 int ans=0; 36 int i; 37 for(i=now-1; i>=0; i--) 38 { 39 int t=newpos[i]; 40 if(!s[t]) 41 { 42 if(!se[p[t]]) 43 { 44 se[p[t]]=1; 45 ans++; 46 } 47 s[t] = 1; 48 s[p[t]] = 1; 49 s[p[p[t]]] = 1; 50 } 51 } 52 return ans; 53 } 54 //最小点覆盖 55 int greedy2() 56 { 57 memset(s,0,sizeof(s)); 58 memset(se,0,sizeof(se)); 59 int ans=0; 60 int i; 61 for(i=now-1; i>=1; i--) 62 { 63 int t=newpos[i]; 64 if(!s[t]&&!s[p[t]]) 65 { 66 se[p[t]]=1; 67 ans++; 68 s[t] = 1; 69 s[p[t]] = 1; 70 } 71 } 72 return ans; 73 } 74 //最大独立集 75 int greedy3() 76 { 77 memset(s,0,sizeof(s)); 78 memset(se,0,sizeof(se)); 79 int ans=0; 80 int i; 81 for(i=now-1; i>=0; i--) 82 { 83 int t=newpos[i]; 84 if(!s[t]) 85 { 86 se[t]=1; 87 ans++; 88 s[t] = 1; 89 s[p[t]] = 1; 90 } 91 } 92 return ans; 93 } 94 void addedge(int i,int j) 95 { 96 e[tot].to=j;e[tot].next=head[i];head[i]=tot++; 97 } 98 void init() 99 { 100 tot=now=0; 101 memset(head,-1,sizeof(head)); 102 memset(select, 0, sizeof(select)); 103 } 104 int main() 105 { 106 /* 读入图*/ 107 select[1]=1; 108 p[1]=1; 109 dfs(1); 110 printf("%d\n",greedy()); 111 return 0; 112 }