树的定义:连通无回路的无向图是一棵树。
有关树的问题:
1、最小生成树。
2、次小生成树。
3、有向图的最小树形图。
4、LCA(树上两点的最近公共祖先)。
5、树的最小支配集、最小点覆盖、最大独立集。
一、最小生成树
解决的问题是:求无向图中边权值之和最小的生成树。
算法有Kruskal和Prim。
Kruskal使用前向星和并查集实现,可以存储重边(平行边),时间复杂度是O(m log m + m),m是边的数量。
Prim使用邻接矩阵建图,不可以存储重边(平行边),如果出现重边,存储的是权值最小的那一条,时间复杂度为O(n*n), n是顶点的数量。使用邻接表建图可能会提高效率。
一般情况下,题目都是比较裸的。难度为易。
模版:建好图以后,直接调用,输出。
prim
const int maxn=,INF=0x3f3f3f3f;
int dist[maxn],Map[maxn][maxn],pre[maxn];
//向外延伸的最短边长,记录图信息,记录连接信息
bool p[maxn];//1表示点已经在树的,0表示点在树外
bool f[maxn][maxn];
int Prim(int n)
{
int i,j,k,Min,ans=;
for(i=;i<=n;i++)
{
p[i]=;
dist[i]=Map[][i];
pre[i]=;
}
dist[]=;
p[]=;
for(i=;i<n;i++)
{
Min=INF;
k=;
for(j=;j<=n;j++)
{
if(!p[j]&&dist[j]<Min)
{
Min=dist[j];
k=j;
}
}
if(k==) return -;//G不连通
ans+=Min;
p[k]=;
for(j=;j<=n;j++)
{
if(!p[j]&&Map[k][j]!=INF&&dist[j]>Map[k][j])
{
dist[j]=Map[k][j];
pre[j]=k;
}
}
}
return ans;
}
Kruskal
const int N=,M=;
int f[N],r[N];
struct node
{
int x, y, len;
}e[M];
int cmp(const node &a,const node &b)
{
return a.len<b.len;
}
void Make_Set(int n)
{
for(int i=;i<=n;i++)
{
f[i]=i;
r[i]=;
}
}
void Link(int a, int b)
{
if (r[a] > r[b]) {f[b] = a; r[a]+=r[b];}
else {f[a] = b; r[b]+=r[a];}
}
int Find_Set(int a)
{
if (a == f[a]) return a;
else return f[a] = Find_Set(f[a]);
}
int Kruskal(int n,int m)
{
int i, j, k, s=,cn=;
Make_Set(n);
sort(e,e+m,cmp);
for(i=;i<m;i++)
{
j=Find_Set(e[i].x);
k=Find_Set(e[i].y);
if(j!=k) {
Link(j,k);
s+=e[i].len;
cn++;
}
if(cn==n-) break;
}
if(cn<n-) return -;
return s;
}
二、次小生成树
1、最朴素的办法就是暴力枚举。求一遍最小生成树以后,依次删除最小生成树上的边,求此时的最小生成树,值最小的就是我们所求的。总共要运行 n次最小生成树算法,时间复杂度为n倍最小生成树算法的复杂度。有些题目能AC。暴力枚举在没有好方法时,也是一种非常好的方法。
2、我们知道,添加任意一条不在最小生成树上的边以后一定会形成一个环。找到环上除了(u,v)以外的权值最大的边,把它删掉,那么此时生成树就可能是次小生成树。基于这种思想:首先求出原图最小生成树,权值之和为MST。在执行最小最小生成树算法的时候,记录任意两点(u,v)路径之间的最大权值,maxe[u][v]。注意,不是记录以u与v为起点和终点的边的最大值,是两点之间的路径,可能有多个点在它们之间。然后枚举每一条边,如果该边不是原本最小生成树上的边,就用MST - maxe[u][v] + Map[u][v],统计结果最小的。
需要知道的是次小生成树的权值和可以与最小生成树的权值和相等。
模版:
prim(直接贴poj1679的代码了)
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std; const int maxn=,INF=0x3f3f3f3f;
int dist[maxn],Map[maxn][maxn],pre[maxn],maxe[maxn][maxn];
//向外延伸的最短边长,记录图信息,记录连接信息,最长边
bool vis[maxn];//1表示点已经在树的,0表示点在树外
bool f[maxn][maxn];//存在边为1,用过为0
int n,m;//顶点数目
int Prim()
{
int i,j,k,Min,ans=;
memset(vis,,sizeof(vis));
memset(f,,sizeof(f));
memset(maxe,,sizeof(maxe));
for(i=;i<=n;i++)
{
dist[i]=Map[][i];
pre[i]=;
}
dist[]=;
vis[]=;
pre[]=;
for(i=;i<n;i++)
{
Min=INF;
k=;
for(j=;j<=n;j++)
{
if(!vis[j]&&dist[j]<Min)
{
Min=dist[j];
k=j;
}
}
if(Min==INF) return -;//G不连通
vis[k]=;
ans+=Min;
f[pre[k]][k] = f[k][pre[k]]=;
for(j=;j<=n;j++)
{
if(vis[j]) maxe[k][j]=maxe[j][k]=max(maxe[j][pre[k]],dist[k]);
if(!vis[j]&&dist[j]>Map[k][j])
{
dist[j]=Map[k][j];
pre[j]=k;
}
}
}
return ans;
}
void init()
{
for(int i=;i<=n;i++)
for(int j=;j<=n;j++)
{
if(i==j)Map[i][j]=;
else Map[i][j]=INF;
}
}
int main()
{
//freopen("test.txt","r",stdin);
int i,j,k,a,b,c,ca;
scanf("%d",&ca);
while(ca--)
{
scanf("%d%d",&n,&m);
init();
for(i=;i<m;i++)
{
scanf("%d%d%d",&a,&b,&c);
Map[a][b]=Map[b][a]=c;
}
int ans=Prim();
if(ans==-) {printf("Not Unique!\n");continue;}
int res=INF;
for(i=;i<=n;i++)
{
for(j=i+;j<=n;j++)
{
if(!f[i][j]&&Map[i][j]!=INF)
{
res=(res,ans+Map[i][j]-maxe[i][j]);
}
}
}
if(res==ans) printf("Not Unique!\n");
else printf("%d\n",ans);
}
return ;
}
kruskal
//O(mlogm+n^2)
const int N=,M=,INF=0x3f3f3f3f;
int f[N],r[N];
int Find(int x)
{
if(x==f[x]) return x;
return f[x]=Find(f[x]);
}
void Link(int x,int y)
{
int a=Find(x), b=Find(y);
if(a!=b)
{
f[b]=a;
r[a]+=r[b];
}
}
struct node
{
int u,v,w;
bool select;
}edge[M];
bool cmp(const node &a, const node &b)
{
if(a.w!=b.w) return a.w<b.w;
if(a.u!=b.u) return a.u<b.u;
return a.v<b.v;
}
struct node1
{
int to,next;
}e[N];
int cnt,tot,head[N],en[N],maxe[N][N];
//e的边数、头结点、尾结点、两点在mst中的最大边
void Kruskal(int n,int m)
{
int k=, i,j,t;
for(cnt=;cnt<n;cnt++)
{
e[cnt].to=cnt+;
e[cnt].next=head[cnt+];
en[cnt+]=cnt;
head[cnt+]=cnt;
}
sort(edge,edge+m,cmp);
for(i=;i<m;i++)
{
if(k==n-) break;
if(edge[i].w<) continue;
int a=Find(edge[i].u), b=Find(edge[i].v);
if(a!=b)
{
for(j=head[a];j!=-;j=e[j].next){
for(t=head[b];t!=-;t=e[t].next){
int u=e[j].to, v=e[t].to;
maxe[u][v]=maxe[v][u]=edge[i].w;
}
}
e[en[b]].next=head[a];
en[b]=en[a];
Link(a,b);
k++;
edge[i].select=;
}
}
}
void addedge(int u,int v,int w)
{
edge[tot].u=u;edge[tot].v=v;edge[tot].w=w;tot++;
}
void init()
{
tot=;
memset(head,-,sizeof(head));
memset(en,-,sizeof(en));
for(int i=;i<N;i++){
edge[i].select=;
f[i]=i;
r[i]=;
}
}
int main()
{
//freopen("test.txt","r",stdin);
int mst,secmst;
Kruskal(n,m);
mst=;
for(i=;i<m;i++){
if(edge[i].select) mst+=edge[i].w;
}
secmst=INF;
for(i=;i<m;i++){
if(!edge[i].select)
secmst=min(secmst,mst+edge[i].w-maxe[edge[i].u][edge[i].u]]);
} return ;
}
算法的不同,当然也继承了该算法的特点。Prim不能存重边。这是需要注意的。
题目:Hdu2489 poj1679
hdu4081http://www.cnblogs.com/Potato-lover/p/3938482.html
三、有向图的最小树形图
解决的问题:一水源给菜地供水,水只能从高处向低处流,修水渠需要一定的花费,问怎样才能使得花费最低。
边是有方向的,与最小生成树不同,这可以理解为有向图的最小生成树。
算法是朱刘算法,难得的以国人名字命名的算法。
邻接矩阵建图,时间复杂度为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开始的。
/*newpos[i]表示深度优先遍历序列的第i个点是哪个点,
now表示当前深度优先遍历序列已经有多少个点了。select[]用于
深度优先遍历的判重,p[i]表示点i的父节点的编号。对于greedy(),
s[i]为1表示第i个点被覆盖。se[i]表示点i属于要求的点集*/ const int N = ;
struct node
{
int to, w, next;
}e[N*N];
int head[N],tot,now;
int p[N], newpos[N] ;
bool select[N];
bool s[N],se[N];
//深度优先遍历,得到深度优先遍历序列
void dfs(int x)
{
newpos[now++] = x;
int k;
for(k=head[x]; k!=-; k=e[k].next)
{
if(!select[e[k].to])
{
select[e[k].to]= ;
p[e[k].to]=x;
dfs(e[k].to);
}
}
}
//最小支配集
int greedy1()
{
memset(s,,sizeof(s));
memset(se,,sizeof(se));
int ans=;
int i;
for(i=now-; i>=; i--)
{
int t=newpos[i];
if(!s[t])
{
if(!se[p[t]])
{
se[p[t]]=;
ans++;
}
s[t] = ;
s[p[t]] = ;
s[p[p[t]]] = ;
}
}
return ans;
}
//最小点覆盖
int greedy2()
{
memset(s,,sizeof(s));
memset(se,,sizeof(se));
int ans=;
int i;
for(i=now-; i>=; i--)
{
int t=newpos[i];
if(!s[t]&&!s[p[t]])
{
se[p[t]]=;
ans++;
s[t] = ;
s[p[t]] = ;
}
}
return ans;
}
//最大独立集
int greedy3()
{
memset(s,,sizeof(s));
memset(se,,sizeof(se));
int ans=;
int i;
for(i=now-; i>=; i--)
{
int t=newpos[i];
if(!s[t])
{
se[t]=;
ans++;
s[t] = ;
s[p[t]] = ;
}
}
return ans;
}
void addedge(int i,int j)
{
e[tot].to=j;e[tot].next=head[i];head[i]=tot++;
}
void init()
{
tot=now=;
memset(head,-,sizeof(head));
memset(select, , sizeof(select));
}
int main()
{
/* 读入图*/
select[]=;
p[]=;
dfs();
printf("%d\n",greedy());
return ;
}