【数据结构与算法暑期实习】PTA 森森旅游(邻接表+堆优化的Dijkstra算法)

一、题目

好久没出去旅游啦!森森决定去 Z 省旅游一下。

Z 省有 n 座城市(从 1 到 n 编号)以及 m 条连接两座城市的有向旅行线路(例如自驾、长途汽车、火车、飞机、轮船等),每次经过一条旅行线路时都需要支付该线路的费用(但这个收费标准可能不止一种,例如车票跟机票一般不是一个价格)。

Z 省为了鼓励大家在省内多逛逛,推出了旅游金计划:在 i 号城市可以用 1 元现金兑换 ai​ 元旅游金(只要现金足够,可以无限次兑换)。城市间的交通即可以使用现金支付路费,也可以用旅游金支付。具体来说,当通过第 j 条旅行线路时,可以用 cj​ 元现金或 dj​ 元旅游金支付路费。注意: 每次只能选择一种支付方式,不可同时使用现金和旅游金混合支付。但对于不同的线路,旅客可以自由选择不同的支付方式。

森森决定从 1 号城市出发,到 n 号城市去。他打算在出发前准备一些现金,并在途中的某个城市将剩余现金 全部 换成旅游金后继续旅游,直到到达 n 号城市为止。当然,他也可以选择在 1 号城市就兑换旅游金,或全部使用现金完成旅程。

Z 省政府会根据每个城市参与活动的情况调整汇率(即调整在某个城市 1 元现金能换多少旅游金)。现在你需要帮助森森计算一下,在每次调整之后最少需要携带多少现金才能完成他的旅程。

输入格式:
输入在第一行给出三个整数 n,m 与 q(1≤n≤1e5,1≤m≤2×1e5,1≤q≤1e5),依次表示城市的数量、旅行线路的数量以及汇率调整的次数。

接下来 m 行,每行给出四个整数 u,v,c 与 d(1≤u,v≤n,1≤c,d≤1e9),表示一条从 u 号城市通向 v 号城市的有向旅行线路。每次通过该线路需要支付 c 元现金或 d 元旅游金。数字间以空格分隔。输入保证从 1 号城市出发,一定可以通过若干条线路到达 n 号城市,但两城市间的旅行线路可能不止一条,对应不同的收费标准;也允许在城市内部游玩(即 u 和 v 相同)。

接下来的一行输入 n 个整数 a1​,a2​,⋯,an​(1≤ai​≤109),其中 ai​ 表示一开始在 i 号城市能用 1 元现金兑换 ai​ 个旅游金。数字间以空格分隔。

接下来 q 行描述汇率的调整。第 i 行输入两个整数 xi​ 与 ai′​(1≤xi​≤n,1≤ai′​≤109),表示第 i 次汇率调整后,xi​ 号城市能用 1 元现金兑换 ai′​ 个旅游金,而其它城市旅游金汇率不变。请注意:每次汇率调整都是在上一次汇率调整的基础上进行的。

输出格式:
对每一次汇率调整,在对应的一行中输出调整后森森至少需要准备多少现金,才能按他的计划从 1 号城市旅行到 n 号城市。

再次提醒: 如果森森决定在途中的某个城市兑换旅游金,那么他必须将剩余现金全部、一次性兑换,剩下的旅途将完全使用旅游金支付。

输入样例:
6 11 3
1 2 3 5
1 3 8 4
2 4 4 6
3 1 8 6
1 3 10 8
2 3 2 8
3 4 5 3
3 5 10 7
3 3 2 3
4 6 10 12
5 6 10 6
3 4 5 2 5 100
1 2
2 1
1 17

输出样例:
8
8
1

样例解释:
对于第一次汇率调整,森森可以沿着 1→2→4→6 的线路旅行,并在 2 号城市兑换旅游金;
对于第二次汇率调整,森森可以沿着 1→2→3→4→6 的线路旅行,并在 3 号城市兑换旅游金;
对于第三次汇率调整,森森可以沿着 1→3→5→6 的线路旅行,并在 1 号城市兑换旅游金。

二、思路

