学习笔记:最短路

概念

最短路,图论中的一个问题。指的是一个图,有起点 s s s和终点 t t t,中间的边有权值。要求一个从 s s s t t t的路径,使得经过的边的权值和最小。

算法

Floyd算法

方法

F l o y d Floyd Floyd就是一个 d p dp dp的思想,设 f k , i , j f_{k,i,j} fk,i,j表示允许从 1... k 1...k 1...k的点经过,从 i i i j j j的最短路。首先,不允许从任何点经过,那么只有两点直接到达,反之就不能到达。然后每次看看从新允许的 k k k经过和原来的路径哪个好,即 f k , i , j = min ⁡ ( f k − 1 , i , j , f k − 1 , i , k + f k − 1 , k , j ) f_{k,i,j}=\min(f_{k-1,i,j},f_{k-1,i,k}+f_{k-1,k,j}) fk,i,j=min(fk1,i,j,fk1,i,k+fk1,k,j)。发现只会从 k − 1 k-1 k1迭代,省去第一维 k k k。可以解决多源最短路。
时间复杂度 Θ ( n 3 ) \Theta(n^3) Θ(n3)

判环

F l o y d Floyd Floyd还有一个用处:判断一个有向图中是否有环。首先,自己到自己的距离设为正无穷,然后 F l o y d Floyd Floyd改成修改两点之间能否到达,即状态转移方程改成 f i , j ∣ = f i , k & f k , j f_{i,j}|=f_{i,k}\&f_{k,j} fi,j=fi,k&fk,j。最后,如果存在一个 x x x使得 f x , x = 0 f_{x,x}=0 fx,x=0,则说明 x x x找到了一个 k k k过去后又回来了,那么就是一个环。因为无向图本来过去了就可以回来,所以本方法不适用找无向图的环。

例题

AcWing 1125

题目所说的牧区就是指连通块,牧区的直径即连通块内最短距离距离最长的两个点。要求出一个连通块内最短距离距离最长的两个点,则需要计算任意两点之间的距离。因为本题是稠密图,所以用 F l o y d Floyd Floyd更优。如何计算新图的最大直径?按题目要求连接了两个牧场之后,新图的最大直径只可能是以下两种:1、原图的最大直径 ;2、新组成的牧场的直径。如果新图的最大直径就是原图的最大直径,那么新图的直径就是 max ⁡ ( d a , b , d a , b ≠ + ∞ ) \max(d_{a,b},d_{a,b}\not=+\infty) max(da,b,da,b=+)。如果最大直径是新组成的牧场的直径,那就要求出连哪一条边能让新牧场的直径最短。设 m a x x i maxx_i maxxi表示第 i i i个点所能到达的最远的点, l e n x , y len_{x,y} lenx,y表示两点之间的直线距离,新加的边的端点是 a a a b b b,要求原图中 a a a b b b连接了两个牧区才需要加这条边,即 a a a b b b不连通。新连接起来的牧场的直径就是: m a x x a + m a x x b + l e n a , b maxx_a+maxx_b+len_{a,b} maxxa+maxxb+lena,b要求出加上哪一条边得到的新牧区的直径最短,只需让原式最小即可。两种情况的最大直径中较长的一个就是新图的最大直径。另外,会不会出现新牧区的直径没有包含新边的情况呢?的确有这种可能,但是就算出现了这样的情况,也不会对答案有任何影响,因为新牧区的直径等于原来的两个牧区中直径较长的那一个,这样的话第一种情况的计算就会计算了新牧区的直径。

#include<bits/stdc++.h>
using namespace std;
const int NN=154;
pair<double,double>p[NN];
double d[NN][NN],maxx[NN];
char g[NN][NN];
double len(int x,int y)
{
    double lx=p[x].first-p[y].first,ly=p[x].second-p[y].second;
    return sqrt(lx*lx+ly*ly);
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%lf%lf",&p[i].first,&p[i].second);
    for(int i=1;i<=n;i++)
        scanf("%s",g[i]+1);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(g[i][j]=='1')
                d[i][j]=len(i,j);
            else if(i==j)
                d[i][j]=0;
            else
                d[i][j]=1e9;
    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]);
    double ans1=0,ans2=1e9;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
            if(d[i][j]!=1e9)
                maxx[i]=max(maxx[i],d[i][j]);
        ans1=max(ans1,maxx[i]);
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(d[i][j]==1e9)
                ans2=min(ans2,maxx[i]+maxx[j]+len(i,j));
    printf("%.6lf",max(ans1,ans2));
    return 0;
}
AcWing 343

