算法——搜索与图论

DFS

BFS

树与图的存储

树是一种特殊的图,与图的存储方式相同。对于无向图,看成特殊有向图,存储边a->b,b->a。

方式一:邻接矩阵(适合稠密图)

//a,b间有一条长度为w的边
cin >> a >> b >> x;
g[a][b] = min(g[a][b] , x);//可能a,b间的边输入两次,取最短的边

方式二:邻接图表(适合稀疏图)

//对于每一个节点k,开一个单链表,存储这个点能走到哪些点,h[k]存储这个链表的头节点
int h[N],e[M],ne[M],idx;

//添加一个边a->b
void add(int a, int b)
{
    e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
}

//初始化
memset(h,-1,sizeof h);
idx = 0;

树与图的遍历

深度优先遍历

模板

int dfs(int u)
{
    st[u] = true;
    for(int i=h[u]; i!=-1; i=ne[i])
    {
      int j = e[i];
      if(!st[j]) dfs(j);
    }
}

题目:树的重心(找出树的重心,删除这个点得到的最大连通块最小)

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

using namespace std;

const int N = 100010,M=2*N;

int h[N],e[M],ne[M],idx;

int n;
bool st[N];
int ans=N;
void add(int a,int b)
{
    e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
}

//以u为根结点的子树有多少个结点
int dfs(int u)
{
    st[u] = true;
    
    int res=0,sum=1;//res表示删除当前结点最大的连通块结点数,sum表示当前子数的结点数
    for(int i=h[u]; i != -1; i=ne[i])
    {
        int j=e[i];
        if(!st[j])
        {
           int s=dfs(j);
           res=max(res,s); //子数也是一个连通块,找最大连通块数
           sum+=s;
        }
    }
    res=max(res,n-sum);
    ans=min(ans,res);
    
    return sum;
}

int main()
{
    memset(h, -1, sizeof h);//初始化
    cin>>n;
    
    for(int i=0;i<n-1;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);add(b,a);
    }
    
    dfs(1);//从任意小于n的结点开始都行
    
    cout<<ans<<endl;
    
    return 0;
}

广度优先遍历

模板

void bfs()
{
    queue<int> q;
    q.push(1);
    st[1]=true;
    while(q.size())
    {
        int t=q.front();
        q.pop();
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(!st[j])
            {
                st[j]=true;
                q.push(j);
            }
        }
    }
}

题目:图中点的层次(输出从1号点到n号点的最短距离)

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

using namespace std;
const int N=100010,M=2*N;

int h[N],e[M],ne[M],idx;
int n,m;
int d[N];//每个结点i到1的最短距离

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

int bfs()
{
    memset(d,-1,sizeof d);
 
    queue<int> q;
    d[1]=0;
    q.push(1);
    
    while(q.size())
    {   
        //取出队头        

        int t=q.front();
        q.pop();
        //用队头更新所有队头能到达的点的距离
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(d[j]==-1)
            {
                d[j]=d[t]+1;
                q.push(j);
            }
        }
    }
    return d[n];
}

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

拓扑排序

步骤:①找入度为0的点存入队列中。

           ②当队列不为空,取出队头,将所有队头能到达的点入度减1;

           ③入度减为零的点入队。

题目:有向图求拓扑结构

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

using namespace std;

const int N=10e+5;
const int M=2*N;
int h[N],e[M],ne[M],idx;
int n,m;
int d[N];//入度
int top[N],cnt;

queue<int> q;//队列中存储入度为0的点
void add(int a,int b)
{
    e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}

bool topsort()
{
    
    for(int i=1;i<=n;i++)
    if(!d[i]) q.push(i);
    
    while(q.size())
    {
        auto t=q.front();
        top[cnt++]=t;
        q.pop();
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            d[j]--;
            if(!d[j]) q.push(j);
        }
    }
    //如果都入队了,说明存在拓扑结构,否则不存在拓扑结构
    return cnt==n;
}


int main()
{
    cin>>n>>m;
    int a,b;
    memset(h,-1,sizeof h);
    while(m--)
    {
        cin>>a>>b;
        add(a,b);
        d[b]++;
    }
    if(!topsort())
    puts("-1");
    else{
        for(int i=0;i<n;i++) printf("%d ",top[i]);
    }
    return 0;
}

最短路问题

Dijkstra求最短路I(朴素版) O(n^2)

不能有负环,求单源最短路径,没有负权边,适合稠密图,时间复杂度O(n^2)

