最优贸易(普通和加强tarjan算法)

Description

C国有 n 个大城市和 m 条道路,每条道路连接这 n 个城市中的某两个城市。

任意两个城市之间最多只有一条道路直接相连。

这 m 条道路中有一部分为单向通行的道路,一部分为双向通行的道路,双向通行的道路在统计条数时也计为 1 条。

C国幅员辽阔,各地的资源分布情况各不相同,这就导致了同一种商品在不同城市的价格不一定相同。

但是,同一种商品在同一个城市的买入价和卖出价始终是相同的。

商人阿龙来到 C国旅游。

当他得知“同一种商品在不同城市的价格可能会不同”这一信息之后,便决定在旅游的同时,利用商品在不同城市中的差价赚一点旅费。

设 C国 n 个城市的标号从 1 ∼ n,阿龙决定从 1 号城市出发,并最终在 n 号城市结束自己的旅行。

在旅游的过程中,任何城市可以被重复经过多次,但不要求经过所有 n 个城市。

阿龙通过这样的贸易方式赚取旅费:他会选择一个经过的城市买入他最喜欢的商品——水晶球,并在之后经过的另一个城市卖出这个水晶球,用赚取的差价当做旅费。

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

现在给出 n 个城市的水晶球价格,m 条道路的信息(每条道路所连接的两个城市的编号以及该条道路的通行情况)。

请你告诉阿龙,他最多能赚取多少旅费。

Input

第一行 2 个正整数 n 和 m  ( 1 ≤ n ≤ 100000 ,  1 ≤ m ≤ 500000 ),中间用一个空格隔开,分别表示城市的数目和道路的数目。

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

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

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

Output

一个整数,表示答案

Sample Input

5 5
4 3 5 6 1
1 2 1
1 4 1
2 3 2
3 5 1
4 5 2

Sample Output

5

发现这道题如果会强连通分量的话用强连通分量解题并不难

如果没有学过的话建议先去b站看视频学完再回来看

思路:用强连通分量的话只需要按照一般流程缩点建新图,再利用拓扑序递推出答案即可

需要注意一些小细节:因为人物是从结点1出发,所以从结点1所在连通块开始递推,而不是从所有入度为0的连通块开始递推

以下是普通tarjan算法代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 200, M = 5 * 1e5 + 20;

int n, m;
///各结点的价格,走到当前连通块的最大获利,走到当前连通块时遇到的最小价格
int price[N], ans[N], dist[N];

//连通块数量,结点所在的连通块,各连通块的最大价格,各连通块的最小价格,各连通块的入度
int scc_cnt, id[N], ma[N], mi[N], in[N];
int dfn[N], low[N], time_;
int stk[N], in_stk[N], top; //栈,记录是否入栈,栈顶

//h1用于建原图,h2用于建新图,考虑双向边所以开2*M,新旧两个图所以开2*2*M
int h1[N], h2[N], e[4 * M], ne[4 * M], idx;
void add1(int a, int b) {e[idx] = b, ne[idx] = h1[a], h1[a] = idx++;}
void add2(int a, int b) {e[idx] = b, ne[idx] = h2[a], h2[a] = idx++;}

void trajan(int u) {
    dfn[u] = low[u] = ++time_;
    stk[++top] = u;
    in_stk[u] = 1;

    for (int i = h1[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            trajan(j);
            low[u] = min(low[j], low[u]);
        }
        else if (in_stk[j])
            low[u] = min(low[j], low[u]);
    }

    if (low[u] == dfn[u]) {
        int y;
        scc_cnt++;
        ma[scc_cnt] = -0x3f3f3f3f, mi[scc_cnt] = 0x3f3f3f3f;
        do {
            y = stk[top--];
            id[y] = scc_cnt;
            in_stk[y] = 0;
            ma[scc_cnt] = max(ma[scc_cnt], price[y]);
            mi[scc_cnt] = min(mi[scc_cnt], price[y]);
        } while (y != u);
    }
}


void topsort() {
    //因为从结点1出发,因此只将结点1所在的连通块入队
    int q[N], tt = -1, hh = 0;
    q[++tt] = id[1]; dist[id[1]] = mi[id[1]];//dist[]用于记录人物走到当前结点时遇到的最小价格

    while (hh <= tt) {
        int t = q[hh++];
        for (int i = h2[t]; i != -1; i = ne[i]) {
            int j = e[i];
            dist[j] = min(dist[t], mi[j]);
            ans[j] = max(ans[t], ma[j] - dist[j]);  //ans[]记录人物走到当前连通块时的最大获利
            if (--in[j] == 0)q[++tt] = j;
        }
    }
}