本题是不等式,如果用 g i , j = 1 g_{i,j}=1 gi,j=1表示 i > j i>j i>j,那么就不能出现一个环,使得环上所有边边权都是 1 1 1,则本题就是有向图的判环。每次新加一条边,用 F l o y d Floyd Floyd判环即可。如果有一次每两个数的大小关系都确定了,即任意的 a , b a,b a,b,它们两个之间一定有且仅有一个 g x , y = 1 g_{x,y}=1 gx,y=1(否则也是有环,在判环时已经退出了),那么就已经确定了每个点之间的关系,直接可以退出了。如果有一对 a , b a,b a,b,使 g x , y g_{x,y} gx,y都等于 0 0 0,那么关系就没有确定,按题目要求继续计算。每一次判环执行一次 F l o y d Floyd Floyd即可,也可以增量计算。第二种时间更快,但是第一种时间已经足够了。最后考虑如何按从小到大的顺序输出这些数,因为本题范围很小,可以每一次寻找没被标记且最小的一个,输出并标记该点。考虑如何寻找最小,那么就是任何点比它大,设 v i s v visv visv表示被标记了的点集,即 x ∉ v i s v , ∀ i ∉ v i s v , g i , x = 1 x\notin visv,\forall i\notin visv,g_{i,x}=1 x/visv,i/visv,gi,x=1,则 x x x是最小的一个点。

#include<bits/stdc++.h>
using namespace std;
const int NN=26;
int n,m;
bool d[NN][NN],g[NN][NN],vis[NN];
void floyd()
{
    memcpy(d,g,sizeof(d));
    for(int k=0;k<n;k++)
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                d[i][j]|=d[i][k]&&d[k][j];
}
int check()
{
    for(int i=0;i<n;i++)
        if(d[i][i])
            return 1;
    for(int i=0;i<n;i++)
        for(int j=0;j<i;j++)
            if(!d[i][j]&&!d[j][i])
                return 0;
    return 2;
}
char getmin()
{
    for(int i=0;i<n;i++)
        if(!vis[i])
        {
            bool ok=true;
            for(int j=0;j<n;j++)
                if(!vis[j]&&d[j][i])
                {
                    ok=false;
                    break;
                }
            if(ok)
            {
                vis[i]=true;
                return i+'A';
            }
        }
}
int main()
{
    while(scanf("%d%d",&n,&m)!=EOF&&n&&m)
    {
        memset(g,false,sizeof(g));
        int ok=0,stop;
        for(int i=1;i<=m;i++)
        {
            char s[4];
            scanf("%s",s);
            int x=s[0]-'A',y=s[2]-'A';
            if(!ok)
            {
                g[x][y]=true;
                floyd();
                ok=check();
                if(ok)
                    stop=i;
            }
        }
        if(!ok)
            puts("Sorted sequence cannot be determined.");
        else if(ok==1)
            printf("Inconsistency found after %d relations.\n",stop);
        else
        {
            printf("Sorted sequence determined after %d relations: ",stop);
            memset(vis,false,sizeof(vis));
            for(int i=1;i<=n;i++)
                printf("%c",getmin());
            puts(".");
        }
    }
    return 0;
}

dijkstra算法

方法

d i j k s t r a dijkstra dijkstra算法是一种贪心思想,可以解决多汇最短路问题,不能处理负权边。 d i d_i di表示从起点到 i i i,从未标记的点中找一个 d i d_i di最小的点 i i i,更新与其相邻的点的最短路,标记点 i i i。证明很复杂,在此就不多讲了。可以用堆优化,把修改了值的点放进优先级队列里即可,证明更复杂,也不多讲。
不堆优化的时间复杂度是 O ( n 2 ) O(n^2) O(n2),适合稠密图。
堆优化的时间复杂度是 O ( m log ⁡ n ) O(m\log n) O(mlogn),适合稀疏图。

例题

AcWing 1129

最短路板子,没有负权边,直接 d i j dij dij解决。

#include<bits/stdc++.h>
using namespace std;
const int NN=2504;
vector<pair<int,int> >g[NN];
int d[NN];
bool vis[NN];
void dij(int s)
{
    memset(d,0x3f,sizeof(d));
    d[s]=0;
    priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
    q.push({d[s],s});
    while(q.size())
    {
        int t=q.top().second;
        q.pop();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
            int v=g[t][i].first,w=g[t][i].second;
            if(d[v]>d[t]+w)
            {
                d[v]=d[t]+w;
                if(!vis[v])
                    q.push({d[v],v});
            }
        }
    }
}
int main()
{
    int n,m,s,e;
    scanf("%d%d%d%d",&n,&m,&s,&e);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back({v,w});
        g[v].push_back({u,w});
    }
    dij(s);
    printf("%d",d[e]);
    return 0;
}

bellman-ford算法

方法

枚举 n n n次,每次枚举 e e e条边,用这些边更新最短路。因为如果经过了某个点,那么再经过一次是没有意义的,则最短路的边数一定不大于 n n n,所以枚举 n n n次即可。枚举几次就表示最多用几条边,所以也可以解决有边数限制的最短路问题。
时间复杂度 Θ ( n e ) \Theta(ne) Θ(ne)

例题

AcWing 345

本题就是有边数限制的最短路问题。但是题目要求的是恰好这么多条边,该怎么处理呢?很容易想到一种方法,那就是暴力转换,就是你必须找一条边迭代,否则你就无法到达。注意,以上不是正解,这个算法要开 O 2 O2 O2才能过(写读入优化都无法急救)。本题的正解也是最短路,将会作为习题。