步骤:①初始化所有点,到1的距离为最大值。

           ②找到未入队的点的距离1最近的点t。

           ③用点更新其他所有点的距离。

题目:迪杰斯特拉求最短路

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

using namespace std;
const int N=510;

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

int dijkstra()
{   
    //初始化所有点到1的距离为最大值
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    
    for(int i=0;i<n-1;i++)
    {
        int t=-1;
        //找到没有入队的距离第一个点最近的点t
        for(int j=1;j<=n;j++)
        {
            if(!st[j] && (t==-1||dist[t]>dist[j]))
            t=j;
        }
        //用点t更新其他点
        for(int j=1;j<=n;j++)
        {
            dist[j]=min(dist[j],g[t][j]+dist[t]);
        }
        st[t]=true;
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    cin>>n>>m;
    //将所有点之间的距离初始化为最大值
    memset(g,0x3f,sizeof g);
    
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=min(g[a][b],c);
    }
    
    cout<<dijkstra()<<endl;
    
    return 0;
}

Dijkstra求最短路II(堆优化版) O(mlogn)

单源最短路径,无负权边,适合稀疏图,时间复杂度O(mlogn)

优先队列保存的是当前未确定最小距离的点。

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

using namespace std;
const int N=1000010;

int h[N],e[N],ne[N],w[N],idx;
int n,m;
typedef pair<int,int> PII;
int 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[1]=0;
   heap.push({0,1});
   
   while(heap.size())
   {
       auto t=heap.top();
       heap.pop();
       
       int ver=t.second;int 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]>dist[ver]+w[i])
           {
               dist[j]=dist[ver]+w[i];
               heap.push({dist[j],j});
           }
       }
   }
   
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
}

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

Bellman-ford O(mn)

求单源最短路,有负权边,时间复杂度O(mn) (算法简单)

可以用结构体存储所有边的值合两端点。

算法步骤:

for n次(允许走的边数)
     for 所有边 a,b,w (松弛操作)
           dist[b] = min(dist[b],back[a] + w)

题目:有边数限制的最短路

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

using namespace std;

const int N=510,M=100010;
int n,m,k;
int dist[N],last[N];
struct Edge
{
    int a,b,c;
}edge[M];

void bellman_ford()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    
    for(int i=0;i<k;i++)
    {   //防止串联
        memcpy(last,dist,sizeof dist);
        for(int j=0;j<m;j++)
        {
            auto e=edge[j];
            dist[e.b]=min(dist[e.b],last[e.a]+e.c);
        }
    }
    
}


int main()
{
    cin>>n>>m>>k;
    
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        edge[i]={a,b,c};
    }
    
    bellman_ford();
    if(dist[n]>0x3f3f3f3f/2) cout<<"impossible"<<endl;
    else{
        cout<<dist[n]<<endl;
    }
    return 0;
}

Spfa  一般是O(m),最差是O(mn)

1.求单源最短路,有负权边,时间复杂度O(mn),是Bellman_ford算法的优化。

 Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。

Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。

给定一个 𝑛个点 𝑚 条边的有向图,图中可能存在重边和自环, 边权可能为负数。但不存在负权回路。

题目:spfa求最短路

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

using namespace std;

typedef pair<int,int> PII;//到源点的距离,下标号

const int N=100010;

int n,m;
int h[N],e[N],ne[N],idx,w[N];
int 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 spfa()
{
    queue<PII> q;
   memset(dist,0x3f,sizeof dist);
   dist[1]=0;
   q.push({0,1});
   st[1]=true;
   
   while(q.size())
   {
       auto x=q.front();
       q.pop();
       int t=x.second;
       st[t]=false;
       
       for(int i=h[t];i!=-1;i=ne[i])
       {
           int j=e[i];
           if(dist[j]>dist[t]+w[i])
           {
               dist[j]=dist[t]+w[i];
               if(!st[j])
               { st[j]=true;
                 q.push({dist[j],j});
               }
               
           }
       }
       
   }
   return dist[n];
}

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

    if (t == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", t);
}

spfa判断负环

求负环一般使用SPFA算法,方法是用一个cnt数组记录每个点到源点的边数,一个点被更新一次就+1,一旦有点的边数达到了n那就证明存在了负环。

不用初始化dist,只更新有负环的dist.

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

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[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;

    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }

    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])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;

                if (cnt[j] >= n) return true;
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    if (spfa()) puts("Yes");
    else puts("No");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/48499/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Floyd(On^3)

求多源汇最短路

给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出“impossible”。

数据保证图中不存在负权回路。

#include <iostream>
#include <algorithm>

