暑假练习(三) 图论

暑假练习(三) 图论

1.最短路

在这里插入图片描述

1.1 Floyd算法

Stockbroker Grapevine POJ - 1125
理解了题意即可,几乎是模板题

#include<stdio.h>
#include<string.h>
#include<iostream>
#include<algorithm>
#define INF 0x3f3f3f3f
using namespace std;
const int N=110;
int n,map[N][N]; 

void floyd()
{
    for(int t=1;t<=n;t++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                map[i][j]=min(map[i][j],map[i][t]+map[t][j]);
}
int main()
{
    while(1)
    {
        cin>>n;
        if(n==0) break;
        memset(map,INF,sizeof(map)); //记得初始化
        for(int i=1;i<=n;i++)  //输入
        {
            int t,j,x;
            cin>>t;
            while(t--)
            {
                scanf("%d%d",&j,&x);
                map[i][j]=x;
            }
        }
        //floyd算法
        floyd();
        int ans=INF,time;
        for(int i=1;i<=n;i++)
        {
            int tmp=0;
            for(int j=1;j<=n;j++)
            {
                if(i==j) continue;
                if(map[i][j]>tmp)
                    tmp=map[i][j];
            }
            if(ans>tmp)
            {
                ans=tmp;
                time=i;
            }
        }
        printf("%d %d\n",time,ans);
    }
    getchar();getchar();
    return 0;
}  
1.2 Dijkstra求最短路

朴素版(对于稠密图而言用邻接矩阵储存)
模板:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;

const int N=510;
int g[N][N]; //为稠密阵所以用邻接矩阵存储
int dist[N]; //用于记录每一个点距离第一个点的距离
bool st[N]; //用于记录该点的最短距离是否已经确定
int n,m;

int dijkstra()
{
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0; //第一个点到自身的距离为0

    for(int i=0;i<n;i++) //有n个点所以要进行n次迭代
    {
        int t=-1; //t存储当前访问的点
        for(int j=1;j<=n;j++)  //这里的j代表的是从1号点开始
            if(!st[j]&&(t==-1||dist[t]>dist[j])) //不在s集合,并且距离起点最小的点,
                t=j;       //如果没有更新过,则进行更新,发现更短的路径, 则进行更新  
        
        st[t]=true; 

        //找到了距离最小的点t,并用最小的点t去更新其他的点到起点的距离
        for(int j=1;j<=n;j++)   //依次更新每个点所到相邻的点路径值
            dist[j]=min(dist[j],dist[t]+g[t][j]);
    }
    if(dist[n]==0x3f3f3f3f) return -1;   //如果第n个点路径为无穷大即不存在最低路径
    return dist[n];
}
int main()
{
    cin>>n>>m;
    memset(g,0x3f,sizeof(g)); //初始化图 因为是求最短路径,每个点初始为无限大
    while(m--)
    {
        int x,y,z;
        cin>>x>>y>>z;
        g[x][y]=min(g[x][y],z); //如果发生重边的情况则保留最短的一条边
    }
    cout<<dijkstra()<<endl;
    return 0;
}

堆优化版(对于稀疏图用邻接表存储)

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;

typedef pair<int, int> PII;
const int N = 1000010;

// 稀疏图用邻接表来存
int h[N], e[N], ne[N], idx;
int w[N]; // 用来存权重
int dist[N];
bool st[N]; // 如果为true说明这个点的最短路径已经确定

int n, m;

void add(int x, int y, int c)
{
    w[idx] = c; 
    // 有重边也不要紧,假设1->2有权重为2和3的边,再遍历到点1的时候
    //2号点的距离会更新两次放入堆中
    e[idx] = y; 
    // 这样堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值
    //2+x(x为之前确定的最短路径)
    ne[idx] = h[x]; 
    // 标记st为true,所以下一次弹出3+x会continue不会向下执行。
    h[x] = idx++;
}

int dijkstra()
{
    memset(dist, 0x3f, sizeof(dist));
    dist[0] = 1;
    priority_queue<PII, vector<PII>, greater<PII>> heap; // 定义一个小根堆
    // 这里heap中为什么要存pair呢,
    // 首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时    
    // 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
    heap.push({ 0, 1 }); 
    // 这个顺序不能倒,pair排序时是先根据first,再根据second,这里显然要根据距离排序
    while(heap.size())
    {
        PII k = heap.top(); // 取不在集合S中距离最短的点
        heap.pop();
        int ver = k.second, distance = k.first;

        if(st[ver]) continue;
        st[ver] = true;

        for(int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。
            if(dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({ dist[j], j });
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    memset(h, -1, sizeof(h));
    scanf("%d%d", &n, &m);
    while (m--)
    {
        int x, y, c;
        scanf("%d%d%d", &x, &y, &c);
        add(x, y, c);
    }
    cout << dijkstra() << endl;
    return 0;
}

1.3 Bellman–Ford算法

Bellman–Ford主要用于存在负权重的方向图中(没有负权重也可以用,但是效率比Dijkstra低很多),搜索出源点到各个节点的最短路径

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;

const int N=510,M=10010;
struct edge
{
    int a,b,w;
}edge[M]; //把每个边保存下来即可
int dist[N];
int back[N]; //备份数组放置串联
int n,m,k; //k代表最短路径最多包涵k条边

int bell_ford()
{
    memset(dist,0x3f,sizeof(dist));
    dist[1]=1;
    for(int i=0;i<k;i++) //k次循环
    {
        //把dist复制到backup经行遍历松弛,不会产生串联
        memcpy(back,dist,sizeof(dist));
        for(int j=0;j<m;j++) //遍历所有边
        {
            int a=edge[j].a,b=edge[j].b,w=edge[j].w;
            dist[b]=min(dist[b],back[a]+w); //(dist(a) +weight(ab)) < dist(b)则说明存在到b的更短的路径,取最小值 
        }
    }
    if(dist[n]>0x3f3f3f3f/2) return -1;
    else return dist[n];
}
int main()
{
    scanf("%d%d%d",&n,&m,&k); //题目限制,最多经过k条边
    for(int i=0;i<m;i++)
    {
        int a,b,c;  //表示存在一条从点a到点b的有向边,边长为c
        scanf("%d%d%d",&a,&b,&c);
        edge[i]={a,b,c};
    }
    int res=bell_ford();
    if(res=-1) puts("impossible");
    else cout<<res<<endl;
    return 0;
}
1.4 拓扑排序:

定义:将有向图中的顶点以线性方式进行排序。即对于任何连接自顶点u到顶点v的有向边uv,在最后的排序结果中,顶点u总是在顶点v的前面。

不妨考虑一个非常非常经典的例子——选课。我想任何看过数据结构相关书籍的同学都知道它吧。假设我非常想学习一门机器学习的课程,但是在修这么课程之前,我们必须要学习一些基础课程,比如计算机科学概论,C语言程序设计,数据结构,算法等等。那么这个制定选修课程顺序的过程,实际上就是一个拓扑排序的过程,每门课程相当于有向图中的一个顶点,而连接顶点之间的有向边就是课程学习的先后关系。只不过这个过程不是那么复杂,从而很自然的在我们的大脑中完成了。将这个过程以算法的形式描述出来的结果,就是拓扑排序。

那么是不是所有的有向图都能够被拓扑排序呢?显然不是。继续考虑上面的例子,如果告诉你在选修计算机科学概论这门课之前需要你先学习机器学习,你是不是会被弄糊涂?在这种情况下,就无法进行拓扑排序,因为它中间存在互相依赖的关系,从而无法确定谁先谁后。在有向图中,这种情况被描述为存在环路。因此,一个有向图能被拓扑排序的充要条件就是它是一个有向无环图。
在这里插入图片描述
算法实现:
一、从有向图中选取一个没有前驱的顶点,并输出之;
二、从有向图中删去此顶点以及所有以它为尾的弧;
重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。没有前驱 – 入度为零,删除顶点及以它为尾的弧-- 弧头顶点的入度减1。
算法思想:bfs
(1)把所有入度为0的点全部放入队列。
(2)只要有入度为0的节点(队列不为空)则把该点输出,并把以该点t为入度的所有节点的入度减一。
(3)把每个入度被减成0的节点放入队列
模板:(acwing848. 有向图的拓扑序列)

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define INF 0x3f3f3f3f
using namespace std;

const int N=100010;
int n,m; //m个节点,n条边
int h[N],e[N],ne[N],idx; //邻接表,h存储头节点,e存储节点的值,ne存储next值,idx为单链表指针
int q[N]; //队列存储层次遍历序列
int d[N]; //储存i号节点的入度

void add(int a,int b) //在a的邻接表即链表中,插入节点b
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
    // 保存值
    // 每次都是在链表头部插入节点
    // 更新邻接表的表头保存的链表的首节点的索引
}

bool topsort()  //返回布尔序列是否存在, 若存在,则存储在q数组中
{
    //队列是队尾插入元素,队头弹出元素
    int hh=0,tt=-1; //hh表示队头,tt表示队尾
    for(int i=1;i<=n;i++)  //遍历每一个节点, 入度为零则入队
        if(!d[i])
            q[++tt]=i;

    while(hh<=tt)
    {
        int t=q[hh++];  //队列不空,则取出头节点
        for(int i=h[t];i!=-1;i=ne[i]) //遍历头节点的每一个出边
        {
            int j=e[i];
            d[j]--;    //出边能到达的节点,入度减1
            if(d[j]==0)   //如果结点j,入度为0则入队
                q[++tt]=j;
        }
    }
    return tt==n-1;
}
int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof(h));
    //是有向图哦
    for(int i=0;i<m;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);  //插入a指向b的边
        d[b]++;  //注意是a指向b,所以b节点度数加1
    }
    if(topsort())
    {
        for(int i=0;i<n;i++)
            cout<<q[i]<<" ";
        puts(" ");
    }
    else
        puts("-1");

    getchar();getchar();
    return 0;
}  

