3.4图论(算法提高课)

目录

一,二分图

1,关押罪犯

2,棋盘覆盖

3,机器任务

4,骑士放置

5,捉迷藏

二,欧拉路径与欧拉回路

1,铲雪车

2,欧拉回路

 3,骑马修栅栏

4,单词

三,拓扑排序

1,家谱树

2,奖金

 3,可达性统计

4,车站分级


一,二分图

1,关押罪犯

题目链接:https://www.acwing.com/problem/content/description/259/

这题之间高级数据结构那章讲过,可以用二分+带权并查集来做,这里给出一个二分+染色法判定二分图的做法。我们二分枚举答案mid,将边权大于mid的边用染色法来判定是否能变成二分图,如果可以,我们就缩小答案,如果不行,我们就扩大答案,时间复杂度位O(log1e9(N+M)),比二分+带权并查集要慢一点,但是代码量要少一些

代码如下:

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

using namespace std;

const int N=20010,M=200010;

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

void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool dfs(int u,int c,int mid)//染色法判定二分图,u表示当前节点,c表示要染色颜色,mid表示边权下界
{
    color[u]=c;//给当前点染上色
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(w[i]<=mid)continue;//只对边权大于mid的边判定是否能成功匹配二分图
        if(!color[j])//如果当前点还没染色
        {
            if(!dfs(j,3-c,mid))//染上另一个色
                return false;
        }
        else if(color[j]==c)return false;//如果点u的子节点染的色与点j相同,说明不是二分图
    }
    return true;//说明能成功匹配二分图
}
bool check(int mid)//判定边权大于mid的边是否能成功匹配成二分图
{
    memset(color,0,sizeof color);//多次check,初始化
    for(int i=1;i<=n;i++)
        if(!color[i])
            if(!dfs(i,1,mid))//说明不能成功匹配人二分图
                return false;
    return true;//说明能成功匹配二分图
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c),add(b,a,c);//无向边建图
    }
    int l=0,r=1e9;//二分答案
    while(l<r)
    {
        int mid=l+r>>1;
        if(check(mid))r=mid;
        else l=mid+1;
    }
    printf("%d",r);
    return 0;
}

2,棋盘覆盖

题目链接:https://www.acwing.com/problem/content/description/374/

这题看起来很像状态压缩动态规划问题,但是由于数据范围很大,如果用状态压缩的话要2^100,必定会超时,因此我们我们看是否有其他办法,对于一个1×2的小方块,我们可以看成是一条边连接了这两个点, 那么题目就变成了在给定的边中,找到最多的边,满足其节点互不相同。这就是一个最大匹配的问题,因此我们可以考虑是否能用匈牙利算法来做,要想用匈牙利算法来做,需要先判断该图是否是一个二分图,如果一个图是二分图,那么我们可以将每条边上的两个点染成不同颜色,很明显,我们可以用白色和绿色将一个正方形染色成如下:

那么说明该图是一个二分图,那么我们就可以用匈牙利算法来做,结合我们之前讲的匈牙利算法,我们可以把绿色格子看成男生,白色格子看成女生,只用枚举男生即可

二分图匹配模型有两个要素:

1,节点能分成独立的两个集合,集合内部有0条边

2,每个节点只能与1条匹配边相连

我们把它简称为“0要素”和“1要素”。在把实际问题抽象成二分图匹配时,我们就要寻找题目中具有这种“0”和“1”性质的对象,从而发现模型构建的突破口。在本题中,任意两张骨牌都不重叠,也就是每个格子只能被1张骨牌覆盖,而骨牌的大小为1*2覆盖2个相邻的格子。这恰好与“1要素”对应。于是我们可以把棋盘上没有被禁止的格子作为节点,把骨牌作为无向边(两个相邻的格子对应的节点之间连边)。上述白色格子和绿色格子中,两个相同的颜色的格子不可能被同一骨牌覆盖,也就是同色格子之间没有边相连。这恰好与“0要素"对应。于是刚才建立的无向图是一张二分图,可把绿色格子作为男生,白色格子作为女生进行匹配

代码如下:

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

#define x first
#define y second

using namespace std;

