洛谷 P1073 最优贸易 (分层图状态转移+SPFA,求最长路径;另附某dalao的超短代码:暴力+动规)

本文介绍了一种解决最优贸易路径问题的方法,通过构建分层图实现状态转移,结合SPFA算法求解从起始城市到终点城市间通过一次买卖水晶球获得的最大利润。文章详细阐述了分层图的构建过程,以及如何确保决策过程中只进行一次交易。

题目链接1
题目链接2
另附某dalao的超短代码:暴力+动规

P1073 最优贸易

题目描述

C国有 n 个大城市和 m 条道路,每条道路连接这 n 个城市中的某两个城市。
任意两个城市之间最多只有一条道路直接相连。
这 m 条道路中有一部分为单向通行的道路,一部分为双向通行的道路,双向通行的道路在统计条数时也计为1条。

C国幅员辽阔,各地的资源分布情况各不相同,这就导致了同一种商品在不同城市的价格不一定相同。
但是,同一种商品在同一个城市的买入价和卖出价始终是相同的。

商人阿龙来到C国旅游。
当他得知“同一种商品在不同城市的价格可能会不同”这一信息之后,便决定在旅游的同时,利用商品在不同城市中的差价赚一点旅费。
设C国 n 个城市的标号从 1~n,阿龙决定从1号城市出发,并最终在 n 号城市结束自己的旅行。

在旅游的过程中,任何城市可以被重复经过多次,但不要求经过所有 n 个城市。
阿龙通过这样的贸易方式赚取旅费:他会选择一个经过的城市买入他最喜欢的商品——水晶球,并在之后经过的另一个城市卖出这个水晶球,用赚取的差价当做旅费。

因为阿龙主要是来C国旅游,他决定这个贸易只进行最多一次,当然,在赚不到差价的情况下他就无需进行贸易。

现在给出 n 个城市的水晶球价格,m 条道路的信息(每条道路所连接的两个城市的编号以及该条道路的通行情况)。
请你告诉阿龙,他最多能赚取多少旅费。

注意:本题数据有加强。

输入格式

第一行包含 2 个正整数 n 和 m,中间用一个空格隔开,分别表示城市的数目和道路的数目。

第二行 n 个正整数,每两个整数之间用一个空格隔开,按标号顺序分别表示这 n 个城市的商品价格。

接下来 m 行,每行有 3 个正整数,x,y,z,每两个整数之间用一个空格隔开。

如果z=1,表示这条道路是城市 x 到城市 y 之间的单向道路;如果z=2,表示这条道路为城市 x 和城市 y 之间的双向道路。

输出格式

一 个整数,表示最多能赚取的旅费。如果没有进行贸易,则输出 000。

输入输出样例

输入 #1
5 5
4 3 5 6 1
1 2 1
1 4 1
2 3 2
3 5 1
4 5 2

输出 #1
5

(别人的)思路

刚开始做这道题的时候,不知道什么是分层图状态转移,上来就直接dfs暴力,然后直接TLE了。后来想用bfs,但是想不通。
最后看了解析,才知道是分层图状态转移,配上一个spfa想过很好。
如下:

读完这道题,可以发现这样的事实:

你可以在图上任意走动

最终答案只与你的买入与卖出价格有关(我们就把买入卖出价值作为边权)
如果你买入了一个水晶球,你是没有不卖它的道理的(显然咯,买了不卖血亏...)

n平方的算法不难得出:

我只关心我在哪里买了这个水晶球,在哪里把它卖出去,并且,我能否从起点走到我的买入点,从买入点走到卖出点,然后在走到n

因此,先枚举两个点再bfs检查能否到达,然后更新答案。

而此题的难点在与你如何知道你是否能够到达买入,卖出,钟点(即上两行 并且 后面我说的话),和你能否把所有可能的情况考虑在内。

分层图可以很好的解决这个问题。

由于可以任意走动,所以我们可以建一张图,令图上的边全都是0,表示我的走动对我最终的结果没有影响。

考虑某个点 i ,它买入或者卖出水晶球的花费是v[i] 。

那么 当我们进行买入操作,我们就建立一条有向边转移到一张新图上,边的大小为-v[i],指向点i所能到达的点(在第二层图上)而这张新图就是我们的第二层图。

它表示:假如我选择走了这条边,就是我在这个点买了这个水晶球,我不会反悔,并且我接下来考虑在某个点卖它。

当我们进行卖出操作,我们建立一条有向边转移到第三层图上,边的大小为v[i],指向i所能到达的点(在第三层图上)。