#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
struct node
{
    int u,v,w;
}edge[104];
int d1[NN],d2[NN];
int main()
{
    int n,t,s,e;
    scanf("%d%d%d%d",&n,&t,&s,&e);
    for(int i=1;i<=t;i++)
    {
        int w,u,v;
        scanf("%d%d%d",&w,&u,&v);
        edge[i]={u,v,w};
    }
    memset(d1,0x3f,sizeof(d1));
    d1[s]=0;
    for(int i=1;i<=n;i++)
    {
        memcpy(d2,d1,sizeof(d1));
        memset(d1,0x3f,sizeof(d1));
        for(int j=1;j<=t;j++)
        {
            int u=edge[j].u,v=edge[j].v,w=edge[j].w;
            d1[u]=min(d1[u],d2[v]+w);
            d1[v]=min(d1[v],d2[u]+w);
        }
    }
    printf("%d",d1[e]);
    return 0;
}

SPFA算法

方法

b f s bfs bfs算法的最短路版本,某一个点出队时更新与它有连边的点,并将修改了值,且不在队列里的点入队。同时,这也是 b e l l m a n − f o r d bellman-ford bellmanford算法的优化版,因为只有被更新了的点才会更新其他的点。这个算法可以判断一个图有没有从源点能到的负环,只要某一个点入队 n n n次及以上就是有从源点能到达的负环。有从源点能到达的负环的图就不存在最短路,或者说最短路是负无限。
时间复杂度 O ( n e ) O(ne) O(ne)

优化

目前常用的优化有三种: S L F SLF SLF优化、 L L L LLL LLL优化和 d f s dfs dfs优化。第二种是第一种的扩展,优化的时间从少到多排列。

SLF优化

改成用双端队列,如果加入时从源点到该点的距离比到队头元素的小,那么就加入到队头。时间能优化 20 % 20\% 20% 30 % 30\% 30%

LLL优化

要统计源点到每个点的距离的平均值,如果队头元素比平均值大就放到队尾,反之就用它。时间复杂度能优化 0 % 0\% 0%(有点无法到达)到 40 % 40\% 40%。但是如果和 S L F SLF SLF优化并用是可以优化 20 % 20\% 20% 55 % 55\% 55%

dfs优化

就是把 s p f a spfa spfa的队列扩展变成 d f s dfs dfs来搜。时间复杂度直接降到了 O ( n ) O(n) O(n),但是我感觉没有这么夸张。

例题

AcWing 342

这个题是一个最短路的板子题,但是因为有负权边,用 s p f a spfa spfa的板子即可。注意,本题有一个要求:

事实上,由于最近恐怖主义太嚣张,为了社会和谐,出台了一些政策:保证如果有一条航线可以从Ai到Bi,那么保证不可能通过一些道路和航线从Bi回到Ai。

这话什么意思?就是两个点通过负权边相连,那么就一定不能再通过负权边直接或间接回来。说白了:没有负环。于是我们就可以省去判负环的操作了。但是本题并不简单,因为用 s p f a spfa spfa很容易被卡成 Θ ( n m ) \Theta(nm) Θ(nm),但是这样很明显时间不够用。怎么办?考虑优化,用 S L F SLF SLF就足够了。

#include<bits/stdc++.h>
using namespace std;
const int NN=400004;
int head[NN],g[NN],nxt[NN],e[NN],vis[NN],dis[NN],tot;
void add_edge(int u,int v,int w)
{
    e[tot]=v;
    g[tot]=w;
    nxt[tot]=head[u];
    head[u]=tot++;
}
void spfa(int s)
{
    memset(dis,0x3f,sizeof(dis));
    deque<int>q;
    dis[s]=0;
    vis[s]=true;
    q.push_back(s);
    while(q.size())
    {
        int u=q.front();
        vis[u]=false;
        q.pop_front();
        for(int i=head[u];~i;i=nxt[i])
        {
            int j=e[i];
            if(dis[j]>dis[u]+g[i])
            {
                dis[j]=dis[u]+g[i];
                if(!vis[j])
                {
                    vis[j]=true;
                    if(q.size()&&dis[j]<dis[q.front()])
                        q.push_front(j);
                    else
                        q.push_back(j);
                }
            }
        }
    }
}
int main()
{
    int t,r,p,s;
    scanf("%d%d%d%d",&t,&r,&p,&s);
    memset(head,-1,sizeof(head));
    for(int i=1;i<=r;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add_edge(u,v,w);
        add_edge(v,u,w);
    }
    for(int i=1;i<=p;i++)
    {
    	int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add_edge(u,v,w);
    }
    spfa(s);
    for(int i=1;i<=t;i++)
    {
        if(dis[i]==0x3f3f3f3f)
            puts("NO PATH");
        else
            printf("%d\n",dis[i]);
    }
    return 0;
}

最短路应用

