搜索与图论

这部分内容要用到树的数据结构,我们有以下几种方式来存储节点
邻接表

邻接表就是用类似链表的结构来存储数据,先创建一个数组h,每一个位置都有一个表头。然后e数组和ne数组分别表示节点的值是多少,节点的下一个节点的编号是多少,这种方式一般用在稠密图中,也就是节点数跟边数相差很大。

邻接矩阵

邻接矩阵就是用一个二位数组g[a][b]来表示从a到b的距离是多少,当然,a和b是相邻的节点,这种方式一般用在稀疏图中,也就是节点数跟边数相差不大。

结构体

用结构体来存储边

树与图的优先遍历

深度优先遍历

这个算法就是深搜结合数据结构中的树,不过,具体实现要看题目的不同。

846. 树的重心 - AcWing题库

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>

using namespace std;

const int N = 200010;

int h[N],e[N],ne[N],idx,ans = N; //用邻接表的存储方式
int n;
bool st[N];

void add(int a,int b) //增加节点的模板函数
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx ++;
}

int dfs(int u) //深搜
{
    st[u] = true; //标记一个位置是否被计算过
    int sum = 1,res = 0; //sum表示当前节点加上接上的节点总数,res是最大的分块的节点数
    for(int i = h[u];i != -1;i = ne[i])
    {
        int j = e[i];
        if(!st[j])
        {
            int s = dfs(j); //用递归来算分块节点数
            
            sum +=s; 跟新sum
            res = max(res,s);更新res
        }
        
    }
    res = max(res,n - sum);
    ans = min(ans,res); //更新答案
    
    return sum;
    
}

int main()
{
    memset(h,-1,sizeof h);
    cin>>n;
    int a,b;
    for(int i = 0;i < n - 1;i ++)
    {
        cin>>a>>b;
        add(a,b),add(b,a); //加入无向边
    }
    
    dfs(1);
    
    cout<<ans<<endl;
    
}

这道题目的大致思路是:
首先树的重心是删除这个数后连通块里面的最大连通块的节点数最小的那个点,我们可以遍历所有的节点,把每个节点删除的情况求一遍,然后记录最小答案就行。

而求某一个点的连通块的节点就要用到dfs来深度遍历了

宽度优先遍历

同样也是宽搜和树的结合

847. 图中点的层次 - AcWing题库

#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>
#include <map>
#include <string>
#include <cstring>
#include <queue>
using namespace std;

const int N = 200010;
int n,m;
int h[N],e[N],ne[N],idx; //用邻接表来存储数据
int d[N],q[N]; // 宽搜用到的队列q

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

int bfs()
{
    memset(d,-1,sizeof(d));
    d[1] = 0;
    int hh = 0,tt = 0;
    q[0] = 1;
    
    while(hh <= tt)
    {
        int x = q[hh ++];
        
        for(int i = h[x];i != -1;i = ne[i])
        {
            int j = e[i];
            if(d[j] == -1)
            {
                d[j] = d[x] + 1; //当j点没到过时更新原点到节点的距离,是上一个节点加一
                q[++ tt] = j;
            }
            
        }
    }
    
    return d[n];
}


int main()
{
    memset(h,-1,sizeof(h));
    
    cin>>n>>m;
    int a,b;
    
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b;
        add(a,b);
        
    }
    
    cout<<bfs()<<endl;

    return 0;
}

宽度搜索终点的最短距离可以保证第一个到达的就是最短的,也就是我们宽度遍历所有点,可以保证到达终点的路径长度是最短的。

拓扑排序

首先我们得明白什么是拓扑序列。拓扑序列就是图中的边都指向后面的元素比如我们有一个图,里面的节点指向分别是1 -> 2,2 -> 3,1 -> 3,里面的拓扑序列就是1 2 3

image
可以看见,1 和 2的出边都指向后面的3,所以是拓扑序列。
再来看题目

848. 有向图的拓扑序列 - AcWing题库

#include<iostream>
#include<cstring>

using namespace std;

const int N = 200100;

int h[N],e[N],ne[N],d[N],idx,q[N],n,m; //由于是稀疏图就用邻接表来存
bool st[N];

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