1.大体分析

由题意可知,现金只能在某城市处 “一次性、全部” 兑换成旅游金,也就是基本可以分两段考虑:从城市1花费 x 现金到达城市 i,然后在城市 i 花费y旅游金到城市 n。前半段起点固定,目标为任意位置,正向建图用dijkstra求最短路径;后半段目标固定,起点为任意位置,反向建图用dijkstra求最短路径。

这样就能通过枚举中转点的方式,得到在第i个城市将现金换成旅游金的情况下所需要的现金总额 x+y/qi(qi是第i个城市现金兑换旅游金的汇率)。用 multiset 维护最小值。

2.dijkstra的具体实现:堆优化

Dijkstra算法用于解决单源最短路问题。首先,定义一个数组dist,代表起点到其他各点的距离最小值。其次,将dist数组中除起点以外的所有的元素都赋为INF无穷大。然后,遍历起点的邻结点,找出一个直接距离最短的点,加入已生成的树中,并将连接它们的这条边加入最小生成树中。接着,从已有的最小生成树中的所有点出发,找一个距离最近的点继续加入生成树…直到所有的点都被遍历过。算法运行结束后,将会得到一个处理好的数组dist,其中 dist[i] 代表从起点到节点 i 的最短路长度。
在这里插入图片描述

堆优化的主要思想就是使用一个优先队列(每次弹出的元素一定是整个队列中最小的元素)来代替最近距离的查找,用邻接表代替邻接矩阵,这样可以大幅度节约时间开销。

本题中我所写的堆优化dijkstra算法伪代码如下:

请添加图片描述

几个问题:

1.优先队列的数据类型:

优先队列应该用于快速寻找距离最近的点。由于优先队列只是将最小的那个元素排在前面,因此我们应该定义一种数据类型,使得它包含该节点的编号以及该节点与起点的距离。这里使用pair定义: pair<ll, int>,前者long long类型对应当前结点与起点的距离,后者int类型对应结点编号。
C/C++ pair用法总结

2.在什么时候对队列进行操作
队列操作的地方,首先就是搜索刚开始,要为起点赋初始值,此时必须将起点加入优先队列中。该队列元素的节点编号为起点的编号,该节点当前与起点的距离为0。

3.如果一个节点到起点的最短距离通过其他的运算流程发生了变化,如何处理队列中已经存入的元素?
不需要理会队列中的元素,而是再存入一个就行了。如果要发生变化,只能将节点与起点之间的距离变得更小,而优先队列恰好是先让最小的那个弹出。因此,队列元素弹出时,如果有多个元素的节点编号相同,那么被弹出的一定是节点编号最小的一个。等到后面再遇到这个节点编号的时候,我们只需要将它忽略掉就行了。

(其他待补充)



3.代码完整思路

1.建图,读入数据。
(1)正向建图,边权重只存储每条路径所需的现金金额;若两点之间有多条路径,则只存储最小那条的边权重。
(2)反向建图,边权重只存储每条路径所需的旅游金金额;若两点之间有多条路径,则只存储最小那条的边权重。

2.利用堆优化的dijkstra算法计算最短路径。
dijkstra(dist1, Graph, 1);
dijkstra(dist2, reverseGraph, n);
建立两个数组dist1[ ],dist2[ ],分别用来存储正图、反图的最短路径。dist1数组中的每个元素 dist1[i] 表示从正图的起点(序号为1)到 i 点的最短路径(所需的最小现金数);dist2数组中的每个元素 dist1[i] 表示从反图的起点(序号为n)到 i 点的最短路径(所需的最小旅游金数)。

3.不考虑汇率调整,计算在 “在每个结点 i 处兑换剩余现金”的情况下所需的总现金数。
遍历所有结点,在每一结点处都计算:若在当前结点处开始使用旅游金,则总路程需要准备多少现金。将每一结点计算所得的现金数,插入multiset类型的容器 S 中。multiset 容器可以自动排序,S.begin()函数的返回值就是在当前汇率情况下走完总路程所需携带的最小现金数。
multiset详解:multiset用法总结