AcWing 1128

本题就相当于有无限封信,然后让这些信到处去送,最后得到信的地方收到信的时间是多久。信送达某一个点的时间肯定是最短路,那么就是要求这些最短路中最长的一个。

#include<bits/stdc++.h>
using namespace std;
const int NN=2504;
vector<pair<int,int> >g[NN];
int d[NN];
bool vis[NN];
void dij(int s)
{
    memset(d,0x3f,sizeof(d));
    d[s]=0;
    priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
    q.push({d[s],s});
    while(q.size())
    {
        int t=q.top().second;
        q.pop();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
            int v=g[t][i].first,w=g[t][i].second;
            if(d[v]>d[t]+w)
            {
                d[v]=d[t]+w;
                if(!vis[v])
                    q.push({d[v],v});
            }
        }
    }
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back({v,w});
        g[v].push_back({u,w});
    }
    dij(1);
    int maxx=0;
    for(int i=1;i<=n;i++)
        maxx=max(maxx,d[i]);
    if(maxx==0x3f3f3f3f)
        maxx=-1;
    printf("%d",maxx);
    return 0;
}

AcWing 1127

本题可以枚举一个源点,然后从这个源点计算到其他地方的最短路之和,求一个最短的和即可。注意,本题是稀疏图,如果用 F l o y d Floyd Floyd求多源汇会超时,必须用 d i j dij dij的堆优化求。

#include<bits/stdc++.h>
using namespace std;
const int NN=804;
vector<pair<int,int> >g[NN];
int d[NN],cow[NN],k,n,m;
bool vis[NN];
int dij(int s)
{
    memset(d,0x3f,sizeof(d));
    memset(vis,false,sizeof(vis));
    d[s]=0;
    priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
    q.push({d[s],s});
    while(q.size())
    {
        int t=q.top().second;
        q.pop();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
            int v=g[t][i].first,w=g[t][i].second;
            if(d[v]>d[t]+w)
            {
                d[v]=d[t]+w;
                if(!vis[v])
                    q.push({d[v],v});
            }
        }
    }
    int sum=0;
    for(int i=1;i<=k;i++)
    {
        if(d[cow[i]]==0x3f3f3f3f)
            return 1e9;
        sum+=d[cow[i]];
    }
    return sum;
}
int main()
{
    scanf("%d%d%d",&k,&n,&m);
    for(int i=1;i<=k;i++)
        scanf("%d",&cow[i]);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back({v,w});
        g[v].push_back({u,w});
    }
    int minn=1e9;
    for(int i=1;i<=n;i++)
        minn=min(minn,dij(i));
    printf("%d",minn);
    return 0;
}

AcWing 1126

不难发现,想要用的钱尽量少,转账过程中交的手续费就要尽量少。因为我们是到达最后要得到 100 100 100元,所以可以从 t t t s s s求最短路。注意,每一条边的权是转完账后剩余的钱是转账之前的钱的几分之几,即 w = 1 − z ÷ 100 w=1-z\div 100 w=1z÷100。因为是反着走,所以如果要求原来的钱就是除以 w w w

#include<bits/stdc++.h>
using namespace std;
const int NN=2004;
vector<pair<int,double> >g[NN];
double d[NN];
bool vis[NN];
void dij(int s)
{
    d[s]=100;
    priority_queue<pair<double,int>,vector<pair<double,int> >,greater<pair<double,int> > >q;
    q.push({d[s],s});
    while(q.size())
    {
        int t=q.top().second;
        q.pop();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
            int v=g[t][i].first;
            double w=g[t][i].second;
            if(d[v]>d[t]/w)
            {
                d[v]=d[t]/w;
                if(!vis[v])
                    q.push({d[v],v});
            }
        }
    }
}
int main()
{
	int n,m,s,e;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int u,v;
		double w;
        scanf("%d%d%lf",&u,&v,&w);
        w=1-w/100.0;
        g[u].push_back({v,w});
        g[v].push_back({u,w});
	}
	scanf("%d%d",&s,&e);
	for(int i=1;i<=n;i++)
	    d[i]=1e9;
	dij(e);
	printf("%.8lf",d[s]);
	return 0;
}

AcWing 920

本题的数据范围很小,但是输入很毒瘤,用一个 s t r i n g s t r e a m stringstream stringstream解决即可。因为边长只会是 1 1 1,所以可以用 b f s bfs bfs求最短路。

#include<bits/stdc++.h>
using namespace std;
const int NN=504;
int sum[NN],d[NN],n,m;
bool g[NN][NN];
void bfs()
{
    memset(d,0x3f,sizeof(d));
    d[1]=0;
    queue<int>q;
    q.push(1);
    while(q.size())
    {
        int u=q.front();
        q.pop();
        for(int i=1;i<=n;i++)
            if(g[u][i]&&d[i]>d[u]+1)
            {
                d[i]=d[u]+1;
                q.push(i);
            }
    }
}
int main()
{
	cin>>m>>n;
	for(int i=0;i<=m;i++)
	{
		string str;
		getline(cin,str);
		stringstream input(str);
		int cnt=0,x;
		while(input>>x)
		{
			cnt++;
			sum[cnt]=x;
		}
		for(int j=1;j<cnt;j++)
			for(int k=j+1;k<=cnt;k++)
				g[sum[j]][sum[k]]=true;
	}
	bfs();
	if(d[n]==0x3f3f3f3f)
		cout<<"NO";
	else
		cout<<max(d[n]-1,0);
	return 0;
}