bool topsort()
{
    int hh = 0,tt = -1;
    
    for(int i = 1;i <= n;i ++) //检验入边为0的节点,就是起点,可能有多个起点
    {
        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] --; //减去了一个节点,那么入边减一
            if(d[j] == 0) //如果没有入边了说明这个点不需要了,可以入队列了,入了队列接下来就是对它指向的节点进行判断了
            {
                q[ ++ tt] = j;
            }
        }
    }
    
    return tt == n - 1; // 当tt == n - 1的时候,说明所有的元素都已经入队列了,就是把图中的所有节点排列成了拓扑序列
    
}


int main()
{
    memset(h,-1,sizeof h);
    cin>>n>>m;
    int a,b;
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b;
        add(a,b);
        d[b]++;
    }
    
    if(topsort())
    {
        for(int i = 0;i < n;i ++) //入队列的顺序正好就是拓扑序列的顺序
        {
            cout<<q[i]<<" ";
        }
        cout<<endl;
    }else
    {
        cout<<-1<<endl;
    }
    
}

Dijkstra 算法(戴克斯特拉算法)

朴素版dijkstra

这种版本一般适用于稠密图的求最短路径中,并且要求没有权值为负的边。
根据相应的要求,我们用邻接矩阵来存储数据

朴素版的算法基本步骤是从所有已经到达的节点中找到距离最短也就是的那个值,也就是dist[t]最小,并且这个点没有被标记,可以理解为没有进入答案集合,然后,遍历所有的节点,看能不能用dist[t]来更新距离的最小值

849. Dijkstra求最短路 I - AcWing题库

#include<iostream>
#include<cstring>

using namespace std;

const int N = 510;

int g[N][N],dist[N],n,m; //邻接矩阵
bool st[N]; //标记答案集合

int dijkstra()
{
    dist[1] = 0;
    for(int i = 0;i < n;i ++)
    {
        int t = -1;
        for(int j = 1;j <= n;j ++)
        {
            if(!st[j] &&(t == -1 || dist[t] >dist[j])) //如果不在答案集合里面,或者没有初始值,或者能更新最小dist值
            {
                t = j;
            }
        }
        st[t] =true;
        
        for(int j = 1;j <= n;j ++)
        {
            dist[j] = min(dist[j],dist[t] + g[t][j]);
        }
        
    }
    
    if(dist[n] == 0x3f3f3f3f) //如果dist[n]不等于无穷大
    {
        return -1;
        
    }else return dist[n];
    
}

int main()
{
    cin>>n>>m;
    memset(g,0x3f,sizeof g);
    memset(dist,0x3f,sizeof dist);
    int a,b,c;
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b>>c;
        g[a][b] = min(g[a][b],c);
    }
    int t = dijkstra();
    
    cout<<t<<endl;
    
}

堆优化版dijkstra

在朴素版的代码中,有一部分特别笨重,就是更新的时候我们需要遍历所有节点去寻找最小dist,但是数据结构中的优先队列可以在短时间内给出最小的节点。下面是题目和代码。

AcWing 850. Dijkstra求最短路 II - AcWing

#include<iostream>
#include<cstring>
#include<queue>
#include<vector>

using namespace std;

typedef pair<int,int> PII; //后续我们会用pair来存数据,首位是距离 ,末尾是节点值,就是节点编号


const int N = 1000100;

int h[N],w[N],e[N],ne[N],idx,n,m,dist[N]; //题目给的稀疏图所以我们用邻接表
bool st[N];

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


