进来也不是一两天了,成为一个OIer的日子也过了许久。那么就谈谈自己对最近所学的心得吧。
图(she)论(pi)
遍历与最短路
Floyd
神奇的三重循环,一层套一层,适用于500个点以内的题目。
- 关于floyd
- k循环一定要套在外面!!!
- 看到500别冲动,抑制住你的兴奋和激动,500点的不一定是floyd。
- 关于floyd
献上代码
//N为点数
void floyd()
{
for(int k=1;k<=N;k++)
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
if(a[i][k]+a[k][j]<a[i][j])
a[i][j]=a[i][k]+a[k][j];
//floyd能把整个图弄好,so cool right?
}
Dijkstra
同样是个神奇的东西它能计算某一顶点到其它所有顶点的最短路径。时间复杂度为O(n^2),用斐波那契堆的复杂度O(E+n*lg n)
- 关于Dijkstra
- 用了迪哥的图不能有负权,否则就GG了。
- 算法思想:
(1). 将所有的点放入两个集合内,并将初始点start放入集合1。
(2).在集合2中找一个到start距离最近的顶点k ,距离=d[k]。
(3).把顶点k加到集合1中,同时修改集合2 中的剩余顶点j的d[j]是否经过k后变短。如果变短修改d[j]。
(4).重复(2),直至集合2空为止。 - 迪哥是图灵奖的获得者…………嗯。
- 关于Dijkstra
——————————–
———————————–
- 献上代码
const int maxm=20011//总之定义一个很大的数
int dis[maxm];
bool flag[maxm]={};
void Diskstra(int st)//st为起点作最短路
{
for(int i=1;i<=N;i++)
dis[i]=a[st][i];
flag[st]=1;
dis[st]=0;
for(int i=1;i<=n;i++)
{
int min=1000000,k=0;
for(int j=1;j<=N;j++);
if(!flag[j]&&dis[j]<min)
min=dis[j],k=j;
if(k==0) return;
flag[k]=1;
for(int j=1;j<=N;j++)
if(!flag[j]&&dis[k]+a[k][j]<dis[j])
dis[j]=dis[k]+a[k][j];//三角形迭代,更新最短路
}
}
.
.
.
.
.
//主程序省略掉
cout<<dis[end];//end为终点
Bellman_Ford
- 同样为单源最短路,对于迪哥算法来说出现负权是件很尴尬的事情,于是乎,就应运而生了BF这个神(gui)奇(chu)的东西(其实图论的东西都很神奇)。
- 这个通过三角形的迭代进行松弛,直到松不了为止。时间复杂度为O(n*E)
(E为边数)。
- 关于Bellman_Ford
- 它能够确定是否有负权回路。
- 算法思想:
(1).初始化每点到s点的最短距离为∞
(2).取所有边(x,y),看x能否对y松驰。
(3).如果没有任何松驰,则结束break。
(4).如果松驰次数<N,转(2)。
(5).如果第n次还能松弛,图中有“负圈”。 - 注意事项:
(1).用有序点对(x,y)记录边时,可直接取边。但要请注意对无向图,要注意(y,x)也要松驰。
(2).对于求s到某点t的最短距离,可能因为其它地方有“负环”而出现问题,要预处理。
- 关于Bellman_Ford
- 献上代码
const int maxm=20011;
struct node{
int head,tail,v;
};
node bian;
int dis[maxm];
bool flag=0;
int Bell_Ford(int st)//st为起点
{
memset(dis,10,sizerof(dis));
dis[st]=0;
for(int i=1;i<=N;i++)//N为点数
{
flag=0;
for(int j=1;j<=num;j++)//num为边数
if(dis[bian[j].head]+bian[j].v<dis[a[j].tail])//head为之前的点,v是边的权值,tail就是后面的
{
dis[a[j].tail]=d[a[i].head]+a[i].v;
flag=1;//标记迭代
}
if(!flag)
return dis[end];//end为终点
}
return -1;
}
SPFA
- Bellman-ford算法中,每次都要检查所有的边。这个看起来比较浪费,对于边(x,y),如果上一次dis[x]没有改变,则本次的检查显然是多余的。这SPFA算是对Bellman_Ford的优化吧。
我们每次只要从上次刚被“松驰”过的点x,来看看x能不能松驰其它点即可。
SPFA算法中用BFS中的队列来存放刚被“松驰”过的点。由于顶点个数为|V|,队列如果用数组的话显然要用“循环队列”使用空间。 - SPFA在形式上和宽度优先搜索非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身会再次被改进,于是再次用来改进其它的点,这样反复迭代下去。
—————————-(怎么突然间这么理论了…………)—————————–
- 关于SPFA
好像没什么要讲的……………………
- 关于SPFA
- 献上代码
const int maxm;
int dis[maxm],duilie[maxm],;
bool flag[maxm]={};
void spfa(int st)
{
memset(dis,10,sizeof(dis));
int head=0,tail=1;//队头队尾
dis[st]=0;
duilie[tail]=st;//看名字就知道是队列了
flag[st]=1;
while(head<=tail)
{
int tn=duilie[++head];
for(int i=linkk[tn];i;i=a[i].next)//邻接表储存……
{
int temp=a[i].y;
if(dis[tn]+a[i].v<dis[temp])
{
dis[temp]=dis[tn]+a[i].v;
if(!flag[temp])
flag[temp]=1,duilie[++tail]=temp;
}
}
flag[tn]=0;
}
}
.
.
.
.
cout<<dis[end];//不解释
最小生成树(MST)
一些MST的属性
- 剪切属性:
在图中,剪切将顶点划分成两个不相交集合。交叉边为这些顶点在两个不同集合的边。对于任何一个剪切,各条最小的交叉边都属于某个MST,且每个MST中都包含一条最小交叉边。 - 环属性:
一棵生成树上,增加一条边e,再删除e所在环上的最大边,会得到另一棵“更好”的生成树(如果e不是最大边) - 最小边原则:
图中权值最小的边(如果唯一的话)一定在最小生成树上。 - 唯一性:
一棵生成树上,如果各边的权都不相同,则最小生成树是唯一的。反之不然。
prim
- 和迪哥超级像的的一个算法
- 关于prim
- 算法思想 :
(1).将G(树)剪切成两个集合A、B;A中只有一个点r。
(2).取最小权的交叉边(x,y),x∈A, y∈B。
(3).将y加入A
(4).如果已经加了n-1条边,结束。否则,转 (3)。 - 算法要点:
每次求最小权交叉边时,如果都重新计算,则显然要枚举(x,y)— x∈A ,y∈B。O(n^2)时间复杂度。
其实每次A中只是增加一个新顶点v,最多有交叉边(v,y),修改量只有与v有边的顶点,为O(n)。
只需记录下B中的每个元素y与A所有元素中最小权边,则求最小值最多为O(n)—有时可以用“堆”优化。(stl的set)
- 算法思想 :
- 关于prim
- 献上代码
const int maxm=20011;
bool flag[maxm]={};
int dis[maxm],ans;
void prim()
{
memset(dis,10,sizeof(dis);
//还要给dis数组赋个距离啊什么的
dis[1]=0;
flag[1]=1;
for(int i=1;i<n;i++)
{
int minn=dis[0],k=0;//这个其实很骚的,自己体会
for(int j=1;j<=n;j++)
if(!flag[j]&&dis[j]<minn)
minn=dis[j],k=j;
flag[k]=1;
ans+=dis[k];//ans为最小生成树的距离总和
for(int j=1;j<=n;j++)
if(flag[j]!=1&&dis[j]>a[k][j])
dis[j]=a[k][j];
}
}
Kruskal
- 按边来的MST
- 关于Kruskal
- 算法思想:
(1).将G所有条边按权从小到大排序;图mst开始为空
(2).从小到大次序取边(x,y)
(3).若加入边(x,y),mst就有环,则放弃此边,转(2)
(4).将边(x,y)加入mst,如果已经加了n-1条边,结束。否则,转 (2) - 算法要点:
Kruskal算法的最难点在于怎样判断加入边(x,y)后是否形成了环。
判断边(x,y)的两个顶点x,y在图(实际是森林)MST中最否已经连通。如果已经连通,加入边将形成环;否则,不形成环。
ps:连通点集之类问题,有高效算法—并查集(翔哥的找爸爸)。
- 算法思想:
- 关于Kruskal
struct Road{ //简易的定义一条边
int a, b; //一条边的两个顶点
int w; //边的权值
};
Road road[m];
int v[maxSize] = {0}; //定义并查集数组
int getRoot(int a) //在并查集中找根节点的函数
{
while(a != v[a]) a = v[a]; //自己为自己双亲时,为根节点
return a;
}
void Kruskal(MGraph g, int &sum,Road road[]) //克鲁斯卡尔方法
{
int i,a,b;
sum=0;
for(i=0;i<g.n;++i) v[i]=i; //各自为根节点(一棵树)
sort(road,g.e); //对所有边进行排序
for(i=0;i<g.e;++i) //排过序的边依次判断
{
a=getRoot(road[i].a);
b=getRoot(road[i].b);
if(a!=b) //不同根时,将该边并入(此时不会形成回路)
{
v[a]=b; // b成为a的双亲结点
sum+=road[i].w;
}
}
}