using namespace std;

const int N=210,M=2e5+10,INF=1e9;
int n,m,k,x,y,z;
int d[N][N];

void floyd()
{
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
            d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}

int main()
{
    cin>>n>>m>>k;
    
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        if(i==j) d[i][j]=0;
        else d[i][j]=INF;
    
    while(m--)
    {
      cin>>x>>y>>z;
      d[x][y]=min(d[x][y],z);
    }
    
    floyd();
    
    while(k--)
    {
        cin>>x>>y;
        if(d[x][y]>INF/2) puts("impossible");
        else cout<<d[x][y]<<endl;
    }
    return 0;
}

最小生成树

Prim算法 

dist代表到集合的距离。每次寻找到集合距离最近的点,用这个点更新其他点到集合的距离

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

using namespace std;

const int N=510,INF=0x3f3f3f3f;

int g[N][N],dist[N];

int n,m;
bool st[N];

int prim()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    
    int res=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]))
            t=j;
        if(dist[t]==INF) return 0x3f3f3f3f;//这种情况代表不连通
        
        res+=dist[t];
        
        st[t]=true;
        
        for(int j=1;j<=n;j++)//更新其他点到集合的距离
        dist[j]=min(dist[j],g[t][j]);
    }
    return res;
}

int main()
{
   cin>>n>>m;
   memset(g,0x3f,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) puts("impossible");
   else cout<<t<<endl;
    return 0;
}

Kruskal (Omlogm)

用到了并查集

求最小生成树,算法步骤:

①将所有边按权重大小排序

②枚举每条边a b 

        if b和a不连通,将这条边加入集合中

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

using namespace std;
const int N=100010,M=200010,INF=0x3f3f3f3f;

int n,m;
int p[N];

struct Edge
{
    int a,b,w;
    
    bool operator< (const Edge &W)const
    {
        return w<W.w;
    }
}edges[M];

int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges,edges+m);
    
    
    for(int i=1;i<=n;i++) p[i]=i;
    
    int res=0,cnt=0;
    for(int i=0;i<m;i++)
    {
        int a=edges[i].a,b=edges[i].b,w=edges[i].w;
        
        a=find(a),b=find(b);
        if(a!=b)
        {
            p[a]=b;
            res+=w;
            cnt++;
        }
    }
    if(cnt < n-1) return INF;
    return res;
}

int main()
{
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        edges[i]={a,b,c};
    }
    
    int t=kruskal();
    
    if(t==INF) puts("impossible");
    else cout<<t<<endl;
    return 0;
    
}

二分图

一个图是二分图,当且仅当图中不含有奇数环(奇数环是指环中边的数量是奇数)

染色法O(n+m)

一个图是二分图一定能被正确染色,不是二分图就不会被正确染色。

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

using namespace std;

const int N=100010,M=200010;

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)
{
    color[u]=c;
    
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(!color[j])
        {
            if(!dfs(j,3-c)) return false;
        }
        else if(color[j]==c) return false;
    }
    return true;
}

int main()
{
    memset(h,-1,sizeof h);
    cin>>n>>m;
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);add(b,a);
    }
    //染色法
    bool flag=true;
     //考虑到非连通图的情况,保证遍历到每一个点
    for(int i=1;i<=n;i++)
    {
        if(!color[i])
        {
            if(!dfs(i,1)) 
            {
                flag=false;
                break;
            }
        }
    }
    
    if(flag) puts("Yes");
    else puts("No");
    
    return 0;
}

匈牙利算法 O(nm) 实际时间复杂度远小于O(nm)

题目:二分图的最大匹配

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

using namespace std;

const int N=510,M=100010;
int n1,n2,m;
int match[N];
int h[N],e[M],ne[M],idx;
bool st[N];
void add(int a,int b)
{
    e[idx]=b;ne[idx]=h[a],h[a]=idx++;
}
//能否成功匹配
bool find(int x)
{
    for(int i=h[x];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(!st[j])
        {
        st[j]=true;
        if(match[j]==0 || find(match[j]))
        {
            match[j]=x;
            return true;
        }
        }
    }
    return false;
}

int main()
{
    cin>>n1>>n2>>m;
    memset(h,-1,sizeof h);
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
    }
    
    int res=0;
    //遍历所有左侧点
    for(int i=0;i<=n1;i++)
    {   //所有右侧点都没有访问过
        memset(st,false,sizeof st);
        if(find(i)) res++;
    }
    
    cout<<res<<endl;
    return 0;
    
}

  • 38
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值