typedef pair<int,int>pii;

const int N=110;

int n,m;
bool g[N][N],st[N][N];//g[i][j]表示(i,j)是否禁止放置,true为禁止,st[i][j]表示(i,j)是否已匹配
pii match[N][N];//match[i][j]表示与(i,j)匹配的点 

int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};//上右下左四个方向的偏移量

bool find(int x,int y)//为(x,y)找到匹配的点
{
    for(int i=0;i<4;i++)//枚举四个方向
    {
        int a=x+dx[i],b=y+dy[i];//与之相连的四个方向的点
        if(a<1||a>n||b<1||b>n)continue;//判断是否越界
        if(st[a][b]||g[a][b])continue;//判断是否已配对或是否有障碍
        
        st[a][b]=true;//表示已经给点(a,b)配对
        pii t=match[a][b];//t为点(a,b)配对的点
        if(t.x==0||find(t.x,t.y))//如果点(a,b)还没配对或是能让其配对的点换一个
        {
            match[a][b]={x,y};//点(a,b)配对的点为(x,y)
            return true;//返回配对成功
        }
    }
    return false;//说明无法与(x,y)找到匹配的点
}
int main()
{
    scanf("%d%d",&n,&m);
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        g[a][b]=true;//表示有障碍
    }
    
    int res=0;
    for(int i=1;i<=n;i++)//枚举每个点的横坐标
        for(int j=1;j<=n;j++)//枚举每个点的纵坐标
            if((i+j)%2&&!g[i][j])//如果是奇数点,且没有障碍,才为其配对,这里判断改成偶数点也可以
            {
                memset(st,0,sizeof st);//为每个点配对点,全部点要设置成还没匹配过
                if(find(i,j))//说明能为(i,j)找到一个配对的点
                    res++;
            }
    printf("%d",res);
    return 0;
}

3,机器任务

题目链接:https://www.acwing.com/problem/content/description/378/

这题考察的是二分图的最小点覆盖。给定一张二分图,求出一个最小的点集S,使得图中任意一条边都至少有一个端点属于S。这个问题被称为二分图得最小点覆盖,简称最小覆盖。

定理: 二分图得最小覆盖包含的点数等于二分图最大匹配包含的边数

在上面讲到的二分图的最大匹配问题中,我们经常寻找题目中的”0要素“和’1要素",作为解答的突破口。二分图的最小覆盖的模型特点则是:每条边有两个端点,二者至少要选择一个。我们不妨称之为“2要素”。如果一个题目中具有“2要素”,那么可以尝试抽象成二分图最小覆盖模型求解。

有了这个定理和上面讲的方法,我们就很好做了,如果有任务需要的是模式0,我们可以不用管,因为我们可以在初始时选择A机器或者B机器将这些任务完成,不需要重启。对于剩下的任务,要么在机器A上以模式a【i】执行,要么在B机器上以模式b【i】执行,二者必选其一,满足“2要素”。因此我们可以将机器A的N种模式作为男生,机器B的M种模式作为女生,每个任务看作是无向边,连接a【i】和b【i】,然后用匈牙利算法得到最大匹配数量,即为最小覆盖。

代码如下:

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

using namespace std;

const int N=110;

int n,m,k;
bool g[N][N],st[N];
//g[i][j]表示点i与点j是否有边相连,st[i]表示点i是否已经匹配
int match[N];//match[i]表示与点i匹配的点

bool find(int x)//找到与x匹配的点
{
    for(int i=1;i<m;i++)//遍历b机器的模式
    {
        if(!st[i]&&g[x][i])//如果点i还没匹配,并且点x与点i有一条边
        {
            st[i]=true;//点i已匹配日
            int t=match[i];//与点i匹配的点
            if(t==0||find(t))//如果点i还没匹配或者与之配对的点可以换一个点匹配
            {
                match[i]=x;//点i与点x匹配
                return true;
            }
        }
    }
    return false;//说明不能为点x找到匹配
}
int main()
{
    while(scanf("%d",&n),n)
    {
        scanf("%d%d",&m,&k);
        
        memset(g,0,sizeof g);
        memset(match,0,sizeof match);
        
        while(k--)
        {
            int i,a,b;
            scanf("%d%d%d",&i,&a,&b);
            if(!a||!b)continue;//如果需要模式0,那么可以在初始时直接在a机器或b机器上完成,不需要重启
            g[a][b]=true;//表示点a与点b有一条相连的边
        }
        
        int res=0;
        for(int i=1;i<n;i++)//匈牙利算法求最大匹配数
        {
            memset(st,0,sizeof st);
            if(find(i))
                res++;
        }
        printf("%d\n",res);
    }
    return 0;
}

