3.2图论(算法提高课)

目录

一,最小生成树的典型应用

1,最短网络

2,局域网

3,繁忙的都市

4,联络员

5,连接格点

二,最小生成树的扩展应用

1,新的开始

2,北极通讯网络

3,走廊泼水节

 4,秘密的牛奶运输

三,spfa找负环

1,虫洞

 2,观光奶牛

3,单词环

 四,差分约束

 1,糖果

2,区间

3,排队布局

4,雇佣收银员


一,最小生成树的典型应用

1,最短网络

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

prim模板题

这题就是一个最小生成树的模板题

代码如下:

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

using namespace std;

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

const int N = 110;

int n;
int w[N][N];
int dist[N];
bool st[N];

int prime()
{
    int res=0;
    memset(dist,0x3f,sizeof dist);

    dist[1]=0;//点1为最小生成树的根节点,距离集合的距离为0
    for(int i=1;i<=n;i++)
    {
        int t=-1;
        for(int j=1;j<=n;j++)
            if(!st[j]&&(t==-1||dist[j]<dist[t]))
                t=j;
        st[t]=true;
        res+=dist[t];//res记录答案

        for(int j=1;j<=n;j++)
            dist[j]=min(dist[j],w[t][j]);
    }
    return res;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            scanf("%d",&w[i][j]);
    printf("%d",prime());
    return 0;
}

2,局域网

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

kurskal模板题  

这题其实就是一个kruskal的模板题,对于已经连通的点,后面又出现的边肯定就是需要删去的边,记录下来权值就好了

代码如下:

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

using namespace std;

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

const int N = 1010;

int n,k;
int f[N];
struct Node
{
    int a,b,w;
}edges[N];

bool cmp(Node& a1,Node& a2)//按边权从小到大排序
{
    return a1.w<a2.w;
}
int find(int x)//找到x的父节点,并将x所在集合中的点进行路径压缩
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    scanf("%d%d",&n,&k);

    for(int i=0;i<k;i++)
        scanf("%d%d%d",&edges[i].a,&edges[i].b,&edges[i].w);
    sort(edges,edges+k,cmp);//将所有的边按边权从小到大排序

    for(int i=1;i<=n;i++)f[i]=i;//并查集初始化

    int res=0;
    for(int i=0;i<k;i++)
    {
        int a=edges[i].a,b=edges[i].b,w=edges[i].w;
        int fa=find(a),fb=find(b);
        if(fa!=fb)f[fa]=fb;
        else res+=w;//已经在一个集合中的边就要删去,记录下来权值
    }
    printf("%d",res);
    return 0;
}

3,繁忙的都市

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

kurskal模板题  

这题要求得到一个最小生成树的的边权中最大的那条边权最小,其实就是用kruskal算法将所有边权排序以后,求在一个集合中的最大边权,也是kruskal的模板题,只不过是问法不同

代码如下:

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

using namespace std;

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

const int N = 8010;

int n,m;
int f[N];
struct Node
{
    int a,b,w;
}edges[N];

bool cmp(Node& a1,Node& a2)//将所有边的权值按从小到大排序
{
    return a1.w<a2.w;
}
int find(int x)//找到x的父节点,并将x所在集合中的点进行路径压缩
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;i++)
        scanf("%d%d%d",&edges[i].a,&edges[i].b,&edges[i].w);
    sort(edges,edges+m,cmp);//将所有边按边权从小到大排序

    for(int i=1;i<=n;i++)f[i]=i;//并查集初始化

    int res=0;
    for(int i=0;i<m;i++)
    {
        int a=edges[i].a,b=edges[i].b,w=edges[i].w;
        int fa=find(a),fb=find(b);
        if(fa!=fb)//如果说不在一个集合中,就加入到一个集合中去
        {
            f[fa]=fb;
            res=w;//res就是此时的最大边权
        }
    }
    printf("%d %d",n-1,res);//要想生成已给最小生成树,一定是n个点需要n-1条边
    return 0;
}

4,联络员

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

kurskal模板题  

对于这个题,我们还是用kruskal来解决,可以先把必选的边选上,必选边所连的点先加入到一个集合中,然后按照所有边权从小到大排序,跑一边kruskal就行了

代码如下:

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

int n,m;
int f[N];
struct Node
{
    int a,b,w;
}edges[N];

bool cmp(Node& a1,Node& a2)//将所有边权按从小到大排序
{
    return a1.w<a2.w;
}
int find(int x)//找到x的父节点,并将x所在集合中的点进行路径压缩
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++)f[i]=i;//并查集初始化

    int res=0;//记录答案
    for(int i=0;i<m;i++)
    {
        int p;
        scanf("%d%d%d%d",&p,&edges[i].a,&edges[i].b,&edges[i].w);
        if(p==1)//将所有必选边加入到并查集中
        {
            int fa=find(edges[i].a),fb=find(edges[i].b);
            f[fa]=fb;
            res+=edges[i].w;
        }
    }
    sort(edges,edges+m,cmp);//将所有边从小到大按边权排序

    for(int i=0;i<m;i++)
    {
        int a=edges[i].a,b=edges[i].b,w=edges[i].w;
        int fa=find(a),fb=find(b);
        if(fa!=fb)//如果这两个点不连通,就将他们加入到一个集合到并查集中
        {
            f[fa]=fb;
            res+=w;
        }
    }
    printf("%d",res);
    return 0;
}

5,连接格点

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

kurskal模板题 

 这题跟上一题很像,都是给了一些已经连接好的点,要求把剩下的点全部连通,不同的是这给的是矩阵上的每个点,因此我们可以先给矩阵上的点编号,然后再给每个点的四个方向连边。这里有一个小优化,因为边权只有1和2两种,我们可以先将垂直的边,也就是边权为1的边全部先连上,再连所有边权为2的边,这样当我们用kruskal算法时就省去了一步排序的时间,可以将时间复杂度从O(MlogM)优化成O(M)