int dijkstra()
{
    priority_queue<PII,vector<PII>,greater<PII>> heap; //优先队列
    memset(dist,0x3f,sizeof dist); //初始化dist
    
    dist[1] = 0; 
    heap.push({0,1});//初始化,1号节点入列
    
    while(heap.size())
    {
        auto t = heap.top(); //获取并弹出第一个节点
        heap.pop();
        
        int ver = t.second,distance = t.first;
        
        if(st[ver]) continue; //如果这个节点进入了答案集合就跳过
        
        st[ver] = true;
        
        for(int i = h[ver];i != -1;i = ne[i])
        {
            int j = 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()
{
    cin>>n>>m;
    
    memset(h,-1,sizeof h);
    
    int a,b,c;
    
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b>>c;
        
        add(a,b,c);
    }
    
    int t = dijkstra();
    
    cout<<t<<endl;
    
}

bellman_ford算法

AcWing 853. 有边数限制的最短路 - AcWing

这个算法一般用在有负边但是限制了最多经过的边的最短路问题。这个算法的思路是进行k次循环,k就是限制最多的边数,在k次循环下遍历所有的边更新dist。

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100010, M = 510;
int dist[M], n, m, k, backup[M];

struct edge //用结构体来表示边
{
    int a, b, w; //a到b的边,权重是w
} points[N];

bool bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < k; i++)
    {
        memcpy(backup, dist, sizeof dist); //备份,从上一次的情况开始更新,backup是备份dist的数组。
        for (int j = 0; j < m; j++) //循环所有节点,看能不能更新最短长度
        {
            int a = points[j].a, b = points[j].b, w = points[j].w;
            dist[b] = min(dist[b], backup[a] + w);
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) //如果dist[n]是一个很大的值,那么就是到不了n可能1到不了n和节点p但是节点p到的了n,            
    {                            //这时dist[n]和dist[p]都是无穷,如果n到p是负权边就会比0x3f3f3f3f小
        return false;
    }
    return true;
}

int main()
{

    cin >> n >> m >> k;
    int a, b, c;
    for (int i = 0; i < m; i++)
    {
        cin >> a >> b >> c;
        points[i] = {a, b, c};
    }
    bool t = bellman_ford();
    if (t == false)
    {
        cout << "impossible" << endl;
    }
    else
    {
        cout << dist[n] << endl;
    }
}

因为边数有限所以我们不用考虑负权回路也就是负环的问题

SPFA算法

这个算法一般用于没有负环也没有边数限制的最短路径问题。

首先是这个算法的思路:

我们用队列来存下所有到点1的距离变化的点t,类似于蝴蝶效应,点t的所有子节点都有可能会被更新到1的值,然后子节点的所有子节点也可能会被更新,但是如果,子节点的子节点已经在队列里面了,就不用入队了,因为迟早会轮到它来更新其他点。就用这种方式来遍历所有的点直到没有新的更新操作为止

AcWing 851. spfa求最短路 - AcWing

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

const int N = 100010;

int e[N],ne[N],h[N],dist[N],idx,n,m,w[N];
bool st[N];

void add(int a,int b,int c)//邻接表
{
    e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx ++;
}

bool spfa()
{
    queue<int> q; //用队列来存需要发生改变的点,这个点的所有子节点都有可能会给更行最短距离
    memset(dist,0x3f,sizeof dist);
    
    dist[1] = 0;//初始化,先让点1入队,点1后买面的点都有可能改变
    q.push(1);
    st[1] = true;
    while(q.size())
    {
        int t = q.front();//获取需要改变的节点然后把它抛出
        q.pop();
        st[t] = false;
        for(int i = h[t];i != -1;i = ne[i]) //遍历所有子节点
        {
            int j = e[i]; //获取子节点的代表的数字
            if(dist[j] > dist[t] + w[i]) //如果从1到j的距离大于从1到t再到j的距离就更i新j的dist
            {
                dist[j] = dist[t] + w[i];
                if(!st[j]) //如果j点已经在队列里了,就迟早会对它的子节点进行更新操作,所以不用放进去
                {
                    q.push(j);//如果没有,那就入队
                    st[j] = true;
                }
            }
        }
        
    }
    
    if(dist[n] == 0x3f3f3f3f)//n的dist还是无穷大说明到不了这个点,返回false
    {
        return false;
    }else
    {
        return true;
    }
    
}

int main()
{
    memset(h,-1,sizeof h);
    cin>>n>>m;
    int a,b,c;
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b>>c;
        add(a,b,c);
    }
    
    bool t = spfa();
    
    if(!t)
    {
        cout<<"impossible"<<endl;
    }else
    {
        cout<<dist[n]<<endl;
    }
    
    
}

spfa的另外一种用法是求是否有i负环

我们可以维护每个点到1的最短距离的边数,在求最短距离的同时就可以做到,如果有个点的边数大于n - 1,那么说明图种有负环

Floyd求最短路

这个算法用于求解多元汇最短路

这个算法的时间复杂度是n^3,所以一般用在点数少,边数多的情况。

大致思路是,我们迭代n次也就是k层,循环两层,i层 和 j层,所有dist[ i ] [ j ]都尝试用k来更新一遍

AcWing 854. Floyd求最短路 - AcWing

#include<iostream>
#include<cstring>

using namespace std;