4,骑士放置

题目链接:https://www.acwing.com/problem/content/description/380/

这题考察的最大独立集。给定一张图,图的独立集就是“任意两点之间没有边相连”的点集。包含点数最多的一个就是最大独立集 。对应地,“任意两点之间都有一条边相连”的子图被称为无向图的”团“,点数最多的团被称为”最大团“。

定理1:无向图G的最大团等于其补图G1的最大独立集

定理2:设G是有n个节点的二分图,G的最大独立集的大小等于n减去最大匹配数

定理2证明:选出最多的点构成独立集合<==>在图中去掉最少的边,使剩下的点之间没有边<==>用最少的点覆盖所有的边。

因此,去掉二分图的最小点覆盖,剩余的点就构成二分图的最大独立集。而最小点覆盖数就等于最大匹配数。故最大独立集等于n减去最大匹配数

这题与第二题棋盘覆盖很像,对棋盘用白绿两种颜色染色,若两个格子是”日“字的对角(能相互攻击到),则在它们对应的节点之间连边。容易发现,”日“字的两个对角格子的颜色一定不同。因此我们建出的图一定是二分图,那么就可以用上面的定理了 。这里要求我们求最大独立集,那么我们就可以先把最小点覆盖,即最大匹配数res求出来,答案就是n*m-k- res,k为禁止放棋子的数量

代码如下:

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

#define x first
#define y second

using namespace std;

typedef pair<int,int>pii;

const int N=110;

int n,m,k;
bool g[N][N],st[N][N];
pii match[N][N];

int dx[8]={-2,-1,1,2,2,1,-1,-2};//马走日的八个方向的偏移量
int dy[8]={1,2,2,1,-1,-2,-2,-1};

bool find(int x,int y)
{
    for(int i=0;i<8;i++)
    {
        int a=x+dx[i],b=y+dy[i];//与点(x,y)相连的点(a,b)
        if(a<1||a>n||b<1||b>m)continue;//判断是否越界
        if(st[a][b]||g[a][b])continue;//判断是否已匹配,或是该点禁止放棋子
        
        st[a][b]=true;//点(a,b)已匹配日
        pii t=match[a][b];//与点(a,b)配对的点(t.x,t.y)
        if(t.x==0||find(t.x,t.y))//如果点(a,b)还没配对或是点t能换一个配对
        {
            match[a][b]={x,y};//点(a,b)与点(x,y)配对
            return true;
        }
    }
    return false;//说明不能找到点与点(x,y)配对
}
int main()
{
    scanf("%d%d%d",&n,&m,&k);
    
    for(int i=0;i<k;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        g[a][b]=true;//说明该点不嫩放棋子
    }
    
    int res=0;
    for(int i=1;i<=n;i++)//枚举为其配对的棋子的横坐标
        for(int j=1;j<=m;j++)//纵坐标
        {
            if((i+j)%2&&!g[i][j])//只用枚举(i+j)为奇数的点
            {
                memset(st,0,sizeof st);//每次配对前记得清空
                if(find(i,j))
                    res++;
            }
        }
    printf("%d",n*m-k-res);//最大独立集=所有点-障碍点-最下点覆盖点数
    return 0;
}

5,捉迷藏

题目链接:https://www.acwing.com/problem/content/description/381/

这题考察的是有向无环图的最小路径点覆盖和有向无环图的最小路径可重复点覆盖 。

最小路径点覆盖:给定一张有向无环图,要求用尽量少的不相交的简单路径,覆盖有向无环图的所有顶点(也就是每个顶点恰好被覆盖一次)。这个问题就被称为有向无环图的最小路径点覆盖,简称“最小路径覆盖“。