1.5 最小生成树

Prim算法:
在这里插入图片描述
模板:
Acwing 858. Prim算法求最小生成树

//解题思路:
/*Prim算法求最小生成树的逻辑如下:
1.循环n次,代表向集合中添加n个节点
2.每一次选择距离集合最近的节点
在这里有一个判断就是如果此时还未加入集合中的所有点到集合的距离最小值为负无穷,则说明最小生成树不存在!
3.使用最新加入的这个节点去更新其它节点到集合的距离
*/
//联系:Dijkstra算法是更新到起始点的距离,Prim是更新到集合S的距离
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define INF 0x3f3f3f3f
using namespace std;

const int N=510;
int n,m;
int g[N][N]; //读入连接图
int dist[N]; //存储距离用的数组
bool st[N];

int prim()
{
    memset(dist,INF,sizeof(dist));
    int res=0;

    for(int i=0;i<n;i++)
    {
        int t=-1;
        // 1.找集合之外距离最近的点
        for(int j=1;j<=n;j++)  //开始所有点到集合距离都是无穷,随便选一个,然后用他更新其他点
            if(!st[j]&&(t==-1||dist[j]<dist[t]))  //在当前集合外&&(没有找到||当前比之前存储的小)
                t=j;  
         
        // 2. 标记这个点(并加入集合)
        st[t]=true;

        if(i&&dist[t]==INF) return INF; //如果不是第一个点还距离∞,说明图不连通
        if(i) res+=dist[t]; // 不是第一个点,加入集合
        
        // 3. 用t更新其他点到集合的距离
        for(int j=1;j<=n;j++)
            dist[j]=min(dist[j],g[t][j]);
    }
    return res;
}
int main()
{
    cin>>n>>m;
    memset(g,INF,sizeof(g));

    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;  //输入邻接矩阵
        g[a][b]=g[b][a]=min(g[a][b],c);
    }
    int t=prim();
    if(t==INF) cout<<"impossible"<<endl;
    else cout<<t<<endl;
    getchar();getchar();
    return 0;
} 