AcWing 903

把走到某一个点想象成得到了这个点的物品,那么能优惠就相当于现在如果你获得了 u u u,那么你就可以再花 w w w的代价得到 i i i,从 u u u i i i连一条权为 w w w的边即可。每个东西可以直接买,可以直接从一个虚拟源点走到这里获得这个物品,从虚拟源点连一条权为 p i p_i pi的边。考虑地位问题,我们知道,如果和地位差大于 m m m的人交易那么就是徒劳。所以,只能在地位差小于 m m m的人里交易。首先,我们要满足酋长的要求,于是地位必须在 a 1 − m 、 a 1 + m a_1-m、a_1+m a1ma1+m的区间内交易。然后,为了保证其他的所有部落的地位差都不会超过 m m m,所以区间的长度最大为 m m m,枚举这个区间即可。注意,本题是稠密图,用不堆优化的版本更快。

#include<bits/stdc++.h>
using namespace std;
const int NN=104,INF=0x3f3f3f3f;
int m,n,w[NN][NN],a[NN],d[NN];
bool vis[NN];
int dj(int down,int up)
{
    memset(d,0x3f,sizeof(d));
    memset(vis,0,sizeof(vis));
    d[0]=0;
    for(int i=1;i<=n+1;i++)
	{
        int t=-1;
        for(int j=0;j<=n;j++)
            if(!vis[j]&&(t==-1||d[t]>d[j]))
                t=j;
        vis[t]=true;
        for(int j=1;j<=n;j++)
            if(a[j]>=down&&a[j]<=up)
                d[j]=min(d[j],d[t]+w[t][j]);
    }
    return d[1];
}
int main()
{
    scanf("%d%d",&m,&n);
    memset(w,0x3f,sizeof(w));
    for(int i=1;i<=n;i++)
		w[i][i]=0;
    for(int i=1;i<=n;i++)
	{
        int p,cnt;
        scanf("%d%d%d",&p,&a[i],&cnt);
        w[0][i]=min(w[0][i],p);
        while(cnt--)
		{
            int u,x;
            scanf("%d%d",&u,&x);
            w[u][i]=min(w[u][i],x);
        }
    }
    int res=INF;
    for(int i=a[1]-m;i<=a[1];i++)
		res=min(res,dj(i,i+m));
    printf("%d",res);
    return 0;
}

AcWing 1135

本题数据范围不大,可以先用 d i j dij dij求出任意两点之间的最短路。然后要访问 5 5 5个房子,房子很少,直接累加一条遍历每个房子的最短路即可。当然,也可以只留下这几个点来一次 d i j dij dij,但是很明显暴搜就足够。

#include<bits/stdc++.h>
using namespace std;
const int NN=50004;
vector<pair<int,int> >g[NN];
int d[10][NN],home[NN];
bool vis[NN];
void dij(int s,int d[])
{
    d[s]=0;
    priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
    q.push({d[s],s});
    while(q.size())
    {
        int t=q.top().second;
        q.pop();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
            int v=g[t][i].first,w=g[t][i].second;
            if(d[v]>d[t]+w)
            {
                d[v]=d[t]+w;
                if(!vis[v])
                    q.push({d[v],v});
            }
        }
    }
    memset(vis,false,sizeof(vis));
}
int dfs(int u,int s,int cnt)
{
    if(u>5)
        return cnt;
    int res=1e9;
    for(int i=1;i<=5;i++)
        if(!vis[i])
        {
            vis[i]=true;
            res=min(res,dfs(u+1,i,cnt+d[s][home[i]]));
            vis[i]=false;
        }
    return res;
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=5;i++)
        scanf("%d",&home[i]);
    home[0]=1;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back({v,w});
        g[v].push_back({u,w});
    }
    memset(d,0x3f,sizeof(d));
    for(int i=0;i<=5;i++)
        dij(home[i],d[i]);
    printf("%d",dfs(1,0,0));
    return 0;
}

AcWing 340