4.考虑汇率调整,更新对应结点的总现金数。
读入要更改汇率的城市序号 x,以及汇率更改后的值 a;
找到第3步存入容器S中的、城市x对应的现金数,找到该数值并删除;然后重新计算该城市 x 对应的新的现金数,并重新存入容器S中。

5.输出汇率调整后的最小现金数。
因为 S 是 multiset类型的容器,可自动排序,因此在每次汇率调整完成第4步后,S.begin()输出序列中第一个值即可(也就是当前汇率政策下的最小总现金数)

三、代码

AdjList.h 头文件
邻接表存储结构定义和相关建图函数。
这里需要对我原来写好的建图函数BuildGraph()稍作修改:如果两结点间有多条路径,比较其权重,只留下权重小的路径。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
using namespace std;

#define MaxVertexNum 10001	/* 最大顶点数设为10001 */
typedef int Vertex;         /* 用顶点下标表示顶点,为整型 */
typedef int WeightType;		/* 边的权值设为整型 */
typedef int Elementype;

/* 边的定义 */
typedef struct ENode* PtrToENode;
struct ENode {
    Vertex v1, v2;
    int weight;
};
typedef PtrToENode Edge;


/* 邻接点的定义 */
typedef struct AdjVNode* PtrToAdjVNode;
struct AdjVNode {
    Vertex AdjV;  /* 邻接点下标 */
    WeightType weight;  /* 边权重 */
    PtrToAdjVNode next; /* 指向下一个邻接点的指针 */
};


/*顶点表头结点的定义*/
typedef struct VNode {
    PtrToAdjVNode firstEdge;//边表头指针

}AdjList[MaxVertexNum]; //AdjList是邻接表类型


/*图结点的定义*/
typedef struct GNode* PtrToGNode;
typedef PtrToGNode LGraph;
struct GNode {
    int Nv;
    int Ne;
    AdjList G;//邻接表
}; 

/* 初始化一个有VertexNum个顶点但没有边的图 */
LGraph CreateGraph(int VertexNum)
{
    Vertex v;
    LGraph Graph;

    Graph = (LGraph)malloc(sizeof(struct GNode)); /* 建立图 */
    Graph->Nv = VertexNum;
    Graph->Ne = 0;
    /* 初始化邻接表头指针 */
    for (v = 1; v <= Graph->Nv; v++) {
        Graph->G[v].firstEdge = NULL;
    }
    return Graph;
}



/* 插入边 <V1, V2>,有向图 */
void InsertEdge(LGraph Graph, Edge E)
{
    PtrToAdjVNode newNode;
    /*插入边,为V2建立新的邻接点*/
    newNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
    newNode->AdjV = E->v2;
    newNode->weight = E->weight;
    /*将v2插入v1的表头*/
    newNode->next = Graph->G[E->v1].firstEdge;
    Graph->G[E->v1].firstEdge = newNode;


}



/* 建图 */
LGraph BuildGraph(int Nv,int Ne, long long*v1, long long*v2,long long *money)
{
    LGraph Graph;
    Edge E;
 
    Graph = CreateGraph(Nv); /* 初始化有Nv个顶点但没有边的图 */

    if (Graph->Ne != 0) { /* 如果有边 */
        E = (Edge)malloc(sizeof(struct ENode)); /* 建立边结点 */
        /* "起点 终点 权重",插入邻接矩阵 */
        for (int i = 1; i <= Graph->Ne; i++) {
            E->v1 = v1[i];
            E->v2 = v2[i];
            /*当有重复路径时,只记录权重最小的那条路径*/
            if (E->weight =NULL) {
                E->weight = money[i];
            }
            else if (money[i]< E->weight) {
                E->weight = money[i];
            }
            InsertEdge(Graph, E);
        }
    }

    return Graph;
}

ans.cpp 主程序源文件

#include <bitsstdc++.h>
#include "AdjList.h"
using namespace std;


typedef long long ll;
typedef pair<ll, int> PII;
const int maxsizeN = 1e5 + 10;
const int maxsizeM = 2 * (1e5 + 10);
const ll INF = 0x3f;