代码如下:

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

using namespace std;

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

const int N = 1010, M = N * N, K = 2 * N * N;

int n,m,k;
int ids[N][N];
struct Edge
{
    int a,b,w;
}e[K];
int p[M];

int find(int x)
{
    if(x!=p[x])p[x]=find(p[x]);
    return p[x];
}
void get_edge()
{
    //按上右下左四个方向连做偏移量
    int  dx[4]={-1,0,1,0},dy[4]={0,1,0,-1},dw[4]={1,2,1,2};

    for(int z=0;z<2;z++)//先连垂直的边,再连水平的边
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
                for(int u=0;u<4;u++)
                    if(u%2==z)//下标为0和2的是垂直的边,1和3的是水平的边
                    {
                        int x=i+dx[u],y=j+dy[u],w=dw[u];
                        if(x&&x<=n&&y&&y<=m)
                        {
                            int a=ids[i][j],b=ids[x][y];
                            if(a<b)e[k++]={a,b,w};//两个点只需要连一条边就够了,因此我们确定一个方向连边,当然不确定也不会错
                        }
                    }
}
int main()
{
    cin>>n>>m;
    
    for(int i=1;i<=n*m;i++)p[i]=i;//并查集初始化

    for(int i=1,t=1;i<=n;i++)//给矩阵上的点编号
        for(int j=1;j<=m;j++)
            ids[i][j]=t++;
    
    int x1,y1,x2,y2;
    while(cin>>x1>>y1>>x2>>y2)
    {
        int a=ids[x1][y1],b=ids[x2][y2];//将给定的点加入到一个集合中
        int fa=find(a),fb=find(b);
        p[fa]=fb;
    }

    get_edge();//得到所有需要连接的边

    int res=0;//记录答案
    for(int i=0;i<k;i++)
    {
        int a=find(e[i].a),b=find(e[i].b),w=e[i].w;
        if(a!=b)
        {
            p[a]=b;
            res+=w;
        }
    }
    printf("%d",res);
    return 0;
}

二,最小生成树的扩展应用

1,新的开始

题目链接:https://vjudge.net/problem/LibreOJ-10066

虚拟源点+prim 

这题与一般的最小生成树不太同,我们可以在一个点直接建立供电站,也可以将一个点连接另一个右有供电站的点,因此我们可以把在一个点直接建立供电站看成是从虚拟源点连接这个点需要的花费,然后用prim求最小生成树即可

代码如下:

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

using namespace std;

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

const int N = 310;

int n;
int w[N][N], dist[N];
bool st[N];

int prim()
{
    memset(dist, 0x3f, sizeof dist);
    dist[0] = 0;//0号点为虚拟源点

    int res = 0;
    for (int i = 0; i <= n; i++)
    {
        int t = -1;
        for (int j = 0; j <= n; j++)
            if (!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;

        st[t] = true;
        res += dist[t];

        for (int j = 0; j <= n; j++)dist[j] = min(dist[j], w[t][j]);
    }
    return res;
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &w[0][i]);//直接在一个点建供电站需要的费用等于从虚拟源点到该点的花费
        w[i][0] = w[0][i];//无向图
    }
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            scanf("%d", &w[i][j]);

    printf("%d", prim());
    return 0;
}

2,北极通讯网络

题目链接:https://vjudge.net/problem/LibreOJ-10065

 这题其实跟繁忙的都市那题很像,换了个问法而已。

可以将题目理解为将最小生成树的最大k-1条边删去后的此时最小生成树中边权最大的边是多少。

也可以理解成,假设我们现在已知d,将所有小于d的边权连接起来,此时我们肯定会得到很多连通块。初始时如果d为0,那么连通块的数量为n,我们可以发现,随着d的增大,连通块的数量会减少。所以当随着d增大,此时连通块的数量恰好为k时,d就是所求的满足条件最小的d。

两种理解方式不同,记录答案的方式也不同,代码如下:

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

#define x first
#define y second

using namespace std;

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

const int N = 510,M=N*N/2;

int n,k,m;
int f[N];
struct Edge
{
    int a,b;
    double w;
}e[M];
pii q[N];

double get_dist(pii a1,pii a2)//得到a1和a2两点的距离
{
    int x=a1.x-a2.x,y=a1.y-a2.y;
    return sqrt(x*x+y*y);
}
bool cmp(Edge& a1,Edge& a2)//将所有边的边权按从小到大排序
{
    return a1.w<a2.w;
}
int find(int x)//找到x所在的连通块,并将连通块中的点进行路径压缩
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    scanf("%d%d",&n,&k);
    for(int i=0;i<n;i++)f[i]=i;//并查集初始化

    for(int i=0;i<n;i++)//用一个pair存下来所有点的坐标
        scanf("%d%d",&q[i].x,&q[i].y);
    
    for(int i=0;i<n;i++)//存下来所有的边
        for(int j=i+1;j<n;j++)
            e[m++]={i,j,get_dist(q[i],q[j])};

    sort(e,e+m,cmp);
    //int cnt=0;这是第一种理解方式的记录答案的方式,记录的是此时最小生成树中边的数量
    int cnt=n;//初始时连通块的数量为n
    double res=0;
    for(int i=0;i<m;i++)
    {
        //if(cnt==n-k)break;第一种理解方式的退出循环的条件
        if(cnt<=k)break;//当此时剩下的连通块的数量小于等于k时,就可以直接退出循环了

        int a=e[i].a,b=e[i].b;
        double w=e[i].w;

        int fa=find(a),fb=find(b);
        if(fa!=fb)//如果不在一个集合中,就合并集合,此时连通块的数量会减一
        {
            //cnt++;第一种理解方式cnt的计数方式,表示此时最小生成树中边的数量加1
            cnt--;
            f[fa]=fb;
            res=w;//此时的w为答案,即最小的d,第一种理解和第二种理解记录答案的方式都一样
        }
    }
    printf("%.2lf",res);
    return 0;
}