Highways POJ - 1751

#include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 755;
struct Node {
    int x, y;
} pt[N]; 
int n, m, u, v, pre[N]; //记录上一个自己到集合的那个点连的边 
double g[N][N], d[N];
bool vis[N];
double getD(int i, int j) {
    double x = pt[i].x - pt[j].x;
    double y = pt[i].y - pt[j].y;
    return sqrt(x*x + y*y); 
}
void prime() {
    for (int i = 1; i <= n; i++) {
        int t = -1;
        for (int j = 1; j <= n; j++) {
            if (!vis[j] && (t == -1 || d[t] > d[j])) t = j;
        } 
        vis[t] = true; //加入集合
        //代表到这条边是新添加的 之前存在的路代表d[t] == 0 
        if (pre[t] && d[t] != 0) printf("%d %d\n", pre[t], t); 
        for (int j = 1; j <= n; j++) {
            if (d[j] > g[t][j]) {
                pre[j] = t; 
                d[j] = g[t][j]; //更新通过那个点与集合相连 
            } 
        }
    } 
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d%d", &pt[i].x, &pt[i].y), d[i] = 1e15;
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            //两两点之间建立一条边
            g[i][j] = g[j][i] = getD(i, j); 
        }
    }
    scanf("%d", &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d%d", &u, &v);
        g[u][v] = g[v][u] = 0; //代表花费0就可以连通 
    }
    prime(); 
    return 0;
}