设原来的有向无环图为G=(V,E),n=| V |。把G中的每个点x拆称编号为x和x+n的两个点。建立一张新的二分图,1~n作为二分图左部点,n+1~2n作为二分图右部点,对于原图的每条有向边(x,y),在二分图的左部点x与右部点y+n之间连边。最终得到的二分图被称为G的拆点二分图,记为G2.

定理”有向无环图G的最小路径点覆盖包含的路径条数,等于n减去拆点二分图G2的最大匹配数。

证明:在有向无环图G=(V,E)的最小路径覆盖中,对于任意的X∈V,因为路径不相交,所以x的入度和出度都不超过1。因为每个节点都被覆盖,所以x的入度和出度至少有一个是1.因此,最小路径覆盖中的所有边,在拆点二分图G2中构成一组匹配。最小路径覆盖中每条边(x,y)的起点x与二分图每条匹配边(x,y+n)的左部点x是一一对应的。特别地,对于每条路径的终点t,因为t没有出边,所以在二分图中,t匹配失败。即路径的终点和二分图左部的非匹配点是一一对应的。

路径覆盖包含的路径条数最小<==>路径的终点数(出度为0的点数)最小<==>二分图左部非匹配点最少。故G的最小路径覆盖的路径数等于n减去拆点二分图G2的最大匹配数。

最小路径可重复点覆盖:给定一张有向无环图,要求用尽量少的可相交的简单路径,覆盖有向无环图的所有顶点(也就是一个节点可以被覆盖多次)。这个问题被称为有向无环图的最小路径可重复点覆盖。

在最小路径可重复点覆盖中,若两条路径u->p->v和->x->p->y在点p相交,则我们在原图中添加一条边(x,y)让第二条路径直接走x->y,就可以避免重复覆盖点p。进一步地,如果我们把原图中所有间接连通的点对x,y直接连上有向边(x,y),那么任何“有路径相交的点覆盖”一定都能转化成“没有路径相交的点覆盖”。

综上所述,有向无环图G的最小路径可重复点覆盖,等价于先对有向图传递闭包,得到有向无环图G‘,再在G’上求一般的(路径不可相交的)最小路径点覆盖。

 这题所求的最多能选取的藏身点个数,就等于最小路径可重复点覆盖包含的路径条数。我们对题目中给出的有向无环图G进行传递闭包,得到一张新的有向无环图G’,然后求出拆点二分图G2’的最大匹配,输出N减G2‘的最大匹配即可。

代码如下:

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

using namespace std;

const int N=210;

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

bool find(int x)//匈牙利算法求最大匹配
{
    for(int i=1;i<=n;i++)//并不用真的建立拆点二分图,只需要像这样,把这n个数再看成右部点枚举即可
    {
        if(!st[i]&&g[x][i])
        {
            st[i]=true;
            int t=match[i];
            if(t==0||find(t))
            {
                match[i]=x;
                return true;
            }
        }
    }
    return false;
}
int main()
{
    scanf("%d%d",&n,&m);
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        g[a][b]=true;//说明有一条从a指向b的有向边
    }
    
    //floyd求传递闭包
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                g[i][j]|=g[i][k]&g[k][j];
    
    int res=0;//求最大匹配数量
    for(int i=1;i<=n;i++)
    {
        memset(st,0,sizeof st);
        if(find(i))
            res++;
    }
    printf("%d",n-res);//n-res即为最小可重复路径点覆盖数
    return 0;
}

二,欧拉路径与欧拉回路

欧拉路径:欧拉路是指从图中任意一个点开始到图中任意一个点结束的路径,并且图中每条边通过的且只通过一次。

欧拉回路:欧拉回路是指起点和终点相同的欧拉路。

定理1:对于无向图,所有边都是连通的。

(1)存在欧拉路径的充分必要条件:度数为奇数的点只能有0或2个。

(2)存在欧拉回路的充分必要条件:度数为奇数的点只能有0个。

定理2:对于有向图,所有边都是连通。