3,走廊泼水节

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

当用kruskal算法时,我们会用到并查集合并两个点所在的连通块,那么在合并两个连通块前,连通块内部已经是一个完全图了,我们要做的就是让这两个连通块合并后也变成一个完全图,那么总共需要连边的数量就为A连通块中点的个数*B连通块中点的个数-1,-1是因为最小生成树会将A连通块与B连通块合并起来,所以需要连接的边数要减去1。同时我们观察一下,只要对于AB两连通块,我们连的边的最小值就是最小生成树中连接这两个连通块的边的边权w+1,所以我们需要连边的最小值,就是(A连通块的数量*B连通块的数量-1)*(w+1),将所有的连通块连接起来后,就可以得到总共需要的值,因为每次连接两个连通块取的边权都是最小值,所以最后得到的总值也是最小值

代码如下:

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

using namespace std;

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

const int N = 6010;

int n;
int f[N],size1[N];
struct Edge
{
    int a,b,w;
}e[N];

bool cmp(Edge& a1,Edge& a2)//将所有边权按从小到大排序
{
    return a1.w<a2.w;
}
int find(int x)//找到x所在的连通块,并将连通块中所有的点进行路径压缩
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d",&n);
        for(int i=0;i<n-1;i++)
        {
            int a,b,w;
            scanf("%d%d%d",&a,&b,&w);
            e[i]={a,b,w};
        }
        sort(e,e+n-1,cmp);//将所有边按从小到大排序

        for(int i=0;i<=n;i++)f[i]=i,size1[i]=1;//初始化并查集,每个连通块初始时只有一个点

        int res=0;
        for(int i=0;i<n-1;i++)
        {
            int a=e[i].a,b=e[i].b,w=e[i].w;
            int fa=find(a),fb=find(b);
            if(fa!=fb)//如果两个点不在一个连通块中,就合并两个集合
            {
                res+=(size1[fa]*size1[fb]-1)*(w+1);//前面的-1是减去了最小生成树里包括的那条边,后面的+1是保证变成完全图后此时的最小生成树是唯一的生成树
                size1[fb]+=size1[fa];//合并集合时要更新连通块中点的数量
                f[fa]=fb;//将a合并到b集合中去
            }
        }
        printf("%d\n",res);
    }

    return 0;
}

 4,秘密的牛奶运输

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

这题要求严格次小生成树,首先介绍一下严格次小生成树的定义

次小生成树:给一个带权图,把图中所有生成树按权值从小到大排序,第二小的称为次小生成树 

严格次小生成树与非严格次小生成树的区别就是严格生成树的权值要严格小于最小生成树的权值而非严格次小生成树的权值可以等于最小生成树的权值。

有一个定理:对于一张无向图,如果存在最小生成树和(严格)次小生成树,那么对于任何一棵最小生成树,都存在一棵(严格)次小生成树,使得这两棵树只有一条边不同。

因此有两种方法能求次小生成树:

方法1:先求最小生成树,再枚举删去最小生成树中的边求解。时间复杂度O(mlogm+nm)

方法2:先求最小生成树,依次枚举非树边,然后将该边加入最小生成树中,同时从树中删去一条边,使得最终的图仍是一棵树。如果用dfs查找树中任意两点的路径中权值最大的边权,时间复杂度为O(m+n^2+mlogm),如果用倍增lca来求的化时间复杂度为O(mlogm)

方法2的适用性比较广,既可以求严格次小生成树,同时也可以求非严格次小生成树,因此这里着重讲一下方法2。

在方法2中,我们每次会增加一条非树边,然后删去一条树边,我们用sum表示最小生成树的权值,那么每次操作次小生成树的权值就会变成sum1=sum+w非树-w树,因为这题要求的是严格次小生成树,因此我们要让sum1是严格大于sum的最小值,即w非树-w树要尽可能的小且不为0,对于每次加入一条非树边,假设这条非树边连接的是最小生成树中的点a,b,我们要删去最小生成树中点a到点b的路径上的一条边,那么一定是删去从点a到点b在最小生成树的路径上权值最大的那条边,才会使w树最大,w非树-w树最小,同时要保证w非树严格大于w树,如果w非树等于w树,我们还要判断此时从点a到点b的次大w树,是否满足w非树严格大于w树。对于查找最小生成树中任意两点的路径上的最大边权和次大边权,我们可以用倍增lca来做,时间复杂度会大大的减低,但是我还没学,这里给出另一种方法,因为本题的数据范围不是很大,我们可以用dfs或bfs求出来任意两点路径上的最大边权

