题解:P17833 [CSP-S 2026] 迁徙 / migrate(民间数据)
:::warning[警告]{open}
本篇题解在写完后使用了 GPT 进行润色,润色前的内容放在了文末。因为我感觉自己写的不太像是人话,就用 AI 润色了一下。
:::
为方便描述,我们把岛屿看作数轴上的点,向右为正方向,只考虑从左边的岛屿出发,前往右边的岛屿。
首先,最短路一定不会往回跳。否则,在路径转向的位置,前后两座岛屿可以直接到达,跳过中间的岛屿就能少飞一次。
求最短路的长度也很简单:每次都尽量往右跳,能直接到终点时就跳到终点。因为跳相同的次数,贪心能到达的位置一定最靠右,所以这样得到的飞行次数最少。
接下来,考虑恰好有两条最短路时,这两条路径长什么样。
从起点开始,找到两条路径第一次不同的位置,设它们在这里分别经过
- 前一步仍然合法,因为两条路径在此之前完全相同,共同的前一座岛屿本来就能跳到
b 。 - 后一步也仍然合法,因为
b 比a 更靠右,离下一座岛屿更近。
这里,
因此,替换后仍然是一条最短路。如果原来的两条路径在后面还有不同,那么替换得到的路径就同时不同于原来的两条路径,成了第三条最短路。
所以,两条最短路只能在一个位置不同。
而且,这个位置上的两座岛屿必须相邻。否则,选一座夹在它们中间的岛屿替换这个位置,又能得到第三条最短路。
于是,两条路径一定形如:
只有中间选择了
我们可以枚举这对相邻岛屿
先看左边。一个起点
- 从
u 到i 、i+1 的最短路都只有一条,而且飞行次数相同。 - 从
u 开始一直向右贪心,跳相同的次数后,最远恰好到达i+1 。
第二个条件也不能少。如果同样的次数还能到更右边,就能用更右边的岛屿替换中间的落点,或者直接跳过这一段,产生第三条最短路或更短的路径。
右边的要求完全对称:从终点
满足条件的左右两边可以任意组合。设左边需要跳
- 从
u 出发,k 次内最远只能到i+1 ,从这里到v 至少还要l 次,因此总共至少需要k+l 次。 - 若要恰好跳
k+l 次,第k 次飞行后又不能落在i 左边,因为从v 反向跳l 次,最左只能到i 。
所以,第
因此,枚举
下面考虑如何在线性时间内算出左边的贡献。
先用双指针求出:
维护两个状态:
其中,dpl[i][1]、dpl[i][2]。
这里有一个性质:一条最短路如果唯一,那么除最后一步外,每一步都必须向右贪心。
否则,把某一步的落点换成更右边的岛屿,要么仍能接上下一步,得到另一条最短路;要么可以直接跳到下一座岛屿,少飞一次。两种情况都不符合要求。
因此,我们只需要从
按
首先,把
- 如果
r>i ,从i 到r 只需跳一次,最短路显然唯一,所以f_r\gets f_r+1 。 - 如果
r-1>i ,从i 到r-1 、r 都只需跳一次,且贪心落在r ,所以g_{r-1}\gets g_{r-1}+1 。
然后,考虑把
新的贪心落点是
设新的落点为
如果
- 如果
r>i 且lp_r=i ,则f_r\gets f_r+f_i 。 - 如果
r-1>i 且lp_{r-1}=i ,则g_{r-1}\gets g_{r-1}+f_i 。
第二条转移还要求到
右边的贡献完全对称,从右往左处理,把“向右贪心”换成“向左贪心”即可。
代码中的 dpr[i+1][2],恰好表示相邻岛屿
双指针和两遍 DP 都只需扫描一遍数组,每组数据的时间复杂度为
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
ll pos[2000005],lp[2000005],rp[2000005];
ll dpl[2000005][3],dpr[2000005][3];
int main(){
ios::sync_with_stdio(0);cin.tie(0);
int _id,T;
cin>>_id>>T;
while(T--){
int n;
ll d;
cin>>n>>d;
for(int i=1;i<=n;i++)cin>>pos[i];
int p=1;
for(int i=1;i<=n;i++){
while(p<=n&&pos[p]-pos[i]<=d)p++;
rp[i]=p-1;
}
p=n;
for(int i=n;i>=1;i--){
while(p>=1&&pos[i]-pos[p]<=d)p--;
lp[i]=p+1;
}
for(int i=1;i<=n+1;i++)for(int j=1;j<=2;j++)dpl[i][j]=dpr[i][j]=0;
for(int i=1;i<=n;i++){
if(rp[i]>i)dpl[rp[i]][1]++;
if(rp[i]-1>i)dpl[rp[i]-1][2]++;
for(int j=1;j<=2;j++){
if(rp[i]-j+1<=i)break;
if(lp[rp[i]-j+1]!=i)break;
dpl[rp[i]-j+1][j]+=dpl[i][1];
}
}
for(int i=n;i>=1;i--){
if(lp[i]<i)dpr[lp[i]][1]++;
if(lp[i]+1<i)dpr[lp[i]+1][2]++;
for(int j=1;j<=2;j++){
if(lp[i]+j-1>=i)break;
if(rp[lp[i]+j-1]!=i)break;
dpr[lp[i]+j-1][j]+=dpr[i][1];
}
}
ll ans=0;
for(int i=1;i<n;i++)ans+=dpl[i][2]*dpr[i+1][2];
cout<<ans<<'\n';
}
}
:::info[AI 润色前的内容] 为了方便,把坐标看作数轴,正方向为右。首先,最短路的长度显然是尽量贪心往右跳,至少跳的次数。
然后,最短路恰好有两条,说明两条最短路必须只有一个不同点,而且这两个点要是相邻的。因为如果不相邻,考虑取中间的点显然满足条件。如果不同点不止一个(假设有两个),考虑如果这两个位置不相邻,则把一个换成另一条最短路中的对应点显然还是合法的最短路。如果相邻,则化简后有两种大小关系:
因此我们枚举这两个相邻的不同点,考虑计算左右两边的贡献。不难发现要求是两边的点,贪心跳的过程中,跳到这相邻两个点的最短路都只有一条(且长度相等),且其中一个点是贪心跳的端点(即不能跳的再远了)。显然可以分开计算左右两边的贡献然后乘起来。
| 然后我们以左边的贡献为例,维护 |
|---|