int n, m, q;
long long  v1[maxsizeN], v2[maxsizeN];
long long exchangeRate[maxsizeN];
long long dist1[maxsizeN], dist2[maxsizeN];
long long visited[maxsizeN];
long long cash[maxsizeN], Emoney[maxsizeN];


	
void dijkstra(ll dist[], LGraph Graph, int start);


int main() {

	/* 输入数据 */
	scanf_s("%d %d %d", &n, &m, &q);
	for (int i = 1; i <= m; i++) {
		scanf_s("%lld %lld %lld %lld", &v1[i], &v2[i], &cash[i], &Emoney[i]);
	}
	for (int i = 1; i <= n; i++) {
		scanf_s("%lld", &exchangeRate[i]); //每座城市的汇率
	}


	/* 建图 */
	LGraph Graph = BuildGraph(n, m, v1, v2, cash);//正图,现金
	LGraph reverseGraph = BuildGraph(n, m, v2, v1, Emoney);//反图,旅游金
	dijkstra(dist1, Graph, 1);
	dijkstra(dist2, reverseGraph, n);

	multiset<ll> S;

	/*遍历所有结点,在每一结点处都计算:若在当前结点处开始使用旅游金,则总路程需要准备多少现金。
	  每一结点计算对应所需的现金数,插入multiset容器S中。*/
	for (int i = 1; i <= n; i++)
		// 节点可达才更新
		if (dist1[i] != 0x3f && dist2[i] != 0x3f) {
			// 上取整固定写法(a+b-1)/b
			S.insert(dist1[i] + (dist2[i] + exchangeRate[i] - 1) / exchangeRate[i]);
		}


	/* 分析汇率调整后的情况。注意汇率调整的效果是累加的(每次汇率调整都在上一次调整的基础上进行)*/
	while (q--) {
		int x, a;
		scanf_s("%d%d", &x, &a);
		if (dist1[x] != 0x3f && dist2[x] != 0x3f) {
			S.erase(S.find(dist1[x] + (dist2[x] + exchangeRate[x] - 1) / exchangeRate[x]));
			exchangeRate[x] = a;
			S.insert(dist1[x] + (dist2[x] + exchangeRate[x] - 1) / exchangeRate[x]);
		}
		printf("%lld\n", *S.begin());
	}

	return 0;
}


void dijkstra(ll dist[], LGraph graph, int start) {
	memset(dist, 0x3f, sizeof(dist1));
	memset(visited, 0, sizeof(visited));
	priority_queue<PII, vector<PII>, greater<PII>> heap;//建立优先队列

	heap.push({ 0,start }); //0代表距离,start代表起点
	dist[start] = 0;//起点的“最短路径”设置为0,其他点还是无穷大

	while (!heap.empty()) {

		int ver = heap.top().second;//记录出队点的下标,即下一个要走的点

		heap.pop();//队首元素出队。因为是小顶堆的优先队列,先出队的应该是最小的。

		if (visited[ver]) continue;
		visited[ver] = 1;

		PtrToAdjVNode w;

		w = graph->G[ver].firstEdge;
		while (w!=NULL) {
			if (dist[w->AdjV] > dist[ver] + w->weight) {
				dist[w->AdjV] = dist[ver] + w->weight;
				heap.push({ dist[w->AdjV],w->AdjV });
			}
			w = w->next;
		}
	}
}

四、其他

1.多看书、讲义上的标准代码,尤其是各常用数据结构的定义。因为很常用,数据结构定义和常用函数这个东西一旦你写好一份后,它就可以直接拿来用于各种不同场合了。而且如果你一开始定义的很标准规范的话,后期根据实际应用情况修改时也很好修改。
2.变量名定义。不怕长,尽量用英文命名出该变量的实际含义,不然代码可读性太差了。
3.断点调试。这次暑期实习之前我还没有意识到它有多么实用。。。。
4.这个题新学到了两个东西:< vector > < multiset >,挺好使的。有空整理。
5.这道题的优先队列我暂时用的是 stl 里现成的。我尽量找时间自己再写一个。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值