(注:本题代码对于求严格次小生成树有误,对于求非严格次小生成树无误。因为对于求严格次小生成树需要求出任意两点的距离上的最大边权和次大边权,这里只求了最大边权。对于求非严格次小生成树只需要求最大边权,用倍增lca求严格次小生成树的代码在3.3图论中的《次小生成树》一题中,因此本题代码只适用于求非严格次小生成树

代码如下:

#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 = 20010;

int n,m;
int f[N];//并查集
int h[N],e[M*2],ne[M*2],w[M*2],idx;//邻接表存储最小生成树中的所有边以及边权
int dist[N][N];//dist[i][j]表示在最小生成树中从点i到点j这条路径上边权最大的权值
struct Edge
{
    int a,b,w;
    bool flag;//判断这条边是否在最小生成树中
}edges[M];

bool cmp(Edge& a1,Edge& a2)//将所有边按从小到大排序
{
    return a1.w<a2.w;
}
int find(int x)//找到x所在的连通块,并将连通块中的点进行路径压缩
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
void add(int a,int b,int c)//邻接表存储最小生成树中的所有边以及边权
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
//u表示当前节点,fa表示当前节点的父节点,maxv表示从最初的节点走到当前节点的路径上的最大边权
void dfs(int u,int fa,int maxv,int d[])//d[]表示与点i直接或间接相连的点路径中的最大边权
{
    d[u]=maxv;//表示最小生成树中从点i到点u的路径中最大的边权为maxv
    for(int i=h[u];i!=-1;i=ne[i])//遍历与点u连接的点
    {
        int j=e[i];
        if(j!=fa)//不能是父节点
            dfs(j,u,max(maxv,w[i]),d);//记得取max
    }
}
int main()
{
    scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++)f[i]=i;

    for(int i=0;i<m;i++)
    {
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        edges[i]={a,b,w};
    }
    sort(edges,edges+m,cmp);//将所有边权按从小到大的顺序排序

    memset(h,-1,sizeof h);//邻接表头初始化
    ll sum=0;//记录最小生成树中的边权总和
    for(int i=0;i<m;i++)
    {
        int a=edges[i].a,b=edges[i].b,w=edges[i].w;
        int fa=find(a),fb=find(b);
        if(fa!=fb)
        {
            sum+=w;
            add(a,b,w),add(b,a,w);//将最小生成中的边用邻接表存起来
            f[fa]=fb;
            edges[i].flag=true;//标记这条边在最小生成树中
        }
    }

    for(int i=1;i<=n;i++)//dfs查找与点i直接或间接相连的点的路径上的最大边权
        dfs(i,-1,0,dist[i]);


    ll res=1e18;//记录答案,要求最小值,初始化成极大值
    for(int i=0;i<m;i++)
    {
        if(!edges[i].flag)//枚举不在最小生成树中的边
        {
            int a=edges[i].a,b=edges[i].b,w=edges[i].w;
            if(w>dist[a][b])//如果当前边加入后会将最小生成树的边权总和严格增加,才计算
                res=min(res,sum+w-dist[a][b]); //因为可能存在w==dist[a][b]的情况,那样sum就是最小值,就不是严格次小生成树了
        }
    }
    printf("%lld",res);
    return 0;
}

三,spfa找负环

用spfa找负环一般有两种方法

(1)统计每个点入队的次数,如果某个点入队n次,则说明存在负环

(2)统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n,则说明有负环

第一中方法的时间复杂度为稳定O(NM),第二种方法最坏为O(NM),因此我们一般选择第二种方法,第二种方法的实现也很简单,只用多开一个数组记录下来当前点的最短路上包含几条边

下面看几个例题

1,虫洞

题目链接:https://vjudge.net/problem/LibreOJ-10085

这题就是一个spfa找负环的模板题,对于虫洞我们建立负权边即可

代码如下:

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

using namespace std;

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

const int N = 510, M =5210;

int n,m,W;
int h[N],e[M],ne[M],w[M],idx;
int dist[N],q[N], cnt[N];//dist表示距离源点的距离,q表示循环队列,cnt当前点的最短路中包含的边数
bool st[N];//st[i]表示点i是否在循环队列中

void add(int a,int b,int c)//邻接表存图
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool spfa()//spfa求负环
{
    memset(dist,0,sizeof dist);//初始化成0和正无穷都没影响,因为是求负环,如果有负环说明肯定能已知更新dist
    memset(cnt,0,sizeof cnt);
    memset(st,0,sizeof st);
    int hh=0,tt=0;
    for(int i=1;i<=n;i++)//首先将全部点入队
    {
        q[tt++]=i;
        st[i]=true;
    }
    while(hh!=tt)
    {
        int t=q[hh++];
        if(hh==N)hh=0;//循环队列
        st[t]=false;//出队的点打上标记
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]>dist[t]+w[i])//如果点t能更新点j
            {
                dist[j]=dist[t]+w[i];
                cnt[j]=cnt[t]+1;//点j的最短路边数就等于点t的最短路边数+1
                if(cnt[j]>=n)return true;//说明点j的最短路边数大于等于n,就可以判断有负环
                if(!st[j])//如果当前点被更新了且不在队列中,就将该点加入到队列中
                {
                    q[tt++]=j;
                    if(tt==N)tt=0;
                    st[j]=true;
                }
            }
        }
    }
    return false;//说明没有找到负环
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d%d",&n,&m,&W);
        memset(h,-1,sizeof h);
        idx=0;
        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);//无向边
        }
        for(int i=0;i<W;i++)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            add(a,b,-c);//有向边
        }
        if(spfa())puts("YES");
        else puts("NO");
    }
    return 0;
}

 2,观光奶牛

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

 这题需要求点权 f 除边权 t 的总和最大,对于在图论中需要求一个总和除另一个总和的最大这类问题,都可以用01规划来做。01规划就是二分答案mid,我们看当前\frac{\sum f[i]}{\sum t[i]}>mid是否满足,如果满足就可以进一步将mid进一步二分扩大,上面那个等式可以变成\sum f[i] -mid*\sum t[i]>0,我们可以将点权放到每个点的出边或入边上,将每条边权变成f[i]-mid*t[i],每条边的边权全部加起来就变成了第二个等式,这样转换边权是等价的。所以题目就转换成了我们二分答案mid,改变了图中每条边的边权后,看图中是否存在正环,如果存在就可以继续二分扩大mid,否则不能,求正环与求负环是一样的,只需要把三角不等式的判断条件改一下就行了,其实把上面的等式换到右边去,求的就变成负环了,都是一样的

代码如下:

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

using namespace std;

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

