csp 2021-04-05 疫苗运输

  • 本题主要考察两个知识点:数论中的扩展欧几里得算法和图论中的迪杰斯特拉算法,都是经典常用的好算法,记不清楚的童鞋可以百度一下~
  • 此类题目我们很容易猜到要用图论中的一些算法,但是更重要的是如何抽象出 “图中的点” 以及 “图中点与点之间的连线权重”,因为往往题目中显式提出的点和线,不能直接看作我们要处理的“图”中的点和线。
  • 本题也是一样,真正被看作迪杰斯特拉算法中的“点”的,其实是疫苗运输线路,或者说是运输车,点与点之间是否存在连线取决于两条运输线路上的运输车是否能在同一时间到达同一站点,连线的权重则是最早在同一站点相遇的时间。
  • 这样想明白后,思路就清晰了(加引号的表述指的是迪杰斯特拉算法中的图对应的概念):先依次遍历所有线路经过的所有站点,凡是经过1站点的线路,都可以初始化该线路最早获取疫苗的时间为首次经过1站点的时间,即这些“点”和“起点”有连线。然后使用迪杰斯特拉算法,每轮选取一个获取疫苗时间最早的,且之前未选取过的线路,然后针对该线路经过的站点,依次使用扩展欧几里得算法,判断是否可以通过该站点转运到其他线路,即判断其是否与其他“点”存在连线,并计算出“权重”,然后根据迪杰斯特拉算法更新各“点”的“最短路径”。
  • 经过上述处理后,各线路获取疫苗的的站点和最早时间等数据都已知晓,只需依次遍历各线路经过的站点,更新其最早获取疫苗的时间即可。
  • 下面具体介绍一下如何使用扩展欧几里得算法计算两个线路是否可以同时到达交点处,以下图中的两条线路为例:
  1. 假设左边的车从编号为2的点走到1号点后,又转了X圈,右边的车从起点走到2号点后,又转了Y圈后在交点相遇,令a = a + d,则有等式a + b * X = x + y * Y,我们求出最小的非负X即可得到第一次相遇的时间(这里当X为非负时,Y一定也非负,因为y>x,若Y<0,则等式右边<0,而等式左边在X非负时一定大于0)。整理等式得到:b * X - y * Y = x - a。可以使用扩展欧几里得算法求解。
  2. 对于 a * x + b * y=c,当gcd(a, b)能整除c的时候,x, y存在解。使用扩展欧几里得算法首先求出a * x + b * y = gcd(a,b)的解x, y,然后还需要扩大c / gcd(a, b)倍可以得到一个解x0,y0。
  3. 我们可以使用扩展欧几里得算法求解,假设我们得到一组解x0,y0,则所有解的通式为:
    { x = x 0 + b / d ∗ k , y = y 0 − a / d ∗ k , k ⊆ Z } \left\{ x=x0 + b/d * k, y = y0 - a/d * k, k \subseteq Z \right\} {x=x0+b/dk,y=y0a/dk,kZ}
  4. 我们需要求解的是最小的正整数x,关于x的通式为:x=x0 + b/d * k,令b /= d,则最后的结果为
    (x0 % b + b) % b,这里为什么求余之后再加b,是因为负数求余可能是负数,例如:
    -5 % 7 = -5,-9 % 7 = -2。

代码如下:

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

using namespace std;

typedef long long LL;
typedef pair<int, int> PII;

const int N = 510;
const LL INF = 0x3f3f3f3f3f3f3f3fll;

int n, m;   // n个物流站,m条线路
int len[N];  // 每一条线路的总用时
struct Node {  // 存储线路信息
    int cid;  // 线路id
    int distance;  // 当前线路内的站点pid到起点的距离
    int pid;  // 线路上的站点赋予的pid,从0开始编号(起点为0,但是站点号不一定和编号相同)
};
// ps[ver] = {{1, 100, j}, {4, 150, k}}: 表示经过站点ver的线路有线路1和线路4
// 线路1编号为j的点经过此站点,j距离线路1的起点(编号为0)为100
vector<Node> ps[N];
// line[i][j] = {ver, y}: 表示线路i上编号(pid)为j的站点ver到下一个站点的距离(时间)是y
vector<PII> line[N];
LL dist[N];  // 每辆车最早拿到疫苗的时间, 通过该时间可以更新该线路上其他点最早拿到疫苗的时间
LL ans[N];  // 每个站点最早拿到疫苗的时间
bool st[N];  // dijkstra中的判重数组
// 存储每条线路最早拿到疫苗的点,例如pid[2]=1: 表示线路2最早拿到疫苗的点编号为1(Node中的pid)
int pid[N];

LL cal_gcd(LL a, LL b, LL& x, LL& y) {  // 求x, y, 使得ax + by = gcd(a, b)
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    LL d = cal_gcd(b, a % b, y, x);
    y -= (a / b) * x;
    return d;
}

void dijkstra()  // 求1号点到n号点的最短路距离
{
    memset(dist, 0x3f, sizeof dist);
    for (int i = 0; i < m; i++) {
        int d = 0;
        for (int j = 0; j < line[i].size(); j++) {
            if (line[i][j].first == 1) { // 如果线路i上的第j个点站点号是1的话,说明可以作为起点
                dist[i] = d;    // 类似于迪杰斯特拉算法图中的点和起点直接相连,权重为时间
                pid[i] = j;     // 线路i上最早获得疫苗的点的编号是j
                break;
            }
            d += line[i][j].second;
        }
    }
    for (int i = 0; i < m; i++) {
        int tmp = -1;
        for (int j = 0; j < m; j++) {   //选取一个不在集合中的,到起点距离最短的“点”,此处判断条件颇耐人寻味~
            if (!st[j] && (tmp==-1 || dist[j] < dist[tmp]))
                tmp = j;
        }
        st[tmp] = true;
        auto d = dist[tmp];
        for (int j = pid[tmp], k = 0; k < line[tmp].size(); k++, j = (j + 1) % line[tmp].size()) {
            for (auto& c : ps[line[tmp][j].first]) {
                if (st[c.cid]) continue;
                LL a = d, b = len[tmp];
                LL x = c.distance, y = len[c.cid];
                // 扩展欧几里得算法
                LL X, Y;
                LL D = cal_gcd(b, y, X, Y);
                if ((x - a) % D) continue; //不能同时到达交点,则迪杰斯特拉图中的两点无连线
                X = (x - a) / D * X;
                y /= D;
                X = (X % y + y) % y;
                if (dist[c.cid] > a + b * X) {//dijkstra更新路径
                    dist[c.cid] = a + b * X;
                    pid[c.cid] = c.pid;
                }
            }
            d += line[tmp][j].second;
        }
    }
}


int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++) {
        int cnt, sum=0;
        scanf("%d", &cnt);
        for (int j = 0; j < cnt; j++) {
            int ver, t;
            scanf("%d%d", &ver, &t);
            ps[ver].push_back({ i, sum, j });
            line[i].push_back({ ver, t });
            sum += t;
        }
        len[i] = sum;
    }

    dijkstra();
    memset(ans, 0x3f, sizeof ans);
    for (int i = 0; i < m; i++) {
        if (dist[i] == INF) continue;
        LL d = dist[i];
        for (int j = pid[i], k = 0; k < line[i].size(); j = (j + 1) % line[i].size(), k++) {
            int ver = line[i][j].first;
            ans[ver] = min(ans[ver], d);
            d += line[i][j].second;
        }
    }

    for (int i = 2; i <= n; i++) {
        if (ans[i] == INF) puts("inf");
        else printf("%lld\n", ans[i]);
    }

    return 0;
}
  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值