(1)存在欧拉路径的充分必要条件:要么所有点的出度均等于入度:要么除了两个点之外,其余所有点的出度等于入度,剩余的两个点:一个满足出度比入度多1(起点),另一个满足入度比出度多1(终点)。

(2)存在欧拉回路的充分必要条件:所有点的出度均等于入度。

1,铲雪车

题目链接: http://ybt.ssoier.cn:8088/problem_show.php?pid=1374

 这题保证了起点一定可以达到任何街道,说明起点在道路上,这点很重要,并且给我们的是一个连通图,对于每个点的度数都是偶数,所以这是一个欧拉回路,我们只需要求出来所有距离的总和,除20再四舍五入算出时间即可。

代码如下:

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

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;

const int N = 1e5 + 10;

int main()
{
    double x1,y1,x2,y2;
    cin>>x1>>y1;//起点没用
    double sum=0;
    while(cin>>x1>>y1>>x2>>y2)
    {
        double x=x1-x2;
        double y=y1-y2;
        sum+=sqrt(x*x+y*y)*2;//求出所有路的距离总和
    }
    int minutes=round(sum/1000/20*60);//要四舍五入,求出所花费的时间
    int hours=minutes/60;
    minutes%=60;
    printf("%d:%02d",hours,minutes);//对于分钟,不足两位要补0
    return 0;
}

2,欧拉回路

题目链接:http://ybt.ssoier.cn:8088/problem_show.php?pid=1527

没搞太懂,后面回来补

代码如下:

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

using namespace std;

const int N = 100100, M = 400100;

int h[N],e[M],ne[M],idx;
int ans[N*2],cnt;
bool used[M];
int din[N],dout[N];
int n,m,ver;

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

void dfs(int u){
    for(int &i = h[u]; ~i; ){
        if(used[i]){  //如果这条边用过了
            i = ne[i];   //删除这条边
            continue;
        }

        used[i] = true;  //标记这条边已使用
        if(ver == 1) used[i^1] = true;   //如果是无向图,那么这条边的反向边也要标记使用过了

        int t;
        if(ver == 1){
            t = i/2 + 1;
            if(i&1) t = -t;  //(0,1) (2,3) (4,5) 奇数编号是返回的边

        }else t = i+1;

        int j = e[i];
        i = ne[i];
        dfs(j);
        ans[cnt++] = t;
    }
}
int main()
{
    scanf("%d%d%d",&ver,&n,&m);
    memset(h,-1,sizeof h);

    for(int i = 0; i<m; i++){
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
        if(ver == 1) add(b,a);  //无向边
        din[b]++, dout[a]++;   
    }

    if(ver == 1){
        for(int i = 1; i<=n; i++){
            if(din[i]+dout[i] &1){
                //无向图含欧拉回路的充要条件是每个点的度都为偶数
                puts("NO");
                return 0;
            }
        }
    }else{
        for(int i = 1; i<=n; i++){
            if(din[i] != dout[i]){
                //有向图含欧拉回路的充要条件是每个点的入度等于出度
                puts("NO");
                return 0;
            }
        }
    }

    for(int i = 1; i<=n; i++){
        if(~h[i]) {
            dfs(i);
            break;
        }
    }

    if(cnt < m){
        puts("NO");
        return 0;
    }

    puts("YES");
    for(int i = cnt-1; i>=0; --i){
        cout<<ans[i]<<" ";
    }
    return 0;
}

 3,骑马修栅栏

题目链接:https://www.luogu.com.cn/problem/P2731

因为题目已经保证了有解,所以不用判断无解的情况。这题只需要记录下来所有点的度数,看是否有奇数点,如果有奇数点 就从奇数点开始搜索,如果没有奇数点就从1好点开始搜索,因为要保证答案的字典序最小,所以我们可以用邻矩阵存图,这样遍历的时候编号是从小到大遍历的

代码如下:

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

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;

const int N = 510;

int n=500,m;
int g[N][N];
int ans[1100],cnt;
int d[N];