const int N = 210;
int INF = 0x3f3f3f3f;
int d[N][N];
int n,m,Q;

void floyd()
{
    for(int k = 1;k <= n;k ++) //k层
    {
        for(int i = 1;i <= n;i ++)//i层
        {
            for(int j = 1;j <=n;j ++)//j层
            {
                d[i][j] = min(d[i][j],d[i][k] + d[k][j]); //尝试更新
            }
        }
    }
}

int main()
{
    cin>>n>>m>>Q;
    
    for(int i = 1;i <= n;i ++)
    {
        for(int j = 1;j <= n;j ++) //初始化所有点,让自环距离等于0
        {
            if(i == j)d[i][j] = 0;
            else d[i][j] = INF;
        }
    }
    int a,b,c;
    for(int i = 1;i <= m;i ++) //读入边长
    {
        cin>>a>>b>>c;
        d[a][b] = min(d[a][b],c); //保留最短边
    }
    floyd();
    
    for(int i =0;i < Q;i ++)
    {
        cin>>a>>b;
        if(d[a][b] > INF / 2) cout<<"impossible"<<endl; //如果到达不了某个点就false掉
        else cout<<d[a][b]<<endl;
    }
}


最短生成树问题

生成树就是给的图,n个点用n - 1条边连接起来,最短生成树就是这些边的总权重最小

Prim算法

这个问题可以抽象成一个最短路问题,点t到1的最短距离所走过的边数是唯一的,所以我们在求最短路的同时维护走过的路径数组就行

Prim算法的思路大致是这样,我们定义一个答案集合,就是所有已经确定是最短路的节点,我们用st来标记,然后,找到所有节点中距离答案集合最近的节点,用这个节点来更新其他所有节点到答案集合的距离,然后让它进入答案集合,一共进行n次入集合操作。

AcWing 858. Prim算法求最小生成树 - AcWing

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 510,INF = 0x3f3f3f3f;

int g[N][N],dist[N],n,m;
bool st[N];

int prim()
{
    memset(dist,0x3f,sizeof dist); //初始化
    int res = 0; //存答案
    for(int i = 0;i < n;i ++) //n层迭代
    {
        int t = -1;
        for(int j = 1;j <= n;j ++) //找距离答案集合的最小边长
        {
            if(!st[j] && (t == -1 || dist[j] < dist[t])) t = j; //如果点j没有进入答案集合,并且距离答案集合更小或者t==-1时(就是刚循环)
        }
        
        if(i && dist[t] == INF) return INF; //如果这层迭代不是第第一次也就是i == 0时,t距离集合没有变,还是无穷大那么就是题目给的节点不是联通的
        if(i) res += dist[t];
        
        for(int j = 1;j <= n;j ++) //以节点t更新其余没有进入答案集合的点到答案集合的最短距离
        {
            dist[j] = min(dist[j],g[t][j]);
        }
        st[t] = true;
        
    }
    
    return res;
}

int main()
{
    cin>>n>>m;
    
    memset(g,0x3f,sizeof g);
    int a,b,c;
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b>>c;
        g[a][b] = g[b][a] = min(g[a][b],c);
    }
    
    int t = prim();
    
    if(t == 0x3f3f3f3f) cout<<"impossible"<<endl;
    else
    {
        cout<<t<<endl;
    }
    
}

kruskal 算法

这个算法和Prim算法不同,这个算法是通过边长来做的,是求最小生成树的比较优秀的算法

这个算法的大致思路是,首先对所有的边进行排序,然后选取最小的边,如果,这个边的两个节点不在同一个连通块中,那么就可以加进去,如果不行,就往下一个边找,直到n - 1条边都到位为止

这个算法在询问是否在同一个连通块中还要用到并查集的知识。

AcWing 859. Kruskal算法求最小生成树 - AcWing

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 200010;
struct edge //用结构体来存边
{
    int a,b,w;
    
    bool operator<(const edge &W)const  //运算符重载
    {
        return w < W.w;
    }
}edges[N];
int p[N]; //并查集数组,用来询问两个点的祖宗节点是否是同一个,也就是两个点是否在同一个连通块

int find(int a) //查询函数,一直找到祖宗节点为止
{
    if(p[a] != a) p[a] = find(p[a]);
    else return p[a];
}



