P3406 海底高铁

海底高铁

题目描述

该铁路经过 N N N 个城市,每个城市都有一个站。不过,由于各个城市之间不能协调好,于是乘车每经过两个相邻的城市之间(方向不限),必须单独购买这一小段的车票。第 i i i 段铁路连接了城市 i i i 和城市 i + 1 ( 1 ≤ i < N ) i+1(1\leq i<N) i+1(1i<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 N1 行,表示第 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 N1000M1000

对于 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,N105Ai,Bi,Ci105

思路

先考虑暴力的方式如何解决,即先统计经过每段铁路的次数,然后再判断全部购买纸质票便宜还是使用 I C IC IC 卡便宜。

第二部操作很简单,单次操作的时间复杂度为 O ( 1 ) O(1) O(1) 。假设经过第 i i i 段铁路的次数为 s u m sum sum ,比较 A ∗ s u m A * sum Asum B ∗ s u m + C B * sum + C Bsum+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[i1] 。这样 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[i1]+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] [i1,j+1] 是否如我们想要的变化了。

i − 1 i - 1 i1 以前的元素都没有改变。
a [ i − 1 ] = a [ i − 2 ] + d [ i − 1 ] a[i - 1] = a[i - 2] + d[i - 1] a[i1]=a[i2]+d[i1]。由于 d [ i − 1 ] , a [ i − 2 ] d[i - 1], a[i - 2] d[i1],a[i2] 不变,故 a [ i − 1 ] a[i - 1] a[i1] 不变。
a [ i ] = a [ i − 1 ] + d [ i ] a[i] = a[i - 1] + d[i] a[i]=a[i1]+d[i]。由于 d [ i ] d[i] d[i] 加一,且 a [ i − 1 ] a[i - 1] a[i1] 不变,故 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 j1 个元素。
a [ j − 1 ] = a [ j − 2 ] + d [ j − 1 ] a[j - 1] = a[j - 2] + d[j - 1] a[j1]=a[j2]+d[j1]。由于 d [ j − 1 ] d[j - 1] d[j1] 不变,但 a [ j − 2 ] a[j - 2] a[j2] 加一,故 a [ j − 1 ] a[j - 1] a[j1] 加一。
a [ j ] = a [ j − 1 ] + d [ j ] a[j] = a[j - 1] + d[j] a[j]=a[j1]+d[j]。由于 d [ j ] d[j] d[j] 不变,但 a [ j − 1 ] a[j - 1] a[j1] 加一,故 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 就是区间右端点的下一个元素的下标。

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值