void dfs(int u)//dfs找欧拉路径
{
    for(int i=1;i<=n;i++)
        if(g[u][i])//如果点u到点i之间有边的话
        {
            g[u][i]--,g[i][u]--;//正向边和反向边数量减1
            dfs(i);//从点i继续搜索
        }
    ans[++cnt]=u;//点u的子节点全部搜索完过后将点u加入到答案中
}
int main()
{
    cin>>m;
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        g[a][b]++,g[b][a]++;
        d[a]++,d[b]++;//度数
    }

    int start=1;
    for(int i=1;i<=n;i++)
        if(d[i]%2)//找到度数为奇数的点作为起点开始搜索
        {
            start=i;
            break;
        }
    dfs(start);
    for(int i=cnt;i;i--)printf("%d\n",ans[i]);//逆序输出答案
    return 0;
}

4,单词

题目链接:http://ybt.ssoier.cn:8088/problem_show.php?pid=1528

 这题我们对于每个单词的首尾两个字母连有向边,然后统计每个字母的入度和出度,判断该图是否存在欧拉路径,判断一个有向图是否存在欧拉路径,我们判断其每个点的度数是否满足上述讲的充分必要条件即可,然后再判断整个图是否连通,这个我们可以用并查集

代码如下:

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

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;

const int N = 30;

int n;
int din[N], dout[N], p[N];
bool st[N];

int find(int x)
{
    if (x != p[x])
        p[x] = find(p[x]);
    return p[x];
}
int main()
{
    char str[1010];

    int T;
    scanf("%d", &T);
    while (T--)
    {
        scanf("%d", &n);
        memset(din, 0, sizeof din);//多组测试数据,记得初始化
        memset(dout, 0, sizeof dout);
        memset(st, 0, sizeof st);
        for (int i = 0; i < 26; i++)p[i] = i;

        for (int i = 0; i < n; i++)
        {
            scanf("%s", str);
            int len = strlen(str);
            int a = str[0] - 'a', b = str[len - 1] - 'a';//将首尾字母转换成数字
            st[a] = st[b] = true;//表示这两个字母出现过
            dout[a]++,din[b]++;//a的出度和b的入度加1
            p[find(a)] = find(b);//将这两个字母合并到一个集合中
        }

        int start = 0, end = 0;//起点和终点的入度
        bool success = true;
        for (int i = 0; i < 26; i++)
            if (din[i] != dout[i])
            {
                if (din[i] == dout[i] + 1)end++;//说明是终点
                else if (din[i] + 1 == dout[i])start++;//说明是起点
                else
                {
                    success = false;
                    break;
                }
            }
        //满足条件的起点和终点的个数要么同时为0,要么同时为1
        if (success && !((!start && !end) || (start == 1 && end == 1)))success = false;

        //判断该图是否连通
        int rep = -1;
        for (int i = 0; i < 26; i++)
            if (st[i])
            {
                if (rep == -1)rep = find(i);
                else if (find(i) != rep)
                {
                    success = false;
                    break;
                }
            }
            
        if (!success)puts("The door cannot be opened.");
        else puts("Ordering is possible.");
    }
    return 0;
}

三,拓扑排序

1,家谱树

 题目链接:http://ybt.ssoier.cn:8088/problem_show.php?pid=1351 

 按给定关系建立拓扑图,然后求拓扑序列即可

代码如下:

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

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;

const int N = 110,M=N*N;

int n;
int h[N],e[M],ne[M],idx;
int d[N],q[N];

void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void top_sort()
{
    int hh=0,tt=-1;
    for(int i=1;i<=n;i++)//首先将入度为0的点加入队列
        if(!d[i])
            q[++tt]=i;

    while(hh<=tt)//用一个bfs求拓扑序
    {
        int t=q[hh++];
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            d[j]--;
            if(!d[j])q[++tt]=j;
        }
    }
}
int main()
{
    scanf("%d",&n);
    memset(h,-1,sizeof h);
    for(int i=1;i<=n;i++)
    {
        int x;
        while(scanf("%d",&x),x)
        {
            d[x]++;//入读加1
            add(i,x);//连一条有向边
        }
    }

    top_sort();//拓扑排序

    for(int i=0;i<n;i++)printf("%d ",q[i]);
    return 0;
}

2,奖金

题目链接:http://ybt.ssoier.cn:8088/problem_show.php?pid=1352

