专栏文章
题解:P5465 [PKUSC2018] 星际穿越
P5465题解参与者 4已保存评论 3
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 3 条
- 当前快照
- 1 份
- 快照标识符
- @miqn1gwd
- 此快照首次捕获于
- 2025/12/04 07:28 3 个月前
- 此快照最后确认于
- 2025/12/04 07:28 3 个月前
很多题解仅仅把这道题的倍增优化 DP 当成类似树上倍增 LCA 的简单倍增。但实际上直观定义出来的 DP 状态在某些方面上没有最优子结构性质(下文会展开叙述),并不适合倍增,而是需要进行一些特殊转化才能用倍增优化,本题解用等效法来构造合适的 DP 状态。
题目分析
形式化题意
形式化的,给定一个数轴上的 个点,编号 ,对每个点 ,给定 ,将其与 内的所有点连一条长度为 的无向边,规定 表示从 到 的最短路径长度。
接下来给出 次询问,每次给出 ,求 ,以最简分数形式输出。
接下来给出 次询问,每次给出 ,求 ,以最简分数形式输出。
注:下文为了区分与联系相关的 和与询问相关的 ,所有与联系相关的 均含有下标(例如 ),与询问相关的全部用单独的 。
编码要求
显然,我们的主要任务对每次询问求出 ,然后使用
std::__gcd 约分即可。考虑到 ,程序的复杂度应该是亚平方级的,例如 (似乎这题单次分块的复杂度是 ,但这题分块编码并不比倍增编码简单)。
暴力算法
容易想到 Floyd 多源最短路算法,复杂度 ,显然不能通过。
但这个算法的真正意义是写对拍,因为 Floyd 编码很简单且不易写错,而且这题构造数据,将输入转为邻接矩阵也很简单,这里不再赘述。
正解 DP
首先容易想到 DP,因为传送过程存在明显的无后效性,前面如何到达 与 之后的传送路径无关。
但是,如果我们直观定义 表示从 出发走 步能到达的最左端位置,会发现它并不符合最优子结构: 并不一定是最后通过 到达的。
例如样例中以 为起点,走 步能到达的最左端位置为 ,走 步能到达的最左端位置为 ,然而这个位置 是从位置 而非位置 走过来的。
这限制了我们用倍增法优化这个 DP 状态,我们需要找到其它有用的 DP 状态。
引理 1
:将第 个星球的联系区间由 改为 ,询问的答案不变。
换句话说,我们可以认为从 出发可以一步到达的点为 内的所有点,而不改变答案。
换句话说,我们可以认为从 出发可以一步到达的点为 内的所有点,而不改变答案。
证明
注:下面各子引理的证明比较废话,读者可自行画图理解。
显然 只是增加了边而没有删除边,那么答案不会增大。
引理 1.1
:如果最短路径存在向右的传送,那么所有向右的传送不在任何向左的传送之后。
证明:假设在某一步向右传送(),在向左传送()之后,有以下情况:
- :无需多言。
- :可得 ,那么可以直接 ,那么原路径就不是最短路径了。
- :可得 ,同样可以 ,原路径不是最短路径。
根据 我们可以知道,最短路径要么一直向左传送,要么先向右传送再一直向左传送。
引理 1.2
:最短路径不存在连续多次向右的传送。
证明:假设引理不成立,根据 ,必然在开始就向右传送到起始点 的右侧,由于终点在 的左侧,必然存在唯一的一步 ,满足 。
- 若 ,无需多言。
- 若 ,则 ,那么可以直接 ,再 两步到达 ,而那些连续多次向右的路径,至少花了 步才到达 。
根据 ,最短路径要么是一直向左传送,要么是先向右传送一次再一直向左传送。
引理 1.3
:如果最短路径以向右的传送开始,那么存在一个第一步相同的最短路径,其下一步向左传送的终点 。
证明:假设 ,有以下情况:
- ,无需多言。
- ,有 ,那么可以从 出发传送到 ,无需向右传送。
- ,对于存在的唯一一步 ,满足 ,有以下情况:
- ,无需多言。
- ,有 ,那么可以直接 , 两步到达 ,而原路径至少花了两步才到达 。
首先,在 的条件下, 依然成立。
由于新条件只扩大了向右传送的区间,而没有改变向左传送的区间,因此新规则下的严格更短路径一定存在向右的移动 ,并且 原本不合法。
根据 ,存在一个至少同样长的严格更短路径, 的下一步 满足 ,有 ,那么 是合法的,矛盾,因此不存在新规则下的严格更短路径,即答案不会减小,得证。
回归题目
接下来的讨论都在 的状态下。
提到,仅有第一步可能是向右的,其它步都是向左的,为此我们可以特殊处理第一步:
显然一步能到达 的所有节点,将其与询问区间 取交集,得到从 出发能一步到达的所有点,然后从询问 中去掉这个交集,并将答案(这里的答案指 ,下同)加上区间长。注意,可能有 ,那么询问区间内的所有点都可一步到达,正确更新答案后直接退出计算即可。
尽管我们只知道终点在 左侧,但是容易贪心地找到第一步的固定终点:那就是使 最小的 ,显然其它第一步终点可直达的点,对于 也是可直达的。注意这个 可能在 的左侧也可能在 的右侧,但一定不是 ,因为 恒成立,因此不用担心出现"原地跳跃"的情况。
定义 ,那么第一步的固定终点的 值等于 ,贡献最小值的 并不重要。
将等效起点转移到 上,剩余的 上的点都要走一步 ,答案增加 ,然后将起点转移到 上即可。
考虑第 步的起点,即第 步的终点,我们发现选择 步可达的所有点中 最小的点一定不劣,和上面选择第一步的终点是同一个道理。
但是这样还是太麻烦了,为什么不把那个最优点计入到 步可达的左端点上呢?换言之,尽管最左端点不是下一步的好"跳板",但它可以"借用"右边的点做"跳板"。
引理 2
:如果起点 满足 ,那么将第 个星球的联系区间由 改为 ,询问的答案不变。
证明
注:证明还是比较废话,同样容易画图理解。
用归纳法证明,显然新条件的答案不会增大。
因此我们只需要证明新条件下 步可达的点,在原条件下可以至多 步到达即可。
因此我们只需要证明新条件下 步可达的点,在原条件下可以至多 步到达即可。
-
当 时,因为 ,否则与引理前提矛盾,引理显然成立。
-
当 时,,设 步可达的最左端点为 (由归纳法两种条件下都是一样的),列出两种条件下计算得到的 步可达最左端点:
- 原条件:
- 新条件:
考虑 的递推定义: ,上式等价于
形式上都一模一样,引理成立。
我们第一步完成后的等效起点其实就符合 的前提。
DP 状态定义
在 的状态下,定义 表示从 出发 步可达的最左端点。
由于允许使用右侧的节点代替做"跳板",新的 DP 状态具有明显的最优子结构特性: 可以从 转移过来,转移方程如下:
有了新的最优子结构特性,容易得到倍增优化的转移方程:
求解答案
首先按照前面的方法特殊处理第一步,然后跳到新的等效起点,这个等效起点符合 的前提,可以使用 加速跳跃。
接下来用倍增跳跃用最少步数 到达 ,方法和树上倍增求 LCA 的第二阶段类似,如果从当前点 出发走 不能到达目标,则走 步,并移动到 上。
这里仍然使用等效起点的思路,将起点转移到 上,然后答案增加 。
这里仍然使用等效起点的思路,将起点转移到 上,然后答案增加 。
做完上面的跳跃后,一定可以只花一步就能到达 :
如果 ,即从当前等效起点出发可以一步到达 的所有点,那么答案增加 ,然后直接退出计算即可。如果 ,答案增加 ,将等效起点转移到 上,答案再增加 ,合起来还是增加 。
但是接下来的部分怎么处理呢?由于接下来的连续的点全部都要到达恰好一次,我们可以定义 表示从 出发到达所有 步可达的点的总最短距离(包不包括 都没事,因为到自己的最短距离为 ),同样是在 的状态下。
转移方程如下:
中间的 是根据等效起点法得到的,请自行思考。
倍增优化后的转移方程如下;
这样,我们可以继续套用同样的倍增方式:如果走 步仍未到达 ,则走下这 步,然后答案增加 ,等效移动起点到 ,答案再增加 。
最后还剩下一片散块,散块内的点全部可以一步从当前等效起点 到达,答案增加 ,终于结束了!
代码
警示: 可能超出 的范围,不仅仅是 的定义,相关计算过程的每一项可能超出 的表达式都要注意类型正确。
CPP#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long int64;
int n, L[300001], q; // 用大 L 区分询问小 l
int s[300001]; // s[i] : L[i~n] 的最小值
int dp[300001][19]; // dp[i][j] : 从 i 出发走 j 步可达的最左端点
int64 sum[300001][19]; // sum[i][j] : 从 i 出发到 j 步可达的所有点的最短距离和
void init(){
s[n] = L[n];
dp[n][0] = L[n];
sum[n][0] = n-L[n];
for(int i=n-1; i; --i){
s[i] = min(s[i+1], L[i]); // 递推求 s
dp[i][0] = s[i]; // dp 的初始状态
sum[i][0] = i-dp[i][0]; // sum 的初始状态
}
for(int j=1; j<=18; ++j){
for(int i=1; i<=n; ++i){
// dp 的倍增递推转移方程
dp[i][j] = dp[dp[i][j-1]][j-1];
// sum 的倍增递推转移方程
sum[i][j] = sum[i][j-1]+sum[dp[i][j-1]][j-1]+(1ll<<(j-1))*(dp[i][j-1]-dp[i][j]);
}
}
}
int64 solve(int l, int r, int x){
int64 ans = 0;
// 处理第一步可达的点
if(L[x]<=r){
if(L[x]<=l){ // 一步可以到达所有点
return r-l+1;
}else{
ans += r-L[x]+1;
r = L[x]-1; // 从询问区间中删除交集
}
}
// 转移等效起点
ans += r-l+1;
x = L[x];
// 第一部分:r 所在的散块
for(int j=18; ~j; --j){
if(dp[x][j]>r){
ans += (r-l+1)*(1ll<<j);
x = dp[x][j];
}
}
ans += r-l+1;
if(dp[x][0]<=l) return ans;
// 第二部分:[l, r] 中间的完整块
x = dp[x][0];
for(int j=18; ~j; --j){
if(dp[x][j]>l){
ans += sum[x][j]+(1ll<<j)*(dp[x][j]-l);
x = dp[x][j];
}
}
// 第三部分:l 所在的散块
ans += x-l;
return ans;
}
int main(){
scanf("%d", &n);
L[1] = 1; // 注意 L[1] 要手动赋值
for(int i=2; i<=n; ++i) scanf("%d", L+i);
init();
scanf("%d", &q);
while(q--){
int l, r, x;
scanf("%d%d%d", &l, &r, &x);
int64 sum = solve(l, r, x), len=r-l+1;
int64 gcd = __gcd(sum, len);
printf("%lld/%lld\n", sum/gcd, len/gcd);
}
return 0;
}
相关推荐
评论
共 3 条评论,欢迎与作者交流。
正在加载评论...