本题可以二分所需要支付的钱,如果小于等于这个支付的钱那么就可以将其选择成要购买的边;如果大于,那么就必须使用一个免费建设的机会。一遍最短路求一下怎么走需要免费建设的边才会最少,如果最小的方案需要免费建设的边数小于等于 k k k,那么这个支付的金额就是可行的,反之不行。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
vector<pair<int,int> >g[NN];
int d[NN],n,m,k;
bool vis[NN];
bool check(int mid)
{
    memset(d,0x3f,sizeof(d));
    memset(vis,false,sizeof(vis));
    deque<int>q;
    q.push_back(1);
    d[1]=0;
    while(q.size())
    {
        int t=q.front();
        q.pop_front();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
            int v=g[t][i].first,w=g[t][i].second>mid;
            if(d[v]>d[t]+w)
            {
                d[v]=d[t]+w;
                if(!vis[v])
                {
                    if(!w)
                        q.push_front(v);
                    else
                        q.push_back(v);
                }
            }
        }
    }
    return d[n]<=k;
}
int main()
{
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back({v,w});
        g[v].push_back({u,w});
    }
    int l=0,r=1e9;
    while(l<r)
    {
        int mid=l+(r-l)/2;
        if(check(mid))
            r=mid;
        else
            l=mid+1;
    }
    if(r==1e9)
        printf("-1");
    else
        printf("%d",r);
    return 0;
}

AcWing 341

一条路径可以分为两部分,前半部分和后半部分。可以假设在前半部分买入,在后半部分卖出。设 f i f_i fi表示从 s s s i i i中水晶球价格最小的城市,表示从这里买入, g i g_i gi表示从 i i i t t t中水晶球价格最高的城市,表示从这里卖出。这样,枚举每一个点作为路径的划分点,则答案就是 min ⁡ ( g i − f i ) \min(g_i-f_i) min(gifi)。因为第二个是单汇最短路,可以建立反向边从 t t t算最短路。

#include<bits/stdc++.h>
using namespace std;
const int NN=1000004;
int n,m,ans,f[NN],g[NN],a[NN];
bool vis[NN];
struct node1
{
    int head[NN],e[NN],nxt[NN],idx;
    void add(int u,int v)
    {
        e[++idx]=v;
        nxt[idx]=head[u];
        head[u]=idx;
    }
    void spfa(int s)
    {
        memset(vis,false,sizeof(vis));
        memset(f,0x3f,sizeof(f));
        queue<int>q;
        q.push(s);
        f[s]=a[s];
        while(q.size())
        {
            int t=q.front();
            q.pop();
            vis[t]=false;
            for(int i=head[t];i;i=nxt[i])
            {
                int v=e[i],w=min(f[t],a[v]);
                if(f[v]>w)
                {
                    f[v]=w;
                    if(!vis[v])
                    {
                        vis[v]=true;
                        q.push(v);
                    }
                }
            }
        }
    }
}g1;
struct node2
{
    int head[NN],e[NN],nxt[NN],idx;
    void add(int u,int v)
    {
        e[++idx]=v;
        nxt[idx]=head[u];
        head[u]=idx;
    }
    void spfa(int s)
    {
        memset(vis,false,sizeof(vis));
        memset(g,-0x3f,sizeof(f));
        queue<int>q;
        q.push(s);
        g[s]=a[s];
        while(q.size())
        {
            int t=q.front();
            q.pop();
            vis[t]=false;
            for(int i=head[t];i;i=nxt[i])
            {
                int v=e[i],w=max(g[t],a[v]);
                if(g[v]<w)
                {
                    g[v]=w;
                    if(!vis[v])
                    {
                        vis[v]=true;
                        q.push(v);
                    }
                }
            }
        }
    }
}g2;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
    {
        int u,v,opt;
        scanf("%d%d%d",&u,&v,&opt);
        if(opt==1)
        {
            g1.add(u,v);
            g2.add(v,u);
        }
        else
        {
            g1.add(u,v);
            g1.add(v,u);
            g2.add(u,v);
            g2.add(v,u);
        }
    }
    g1.spfa(1);
    g2.spfa(n);
    for(int i=1;i<=n;i++)
        ans=max(ans,g[i]-f[i]);
    printf("%d\n",ans);
    return 0;
}

AcWing 1137

本题就是多源单汇最短路,可以把每个源点到距离设为 0 0 0再求最短路即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
vector<pair<int,int> >g[NN];
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
int d[NN];
bool vis[NN];
void dij()
{
    while(q.size())
    {
        int t=q.top().second;
        q.pop();
        vis[t]=true;
        for(int i=0;i<g[t].size();i++)
        {
            int v=g[t][i].first,w=g[t][i].second;
            if(d[v]>d[t]+w)
            {
                d[v]=d[t]+w;
                if(!vis[v])
                    q.push({d[v],v});
            }
        }
    }
}
int main()
{
    int n,m,e;
    while(scanf("%d%d%d",&n,&m,&e)!=EOF)
    {
        for(int i=1;i<=n;i++)
            g[i].clear();
        for(int i=1;i<=m;i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            g[u].push_back({v,w});
        }
        int w,ans=1e9;
        scanf("%d",&w);
        memset(d,0x3f,sizeof(d));
        memset(vis,false,sizeof(vis));
        for(int i=1;i<=w;i++)
        {
            int s;
            scanf("%d",&s);
            d[s]=0;
            q.push({0,s});
        }
        dij();
        printf("%d\n",d[e]==0x3f3f3f3f?-1:d[e]);
    }
    return 0;
}