这题看起来跟差分约束很像,可以用差分约束来做,判断是否有解我们判断是否有正环即可,因为要求的最小值,因此我们需要跑一遍最长路,不等式要写成≥的形式,由题意可以得到不等式关系a≥b+1,a≥100,按不等式的关系建图,对于第二个不等式,我们建一个虚拟源点指向a。然后用spfa求一遍最长路得到每个点的最小值,加起来即为答案的最小值。

但是我们之前总结了,对于差分约束的题,如果整个图中的边权大于等于0,我们就可以用tarjan算法求scc来判断是否有正环,然后按scc编号递减的顺序就是拓扑序,我们再按拓扑序求一遍最长路即可。对于这道题,边权是严格大于0的,因此我们可以不用tarjan来求scc判断正环,直接求一遍拓扑序,按拓扑序求最长路即可,如果有正环,那么我们就不能得到拓扑序,此时拓扑序列中的元素个数就会小于n,通过判断这个就可以判断是否无解的情况,然后按拓扑序求一遍最长路即可。 

代码如下:

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

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;

const int N = 1e4 + 10,M=2*N;

int n,m;
int h[N],e[M],ne[M],w[M],idx;
int dist[N],d[N];//dist表示距离源点的距离,d[i]表示点i的入度
int q[N];//数组模拟队列

void add(int a,int b,int c)//邻接表加边函数
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool top_sort()//bfs求拓扑序列
{
    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];
            if(--d[j]==0)q[++tt]=j;
        }
    }
    return tt==n-1;//判断是否能得到一个拓扑序,即队列中的元素个数是否为n
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);//邻接表头初始化
    for(int i=0;i<m;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add(b,a,1);//从b到a连一条边权为1的边
        d[a]++;//点a的入度加1
    }

    if(!top_sort())puts("Poor Xed");
    else
    {
        for(int i=1;i<=n;i++)dist[i]=100;//每个人最低有100块,等价于从虚拟源点像每个点连一条长度是100的边
        for(int i=0;i<n;i++)//按拓扑序求一遍最长路
        {
            int t=q[i];//在拓扑序列中第i个位置的元素是哪个
            for(int j=h[t];j!=-1;j=ne[j])
            {
                int k=e[j];
                dist[k]=max(dist[k],dist[t]+w[j]);//求最长路
            }
        }
        int res=0;
        for(int i=1;i<=n;i++)res+=dist[i];//总和就是最小值
        printf("%d",res);
    }
    return 0;
}

 3,可达性统计

题目链接:164. 可达性统计 - AcWing题库

这题要求我们求出来一个有向无环图中,每个点能到达的点的数量,那么如果在一个图中,如果我们求出来一个点a指向的另一个节点b能到达的点的数量,那么这个点a能到达的点的数量就是点a能到达的点的集合并上点b能到达的点的集合,因此我们可以想到将这个有向无环图进行拓扑排序,拓扑排序后,前面的节点一定是指向后面的节点的,然后我们从后往前遍历,求出来每个点能到达的点的集合,然后就可以求出来他的父节点能到达的点的集合,但是如果我们暴力去做的话,时间复杂度是O(N^2)的,那么我们如何优化呢

这里可以用到二进制来优化,假设图中总共有n个点,对于每一个点能到达的点的我们用一个n位二进制来表示,为1表示能到,为0表示不能到,每一个点的二进制就表示一个集合,对于所有集合的并就可以用或运算,将一个点的集合或上他指向的另一个点的集合恰好就是两个集合的并

代码如下:

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

using namespace std;

const int N = 30010;

int n,m;
int e[N],ne[N],h[N],idx;//单链表
int d[N],seg[N];//d[i]表示点i的入度,用于拓扑排序,seg记录拓扑排序的结果
bitset<N>f[N];//f[i]表示一个n位二进制的集合,即点i能到达的点的集合