畅通工程再续 HDU - 1875

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<cmath>
#define INF 0x3f3f3f3f
using namespace std;
const int N=6010;
int n,m;
double g[N][N],dist[N],res;
bool vis[N];
struct Node
{
    int x,y;
}pt[N];
double getd(int i,int j)
{
    double x=pt[i].x-pt[j].x;
    double y=pt[i].y-pt[j].y;
    return sqrt(x*x+y*y);
}

double prime()
{
    
    double res=0;
    for(int i=1;i<=n;i++)
        dist[i]=INF;
    for(int i=0;i<n;i++)
    {
        int t=-1;
        // 1.找集合之外距离最近的点
        for(int j=1;j<=n;j++)  //开始所有点到集合距离都是无穷,随便选一个,然后用他更新其他点
            if(!vis[j]&&(t==-1||dist[j]<dist[t]))  //在当前集合外&&(没有找到||当前比之前存储的小)
                t=j;  
         
        // 2. 标记这个点(并加入集合)
        vis[t]=true;

        if(i&&dist[t]==INF) return -1; //如果不是第一个点还距离∞,说明图不连通
        if(i) res+=dist[t]; // 不是第一个点,加入集合
        
        // 3. 用t更新其他点到集合的距离
        for(int j=1;j<=n;j++)
            dist[j]=min(dist[j],g[t][j]);
    }
    return 100*res;
}

int main()
{
    scanf("%d",&m);
    while(m--)
    {
        scanf("%d",&n);
        memset(vis,0,sizeof(vis));
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                g[i][j]=INF;
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d",&pt[i].x,&pt[i].y);
        }
         
        for(int i=1;i<=n;i++)
        {
            for(int j=i+1;j<=n;j++)
            {
                if(getd(i,j)>=10&&getd(i,j)<=1000)
                    g[i][j]=g[j][i]=getd(i,j);
                else
                    g[i][j]=g[j][i]=INF;
            }
        }
        double t=prime();
        if(t>0) printf("%.1lf\n",t);
        else cout<<"oh!"<<endl;
    }
    getchar();getchar();
    return 0;
}
1.6 染色法判定二分图

二分图的定义
在这里插入图片描述
一个图是二分图的充分必要条件是:
至少有两个顶点,而且要么没有回路,要么只有边数为偶数的回路。

判断二分图的算法
染色法是用于判断二分图的算法,算法流程如下:
步骤一:随便找一个连通块,随便找一个起点,将其染色。
步骤二:接着遍历这个连通块中的点,将这些点染成与相连点相反的颜色。
如果出现矛盾,说明不是二分图。
如果无矛盾:
图中所有的连通块都染好色了,说明这个图是二分图。
还有未染色的连通块,执行步骤一。
Acwing 860. 染色法判定二分图

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#define INF 0x3f3f3f3f
using namespace std;

const int N=100010,M=200010; //无向图, 所以最大边数是2倍
int h[N],e[M],ne[M],idx; //邻接表
int color[N]; //为0代表还未染色省去一个bool数组,1代表染颜色1,2代表染颜色2