AcWing 1131

本题就是一个 b f s bfs bfs搜路径的问题。把某一个点的钥匙和当前已经获得的钥匙的信息二进制压缩存储即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=14,dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
int vis[NN][NN][1<<NN],st[NN][NN][NN][NN],key[NN][NN],n,m;
struct node
{
	int x,y,key,d;
};
int bfs()
{
    queue<node>q;
	q.push((node){1,1,key[1][1],0});
	vis[1][1][key[1][1]]=1;
	while(!q.empty())
	{
		node u=q.front();
		q.pop();
		for(int i=0;i<4;i++)
		{
			int nx=u.x+dx[i],ny=u.y+dy[i];
			if(nx<1||ny<1||nx>n||ny>m)
				continue;
			if(st[u.x][u.y][nx][ny]==-1)
				continue;
			if((st[u.x][u.y][nx][ny]&u.key)!=st[u.x][u.y][nx][ny])
				continue;
			if(vis[nx][ny][u.key|key[nx][ny]])
				continue;
			if(nx==n&&ny==m)
				return u.d+1;
			vis[nx][ny][u.key|key[nx][ny]]=true;
			q.push((node){nx,ny,u.key|key[nx][ny],u.d+1});
		}
	}
	return -1;
}
int main()
{
	int k;
	scanf("%d%d%*d%d",&n,&m,&k);
	while(k--)
	{
		int x1,y1,x2,y2,z;
		scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&z);
		st[x1][y1][x2][y2]=st[x2][y2][x1][y1]=!z?-1:1<<z-1;
	}
	scanf("%d",&k);
	while(k--)
	{
		int x,y,p;
		scanf("%d%d%d",&x,&y,&p);
		key[x][y]|=1<<p-1;
	}
	printf("%d",bfs());
	return 0;
}

AcWing 1134

本题是最短路的计数问题,其实不难发现,如果一个点可以从另一个点转移过来且得到最短路,那么就可以累加到达那个点的方案数。因为边权只有 1 1 1,可以用 b f s bfs bfs做。边界条件:最开始到达起点有一种方法。

#include<bits/stdc++.h>
using namespace std;
const int NN=1e5+4;
vector<int>g[NN];
int d[NN],cnt[NN];
bool vis[NN];
void bfs(int s)
{
    memset(d,0x3f,sizeof(d));
    d[s]=0;
    cnt[s]=1;
    vis[1]=true;
    queue<int>q;
    q.push(s);
    while(q.size())
    {
        int t=q.front();
        q.pop();
        for(int i=0;i<g[t].size();i++)
        {
            int v=g[t][i];
            if(!vis[v])
            {
                vis[v]=true;
                q.push(v);
                d[v]=d[t]+1;
            }
            if(d[v]==d[t]+1)
                (cnt[v]+=cnt[t])%=100003;
        }
    }
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    bfs(1);
    for(int i=1;i<=n;i++)
        printf("%d\n",cnt[i]);
    return 0;
}

AcWing 383

只多一个航程,肯定是次小方案,所以可以统计到每个点的最短和次短距离,最后再看看是否差一个航程,那么就可以累加次大的方案。思考如何迭代,老方法,如果从某一个点的最短路或次短路迭代可以得到当前点的最短路或次短路,那么当前点的最短路或次短路的方案数就加上那个点的最短路或次短路的方案。如果更新的最短路大于了原来的最短路,那么更新最大和次大即可,如果大于了次短路,那么更新次短路即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int d[NN][2],cnt[NN][2];
vector<pair<int,int> >g[NN];
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        for(int i=1;i<=NN;i++)
            g[i].clear();
        memset(d,0x3f,sizeof(d));
        memset(cnt,0,sizeof(cnt));
    	int n,m;
        scanf("%d%d",&n,&m);
        for(int i=1;i<=m;i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            g[u].push_back({v,w});
        }
        int s,f;
        scanf("%d%d",&s,&f);
        d[s][0]=0;
        cnt[s][0]=1;
        priority_queue<pair<int,pair<int,int> >,vector<pair<int,pair<int,int> > >,greater<pair<int,pair<int,int> > > >q;
        q.push({0,{s,0}});
        while(!q.empty())
        {
            pair<int,pair<int,int> >u=q.top();
            q.pop();
            if(d[u.second.first][u.second.second]!=u.first)
				continue;
            for(int i=0;i<g[u.second.first].size();i++)
            {
                pair<int,int> v=g[u.second.first][i];
                if(d[v.first][0]>u.first+v.second)
                {
                    d[v.first][1]=d[v.first][0];
                    cnt[v.first][1]=cnt[v.first][0];
                    q.push({d[v.first][1],{v.first,1}});
                    d[v.first][0]=u.first+v.second;
                    cnt[v.first][0]=cnt[u.second.first][u.second.second];
                    q.push({d[v.first][0],{v.first,0}});
                }
                else if(d[v.first][0]==u.first+v.second)
                    cnt[v.first][0]+=cnt[u.second.first][u.second.second];
                else if(d[v.first][1]>u.first+v.second)
                {
                    d[v.first][1]=u.first+v.second;
                    cnt[v.first][1]=cnt[u.second.first][u.second.second];
                    q.push({d[v.first][1],{v.first,1}});
                }
                else if(d[v.first][1]==u.first+v.second)
                    cnt[v.first][1]+=cnt[u.second.first][u.second.second];
            }
        }
        printf("%d\n",cnt[f][0]+(d[f][1]==d[f][0]+1?cnt[f][1]:0));
    }
    return 0;
}

