原题链接:邮递机器人
【题目分析】
非常好的一道单调队列的题目,因为坑了我半个晚上。
首先还是详细分析题面,需要注意一个关键点,就是必须连号,这样一来,就出现一个1到n的线性序列(不然除了状压实在想不到什么办法),所以我们可以整理出一个状态,那就是令 F [ i ] F[i] F[i]表示将 1 1 1到 i i i个邮包运送完毕,并回到起点所需的最短路程。注意为什么要加上"回到起点",因为题目要求最后回到起点,所以这样设置状态才是符合题意的。
接下来是递推式,显然,一段连续的邮包可以单独计算,于是就有了子状态的表示:
如上图所示,从原点出发,经由
1
1
1、
2
2
2点,然后回到起点,就是
F
[
2
]
F[2]
F[2](注意,本题是曼哈顿距离,图中只是为了表示方便画成了欧几里得距离),机器人的每一次连续的运输都可以看做是这样一个三角形,第一条边是原点到连续邮包的起点,第二条边是连续邮包的起点到连续邮包的终点,第三条边是连续邮包的终点到原点。而对于
F
[
i
]
F[i]
F[i],可以枚举上一段连续邮包的终点进行分割,分割出最优子状态
F
[
j
]
F[j]
F[j]、三条边相加表示的最后一段连续邮包,即:
F
[
i
]
=
m
i
n
(
F
[
j
]
+
d
i
s
[
i
]
+
d
i
s
[
j
+
1
]
+
s
u
m
[
i
]
−
s
u
m
[
j
+
1
]
)
F[i] = min(F[j]+dis[i]+dis[j+1]+sum[i]-sum[j+1])
F[i]=min(F[j]+dis[i]+dis[j+1]+sum[i]−sum[j+1])
其中
F
[
j
]
F[j]
F[j]便是最优子状态,
j
j
j表示枚举到的上一个终点,
d
i
s
[
i
]
dis[i]
dis[i]代表当前段的终点到原点的路程,
d
i
s
[
j
+
1
]
dis[j+1]
dis[j+1]代表当前段的原点到起点的路程(因为
j
j
j是上一段的终点,按照顺序取的规则,
j
+
1
j+1
j+1就是当前段的起点),
s
u
m
[
i
]
−
s
u
m
[
j
+
1
]
sum[i]-sum[j+1]
sum[i]−sum[j+1]是一个前缀和相减,
s
u
m
[
i
]
sum[i]
sum[i]存储了原点开始按顺序到
i
i
i的总路程,所以相减后得到
i
i
i到
j
j
j之间的路程,也就是模型中三角形的第二条边。
然后考虑负重上限
C
C
C的问题,可以利用前缀和优化一下连续一段邮包的总重量,枚举子状态时,中间判断一下,不要让最后一段的总重超过
C
C
C即可。
至此可以得出暴力代码:
#include<cstdio>
#include<cmath>
#define maxn 100039
using namespace std;
typedef long long ll;
int N, C;
int x[maxn], y[maxn], c[maxn];
ll dis_sum[maxn], sum[maxn];
ll f[maxn], g[maxn];
int que[maxn], head, tail;
ll dis(int a, int b){ return abs((ll)x[a]-x[b])+abs(y[a]-y[b]); }
int main(){
//freopen("1.in", "r", stdin);
scanf("%d%d", &C, &N);
x[0] = y[0] = 0;
for(int i = 1; i < N+1; i++)scanf("%d%d%d", x+i, y+i, c+i);
sum[1] = c[1];
for(int i = 2; i < N+1; i++)sum[i] = sum[i-1]+c[i]; //路程前缀和
for(int i = 1; i < N+1; i++)dis_sum[i] = dis_sum[i-1]+dis(i, i-1); //重量前缀和
for(int i = 1; i < N+1; i++){
//暴力
int min = i-1;
for(int j = i-1; j > -1 && sum[i]-sum[j]<=C ; j--)
if(f[j]+dis(0, i)+dis(0, j+1)+dis_sum[i]-dis_sum[j+1]<f[min]+dis(0, i)+dis(0, min+1)+dis_sum[i]-dis_sum[min+1])
min = j;
f[i] = f[min]+dis(0, i)+dis(0, min+1)+dis_sum[i]-dis_sum[min+1];
}
printf("%lld", f[N]);
return 0;
}
但是这样一来是 O ( n 2 ) O(n^2) O(n2)的,极限数据会被卡(虽然Vijos上的数据已经能过了)。
我们还是要考虑一下优化,容易想到单调队列的模型,我们改一下这个DP式的外形:
F
[
i
]
=
m
i
n
(
F
[
j
]
+
d
i
s
[
i
]
+
d
i
s
[
j
+
1
]
+
s
u
m
[
i
]
−
s
u
m
[
j
+
1
]
)
F[i] = min(F[j]+dis[i]+dis[j+1]+sum[i]-sum[j+1])
F[i]=min(F[j]+dis[i]+dis[j+1]+sum[i]−sum[j+1])
修改为:
F
[
i
]
=
m
i
n
(
F
[
j
]
+
d
i
s
[
j
+
1
]
−
s
u
m
[
j
+
1
]
)
+
d
i
s
[
i
]
+
s
u
m
[
i
]
F[i] = min(F[j]+dis[j+1]-sum[j+1])+dis[i]+sum[i]
F[i]=min(F[j]+dis[j+1]−sum[j+1])+dis[i]+sum[i]
其实就是将
i
i
i的相关项都提取出来放到
m
i
n
min
min函数的外面,因为对于固定的i,这些都是常数项,而
F
[
j
]
+
d
i
s
[
j
+
1
]
−
s
u
m
[
j
+
1
]
F[j]+dis[j+1]-sum[j+1]
F[j]+dis[j+1]−sum[j+1]作为变量,放在
m
i
n
min
min的内部。
或者干脆把
F
[
j
]
+
d
i
s
[
j
+
1
]
−
s
u
m
[
j
+
1
]
F[j]+dis[j+1]-sum[j+1]
F[j]+dis[j+1]−sum[j+1]替换成
g
[
j
]
g[j]
g[j],这样一来,一个单调队列的模型就出来了:
F
[
i
]
=
m
i
n
(
g
[
j
]
)
+
d
i
s
[
i
]
+
s
u
m
[
i
]
F[i] = min(g[j])+dis[i]+sum[i]
F[i]=min(g[j])+dis[i]+sum[i]
因为有C的限制,枚举区间只会向右移动,符合单调队列使用情景。
于是套上单调队列:
#include<cstdio>
#include<cmath>
#define maxn 100039
using namespace std;
typedef long long ll;
int N, C;
int x[maxn], y[maxn], c[maxn];
ll dis_sum[maxn], sum[maxn];
ll f[maxn], g[maxn];
int que[maxn], head, tail;
ll dis(int a, int b){ return abs((ll)x[a]-x[b])+abs(y[a]-y[b]); }
void push(int x){
while(head<tail&&g[x]<g[que[tail-1]])tail--;
que[tail++] = x;
return;
}
int main(){
//freopen("1.in", "r", stdin);
scanf("%d%d", &C, &N);
x[0] = y[0] = 0;
for(int i = 1; i < N+1; i++)scanf("%d%d%d", x+i, y+i, c+i);
sum[1] = c[1];
for(int i = 2; i < N+1; i++)sum[i] = sum[i-1]+c[i];
for(int i = 1; i < N+1; i++)dis_sum[i] = dis_sum[i-1]+dis(i, i-1);
head = tail = 1;
push(0);
for(int i = 1; i < N+1; i++){
while(sum[i]-sum[que[head]]>C)head++;
f[i] = g[que[head]]+dis(0, i)+dis_sum[i];
g[i] = f[i]+dis(0, i+1)-dis_sum[i+1];
push(i);
}
printf("%lld", f[N]);
return 0;
}
这题我几乎直接得出单调队列优化DP的结论了,但是由于单调队列应用不熟悉,还是栽在了套路做题的习惯上,想当然地把
F
[
j
]
F[j]
F[j]直接拿来做单调队列,忽视了公式的具体分析。
总结一下,是一道单调队列的好题。