[SMOJ1870]旅行

97 篇文章 0 订阅
10 篇文章 0 订阅

这题我一开始想到应该是最大流,但不知道怎么处理天数的问题。因此直接只跑了一遍最大流,加了一个奇怪的式子,居然水了 40 分。

不过,如果意识到答案具有单调性,问题就会好办很多了。而这个单调性,其实并不难看出,题目中已经有明显暗示“最后到达 B 市的人的到达时间最早”。
值得一提的是二分边界的问题。最坏情况下,可能需要多少天呢?不妨设想一条长度为 N 的链,只有相邻点之间有边,且容量均为 1。则第一个人需要 (N1) 天到达,第二个人在第一个人出发后紧随其后,则其到达时为第 N 天……依此类推,最坏情况下的到达时间为 (N+T) 天。

而对于当前二分到的天数 k ,如何判断能否在 k 天内到达?经验主义告诉我们应该跑最大流,若满流则说明可以。思路没有问题,关键是,如何构图?
构图的关键,就是针对题目的特性和限制。而在本题中,特性和限制主要有:每条边在同一天内被经过的最大次数是受限制的,也可以由此推导出,对于部分到达某城市后无法即时次日换乘的人,可以在该城市中“滞留”一段时间。
如何解决“边被经过的次数受限制”?对于顶点的同样限制,我们的方法是拆点。而实际上,边也可以理解为这样处理,不过本质上还是点的拆分。
将每个点拆成 (k+1) 层,分别表示第 0 天,第 1 天,……,第 k 天时的该点。对于原图中容量为 c 的边 (u,v) ,从所有第 i 层的 u 点向第 (i+1) 层的 v 点连一条容量为 c 的边,表示在第 i 天从 u 出发,在第 (i+1) 天到达 v 。(不要按题目说的“上午下午”来考虑,直接认为一趟飞行需要一天即可)
同时,为了应对“滞留”的问题,可以从所有第 i 层的 u 点向第 (i+1) 层的 u 连一条容量为 的边。
因为起点和终点是限定的,因此从源点向第 0 层的第 1 个点连容量为 T 的边,第 k 层的第 N 个点向汇点连容量为 T 的边。

另外,这题的建图在实现上也有技巧。实际上,考虑到答案不会太大,完全可以不用二分,直接从小到大枚举答案,这样每次不需要重新建图,而是可以直接在原图的基础上动态地新增一层即可。经实测,速度大约快 10 倍。
总的来说,这题的关键就是活用“拆点”的技巧,从而满足题目中的限制。最大流问题的变体还是比较多的,对模型的把握要结合具体题目,当然更离不开勤奋的练习、用心的总结和时常的复习。

参考代码:

//1870.cpp
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>

using namespace std;

const int MAXN = 50 + 10;
const int MAXM = 2450 + 50;
const int MAXT = 50 + 10;
const int INF = 0x3f3f3f3f;

#define MAXK (MAXN + MAXT)

struct Edge {
    Edge *next;
    int cap;
    int dest;
} edges[(MAXM + MAXN) * MAXK << 1], *current, *first_edge[MAXK * MAXN]; //注意数组开够

int N, M, T;
int X[MAXM], Y[MAXM], Z[MAXM];
bool vis[MAXK * MAXN];

int s, t;

Edge *counterpart(Edge *x) {
    return edges + ((x - edges) ^ 1);
}

void insert(int u, int v, int c) {
    current -> next = first_edge[u];
    current -> cap = c;
    current -> dest = v;
    first_edge[u] = current ++;
}

int dfs(int u, int f) {
    if (u == t) return f;
    if (vis[u]) return 0; else vis[u] = true;
    for (Edge *p = first_edge[u]; p; p = p -> next)
        if (p -> cap)
            if (int res = dfs(p -> dest, min(f, p -> cap))) {
                p -> cap -= res;
                counterpart(p) -> cap += res;
                return res;
            }
    return 0;
}

bool check(int k) {
    s = 0; t = (k + 1) * N + 1; //同一层的点放一起
    current = edges; fill(first_edge, first_edge + t + 1, (Edge*)0);
    insert(s, 1, T); insert(1, s, 0); insert(t - 1, t, T); insert(t, t - 1, 0);
    for (int i = 0; i < k; i++) {
        for (int j = 1; j <= N; j++) insert(i * N + j, (i + 1) * N + j, INF), insert((i + 1) * N + j, i * N + j, 0);
        for (int j = 0; j < M; j++) insert(i * N + X[j], (i + 1) * N + Y[j], Z[j]), insert((i + 1) * N + Y[j], i * N + X[j], 0);
    }
    int flows = 0;
    for (;;) {
        memset(vis, false, sizeof vis);
        if (int res = dfs(s, INF)) flows += res; else return flows == T;
    }
}

int solve() {
    int l = 0, r = N + T; //(l, r]
    while (l + 1 < r) {
        int mid = l + r >> 1;
        if (check(mid)) r = mid; else l = mid;
    }
    return r;
}

int main(void) {
    freopen("1870.in", "r", stdin);
    freopen("1870.out", "w", stdout);
    scanf("%d%d%d", &N, &M, &T);
    for (int i = 0; i < M; i++) scanf("%d%d%d", &X[i], &Y[i], &Z[i]);
    printf("%d\n", solve());
    return 0;
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值