序论
我决定开始对我所学过的知识点进行一个复习,由初级图论开始,向上学习的同时也需要巩固基础知识。
目录
图的遍历——前向星
路径问题
Dijkstra
SPFA
匹配
匈牙利算法
KM算法
树
最小生成树
Prime/Kruskal
树的直径
图的遍历——前向星
我们知道在STL中可以用“vector<int> vt[maxN];”这样的方式去存对应的vt[u]的后面的所有的指向的点,也就是"u->v"这样的点的指向。
但是我们不难看出对于那种需要初始化的多组输入,每次要清空vector<>的vt[]全部的数组,这是很要命的一件事,这么高的时间复杂度,还有这么大的空间复杂度,吃不消啊……
所以,我们引入了一个叫做链式前向星的东西,算不上是一种算法,因为它太基础了。简单的说,对应的点,找之前的边,所以,我们将边存在一个struct的结构体里,然后我们不断的去新建边“u->v, 同时继承上一个边的序号”,去将边的序号更新进所存的点去。我们定义这样的存边的点,给予一个数组head[maxN],是所有的点的上一个以它为u的边的序号。
具体这样子的理解有点复杂,看下代码做下模拟试试。
const int maxN = 1e3 + 7, maxE = 2e5 + 7;
这些都是开始的定义的。
int cnt, head[maxN];
这个就是初始化。时间复杂度O(N)
void init()
{
cnt = 0;
memset(head, -1, sizeof(head));
}
定义这样的结构体,nex是指上一个head[]这样我们就可以去找到上一条边的序号了;to是"u->v"这里的v;flow有时候并不需要这个定义,它只是用来存某种价值的东西,譬如"u->v的代价是flow"这样子的情况,用来做一些最段路之类的会用到。
struct Eddge
{
int nex, to, flow;
Eddge(int a=-1, int b=0, int c=0):nex(a), to(b), flow(c) {}
}edge[maxE];
接下去就是建边的操作了,可以理解一下这个addEddge(u, v, flow)的操作(有时候不需要用到flow)。
void addEddge(int u, int v, int flow)
{
edge[cnt] = Eddge(head[u], v, flow);
head[u] = cnt++;
}
举个例子,下面是建立"u->v"权值为1的边。
_add(u, v, 1);
对应的从u出发的节点,我们找这样的v,就可以这样子找了。
for(int i=head[u], v, f; ~i; i=edge[i].nex)
{
v = edge[i].to; f = edge[i].flow;
/*
这里放一些自己的操作。
*/
}
自己多多理解啦,就讲到这里,不懂的可在评论提问。
Dijkstra
狄杰斯特拉算法,主要就是讲解从一点出发,达到其余各点的最短路的算法。我们从一个点出发,更新它到其余各点的距离,然后我们接下去从剩下的N-1个节点中不断的找寻出被到达的最短的距离,然后取出其中被到达的最短距离,在以它开始更新它所能到达的其余点的最短距离,然后再选出一个最短的点,不断的进行这样的循环操作,我们不难发现,只需要取出N-1次的点,就能把所有的点的由起点出发到达的最短距离给求出来了。
这里,我引入这道题来做解释。附上了对应的注释。之后的算法讲解都是以这道题为模板展开。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <limits>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <map>
#define lowbit(x) ( x&(-x) )
#define pi 3.141592653589793
#define e 2.718281828459045
#define efs 1e-8
#define INF 0x3f3f3f3f
#define HalF (l + r)>>1
#define lsn rt<<1
#define rsn rt<<1|1
#define Lson lsn, l, mid
#define Rson rsn, mid+1, r
#define QL Lson, ql, qr
#define QR Rson, ql, qr
#define myself rt, l, r
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int maxN = 55;
int N;
double sx, sy, tx, ty, S, T, mp[maxN][maxN], dis[maxN];
struct node
{
double x, y, r;
node(double a=0, double b=0, double c=0):x(a), y(b), r(c) {}
}a[maxN];
double Get_dis(int e1, int e2) { return sqrt( (a[e1].x - a[e2].x) * (a[e1].x - a[e2].x) + (a[e1].y - a[e2].y) * (a[e1].y - a[e2].y) ) - a[e1].r - a[e2].r; } //对应的圈的距离
bool In_round(double x, double y, double rx, double ry, double r) { return fabs( sqrt( (rx - x) * (rx - x) + (ry - y) * (ry - y) ) - r ) <= efs; } //圈内
bool vis[maxN];
double Dijkstra(int st, int ed) //起点、终点
{
memset(vis, false, sizeof(vis)); //所有的点只能被取出来一次,因为那时候,它一定是最小点
vis[st] = true;
for(int i=1; i<=N; i++) dis[i] = mp[st][i];
int pos = 0;
for(int line=1; line<N; line++)
{
double minn = 1e9 + 7.;
for(int i=1; i<=N; i++)
{
if(!vis[i] && minn > dis[i]) //寻找这样的点
{
pos = i;
minn = dis[i];
}
}
vis[pos] = true; //好了,取出来过了
for(int i=1; i<=N; i++) if(!vis[i]) dis[i] = min(dis[i], dis[pos] + mp[pos][i]); //其实可以不用判断是否被取出来过,因为每次取出来的都是最小的点,不可能能把已经取出来的点再更新
}
return dis[ed];
}
int main()
{
int Cas; scanf("%d", &Cas);
while(Cas--)
{
scanf("%d", &N);
scanf("%lf%lf%lf%lf", &sx, &sy, &tx, &ty);
S = 0; T = 0;
for(int i=1; i<=N; i++)
{
scanf("%lf%lf%lf", &a[i].x, &a[i].y, &a[i].r);
if(!S && In_round(sx, sy, a[i].x, a[i].y, a[i].r)) S = i;
if(!T && In_round(tx, ty, a[i].x, a[i].y, a[i].r)) T = i;
for(int j=1; j<i; j++)
{
mp[i][j] = mp[j][i] = Get_dis(i, j); //定义距离,i<->j距离(双向边)
}
}
printf("%.6lf\n", Dijkstra(S, T));
}
return 0;
}
总结,Dijkstra是一个没有优化的算法,之后的Prime算法(最小生成树)与它很像,时间复杂度O(N^2),有时候需要优化。
优先队列优化的Dijkstra
就像是写优先队列下的BFS一样,我们在这里用在了Dijkstra上面,我们不断的去更新最短的距离,每次出队的是最短的节点,这样子就可以避免了上面那次O(N)的查询操作,我们将查询操作优化到了O(log(N))这样子的时间复杂度,使得最后的时间总和的复杂度为O(N*logN)。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <limits>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <map>
#define lowbit(x) ( x&(-x) )
#define pi 3.141592653589793
#define e 2.718281828459045
#define efs 1e-8
#define INF 0x3f3f3f3f
#define HalF (l + r)>>1
#define lsn rt<<1
#define rsn rt<<1|1
#define Lson lsn, l, mid
#define Rson rsn, mid+1, r
#define QL Lson, ql, qr
#define QR Rson, ql, qr
#define myself rt, l, r
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int maxN = 55;
int N;
double sx, sy, tx, ty, S, T, mp[maxN][maxN], dis[maxN];
struct node
{
double x, y, r;
node(double a=0, double b=0, double c=0):x(a), y(b), r(c) {}
}a[maxN];
double Get_dis(int e1, int e2) { return sqrt( (a[e1].x - a[e2].x) * (a[e1].x - a[e2].x) + (a[e1].y - a[e2].y) * (a[e1].y - a[e2].y) ) - a[e1].r - a[e2].r; } //对应的圈的距离
bool In_round(double x, double y, double rx, double ry, double r) { return fabs( sqrt( (rx - x) * (rx - x) + (ry - y) * (ry - y) ) - r ) <= efs; } //圈内
struct Point
{
int id;
double dis;
Point(int a=0, double b=0):id(a), dis(b) {}
friend bool operator < (Point e1, Point e2) { return e1.dis > e2.dis; }
};
priority_queue<Point> Q;
bool vis[maxN];
double Dijkstra(int st, int ed) //起点、终点
{
while(!Q.empty()) Q.pop();
memset(vis, false, sizeof(vis));
Q.push(Point(st, 0));
for(int i=1; i<=N; i++) dis[i] = 1e9 + 7.;
dis[st] = 0.;
while(!Q.empty())
{
Point tmp = Q.top(); Q.pop();
if(vis[tmp.id]) continue;
vis[tmp.id] = true;
if(tmp.id == ed) return tmp.dis;
for(int i=1; i<=N; i++)
{
if(!vis[i] && dis[i] > dis[tmp.id] + mp[tmp.id][i])
{
dis[i] = dis[tmp.id] + mp[tmp.id][i];
Q.push(Point(i, dis[i]));
}
}
}
return 0;
}
int main()
{
int Cas; scanf("%d", &Cas);
while(Cas--)
{
scanf("%d", &N);
scanf("%lf%lf%lf%lf", &sx, &sy, &tx, &ty);
S = 0; T = 0;
for(int i=1; i<=N; i++)
{
scanf("%lf%lf%lf", &a[i].x, &a[i].y, &a[i].r);
if(!S && In_round(sx, sy, a[i].x, a[i].y, a[i].r)) S = i;
if(!T && In_round(tx, ty, a[i].x, a[i].y, a[i].r)) T = i;
for(int j=1; j<i; j++)
{
mp[i][j] = mp[j][i] = Get_dis(i, j); //定义距离,i<->j距离(双向边)
}
}
printf("%.6lf\n", Dijkstra(S, T));
}
return 0;
}
这里面,我只改变了Dijkstra函数内的内容,可以自己体会一下代码。
SPFA(Shortest Path Faster Algorithm)
同样是求最短路的算法,SPFA有时候会比Dijkstra优化,我依然是去用dis[]数组去记录每个节点的最短路径的估计值,这里放的有时候不是最短的,因为我们要去松弛它,我们假设从一开始到达的就是最短,然后不断的跑下去,遇到更短的,如果不在队列内就放进去,如果在队列内,同样要去更新它的松弛的估计值。我们需要不断的去更新它的最短路径估计值,那么什么时候会停止呢,就是在当所有点都更新成最短路的时候。所以,一定会有从松弛变成紧锁的时候。
具体,我们看着代码这么分析一下。同样的,在这里我只改变了函数,其他各项我都保持原来的没动。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <limits>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <map>
#define lowbit(x) ( x&(-x) )
#define pi 3.141592653589793
#define e 2.718281828459045
#define efs 1e-8
#define INF 0x3f3f3f3f
#define HalF (l + r)>>1
#define lsn rt<<1
#define rsn rt<<1|1
#define Lson lsn, l, mid
#define Rson rsn, mid+1, r
#define QL Lson, ql, qr
#define QR Rson, ql, qr
#define myself rt, l, r
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int maxN = 55;
int N;
double sx, sy, tx, ty, S, T, mp[maxN][maxN], dis[maxN];
struct node
{
double x, y, r;
node(double a=0, double b=0, double c=0):x(a), y(b), r(c) {}
}a[maxN];
double Get_dis(int e1, int e2) { return sqrt( (a[e1].x - a[e2].x) * (a[e1].x - a[e2].x) + (a[e1].y - a[e2].y) * (a[e1].y - a[e2].y) ) - a[e1].r - a[e2].r; } //对应的圈的距离
bool In_round(double x, double y, double rx, double ry, double r) { return fabs( sqrt( (rx - x) * (rx - x) + (ry - y) * (ry - y) ) - r ) <= efs; } //圈内
struct Point
{
int id;
double dis;
Point(int a=0, double b=0):id(a), dis(b) {}
friend bool operator < (Point e1, Point e2) { return e1.dis > e2.dis; }
};
bool inque[maxN];
queue<int> Q;
double SPFA(int st, int ed) //起点、终点
{
memset(inque, false, sizeof(inque)); //一开始都在队列外面,如果在队内,则是true
while(!Q.empty()) Q.pop();
for(int i=1; i<=N; i++) dis[i] = 1e9 + 7.;
Q.push(st); inque[st] = true; dis[st] = 0.; //进队了
while(!Q.empty())
{
int u = Q.front(); Q.pop(); inque[u] = false; //它又出队了,不在队列内,则是false
for(int i=1; i<=N; i++)
{
if(dis[i] > dis[u] + mp[u][i]) //松弛的操作,不断的去更新点
{
dis[i] = dis[u] + mp[u][i];
if(!inque[i])
{
inque[i] = true;
Q.push(i);
}
}
}
}
return dis[ed];
}
int main()
{
int Cas; scanf("%d", &Cas);
while(Cas--)
{
scanf("%d", &N);
scanf("%lf%lf%lf%lf", &sx, &sy, &tx, &ty);
S = 0; T = 0;
for(int i=1; i<=N; i++)
{
scanf("%lf%lf%lf", &a[i].x, &a[i].y, &a[i].r);
if(!S && In_round(sx, sy, a[i].x, a[i].y, a[i].r)) S = i;
if(!T && In_round(tx, ty, a[i].x, a[i].y, a[i].r)) T = i;
for(int j=1; j<i; j++)
{
mp[i][j] = mp[j][i] = Get_dis(i, j); //定义距离,i<->j距离(双向边)
}
}
printf("%.6lf\n", SPFA(S, T));
}
return 0;
}
SPFA的时间复杂度,最坏是O(E*V),E代表边的数目,V代表点的数目。当然,这是最坏的情况。往往得往好的方面想,不是吗~
匈牙利算法
一些概念性的问题总结在这里,可以去背,但是最好是去理解着记忆,在后继的中级甚至是高级图论中会很有用。
匈牙利算法是由匈牙利数学家Edmonds于1965年提出,因而得名。匈牙利算法是基于Hall定理中充分性证明的思想,它是部图匹配最常见的算法,该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法。
-------等等,看得头大?那么请看下面的版本:
通过数代人的努力,你终于赶上了剩男剩女的大潮,假设你是一位光荣的新世纪媒人,在你的手上有N个剩男,M个剩女,每个人都可能对多名异性有好感(-_-||暂时不考虑特殊的性取向),如果一对男女互有好感,那么你就可以把这一对撮合在一起,现在让我们无视掉所有的单相思(好忧伤的感觉),你拥有的大概就是下面这样一张关系图,每一条连线都表示互有好感。
本着救人一命,胜造七级浮屠的原则,你想要尽可能地撮合更多的情侣,匈牙利算法的工作模式会教你这样做:
===============================================================================
一: 先试着给1号男生找妹子,发现第一个和他相连的1号女生还名花无主,got it,连上一条蓝线
===============================================================================
二:接着给2号男生找妹子,发现第一个和他相连的2号女生名花无主,got it
===============================================================================
三:接下来是3号男生,很遗憾1号女生已经有主了,怎么办呢?
我们试着给之前1号女生匹配的男生(也就是1号男生)另外分配一个妹子。
(黄色表示这条边被临时拆掉)
与1号男生相连的第二个女生是2号女生,但是2号女生也有主了,怎么办呢?我们再试着给2号女生的原配()重新找个妹子(注意这个步骤和上面是一样的,这是一个递归的过程)
此时发现2号男生还能找到3号女生,那么之前的问题迎刃而解了,回溯回去
2号男生可以找3号妹子~~~ 1号男生可以找2号妹子了~~~ 3号男生可以找1号妹子
所以第三步最后的结果就是:
===============================================================================
四: 接下来是4号男生,很遗憾,按照第三步的节奏我们没法给4号男生腾出来一个妹子,我们实在是无能为力了……香吉士同学走好。
===============================================================================
这就是匈牙利算法的流程,其中找妹子是个递归的过程,最最关键的字就是“腾”字
其原则大概是:有机会上,没机会创造机会也要上
题目链接
#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<vector>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=505;
int line[N][N];
int girl[N],used[N];
int k,m,n;
bool found(int x)
{
for(int i=1; i<=n; i++)
{
if(line[x][i]&&!used[i])
{
used[i]=1;
if(girl[i]==0||found(girl[i]))
{
girl[i]=x;
return 1;
}
}
}
return 0;
}
int main()
{
int x,y;
while(scanf("%d",&k)&&k)
{
scanf("%d %d",&m,&n);
memset(line,0,sizeof(line));
memset(girl,0,sizeof(girl));
for(int i=0; i<k; i++)
{
scanf("%d %d",&x,&y);
line[x][y]=1;
}
int sum=0;
for(int i=1; i<=m; i++)
{
memset(used,0,sizeof(used));
if(found(i)) sum++;
}
printf("%d\n",sum);
}
return 0;
}
匈牙利算法,就是一个不断的找可以匹配的,如果我匹配不了,那就尝试让已经匹配上的人换人这样一个霸道总裁式的算法。