const int N = 1010, M = 5010;

int n,m;
int h[N],e[M],ne[M],wt[M],idx;
int f[N], cnt[N], q[N];
double dist[N];
bool st[N];

void add(int a,int b,int c)
{
    e[idx]=b,wt[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool check(double mid)
{
    //上个题已经说明了dist不管初始成什么都是不影响我们求负环的,这里我们求正环同理
    memset(cnt,0,sizeof cnt);//初始化
    memset(st,0,sizeof st);

    int hh=0,tt=0;//循环队列
    for(int i=1;i<=n;i++)//首先将全部点加入到队列中
    {
        q[tt++]=i;
        st[i]=true;
    }
    while(hh!=tt)
    {
        int t=q[hh++];
        if(hh==N)hh=0;//循环队列
        st[t]=false;//出队的点要标记,下次可能还会入队
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]<dist[t]+f[t]-mid*wt[i])//这里求的是正环,所以要把大于改成小于
            {
                dist[j]=dist[t]+f[t]-mid*wt[i];
                cnt[j]=cnt[t]+1;
                if(cnt[j]>=n)return true;//如果这个点的最短路上有n条边,说明有正换
                if(!st[j])
                {
                    q[tt++]=j;
                    if(tt==N)tt=0;//循环队列
                    st[j]=true;//入队要打上标记
                }
            }
        }
    }
    return false;//说明没有正环
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);//邻接表头初始化

    for(int i=1;i<=n;i++)scanf("%d",&f[i]);//点权
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);//有向图,c为边权
    }
    double l=0,r=1000;//二分答案
    while(r-l>1e-4)//经验值,比所求精度在小两倍
    {
        double mid=(l+r)/2;
        if(check(mid))l=mid;
        else r=mid;
    }
    printf("%.2lf",r);
    return 0;
}

3,单词环

题目链接:https://vjudge.net/problem/LibreOJ-10082

这题要求单词环的长度除上单词数最大,跟上题差不多,肯定也是用01规划来解决。问题是这题我们如何建图,如果给每个单词编一个号,给他能连接的单词连一条边,那么总共的点数是10^5,边数为10^10.需要非常大的时间和空间,肯定不可取,因此我们可以对于每个单词的首尾两个字母编号,然后连一条从首两个字母指向尾两个字母长度为单词长度的边,因此图中的边权就为单词长度,点权就为1,然后可以用上一个一样的思路解决

不过这题有一个小优化,我们都知道如果用spfa求负环,最坏时间复杂度为达到O(NM)的,这是有可能超时的,那么有一个经验上的trick,就是当图中的所有点被更新的次数大于一个比较大的数(N的几倍或是十几倍),我们就认为图中有环,就可以直接返回了,但是这个方法并不能保证完全对,如果说在写这类题目出现超时的情况,可以加上这个优化试一试

代码如下:

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

using namespace std;

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

//N最大为676,这里开到700
const int N = 700, M = 1e5+10;

int n;
int h[N],e[M],ne[M],wt[M],idx;
double dist[N];//dist[i]表示到源点的距离
int cnt[N],q[N];//cnt[i]表示点i的最短路中的边数个数,q为循环队列
bool st[N];//st[i]表示点i是否在队列中

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

bool check(double mid)//判断当前mid是否满足条件
{
    memset(cnt,0,sizeof cnt);
    memset(st,0,sizeof st);

    int hh=0,tt=0;
    for(int i=0;i<=676;i++)//总共676个点,全部入队
    {
        q[tt++]=i;
        st[i]=true;
    }
    int count=0;//用于记录图中所有点更新的次数,经验上来说当达到一个比较大的数时,我们就认为图中有环,直接返回true(经验值)
    while(hh!=tt)
    {
        int t=q[hh++];
        if(hh==N)hh=0;//循环队列
        st[t]=false;

        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]<dist[t]+wt[i]-mid)//如果能更新点j的边权
            {
                dist[j]=dist[t]+wt[i]-mid;
                cnt[j]=cnt[t]+1;//最短路的边路也要加1

                if(++count>10000)return true;//经验值,开一个比较大的数,如果图中所有的点更新的次数达到这么多就可以直接返回true了
                if(cnt[j]>=N)return true;//说明一个点的最短路经过了N条边,说明有正环

                if(!st[j])
                {
                    q[tt++]=j;
                    if(tt==N)tt=0;//循环队列
                    st[j]=true;
                }
            }
        }
    }
    return false;//说明没有正环
}
int main()
{
    while(scanf("%d",&n),n)
    {
        memset(h,-1,sizeof h);
        idx=0;
        char str[1010];
        for(int i=0;i<n;i++)
        {
            scanf("%s",str);
            int len=strlen(str);
            if(len>=2)//只有长度大于等于2的单词来可能接到别人前面或后面
            {
                int left=(str[0]-'a')*26+str[1]-'a';//得到单词的首的编号
                int right=(str[len-2]-'a')*26+str[len-1]-'a';//得到单词的尾的编号
                add(left,right,len);//加一条从单词的首到尾的长度为len的边
            }
        }
        if(!check(0))puts("No solution");//如果让所有边的边权最大都没有正环,那么就不可能有正环了
        else
        {
            double l=0,r=1000;//二分答案
            while(r-l>1e-4)
            {
                double mid=(l+r)/2;
                if(check(mid))l=mid;
                else r=mid;
                
            }
            printf("%lf\n",r);
        }

    }
    return 0;
}

 四,差分约束

首先介绍一下什么是差分约束。差分约束是一种特殊的N元一次不等式组。它包含N个变量X1~Xn以及M个约束条件,每个约束条件都是由两个变量作差构成的,形如Xi-Xj<=Ck,其中Ck是常数(可以是非负数,也可以是负数),1<=i,j<=N,1<=k<=M。我们要解决的问题就是:求一组解X1=a1,X2=a2,……Xn=an,使所有的约束条件都得到满足。