AcWing 344

本题就是在无向图中找一个最小的环。我们会用 F l o y d Floyd Floyd判断环,但是不会找最小环。于是我们又回到 F l o y d Floyd Floyd状态的定义:

f k , i , j f_{k,i,j} fk,i,j表示允许从 1... k 1...k 1...k的点经过,从 i i i j j j的最短路。

想到了什么?如果在更新之前,两点之间就能到达了,那么再加上 g i , k g_{i,k} gi,k g k , j g_{k,j} gk,j这两条边那么不就是一个环了吗?于是,每次遇到这样的情况就可以看看环是否更小,如果更小那么就更新最小环的路径,路径就是 k → i → j → k k\to i\to j\to k kijk。记录一个 p a t h i , j path_{i,j} pathi,j表示 i i i j j j的最短路是从哪里转移的,那么每次就利用 p a t h i , j path_{i,j} pathi,j来中转,分成两部分递归输出即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int point[NN],g[NN][NN],d[NN][NN],path[NN][NN],cnt;
void get_point(int x,int y)
{
    int k=path[x][y];
    if(!k)
        return;
    get_point(x,k);
    point[++cnt]=k;
    get_point(k,y);
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    memset(g,0x3f,sizeof(g));
    for(int i=1;i<=n;i++)
        g[i][i]=0;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        g[u][v]=g[v][u]=min(g[u][v],w);
    }
    int res=1e9;
    memcpy(d,g,sizeof(g));
    for(int k=1;k<=n;k++)
    {
        for(int i=1;i<k;i++)
            for(int j=i+1;j<k;j++)
                if((long long)d[i][j]+g[i][k]+g[k][j]<res)
                {
                    res=d[i][j]+g[i][k]+g[k][j];
                    cnt=0;
                    point[++cnt]=k;
                    point[++cnt]=i;
                    get_point(i,j);
                    point[++cnt]=j;
                }
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                if(d[i][j]>d[i][k]+d[k][j])
                {
                    d[i][j]=d[i][k]+d[k][j];
                    path[i][j]=k;
                }
    }
    if(res==1e9)
        printf("No solution.");
    else
        for(int i=1;i<=cnt;i++)
            printf("%d ",point[i]);
    return 0;
}

AcWing 345

现在给出本题法二,也就是正解。回到正解,我们发现本题要求的恰好的边数太大了,但是又不能避免计算它,则考虑倍增。但是 b e l l m a n − f o r d bellman-ford bellmanford一次只能扩展一条边,没办法两倍两倍地扩展。不难想起来, F l o y d Floyd Floyd的状态转移是两个路径,而不是用边来转移,则可以用 F l o y d Floyd Floyd来处理这个问题。考虑如何使用它,因为一次只能用两条路径来扩展,所以把 k k k放在最后一层循环即可。注意,我们只是改成了 F l o y d Floyd Floyd,但是照样要强制转移,因为只有这样,使用的边数才会是 2 2 2的幂次增长,而且我们求的也是恰好。

#include<bits/stdc++.h>
using namespace std;
const int NN=104;
struct node
{
    int a[NN][NN];
    void init()
	{
        memset(a,0x3f,sizeof(a));
    }
};
int idx;
node mul(node a,node b)
{
    node ans;
    ans.init();
    for(int i=1;i<=idx;i++)
        for(int j=1;j<=idx;j++)
            for(int k=1;k<=idx;k++)
                ans.a[i][j]=min(ans.a[i][j],a.a[i][k]+b.a[k][j]);
    return ans;
}
node ksm(node a,int x)
{
    node ans;
    memcpy(ans.a,a.a,sizeof(ans.a));
    x--;
    while(x)
	{
        if(x&1)
			ans=mul(ans,a);
        a=mul(a,a);
        x>>=1;
    }
    return ans;
}
int main()
{
    int n,t,s,e;
    scanf("%d%d%d%d",&n,&t,&s,&e);
    node a;
    a.init();
	map<int,int>mp;
    for(int i=1;i<=t;i++)
	{
        int u,v,w;
        scanf("%d%d%d",&w,&u,&v);
        u=mp[u]?mp[u]:(mp[u]=++idx);
        v=mp[v]?mp[v]:(mp[v]=++idx);
        a.a[u][v]=a.a[v][u]=min(a.a[u][v],w);
    }
    node ans=ksm(a,n);
    printf("%d\n",ans.a[mp[s]][mp[e]]);
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值