void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

bool dfs(int u,int c)
{
    color[u]=c;   //先给自己染色再深搜

    for(int i=h[u];i!=-1;i=ne[i]) //遍历与自己相连的点,尝试染链接边的颜色
    {
        int j=e[i];
        if(!color[j])  //如果color[j]没有染过色
        {
            if(!dfs(j,3-c)) return false; //将其染成相反颜色,若不能成功染色则返回false
        }
        else if(color[j]==c) return false; //如果当前染过颜色且和c相同,返回false
    }
    return true;
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);

    int u,v;
    memset(h,-1,sizeof(h));
    while(m--)
    {
        cin>>u>>v;
        add(u,v),add(v,u); // 无向图
    }

    bool flag=true;
    for(int i=1;i<=n;i++)
    {
        if(!color[i])  //如果未染色
        {
            if(!dfs(i,1))  //如果dfs返回false 说明出现矛盾
            {
                flag=false;
                break;
            }
        }
    }
    if(!flag) puts("No");
    else puts("Yes");
    getchar();getchar();
    return 0;
}  

在这里插入图片描述

1.7 匈牙利算法( 二分图的最大匹配)

算法描述:
如果你想找的妹子已经有了男朋友,
你就去问问她男朋友,
你有没有备胎,
把这个让给我好吧
模板:acwing 861. 二分图的最大匹配

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 510 , M = 100010;
int n1,n2,m;
int h[N],ne[M],e[M],idx;
bool st[N];
int match[N];

void add(int a , int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int find(int x)
{
    //遍历自己喜欢的女孩
    for(int i = h[x] ; i != -1 ;i = ne[i])
    {
        int j = e[i];
        if(!st[j])//如果在这一轮模拟匹配中,这个女孩尚未被预定
        {
            st[j] = true;//那x就预定这个女孩了,只针对当前男生访问过而标记
            //如果女孩j没有男朋友,或者她原来的男朋友能够预定其它喜欢的女孩。配对成功
            if(!match[j]||find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }
    //自己中意的全部都被预定了。配对失败。
    return false;
}
int main()
{
    memset(h,-1,sizeof h);
    cin>>n1>>n2>>m;
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
    }
    int res = 0;
    for(int i = 1; i <= n1 ;i ++)
    {  
        //因为每次模拟匹配的预定情况都是不一样的所以每轮模拟都要初始化
        memset(st,false,sizeof st);
        if(find(i)) res++;
    } 
    cout<<res<<endl;
    getchar();getchar();
    return 0;
}

模板题 过山车 HDU - 2063

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 510 , M = 10010;
int n1,n2,m;
int h[N],ne[M],e[M],idx;
bool st[N];
int match[N];

void add(int a , int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int find(int x)
{
    //遍历自己喜欢的女孩
    for(int i = h[x] ; i != -1 ;i = ne[i])
    {
        int j = e[i];
        if(!st[j])//如果在这一轮模拟匹配中,这个女孩尚未被预定
        {
            st[j] = true;//那x就预定这个女孩了,只针对当前男生访问过而标记
            //如果女孩j没有男朋友,或者她原来的男朋友能够预定其它喜欢的女孩。配对成功
            if(match[j]==0||find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }
    //自己中意的全部都被预定了。配对失败。
    return false;
}
void init()
{
    memset(h,-1,sizeof h);
    memset(ne,0,sizeof(ne));
    memset(e,0,sizeof(e));
    memset(st,0,sizeof(st));
    memset(match,0,sizeof(match));
}
int main()
{
    
    while(scanf("%d",&m))
    {
        init();
        if(m==0) break;
        scanf("%d%d",&n1,&n2);
        while(m--)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            add(a,b);
        }
        int res = 0;
        for(int i = 1; i <= n1 ;i ++)
        {  
            //因为每次模拟匹配的预定情况都是不一样的所以每轮模拟都要初始化
            memset(st,false,sizeof st);
            if(find(i)) res++;
        } 
        cout<<res<<endl;
    }
    
    getchar();getchar();
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

rgb2gray

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

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

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

打赏作者

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

抵扣说明:

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

余额充值