差分约束系统的每个约束条件Xi-Xj<=Ck,可以变形为Xi<=Xj+Ck。这与单源最短路径问题中的三角不等式dist【j】<=dist【i】+Ci非常相似。因为在单源最短路中当每个点j的dist【j】都达到最小值后,对于图中的每个点都会满足dist【j】<=dist【i】+Ci。因此,可以把每个变量Xi看作使有向图的一个节点i,对于每个约束条件Xi-Xj<=Ck,从节点j向节点i连一条长度为Ck的有向边,注意这里是从点j连向点i的有向边,方向不要搞反了!,同时要注意不等式的符号是小于等于还是大于等于。

注意到如果{a1,a2,……an}是一组解,那么对任意的常数p,{a1+p,a2+p,……an+p}显然也是一组解(作差后p恰好被消掉)。所以不妨先求一组负数解,即假设对于任意i,都有Xi<=0,然后再增加一个0号节点,令X0=0,这样一来,就多了N个形如Xi-X0<=0的约束条件,应该从节点0向每个节点i连一条长度为0的有向边。

设dist【0】=0,以0号点位起点求单源最短路。若图中存在负环,则给定的差分约束系统无解。否则,Xi=dist【i】就是差分约束系统的一组解。在某些题目中,约束条件形如Xi-Xj>=Ck。我们仍然可以从j到i连长度位Ck的有向边,只是改为计算单源最长路。与上述同理,当每个点j都达到最大值后,对于图中的每个点都会满足dist【j】>=dist【i】+Ci,若图中有正环则无解。当然,我们也可以把约束条件转换成Xj-Xi<=-Ck,再按照单源最短路进行计算。

不等式的判断符号决定了我们如何建图,以及用最短路求解还是最长路求解,如果等式是上述那种<=,即用最短路求解,如果等式是>=,即用最长路求解.

接下来讲一下差分约束的一些要满足的条件和注意事项。

(1)求不等式的可行解

        源点需要满足的条件:从源点出发,一定可以走到所有的边(重点)

     步骤:【1】先将每个不等式Xj<=Xi+Ci转换成一条从点i连向点j长度位Ci的有向边

                【2】找一个超级源点,即上面说的0号点,使得该源点一定能遍历到所有的边

                【3】从源点求一遍单源最短路,如果有负环,说明原不等式无解,无负环,说明dist【i】就是原不等式的一组解。

(2)如何求最大值和最小值,这里的最值指的是每个变量的最值

        结论:如果求的是最小值,则应该求最长路;如果求的是最大值,则应该求的是最短路。

        问题:如何转换Xi<=C,其中C是一个常数,这类的不等式

        方法:建立一个超级源点0号点,建立0->i,长度为c的边即可

 用差分约束保证源点能到达每条边的原因是,如果图中有负环或者正环我们能找出来,如果对于一些题目,源点不能到达所有边并且我们要建立的虚拟源点到每个点连的边的权值为0的情况,我们可以将所有的点都加入到队列中,就等价于建立了一个虚拟源点,并且这么做还省去了一些操作

 1,糖果

题目链接:[SCOI2011] 糖果 - 洛谷

这题就是一个差分约束的模板题,要求我们求所需糖果的最小值,因此我们要用最长路来求解,设置不等式时要设置成>=的形式,由题意

①当X==1时,A==B <==> A>=B,B>=A

 ②当X==2时,A<B <==> B >=A+1

 ③当X==3时,A>=B <==> A>=B

  ④当X==4时,A>B <==>A>=B+1

  ⑤当X==5时,A<=B <==>B>=A

同时,我们要满足每个小朋友都有糖果,即A>=1,因此我们可以建立一个超级源点X0,可以看成是A>=X0+1,连接一条长度为1的边指向每个小朋友A ,这样以X0号点作为源点同时还保证了源点能到达每条边。

注意建图时时等式右边的点指向左边的点,不要把顺序搞反了。然后跑一边spfa求单源最长路,如果有正环(因为求的是单源最长路)就说明无解,如果没有正环,dist的总和就是答案

在上面spfa判负环中已经讲了一个不一定完全对的小优化,这里其实还有一个另外的小优化,就是我们可以把spfa的队列换乘栈,这样如果有负环的话,我们能很快的找出来,且这么做也一定是对的,因为队列和栈的区别只是出队的顺序,且spfa算法与每个点出队的顺序无关。在一定有环的题目中,我们可以这么做,但是如果不确定一个图是否有环还是建议用队列,因为在队列中的点可能会被优化很多次,当他出队的时候可能已经是最优解了,如果用栈的话一个点要重复出队很多次

代码如下:

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

using namespace std;

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

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

int n,m;
int h[N],e[M],ne[M],w[M],idx;
ll dist[N];//dist[i]表示点i到虚拟源点的最长路
int cnt[N],q[N];//cnt[i]表示点i的单源最长路中的边数,q表示循环队列
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()
{
    memset(dist,-0x3f,sizeof dist);//因为求的是单源最长路,所以每个点初始化成极小值

    int hh=0,tt=1;
    dist[0]=0;
    q[0]=0;//虚拟源点入队

    while(hh!=tt)
    {
        int t=q[--tt];
        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;//记录点j到虚拟源点的最长路中的边数
                if(cnt[j]>=n+1)return false;//因为加了0号点,总共有n+1个点,说明有正环
                if(!st[j])
                {
                    q[tt++]=j;
                    st[j]=true;
                }
            }
        }
    }
    return true;//说明没有正环
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    while(m--)
    {
        int x,a,b;
        scanf("%d%d%d",&x,&a,&b);//按题意建立有向边
        if(x==1)add(a,b,0),add(b,a,0);
        else if(x==2)add(a,b,1);
        else if(x==3)add(b,a,0);
        else if(x==4)add(b,a,1);
        else if(x==5)add(a,b,0);
    }

    for(int i=1;i<=n;i++)add(0,i,1);//建立虚拟源点指向每个点

    if(!spfa())puts("-1");//说明有正环
    else
    {
        ll res=0;
        for(int i=1;i<=n;i++)res+=dist[i];//所有dist的总和就是答案,记录开long long
        printf("%lld",res);
    }
    return 0;
}