void add(int a,int b)//数组模拟单链表
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void topsort()//拓扑排序
{
    queue<int>q;
    for(int i=1;i<=n;i++)//首先将入度为0的点加入队列中
        if(!d[i])
            q.push(i);
    int k=0;
    while(q.size())//拓扑排序
    {
        int t=q.front();q.pop();
        seg[k++]=t;//记录下来拓扑排序的结果
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            d[j]--;
            if(!d[j])q.push(j);
        }
    }
}
int main()
{
    memset(h,-1,sizeof h);//表头初始化
    scanf("%d%d",&n,&m);
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
        d[b]++;//由a指向b,b的入度加1
    }
    
    topsort();//拓扑排序

    for(int i=n-1;i>=0;i--)//从后往前遍历拓扑排序的结果,这里从n-1开始是因为记录的时候是从0开始
    {
        int j=seg[i];//找到当前拓扑序列中的点是哪个
        f[j][j]=1;//表示点j能自己走到自己
        for(int k=h[j];k!=-1;k=ne[k])//点j能到达的点为点j的集合异或上点j的儿子的集合
            f[j]|=f[e[k]];
    }
    for(int i=1;i<=n;i++)printf("%d\n",f[i].count());//f[i]中1的个数就是点i能到达的点的数量
    return 0;
}

4,车站分级

题目链接:http://ybt.ssoier.cn:8088/problem_show.php?pid=1964

对于每个车站,如果一个车从1号点出发,停靠了 1 3 5 6,终点是6号点,那么说明车站2,4的优先级是严格小于1 3 5 6的,那么我们就对2和4分别连向1 3  5 6连一条长度为1的边。这样就建立了n*m=2*4条边。优先级最低是1,因为题目保证了一定有解,因此得到的图一定是拓扑图,然后题目就变成了第2题,求一遍最长路,就可以得到每个车站的最小等级。

但是这题有一点要注意的是,如果按照上述方式建图,我们最坏要建500*(1000-500)*1000=2.5*10^8条边,那么空间一定会超,这里有一个小技巧,对于点2和4,优先级一定小于1 3  5 6,那么我们可以建立一个虚拟源点,对于2 和4连一条边权为0的边指向虚拟源点,然后从虚拟源点连一条边权为1的边指向1 3 5 6.这样我们建立的边就从n*m变成了n+m,最多只有10^6条边。这样就不会超时了。然后剩下的步骤跟上面一样。

代码如下:

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

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;

const int N = 2010,M=1e6+10;//最多1e6条边。2000个点

int n,m;
int h[N],e[M],ne[M],w[M],idx;
int dist[N],d[N];//dist[i]表示点i距离源点的距离,d[i]表示点i的入度
int q[N];//数组模拟队列
bool st[N];//st[i]表示该点是否为停靠车站

void add(int a,int b,int c)//邻接表加边函数
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
    d[b]++;//点b的入度加1
}
void top_sort()//拓扑排序
{
    int hh=0,tt=-1;
    for(int i=1;i<=n+m;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];
            if(--d[j]==0)q[++tt]=j;
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    for(int i=1;i<=m;i++)
    {
        memset(st,0,sizeof st);
        int cnt;
        scanf("%d",&cnt);

        int start=n,end=1;//找到出发车站和终点车站
        while(cnt--)
        {
            int stop;
            scanf("%d",&stop);
            start=min(start,stop);end=max(end,stop);
            st[stop]=true;//停靠了的车站记录下来
        }

        int ver=n+i;//虚拟源点
        for(int j=start;j<=end;j++)
        {
            if(!st[j])add(j,ver,0);//如果不是停靠车站,就连一条长度为0指向虚拟源点的边
            else add(ver,j,1);//否则从虚拟源点指向该节点连一条长度为1的边
        }
    }

    top_sort();//求一遍拓扑排序

    for(int i=1;i<=n;i++)dist[i]=1;//相当于从超级源点连了一条长度为1的边到每个点
    for(int i=0;i<n+m;i++)//根据拓扑序 求最长路
    {
        int t=q[i];//拓扑序中的该点是哪个点
        for(int j=h[t];j!=-1;j=ne[j])
        {
            int k=e[j];
            dist[k]=max(dist[k],dist[t]+w[j]);
        }
    }

    int res=0;
    for(int i=1;i<=n;i++)res=max(res,dist[i]);//所有的级别的最大值,就是答案的最小值
    printf("%d",res);
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值