int main()
{
    int n,m;
    cin>>n>>m;
    int a,b,c;
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b>>c;
        edges[i] = {a,b,c};
    }
    
    for(int i = 1;i <= n;i ++)
    {
        p[i] = i;
    }
    
    
    sort(edges,edges + m); //给边排序,保证对边询问的顺序
    int res = 0,cnt = 0; //res记录边长之和,cnt记录计入答案集合边数个数
    for(int i = 0;i < m;i ++)
    {
        a = edges[i].a,b = edges[i].b,c = edges[i].w;
        a = find(a),b = find(b); //查询两节点是否在同一个连通块
        if(a != b) //如果不是,先把两个节点连通,更行答案和计数变量cnt
        {
            p[a] = b;
            res +=c;
            cnt ++;
        }
    }
    if(cnt < n - 1) cout<<"impossible"<<endl; //如果cnt,也就是边数小于n - 1,说明有一个点没有进去,图不能组成生成树
    else cout<<res<<endl;
    
}

二分图

二分图就是,左右连个集合,集合之间有边链接两个点,集合之内没有的图就叫二分图

染色法

这个算法的大致思路是:

我们需要对所有节点染色,所有颜色为1的节点是一边,为2的是一边,如果我们有一个未被染色的节点,我们把它初始化为1,然后对它的所有子节点进行染色,当然了,子节点染成不同的颜色,为2,染色完成后对下一个没有染色的点进行染色,在过程中如果遇到一个子节点被染色了但是颜色与上一个节点一样的话就不能组成二分图image

上面演示的是染色过后的图。

AcWing 860. 染色法判定二分图 - AcWing

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

const int N = 100010,M = 200020;

int n,m;
int h[N],e[M],ne[M],idx;
int color[N];

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

bool dfs(int u,int c) //dfs遍历子节点
{
    color[u] = c; //首先对u进行染色
    
    for(int i = h[u];i != -1;i = ne[i]) //遍历子节点
    {
        int j = e[i];
        if(!color[j])
        {
            if(!dfs(j,3 - c)) //如果子节点的子节点染色的时候有冲突返回false
            {
                return false;
            }
        }else if(color[j] == c) return false; //如果节点的颜色等于上一个节点颜色返回false
        
        
    }
    
    return true;
}

int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    int a,b;
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b;
        add(a,b),add(b,a); //无向边,加两遍
    }
    
    for(int i = 1;i <= n;i ++)
    {
        if(!color[i]) //如果没有被染色
        {
            if(!dfs(i,1)) 
            {
                cout<<"No"<<endl;
                return 0;
            }
        }
    }
    
    cout<<"Yes"<<endl;
    return 0;
    
}

匈牙利算法

这个算法是在处理二分图的两部分之间的两个节点间只有一条边,我们要让边的总和最大

大致思路:

首先遇到一个没有配对的左节点,在它所有右节点中的子节点中寻找,找到没有被右节点配对的子节点,就算匹配成功,如果这没有配对的左节点,只有一个右节点能配对,恰巧这个右节点有左节点了,那么看这个已经配对的左节点能否换个节点,如果可以就换,不行,就返回false,答案不计入这个点

AcWing 861. 二分图的最大匹配 - AcWing

#include<iostream>
#include<cstring>

using namespace std;

const int N = 100100;

int h[N],e[N],ne[N],idx;
int n1,n2,m;
int match[N];
bool st[N];


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

bool find(int t) //查询函数
{
    for(int i = h[t];i != -1;i = ne[i]) //遍历所有子节点
    {
        int j = e[i];
        if(!st[j])
        {
            st[j] = true;
            if(match[j] == 0|| find(match[j])) //如果右节点没有配对或者右节点配对的子节点能换一个
            {
                match[j] = t; //j节点就配对t
                return true;
            }
        }
    }
    return false;//如果没有配对上就返回false
}


int main()
{
    cin>>n1>>n2>>m;
    memset(h,-1,sizeof h);
    int a,b;
    for(int i = 0;i < m;i ++)
    {
        cin>>a>>b;
        add(a,b);
    }
    int res = 0;
    for(int i = 1;i <= n1;i ++)
    {
        memset(st,0,sizeof st);
        if(find(i))  //配对上了就答案加一
        {
           res++;
        }
    }
    cout<<res<<endl;
    
}
posted @ 2024-09-05 10:33  chhh31  阅读(4)  评论(0编辑  收藏  举报