它表示:假如我选择走了这条边,就是我在这个点卖了这个水晶球,我不会反悔,并且我接下来考虑走向终点。

可以发现,从第一层图走到第二层图走到第三层图走到终点,这就是一个合法的选择,而且分层图把所有可能的决策都考虑到了。

最后走向终点,我们有两种合法的操作:

不买卖直接走向终点

直接在第一层图的n号节点建立边权为0的有向边接入一个“超级终点”

买卖一次后走向终点

在第三层图的n号节点建立边权为0的有向边接入“超级终点”

最后解释一下为什么我们要分层:

因为当你分了层,你就可以从还未买入这个状态,转移到已经买入准备卖出这个状态,然后在转移到从卖出点走向终点的状态。由于有向边的建立,你不能从第二/三层走回第一层图,这保证了你只做一次买卖,而不是无限做买卖,符合了题目的要求

而我们最终的答案,就是求从第一层图的1号点,经过三层图走到“超级终点”的最长路,如图所示。
在这里插入图片描述

到此,本题就解完了

我的代码:
#include <iostream>
#include <queue>
#include <vector>
#include <algorithm>
#include <memory.h>
#define maxn 3*100005+1
#define inf 0x3f3f3f3f
using namespace std;

struct node{
    int v;
    int dis;//到v的长度
    node(int _v,int _dis){
        v=_v;
        dis=_dis;
    }
};

vector<node> adj[maxn];//邻接图
int n,m,d[maxn];//d为长度
bool inq[maxn];//标记数组,判断是否在队列里
int value[(maxn-1)/3];//水晶球价格


void spfa(int s){
    memset(inq,false,sizeof(inq));
    fill(d+1,d+maxn,-inf);//-inf初始化推荐fill,memset只能初始化0和-1
    queue<int> q;
    q.push(s);
    inq[s]=true;//入队列,标记
    d[s]=0;
    while(!q.empty()){
        int u=q.front();
        q.pop();
        inq[u]=false;//出队列,取消标记
        for(int i=0;i<adj[u].size();i++){
            int v=adj[u][i].v;
            int dis=adj[u][i].dis;
            if(d[u]+dis>d[v]){//取更大者
                d[v]=d[u]+dis;
                if(!inq[v]){//如果v没入队列,进入并标记,否则会重复操作
                    q.push(v);
                    inq[v]=true;
                }
            }
        }
    }
}

void addedge(int u,int v){
    adj[u].push_back(node(v,0));//第一层
    adj[u+n].push_back(node(v+n,0));//第二层
    adj[u+2*n].push_back(node(v+2*n,0));//第三次

    adj[u].push_back(node(v+n,-value[u]));//第一层买入后进入第二层
    adj[u+n].push_back(node(v+2*n,value[u]));//第二层卖出后进入第三层

}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>value[i];
    }
    int a,b,c;
    while(m--){
        cin>>a>>b>>c;
        addedge(a,b);
        if(c==2){//双向
            addedge(b,a);
        }
    }
    adj[n].push_back(node(3*n+1,0));//第一层连接超级节点3*n+1
    adj[3*n].push_back(node(3*n+1,0));//第三层连接超级节点3*n+1
    spfa(1);
    cout<<d[3*n+1]<<endl;
    return 0;
}
dalao的超短代码:暴力+动规

dalao的解析链接

第一、输入。存邻接表
第二、我们需要做深搜。可以用递归来做,同时做动规:

#include<bits/stdc++.h>
#define INF 0x7f7f7f7f
#define MAXN 100005
using namespace std;

vector<int> g[MAXN];
int n,m,f[MAXN],mi[MAXN],c[MAXN];

void dfs(int x,int minx,int pre) {
    int flag=1; 
    minx=min(c[x],minx);
    if (mi[x]>minx) mi[x]=minx,flag=0;
    int maxx=max(f[pre],c[x]-minx);
    if (f[x]<maxx) f[x]=maxx,flag=0;
    if (flag) return;
    for (int i=0;i<g[x].size();i++) dfs(g[x][i],minx,x);
}

int main() {
    scanf("%d%d",&n,&m);
    for (int i=0;i<MAXN;i++) mi[i]=INF;
    for (int i=1;i<=n;i++) scanf("%d",&c[i]);
    for (int i=1;i<=m;i++) {
        int t1,t2,t3;
        scanf("%d%d%d",&t1,&t2,&t3);
        g[t1].push_back(t2);
        if (t3==2) g[t2].push_back(t1);
    }
    dfs(1,INF,0);
    printf("%d\n",f[n]);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值