给定一个行
列的棋盘,在上面有
个宝藏,一开始在点
,在
行可以自由的向左或向右走,但是只能在
个给定的“安全列”向
行走,问收集所有宝藏至少需要多少步数?
对于每一行,无论初始在哪里,最少的收集完当前行的策略应该是最后收集左右端点的策略,并且行只能向更高行走,也就是行的答案可以由
行得来,所以可以考虑
,设
分别为收集完前i行,最后站在这个一行的左端点处和右端点处的最少步数,那么现在的位置是可以由上一行的两个端点处走过来的,因此
,
是从上一行的左端点处到目前为止的步数,另外一个则是右端点的。
同时注意到从一个端点移动到上一行应该是先移动到最近的“安全列”所以又有两种情形,向左和向右各有一个最近的安全列,这里最近应该是一个方向上最近,一个方向上经过了一个安全列而不想上走一定是浪费步数的,所以需要维护一个当前列向左向右的最近安全列,这里利用
也是容易得到的,设
为i列是否为安全列,那么从右向左遍历列,如果当前列是安全列,那么向右的最近安全列就是自己,如果不是,那么向右的最近安全列就是右边一个元素的最近向右安全列位置,即
,向左的同理,只不过向左的是正向枚举。同时也需要维护一行的左端点是谁,端点的意思是最左/右的宝藏,为了方便更新,由于向左的宝藏是记录最小的坐标,初始值应该是
,向右的记录最大,初始值则是0。
这道题有很多细节,第一,需要注意起始行,先初始化起始行,在遍历其他行,而起始行不一定是第一行;第二,某一行可能没有宝藏,所以行可能不是
行转移来的,应该是最近的有宝藏的行来的,所以当前行没有宝藏的时候,需要维护一个变量
来表示距离有宝藏的行有多远,因此最后的转移方程是
;第三,
的值有三部分,“上一行的端点到最近的安全列的距离”,“安全列上移动的距离”(这个距离就是
),“从安全列到达当前行后,到达这一行的端点的步数"。
(有没有宝藏就看或者
就可以了,两个同步,任意选择一个即可)
这题里要分类清楚,每个从上一行的两个端点来,每个端点可以向左走也可以向右走,所以一行需要分类讨论8个点,
的值
两种,上一行两个端点两种,端点行走方向两种,共八种。转移种数太多,难度不比模拟小。
最后,自最高层向下遍历,如果当前行有宝藏,那么
,自高向下遍历确保第一个找到的有宝藏的行是最后一行。
#include<bits/stdc++.h>
using namespace std;
int read()
{
int ret=0,base=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') base=-1;
ch=getchar();
}
while(isdigit(ch))
{
ret=(ret<<3)+(ret<<1)+ch-48;
ch=getchar();
}
return ret*base;
}
const int inf=0x3fffffff;
const long long INF=0x3f3f3f3f3f3f3f;
int n,m,k,q;
long long f[300005][2];
int l[300005],r[300005];
int ll[300005],rr[300005];
int safe_l[300005],safe_r[300005];
bool safe[200005];
int solve(int i,int k,int p)
{
//在i行k列上,最后停在p,需要走多少步
if(p==0)//停在最左
{
if(k<=r[i]) return r[i]-k+r[i]-l[i];
return k-l[i];
}
else
{
if(k>=l[i]) return k-l[i]+r[i]-l[i];
return r[i]-k;
}
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) l[i]=inf;
k=read();q=read();
for(int i=1;i<=k;i++)
{
//每一行最靠左或最靠右的
int x=read(),y=read();
l[x]=min(l[x],y);
r[x]=max(r[x],y);
}
for(int i=1;i<=q;i++) safe[read()]=true;
for(int i=m;i>=1;i--)
{
if(safe[i]) safe_r[i]=i;
else safe_r[i]=safe_r[i+1];
}
for(int i=1;i<=m;i++)
{
if(safe[i]) safe_l[i]=i;
else safe_l[i]=safe_l[i-1];
}
for(int i=1;i<=n;i++) f[i][0]=f[i][1]=INF;
int begin=1;
for(int i=1;i<=n;i++)
{
if(r[1])
{
f[i][0]=r[i]-1+r[i]-l[i]+i-1;
f[i][1]=r[i]-1+i-1;
begin=2;
break;
}
if(r[i])
{
if(safe[1])
{
f[i][0]=i-1+solve(i,1,0);
f[i][1]=i-1+solve(i,1,1);
}
else if(safe_r[1])
{
int pos=safe_r[1];
f[i][0]=min(f[i][0],(long long)(pos-1+i-1+solve(i,pos,0)));
f[i][1]=min(f[i][1],(long long)(pos-1+i-1+solve(i,pos,1)));
}
begin=i+1;
break;
}
}
for(int i=begin,gap=1;i<=n;i++)
{
//本行没宝藏
if(!r[i]) gap++;
//本行有宝藏
//f[i][0]
else
{
//从f[i-1][0]转移来
if(safe_r[l[i-gap]])//如果上一行的最左有向右的安全列
{
int pos=safe_r[l[i-gap]];
f[i][0]=min(f[i][0],f[i-gap][0]+solve(i,pos,0)+pos-l[i-gap]+gap);
}
if(safe_l[l[i-gap]])//如果上一行的最左有向左的安全列
{
int pos=safe_l[l[i-gap]];
f[i][0]=min(f[i][0],f[i-gap][0]+solve(i,pos,0)+l[i-gap]-pos+gap);
}
//从f[i-1][1]转移来
if(safe_r[r[i-gap]])
{
int pos=safe_r[r[i-gap]];
f[i][0]=min(f[i][0],f[i-gap][1]+solve(i,pos,0)+pos-r[i-gap]+gap);
}
if(safe_l[r[i-gap]])
{
int pos=safe_l[r[i-gap]];
f[i][0]=min(f[i][0],f[i-gap][1]+solve(i,pos,0)+r[i-gap]-pos+gap);
}
//f[i][1]
//从f[i-1][0]来
//走右边的安全列
if(safe_r[l[i-gap]])
{
int pos=safe_r[l[i-gap]];
f[i][1]=min(f[i][1],f[i-gap][0]+solve(i,pos,1)+pos-l[i-gap]+gap);
}
//走zuo边的安全列
if(safe_l[l[i-gap]])
{
int pos=safe_l[l[i-gap]];
f[i][1]=min(f[i][1],f[i-gap][0]+solve(i,pos,1)+l[i-gap]-pos+gap);
}
//从f[i-1][1]来
if(safe_r[r[i-gap]])
{
int pos=safe_r[r[i-gap]];
f[i][1]=min(f[i][1],f[i-gap][1]+solve(i,pos,1)+pos-r[i-gap]+gap);
}
if(safe_l[r[i-gap]])
{
int pos=safe_l[r[i-gap]];
f[i][1]=min(f[i][1],f[i-gap][1]+solve(i,pos,1)+r[i-gap]-pos+gap);
}
gap=1;//记得重置gap
}
}
for(int i=n;i>=1;i--)
{
if(r[i])
{
printf("%lld\n",min(f[i][0],f[i][1]));
return 0;
}
}
return 0;
}