初级图论

序论

  我决定开始对我所学过的知识点进行一个复习,由初级图论开始,向上学习的同时也需要巩固基础知识。


目录

图的遍历——前向星

路径问题

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;
}

 

  匈牙利算法,就是一个不断的找可以匹配的,如果我匹配不了,那就尝试让已经匹配上的人换人这样一个霸道总裁式的算法。


最小生成树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wuliwuliii

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值