海底高铁
题目描述
该铁路经过 N N N 个城市,每个城市都有一个站。不过,由于各个城市之间不能协调好,于是乘车每经过两个相邻的城市之间(方向不限),必须单独购买这一小段的车票。第 i i i 段铁路连接了城市 i i i 和城市 i + 1 ( 1 ≤ i < N ) i+1(1\leq i<N) i+1(1≤i<N)。如果搭乘的比较远,需要购买多张车票。第 i i i 段铁路购买纸质单程票需要 A i A_i Ai 博艾元。
虽然一些事情没有协调好,各段铁路公司也为了方便乘客,推出了 IC 卡。对于第 i i i 段铁路,需要花 C i C_i Ci 博艾元的工本费购买一张 IC 卡,然后乘坐这段铁路一次就只要扣 B i ( B i < A i ) B_i(B_i<A_i) Bi(Bi<Ai) 元。IC 卡可以提前购买,有钱就可以从网上买得到,而不需要亲自去对应的城市购买。工本费不能退,也不能购买车票。每张卡都可以充值任意数额。对于第 i i i 段铁路的 IC 卡,无法乘坐别的铁路的车。
Uim 现在需要出差,要去 M M M 个城市,从城市 P 1 P_1 P1 出发分别按照 P 1 , P 2 , P 3 , ⋯ , P M P_1,P_2,P_3,\cdots,P_M P1,P2,P3,⋯,PM 的顺序访问各个城市,可能会多次访问一个城市,且相邻访问的城市位置不一定相邻,而且不会是同一个城市。
现在他希望知道,出差结束后,至少会花掉多少的钱,包括购买纸质车票、买卡和充值的总费用。
输入格式
第一行两个整数, N , M N,M N,M。
接下来一行, M M M 个数字,表示 P i P_i Pi。
接下来 N − 1 N-1 N−1 行,表示第 i i i 段铁路的 A i , B i , C i A_i,B_i,C_i Ai,Bi,Ci。
输出格式
一个整数,表示最少花费
样例 #1
样例输入 #1
9 10
3 1 4 1 5 9 2 6 5 3
200 100 50
300 299 100
500 200 500
345 234 123
100 50 100
600 100 1
450 400 80
2 1 10
样例输出 #1
6394
提示
2 2 2 到 3 3 3 以及 8 8 8 到 9 9 9 买票,其余买卡。
对于 30 % 30\% 30% 数据 M = 2 M=2 M=2。
对于另外 30 % 30\% 30% 数据 N ≤ 1000 , M ≤ 1000 N\leq1000,M\leq1000 N≤1000,M≤1000。
对于 100 % 100\% 100% 的数据 M , N ≤ 1 0 5 , A i , B i , C i ≤ 1 0 5 M,N\leq 10^5,A_i,B_i,C_i\le10^5 M,N≤105,Ai,Bi,Ci≤105。
思路
先考虑暴力的方式如何解决,即先统计经过每段铁路的次数,然后再判断全部购买纸质票便宜还是使用 I C IC IC 卡便宜。
第二部操作很简单,单次操作的时间复杂度为 O ( 1 ) O(1) O(1) 。假设经过第 i i i 段铁路的次数为 s u m sum sum ,比较 A ∗ s u m A * sum A∗sum 与 B ∗ s u m + C B * sum + C B∗sum+C 的大小,如果前者大,则经过第 i i i 段铁路时全买纸质票更便宜;否则使用 I C IC IC 卡更便宜。
接下来思考第一部操作——如何统计经过每段铁路的次数,暴力方法是每获得一个区间,就在用于统计的数组的对应区间上加一,如果区间长度为 n n n ,共要去 m + 1 m + 1 m+1 个城市,则时间复杂度为 O ( m n ) O(mn) O(mn) ,对于最大为 105 的 n n n 和 m m m ,显然这样的暴力方法不能解决,于是我们将目光转向算法——差分与前缀和。
该算法可以将刚才提到的 O ( m n ) O(mn) O(mn) 复杂度变成 O ( m ) O(m) O(m) ,但该方法有一个缺点——每次查询的复杂度为 O ( n ) O(n) O(n) ,这个问题最后讲,现在先讲怎样实现它。
对于一个数组,给定一个下标 i i i ,从第一项加到第 i i i 项就是该数组第 i i i 项的前缀和。
差分数组的构造
对于原数组 a [ ] a[] a[] ,我们定义一个新数组——差分数组 d [ ] d[] d[] ,差分数组 d [ ] d[] d[] 第 i i i 项的前缀和为原数组 a [ ] a[] a[] 的第 i i i 项,故有定义: d [ i ] = a [ i ] − a [ i − 1 ] d[i] = a[i] - a[i - 1] d[i]=a[i]−a[i−1] 。这样 d [ i ] d[i] d[i] 的前缀和就是 a [ i ] a[i] a[i] ,然后我们可以得到原数组 a [ ] a[] a[] 的递推公式: a [ i ] = a [ i − 1 ] + d [ i ] a[i] = a[i - 1] + d[i] a[i]=a[i−1]+d[i] 。
差分数组的操作
对原数组 a [ ] a[] a[] 的区间 [ i , j ] [i, j] [i,j] 的所有元素都加一,只需要对差分数组执行以下操作: d [ i ] + = 1 , d [ j + 1 ] − = 1 d[i] += 1, d[j + 1] -= 1 d[i]+=1,d[j+1]−=1 ,时间复杂度为 O ( 1 ) O(1) O(1) 。接下来,我们验证原数组的区间 [ i − 1 , j + 1 ] [i - 1, j + 1] [i−1,j+1] 是否如我们想要的变化了。
第 i − 1 i - 1 i−1 以前的元素都没有改变。
a [ i − 1 ] = a [ i − 2 ] + d [ i − 1 ] a[i - 1] = a[i - 2] + d[i - 1] a[i−1]=a[i−2]+d[i−1]。由于 d [ i − 1 ] , a [ i − 2 ] d[i - 1], a[i - 2] d[i−1],a[i−2] 不变,故 a [ i − 1 ] a[i - 1] a[i−1] 不变。
a [ i ] = a [ i − 1 ] + d [ i ] a[i] = a[i - 1] + d[i] a[i]=a[i−1]+d[i]。由于 d [ i ] d[i] d[i] 加一,且 a [ i − 1 ] a[i - 1] a[i−1] 不变,故 a [ i ] a[i] a[i] 加一。
a [ i + 1 ] = a [ i ] + d [ i + 1 ] a[i + 1] = a[i] + d[i + 1] a[i+1]=a[i]+d[i+1]。由于 d [ i + 1 ] d[i + 1] d[i+1] 不变,但 a [ i ] a[i] a[i] 加一,故 a [ i ] a[i] a[i] 加一。
以此类推,直到第 j − 1 j - 1 j−1 个元素。
a [ j − 1 ] = a [ j − 2 ] + d [ j − 1 ] a[j - 1] = a[j - 2] + d[j - 1] a[j−1]=a[j−2]+d[j−1]。由于 d [ j − 1 ] d[j - 1] d[j−1] 不变,但 a [ j − 2 ] a[j - 2] a[j−2] 加一,故 a [ j − 1 ] a[j - 1] a[j−1] 加一。
a [ j ] = a [ j − 1 ] + d [ j ] a[j] = a[j - 1] + d[j] a[j]=a[j−1]+d[j]。由于 d [ j ] d[j] d[j] 不变,但 a [ j − 1 ] a[j - 1] a[j−1] 加一,故 a [ j ] a[j] a[j] 加一。
a [ j + 1 ] = a [ j ] + d [ j + 1 ] a[j + 1] = a[j] + d[j + 1] a[j+1]=a[j]+d[j+1]。由于 d [ j + 1 ] d[j + 1] d[j+1] 减一,且 a [ j ] a[j] a[j] 加一,故 a [ j + 1 ] a[j + 1] a[j+1] 不变。
第 j + 1 j + 1 j+1 以后的元素都没有改变。
自此,可以证明这种操作成功地让原数组 a [ ] a[] a[] 的区间 [ i , j ] [i, j] [i,j] 的所有元素都加一,并且没有改变其它的元素。
针对本题的优化
本题只使用一次原数组(即输出时),为了不浪费空间,我们使用一个变量 s u m sum sum 来记录 a [ ] a[] a[] 的元素,将 s u m sum sum 初始化为0,则有: a [ 1 ] = s u m 1 = d [ 1 ] + s u m 0 , a [ 2 ] = s u m 2 = d [ 1 ] + s u m 1 a[1] = sum_1 = d[1] + sum_0, a[2] = sum_ 2= d[1] + sum_1 a[1]=sum1=d[1]+sum0,a[2]=sum2=d[1]+sum1 (原数组的下标从1开始)。这个优化在底下的代码中实现。
差分数组的缺点
多次查询的效率很低,因为修改一次数组元素后,需要重新计算原数组的元素,如果每次修改完元素后还需要输出它们,那么最好选择别的方法。差分主要用于多次修改,单次查询。
代码
#include <iostream>
using std::cin, std::cout;
const int ArSize = 1e5 + 5;// 数组的长度
/*
N为城市的数量 M为出差去的城市数量
d[]用于存储差分 A是买纸质票的钱
B是单次使用IC卡扣的钱 C是办IC卡的钱
p1是起点城市 p2是终点城市
*/
int N, M, d[ArSize], A, B, C, p1, p2;
// sum是差分数组的前缀和 ans是答案
long long sum, ans;
int main() {
cin >> N >> M >> p1;
for (int i = 2; i <= M; i++) {
cin >> p2;
// 统计经过每个路段的次数,让左端点减一,右端点加一
// 认为对右端点应该再加上一,请看本文末尾的解释
if (p1 < p2) { // 如果p1小于p2,则左端点为p1
d[p1]++; d[p2]--;
} else { //否则左端点为p2
d[p2]++; d[p1]--;
}
p1 = p2;// 更新p2到下一个城市的位置
}
// n1和n2分别用来存储在相邻城市间往返买纸质票和使用IC卡的钱
long n1, n2;
for (int i = 1; i < N; i++) {
sum += d[i];// 计算相邻城市间往返的次数
cin >> A >> B >> C;
if (sum != 0) { // 避免sum为0时计算浪费时间
n1 = A * sum; // n1是买纸质票总共花的钱
n2 = B * sum + C; // n2是使用IC卡总共花的钱
ans += n1 < n2 ? n1 : n2; // 取较小值加到ans上
}
}
cout << ans;
return 0;
}
对代码的一个小注解
看这些代码
if (p1 < p2) { // 如果p1小于p2,则左端点为p1
d[p1]++; d[p2]--;
} else { //否则左端点为p2
d[p2]++; d[p1]--;
}
有些人可能会疑惑为什么减一的是 p 2 p2 p2 而不是 p 2 + 1 p2 + 1 p2+1 。
对于本题,从 p 1 p1 p1 去到 p 2 p2 p2 只需要通过 [ p 1 , p 2 ) [p1, p2) [p1,p2) ,不需要再通过铁路 p 2 p2 p2 ,故 p 2 p2 p2 就是区间右端点的下一个元素的下标。