题目
题目背景
原来的那条小狗狗长成了大狗狗,令人欣慰!
题目描述
F
i
r
e
W
i
n
g
B
i
r
d
\sf FireWingBird
FireWingBird 是我们最好的
f
a
m
i
l
y
p
e
t
\rm family\;pet
familypet 。它今天要在学校里四处逛逛!其实是因为很久没有人来遛它了。
学校有
n
n
n 个值得去的地方。对于任意两个互相可以直接到达的地点,
S
i
s
t
e
r
\color{black}S\color{red}ister
Sister 已经测出了它们之间的距离(因为她的工作流动性较高),用三元组
⟨
a
,
b
,
w
⟩
\langle a,b,w\rangle
⟨a,b,w⟩ 表示,
F
i
r
e
W
i
n
g
B
i
r
d
\sf FireWingBird
FireWingBird 在
a
a
a(或
b
b
b)时,花费
w
w
w 秒就可以到达
b
b
b(或
a
a
a)了。
虽然 F i r e W i n g B i r d \sf FireWingBird FireWingBird 的跑动速度是普通质量人类的两倍,它还是很难跑完整个学校。于是它决定使用自己的翅膀!
当炽烈的翅膀伸展之时, F i r e W i n g B i r d \sf FireWingBird FireWingBird 将撕裂虚空而来,光耀四海,鸟兽草木莫不惧伏!——《山海经》
当 F i r e W i n g B i r d \sf FireWingBird FireWingBird 在某个地方的时候,它可以放置一个 p r i n c i p a l \rm principal principal 。之后的时间里, F i r e W i n g B i r d \sf FireWingBird FireWingBird 可以给 p r i n c i p a l \rm principal principal 发消息,然后 p r i n c i p a l \rm principal principal 念动咒语:
当黑暗逼近,向天高呼 M e i n G o t t , w a s i s t d a s \it Mein\; Gott,\; was\; ist\; das MeinGott,wasistdas,你便会见到那神圣的光明使者。——《山海经》
然后 F i r e W i n g B i r d \sf FireWingBird FireWingBird 就会立刻到达 p r i n c i p a l \rm principal principal 所在的地方!撕裂虚空是不花费时间的。
注意
p
r
i
n
c
i
p
a
l
\rm principal
principal 不能自己移动,因为他走路是不摆手的。只有
F
i
r
e
W
i
n
g
B
i
r
d
\sf FireWingBird
FireWingBird 做出不雅行为时,他会瞬移到它旁边,表达 “不成体统” 的咒骂。显然
F
i
r
e
W
i
n
g
B
i
r
d
\sf FireWingBird
FireWingBird 可以在任意地点做出不雅行为,有电线杆就行。
注意 F i r e W i n g B i r d \sf FireWingBird FireWingBird 有一种高级操作:在 p r i n c i p a l \rm principal principal 呼唤它的时候,同时做出不雅动作。我就直说了:它会直接和 p r i n c i p a l \rm principal principal 交换位置!
现在, F i r e W i n g B i r d \sf FireWingBird FireWingBird 在地点 1 1 1,准备依次到达 k k k 个地点,分别是 a 1 , a 2 , … , a k a_1,a_2,\dots,a_k a1,a2,…,ak 。在到达 a i a_{i} ai 后、到达 a i + 1 a_{i+1} ai+1 前,可以经过其他任意地点,也就是说,将最终 F i r e W i n g B i r d \sf FireWingBird FireWingBird 经过的地点按照时间顺序排列(一个地点可以出现多次),则 a 1 , a 2 , … , a k a_1,a_2,\dots,a_k a1,a2,…,ak 是一个子序列。
请问 F i r e W i n g B i r d \sf FireWingBird FireWingBird 所需要的时间最小是多少?
数据范围与提示
n
,
k
⩽
300
n,k\leqslant 300
n,k⩽300,三元组的数量不超过
4
×
1
0
4
4\times 10^4
4×104 。学校很大,所以
1
⩽
w
⩽
1
0
9
1\leqslant w\leqslant 10^9
1⩽w⩽109 。吐槽:这个题面魔改的挺多啊……原题是两个传送门,二者之间相当于有一条零权边。可以远程关掉一个,然后在当前位置放置。考虑到大家都一眼看出只需要记录一个传送门的位置,我就略去了。
补记:听狗狗说,可能只有人类会一眼看出?但想起 这 道 题 \color{white}{这道题} 这道题 的时候,似乎只有一个传送门是理所应当啊……
思路
显然是 d p \tt dp dp,用 f ( i , j ) f(i,j) f(i,j) 表示当前在 a i a_i ai,传送门(也就是我们亲爱的 p r i n c i p a l \rm principal principal 校长)在 j j j,最小时间花费。
假如接下来要走到 a i + 1 a_{i+1} ai+1,且传送门变动到了 p p p,我们有什么策略?分步考虑。
首先一定要经过 p p p 。对于 a i → p a_i\rightarrow p ai→p 的过程,显然不需要在过程中新建传送门(如果只为了当前过程),因为传送门的作用是返回一个走过的点。而传送到 j j j 则应该在最初进行。所以只有两个策略:从 a i a_i ai 直接走到 p p p,和从 j j j 走到 p p p 。
然后考虑 p → a i + 1 p\rightarrow a_{i+1} p→ai+1 的过程。同理,本次行走过程中,不应该放置传送门。但是是否有可能使用上一步中,留下的一个传送门呢?
假设需要用。那么第一个过程变成了:从 a i a_i ai(或 j j j,二者相似,故下文默认为 a i a_i ai)走到 x x x 再走到 p p p 。注意由于 x x x 处会放置传送门,所以 a i a_i ai 到 x x x 到 p p p 的过程中都不能放传送门。从 p p p 传送到 x x x 之后,肯定也不会再放传送门(否则 p p p 就没了)。这是一个全步行的过程,如图:
显然我们可以
O
(
n
)
\mathcal O(n)
O(n) 枚举这个点,三个最短路相加。然而这就是
O
(
k
n
3
)
\mathcal O(kn^3)
O(kn3) 了,做锤子哦!反正我当时在这里停了挺久的。
仔细想想,这种情况到底合不合理呢?我们在 p p p 留下一个传送门,不过是为了之后能够立刻到达此处。如果我们 改为在 x x x 处留下传送门 呢?之后要传送到 p p p 处时,就先传送到 x x x 处,再走到 p p p 处。代价相同!
于是我们规避了使用先前路径上留下的传送门。那么从 p p p 到 a i + 1 a_{i+1} ai+1 就只有三种方案:直接走;传送回 j j j(如果第一轮是 a i → p a_i\rightarrow p ai→p,那么 j j j 并不是路径上的点,不能排除);传送回 a i a_i ai(与上面传送回 j j j 同理)。
所以我们做到了 O ( 1 ) \mathcal O(1) O(1) 转移。每个 f ( i , j ) f(i,j) f(i,j) 有 O ( n ) \mathcal O(n) O(n) 个后继状态,总复杂度 O ( k n 2 ) \mathcal O(kn^2) O(kn2) 。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long int_;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
void writeint(int x){
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
inline void getMin(int_&x,const int_&y){
if(y < x) x = y;
}
const int MaxN = 305;
const int_ infty = (1ll<<60)-1;
int a[MaxN<<1], n, k;
int_ g[MaxN][MaxN];
int_ dp[MaxN<<1][MaxN];
int main(){
n = readint();
rep(i,1,n) rep(j,1,n)
if(i != j) g[i][j] = infty;
int m = readint();
k = readint()<<1;
for(int x,y,v; m; --m){
x = readint(), y = readint();
if((v = readint()) < g[x][y])
g[x][y] = g[y][x] = v;
}
rep(p,1,n) rep(i,1,n) rep(j,1,n)
getMin(g[i][j],g[i][p]+g[p][j]);
rep(i,1,k) a[i] = readint();
a[0] = 1; // start
rep(i,1,n) dp[0][i] = g[a[0]][i];
rep(i,1,k) rep(j,1,n){
dp[i][j] = infty; // init
rep(p,1,n){
int_ t = min(g[a[i]][j],g[a[i]][p]);
getMin(t,g[a[i-1]][a[i]]); // three ways
t += min(g[a[i-1]][j],g[j][p]); // two ways
getMin(dp[i][j],dp[i-1][p]+t);
}
}
printf("%lld\n",dp[k][a[k]]);
return 0;
}