2,区间

题目链接:362. 区间 - AcWing题库

这题可以用差分约束来做,但是我们要找不等式的条件,差分约束的难点就是找全不等式。

这题要求的是最小值,因此我们要用最长路来做,不等式要写成大于等于的形式,不等式右边的点指向左边的点

这题我们可以用前缀和的方式来做,用Si表示1~i选的数有多少个,那么一定有

①Si>=Si-1    这个不等式的条件是1~i-1中选的数一定小于等于1~i选的数

②Si-Si-1<=1,即Si-1>=Si  -1,这个不等式的意思是第i个数选还是不选,所以是小于等于1

③Sb-Sa-1>=c,即Sb>=Sa-1  +c,意思是a到b这个区间中选的个数一定大于等于c个,这个是题目的条件

这样所有的约束条件就找完了,因为我们用的是前缀和的思想,所以要让下标从1开始,因为题目给的条件是从0开始的,所以我们要整体+1,然后以0号点为超级源点跑一遍单源最长路,0号点肯定能到达每条边,这也满足了用差分约束的条件。因为题目保证了肯定有解,因为我们可以将全部数都选取,所以不用判断是否有正环

代码如下:

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

using namespace std;

const int N=50010,M=150010;

int n;
int h[N],e[M],ne[M],w[M],idx;
int dist[N],q[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++;
}
void spfa()
{
    memset(dist,-0x3f,sizeof dist);//因为是求最长路,所以初始化要初始化成极小值
    dist[0]=0;//源点到自己的距离为0
    int hh=0,tt=1;
    q[0]=0;//起点入队
    
    while(hh!=tt)
    {
        int t=q[hh++];
        if(hh==N)hh=0;
        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[tt++]=j;
                    if(tt==N)tt=0;
                }
            }
        }
    }
}
int main()
{
    scanf("%d",&n);
    memset(h,-1,sizeof h);//邻接表头初始化
    for(int i=1;i<=50001;i++)//将可能的情况加边
    {
        add(i-1,i,0);
        add(i,i-1,-1);
    }
    for(int i=0;i<n;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        a++,b++;//让下标从1开始,因为我们要将0号点设为超级源点
        add(a-1,b,c);//前缀和的思想
    }
    spfa();//因为这题一定有解,所以不用判断是否有正环
    printf("%d",dist[50001]);//dist[50001]就是最小值
    return 0;
}

3,排队布局

题目链接:信息学奥赛一本通(C++版)在线评测系统

这题很明显要用差分约束,这题的不等式也比上题要好找,因为是求最大值,所以我们要用最短路来做,不等式要写成小于等于的形式,有以下关系

①Xi<=Xi+1,Xi为第i头牛的位置下标(这一条不要忘记了)

②Xb<=Xa+D

③Xa<=Xb-D 

因为用差分约束时我们有一个很重要的条件,就是要保证源点能到达每条边,但是上面给的约束条件中,并不能保证能找到一个源点能到达每条边,因此,我们可以建立一个虚拟源点,用一个长度为0的边连接每个点,然后以虚拟源点为起点跑一遍spfa,如果有负环,就说明无解,输出-1。但是其实我们不用真的建立一个虚拟源点,因为当我门建了一个虚拟源点,以虚拟源点跑spfa时,首先虚拟源点会将所有连接的点进入队列,所以我们可以一开始就将所有点就加入到循环队列中,这样就省去了建立虚拟源点的操作,可以减去一些重复操作。然后我们还要判断点1和点n的距离是否可以任意大,意思就是让我们求点1是否能到达点n,我们只需要以1号点再跑一边单源最短路,就可以判断出点1是否能到达点n,如果不能到达的话,输出-2,如果能到达的话,点n的dist值就是点1到点n的最大距离

因此从这里我们可以推出来,用差分约束保证源点能到达每条边的原因是,如果图中有负环或者正环我们能找出来,如果对于一些题目,源点不能到达所有边并且我们要建立的虚拟源点到每个点连的边的权值为0的情况,我们可以将所有的点都加入到队列中,就等价于建立了一个虚拟源点,并且这么做还省去了一些操作

代码如下:

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

using namespace std;

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

const int N = 1010, M = 2e4+N+10, INF=0x3f3f3f3f;