int main() {
    memset(h1, -1, sizeof h1);
    memset(h2, -1, sizeof h2);
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)scanf("%d", price + i);
    int a, b, z;
    for (int i = 0; i < m; i++) {
        scanf("%d%d%d", &a, &b, &z);
        add1(a, b);
        if (z == 2)add1(b, a);
    }

    //正常缩点
    for (int i = 1; i <= n; i++)if (!dfn[i])trajan(i);

    //正常建新图
    for (int i = 1; i <= n; i++)
        for (int j = h1[i]; j != -1; j = ne[j]) {
            int k = e[j];
            if (id[i] == id[k])continue;
            in[id[k]]++;
            add2(id[i], id[k]);
        }

    //通过拓扑序递推答案
    topsort();

    printf("%d\n", ans[id[n]]);//输出结点所在连通块的最大获利即可

    return 0;

}

注意:上面这份代码在很多平台都可以ac,但是遇到测试数据较强的平台会出现wa,原因是测试数据较强会导致tarjan在递归时栈溢出,遇到这种情况建议先用并查集缩点一次,减小点规模后再跑一遍tarjan即可ac(或者用spfa算法解这道题目)

以下是先用并查集缩点再跑一遍tarjan的代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 200, M = 5 * 1e5 + 20;

int n, m;
///各结点的价格,走到当前连通块的最大获利,走到当前连通块时遇到的最小价格
int price[N], ans[N], dist[N];

//连通块数量,结点所在的连通块,各连通块的最大价格,各连通块的最小价格,各连通块的入度
int scc_cnt, id[N], ma[N], mi[N], in[N];
int dfn[N], low[N], time_;
int stk[N], in_stk[N], top; //栈,记录是否入栈,栈顶

//h1用于建原图,h2用于建新图,考虑双向边所以开2*M,新旧两个图所以开2*2*M
int h1[N], h2[N], e[4 * M], ne[4 * M], idx;
void add1(int a, int b) { e[idx] = b, ne[idx] = h1[a], h1[a] = idx++; }
void add2(int a, int b) { e[idx] = b, ne[idx] = h2[a], h2[a] = idx++; }

vector<pair<int, int> > edge;//存储单向边
int per[N];
int find(int x) {
    if (x == per[x])return x;
    return per[x] = find(per[x]);
}

void trajan(int u) {
    dfn[u] = low[u] = ++time_;
    stk[++top] = u;
    in_stk[u] = 1;

    for (int i = h1[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            trajan(j);
            low[u] = min(low[j], low[u]);
        }
        else if (in_stk[j])
            low[u] = min(low[j], low[u]);
    }

    if (low[u] == dfn[u]) {
        int y;
        scc_cnt++;
        do {
            y = stk[top--];
            id[y] = u;
            in_stk[y] = 0;
            ma[u] = max(ma[u], ma[y]); //更新缩点后该连通块的最大最小价格
            mi[u] = min(mi[u], mi[y]);
            per[y] = u; //这里利用并查集将连通块缩点到u
        } while (y != u);
    }
}


void topsort() {
    //因为从结点1出发,因此只将结点1所在的连通块入队
    queue<int>q;
    int p1 = find(1);
    q.push(p1); dist[p1] = mi[p1];//dist[]用于记录人物走到当前结点时遇到的最小价格
    ans[p1] = ma[p1] - mi[p1];

    while (q.size()) {
        int t = q.front(); q.pop();
        for (int i = h2[t]; i != -1; i = ne[i]) {
            int j = e[i];
            dist[j] = min(dist[t], mi[j]);
            ans[j] = max(ans[t], ma[j] - dist[j]);  //ans[]记录人物走到当前连通块时的最大获利
            if (--in[j] == 0)q.push(j);
        }
    }
}

int main() {

    memset(h1, -1, sizeof h1);
    memset(h2, -1, sizeof h2);
    memset(mi, 0x3f, sizeof mi);
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) { scanf("%d", price + i); per[i] = i; }
    int a, b, z;

    //先用并查集缩点
    for (int i = 0; i < m; i++) {
        scanf("%d%d%d", &a, &b, &z);
        if (z == 1)edge.push_back({ a,b });//存储单向边
        else per[find(a)] = find(b);//如果是双向边则合并为同一连通块
    }

    //建并查集缩点后的图
    for (int i = 0; i < edge.size(); i++) {
        int a = edge[i].first, b = edge[i].second;
        if (find(a) != find(b))
            add1(find(a), find(b));
    }

    //更新并查集各连通块的最大最小价值
    for (int i = 1; i <= n; i++) {
        int a = find(i);
        ma[a] = max(ma[a], price[i]);
        mi[a] = min(mi[a], price[i]);
    }

    //将并查集缩点后的连通块跑一遍tarjan缩点
    for (int i = 1; i <= n; i++)if (!dfn[find(i)])trajan(find(i));

    //建新图
    for (int i = 0; i < edge.size(); i++) {
        int a = edge[i].first, b = edge[i].second;
        int pa = find(a), pb = find(b);
        if (pa != pb) {
            in[pb]++;
            add2(pa, pb);
        }
    }

    //通过拓扑序递推答案
    topsort();

    printf("%d\n", ans[find(n)]);//输出结点所在连通块的最大获利即可

    return 0;

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值