int n,m1,m2;
int h[N],e[M],ne[M],w[M],idx;
int dist[N],cnt[N], q[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(int size)//如果判断负环,需要将全部点都加入到队列中,所以我们传参传的size表示我们需要将哪些点加入到队列中
{
    memset(st,0,sizeof st);//因为要调用两次spfa,所以要初始化
    memset(cnt,0,sizeof cnt);
    memset(dist,0x3f,sizeof dist);//将每个点初始化成极大值,这是为了我们以1号点作为单源最短路时求点1到点n的距离
                                //如果只用判断负环的话不需要管dist
    int hh=0,tt=0;
    for(int i=1;i<=size;i++)
    {
        dist[i]=0;//这一步其实是给1号点做起点时的初始化dist,因为判断负环时dist不会影响我们判断负环
        q[tt++]=i;
        st[i]=true;
    }

    while(hh!=tt)
    {
        int t=q[hh++];
        if(hh==N)hh=0;
        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 false;//说明有负环
                if(!st[j])
                {
                    q[tt++]=j;
                    if(tt==N)tt=0;
                    st[j]=true;
                }
            }
        }
    }
    return true;
}
int main()
{
    scanf("%d%d%d",&n,&m1,&m2);
    memset(h,-1,sizeof h);
    for(int i=1;i<n;i++)add(i+1,i,0);//将给定的约束条件连边
    while(m1--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        if(a>b)swap(a,b);//这里可能有坑,需要保证a<b
        add(a,b,c);
    }
    while(m2--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        if(a>b)swap(a,b);//同样的要保证a<b
        add(b,a,-c);
    }
    if(!spfa(n))printf("-1");//将n个点都加入到队列中,判断是否有负环
    else
    {
        spfa(1);//以1号点求单源最短路,求到达点n的距离
        if(dist[n]==INF)printf("-2");//说明到达不了点n
        else printf("%d",dist[n]);//如果能到达点n,dist[n]就是点1到点n的最大值
    }
    return 0;
}

4,雇佣收银员

题目链接:393. 雇佣收银员 - AcWing题库

这题的不等式条件不是很好找,有些复杂。首先看题目,求的是最小值,那么我们就需要求最长路,不等式要写成大于等于的形式。

我们以num【i】来表示第i个时刻申请的人的数量,Xi表示我们在i时刻选择的员工数量,那么我们需要满足的条件就是

① 0≤Xi≤num【i】 

②Xi-7+Xi-6+Xi-5+Xi-4+…… Xi≥R【i】

这样就把题目中全部的约束条件得到出来了,我们观察第二个约束条件,与我们前面讲到的约束条件不一样,第二个约束条件中有很多的变量,因为我们可以用一个前缀和S【i】来表示1~i时刻选择的员工数量,因为用到了前缀和,我们要前题目中给的从0时刻开始,整体加1变成从1时刻开始,那么上面的约束条件就变成了

①S【i】-S【i-1】≥0,S【i】-S【i-1】≤num【i】

②如果(i≥8)S【i】-S【i-8】≥R【i】,如果(i小于等于7)S【i】+S【24】-S【i+16】≥R【i】

整理一下将所有不等式变成≥的形式就变成了

①S【i】-S【i-1】≥0    ②S【i-1】≥S【i】-num【i】

③如果(i≥8)S【i】-S【i-8】≥R【i】

④如果(i小于等于7)S【i】≥S【i+16】-S【24】+R【i】

这样约束条件就全部得到了,还有一个问题就是,我们发现第四个约束条件有三个约束条件,且我们发现S【24】的可取值不是很大,是0~1000这个区间,因此,我们可以从小到大枚举S【24】的取值,第一次得到满足条件的S【24】的值,也就i是此时图中没有正环时,就表示1时刻到24时刻我们选取的员工数量,此时的S【24】就是答案,然后停止枚举

最后还要注意一点就是,每次枚举,要在图中表示S【24】是一个定值,因为还有其他点会指向S【24】,所以不能直接让S【24】变成一个定值,我们可以让S【24】≥C(定值),S【24】≤C(定值),等价于S【24】≥S0+C,S【24】≤S0+C,让S0==0,这样就可以在图中表达S【24】为定值了

最后要判断一下以0号点作为源点,能否到达每条边,因为有第一条约束条件,所以我们可以知道0号点是能到达每条边的

代码如下:

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

using namespace std;

const int N =30,M=100;

int n;
int h[N],e[M],ne[M],w[M],idx;
int r[N],num[N];
int dist[N],cnt[N],q[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++;
}
void build(int c)
{
    memset(h,-1,sizeof h);//因为要多次建图,表头也一定要每次都初始化
    idx=0;//以及编号
    
    for(int i=1;i<=24;i++)//按推出来的不等式连边
    {
        add(i-1,i,0);
        add(i,i-1,-num[i]);
        if(i>=8)add(i-8,i,r[i]);
        if(i<=7)add(i+16,i,-c+r[i]);
    }
    add(0,24,c),add(24,0,-c);//因为S24是一个定值了,我们要让他成为一个定值
    //就需要让S24>=c,S24<=c, 那么就等价于S24>=S0+c,S24<=S0+c
}
bool spfa(int c)
{
    build(c);//首先建图
    
    memset(cnt,0,sizeof cnt);//多次调用spfa记得初始化
    memset(st,0,sizeof st);
    memset(dist,-0x3f,sizeof dist);//这里初始化成极小值
    
    dist[0]=0;//0号点为超级源点,能保证到达每条边,即S0==0
    int hh=0,tt=1;
    q[0]=0;//0号点入队
    while(hh!=tt)
    {
        int t=q[hh++];
        if(hh==N)hh=0;
        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]>=25)return false;//因为有0号点,所以总共有25个点
                if(!st[j])
                {
                    q[tt++]=j;
                    if(tt==N)tt=0;
                    st[j]=true;
                }
            }
        }
    }
    return true;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        for(int i=1;i<=24;i++)//注意这题的输入,先输入r
            scanf("%d",&r[i]); 
            
        scanf("%d",&n);
        memset(num,0,sizeof num);//多组数据,记得清空数组
        
        for(int i=0;i<n;i++)
        {
            int x;
            scanf("%d",&x);
            num[x+1]++;//每个下标都往后移一位,便于我们待会用前缀和的思想,0号点作为超级源点
        }
        
        bool flag=false;
        for(int i=0;i<=1000;i++)//遍历S24的值,第一次得到满足条件的值就是答案的最小值
        {
            if(spfa(i))
            {
                printf("%d\n",i);//S24就是需要雇佣的员工的最小数量
                flag=true;
                break;
            }
        }
        if(!flag)puts("No Solution");
    }
    
    
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值