题解:P5465 [PKUSC2018] 星际穿越

· · 题解

很多题解仅仅把这道题的倍增优化 DP 当成类似树上倍增 LCA 的简单倍增。

但实际上直观定义出来的 DP 状态在某些方面上没有最优子结构性质(下文会展开叙述),并不适合倍增,而是需要进行一些特殊转化才能用倍增优化,本题解用等效法来构造合适的 DP 状态。

题目分析

形式化题意

形式化的,给定一个数轴上的 n(n\le3\times10^5) 个点,编号 1, 2, 3, \dots, n,对每个点 i,给定 l_i(l_i<i) ,将其与 [l_i, i-1] 内的所有点连一条长度为 1 的无向边,规定 dist(x, y) 表示从 xy 的最短路径长度。
接下来给出 q(q\le3\times10^5) 次询问,每次给出 l<r<x,求 \frac{1}{r-l+1}\times\sum\limits_{i=l}^rdist(x, i),以最简分数形式输出。

注:下文为了区分与联系相关的 l_i 和与询问相关的 l_x,所有与联系相关的 l_i 均含有下标(例如 $l_),与询问相关的全部用单独的 l$*。

编码要求

显然,我们的主要任务对每次询问求出 \sum\limits_{i=l}^rdist(x, i),然后使用 std::__gcd 约分即可。

考虑到 n,q\le3\times10^5,程序的复杂度应该是亚平方级的,例如 O(n\log n), O(n\sqrt n)(似乎这题单次分块的复杂度是 O(n\sqrt n),但这题分块编码并不比倍增编码简单)。

暴力算法

容易想到 Floyd 多源最短路算法,复杂度 O(n^3),显然不能通过。

但这个算法的真正意义是写对拍,因为 Floyd 编码很简单且不易写错,而且这题构造数据,将输入转为邻接矩阵也很简单,这里不再赘述。

正解 DP

首先容易想到 DP,因为传送过程存在明显的无后效性,前面如何到达 ii 之后的传送路径无关。

但是,如果我们直观定义 dp[i][j] 表示从 i 出发走 j 步能到达的最左端位置,会发现它并不符合最优子结构:dp[i][j+1] 并不一定是最后通过 dp[i][j] 到达的。

例如样例中以 6 为起点,走 1 步能到达的最左端位置为 4,走 2 步能到达的最左端位置为 1,然而这个位置 1 是从位置 5 而非位置 4 走过来的。

这限制了我们用倍增法优化这个 DP 状态,我们需要找到其它有用的 DP 状态。

引理 1

换句话说,我们可以认为从 $i$ 出发可以一步到达的点为 $[l_i, n]$ 内的所有点,而不改变答案。 #### 证明 *注:下面各子引理的证明比较废话,读者可自行画图理解*。 显然 $\text{Lemma 1}$ 只是增加了边而没有删除边,那么答案不会增大。 ##### 引理 1.1 $\text{Lemma 1.1}$:**如果最短路径存在向右的传送,那么所有向右的传送不在任何向左的传送之后**。 证明:假设在某一步向右传送($b\rightarrow c$),在向左传送($b\leftarrow a$)之后,有以下情况: 1. $c=a$:无需多言。 1. $c<a$:可得 $l_a\le b<c<a$,那么可以直接 $c\leftarrow a$,那么原路径就不是最短路径了。 3. $c>a$:可得 $l_c\le b<a<c$,同样可以 $a\rightarrow c$,原路径不是最短路径。 根据 $\text{Lemma 1.1}$ 我们可以知道,最短路径要么一直向左传送,要么先向右传送再一直向左传送。 --- ##### 引理 1.2 $\text{Lemma 1.2}$:**最短路径不存在连续多次向右的传送**。 证明:假设引理不成立,根据 $\text{Lemma 1.1}$,必然在开始就向右传送到起始点 $x$ 的右侧,由于终点在 $x$ 的左侧,必然存在唯一的一步 $a\xleftarrow{x}b$,满足 $a<x\le b$。 1. 若 $x=b$,无需多言。 2. 若 $x<b$,则 $l_b\le a<x<b$,那么可以直接 $x\rightarrow b$,再 $a\leftarrow b$ 两步到达 $a$,而那些连续多次向右的路径,至少花了 $3$ 步才到达 $a$。 根据 $\text{Lemma 1.1}, \text{Lemma 1.2}$,最短路径要么是一直向左传送,要么是先向右传送**一次**再一直向左传送。 --- ##### 引理 1.3 $\text{Lemma 1.3}$:**如果最短路径以向右的传送开始,那么存在一个第一步相同的最短路径,其下一步向左传送的终点 $a<l_x$**。 证明:假设 $a\ge l_x$,有以下情况: 1. $a=x$,无需多言。 2. $a<x$,有 $l_x\le a<x$,那么可以从 $x$ 出发传送到 $a$,无需向右传送。 3. $a>x$,对于存在的唯一一步 $b\xleftarrow{x}c$,满足 $b<x\le c$,有以下情况: 1. $x=c$,无需多言。 2. $x<c$,有 $l_c\le b<x<c$,那么可以直接 $x\rightarrow c$, $b\leftarrow c$ 两步到达 $b$,而原路径至少花了两步才到达 $b$。 --- 首先,在 $\text{Lemma 1}$ 的条件下,$\text{Lemma 1.1}, \text{Lemma 1.2}, \text{Lemma 1.3}$ 依然成立。 由于新条件只扩大了向右传送的区间,而没有改变向左传送的区间,因此新规则下的严格更短路径一定存在向右的移动 $x\rightarrow a$,并且 $x\rightarrow a$ 原本不合法。 根据 $\text{Lemma 1.3}$,存在一个至少同样长的严格更短路径,$x\rightarrow a$ 的下一步 $b\leftarrow a$ 满足 $b<l_x$,有 $l_a\le b<l_x\le x<a$,那么 $x\rightarrow a$ 是合法的,矛盾,因此不存在新规则下的严格更短路径,即答案不会减小,得证。 ### 回归题目 接下来的讨论都在 $\text{Lemma 1}$ 的状态下。 $\text{Lemma 1.1}, \text{Lemma 1.2}$ 提到,仅有第一步可能是向右的,其它步都是向左的,为此我们可以特殊处理第一步: > 显然一步能到达 $[l_x, n]$ 的所有节点,将其与询问区间 $[l, r]$ 取交集,得到从 $x$ 出发能一步到达的所有点,然后从询问 $[l, r]$ 中去掉这个交集,并将答案(这里的答案指 $\sum\limits_{i=l}^rdist(x, i)$,下同)加上区间长。 > > 注意,可能有 $[l, r]\subset[l_x, n]$,那么询问区间内的所有点都可一步到达,正确更新答案后直接退出计算即可。 尽管我们只知道终点在 $l_x$ 左侧,但是容易贪心地找到第一步的固定终点:那就是使 $l_y$ 最小的 $y\in[l_x, n]$,显然其它第一步终点可直达的点,对于 $y$ 也是可直达的。注意这个 $y$ 可能在 $x$ 的左侧也可能在 $x$ 的右侧,但一定不是 $x$,因为 $l_{l_x}<l_x$ 恒成立,因此不用担心出现"原地跳跃"的情况。 定义 $s_i=\min\{l_y, i\le y\le n\}$,那么第一步的固定终点的 $l_*$ 值等于 $s_{l_x}$,贡献最小值的 $y$ 并不重要。 将等效起点转移到 $y$ 上,剩余的 $[l, r]$ 上的点都要走一步 $x\rightarrow y$,答案增加 $r-l+1$,然后将起点转移到 $y$ 上即可。 --- 考虑第 $j+1$ 步的起点,即第 $j$ 步的终点,我们发现选择 $j$ 步可达的所有点中 $l_i$ 最小的点一定不劣,和上面选择第一步的终点是同一个道理。 但是这样还是太麻烦了,为什么不把那个最优点计入到 $j$ 步可达的左端点上呢?换言之,尽管最左端点不是下一步的好"跳板",但它可以"借用"右边的点做"跳板"。 ### 引理 2 $\text{Lemma 2}$:**如果起点 $x$ 满足 $\nexists x'>x, l_x'<l_x$,那么将第 $i$ 个星球的联系区间由 $[l_i, n]$ 改为 $[s_i, n]$,询问的答案不变**。 #### 证明 *注:证明还是比较废话,同样容易画图理解。* 用归纳法证明,显然新条件的答案不会增大。 因此我们只需要证明新条件下 $j$ 步可达的点,在原条件下可以至多 $j$ 步到达即可。 - 当 $j=1$ 时,因为 $s_x=l_x$,否则与引理前提矛盾,引理显然成立。 - 当 $j>1$ 时,$j-1\ge1$,设 $j-1$ 步可达的最左端点为 $a$(由归纳法两种条件下都是一样的),列出两种条件下计算得到的 $j$ 步可达最左端点: - 原条件:$\min\{l_i, a\le i\le n\}

我们第一步完成后的等效起点其实就符合 \text{Lemma 2} 的前提。

DP 状态定义

\text{Lemma 2} 的状态下,定义 dp[i][j] 表示从 i 出发 j 步可达的最左端点。

由于允许使用右侧的节点代替做"跳板",新的 DP 状态具有明显的最优子结构特性:dp[i][j+1] 可以从 dp[i][j] 转移过来,转移方程如下:

dp[i][1] = s_i dp[i][j+j']=dp[dp[i][j]][j'], j, j'>1

有了新的最优子结构特性,容易得到倍增优化的转移方程:

dp[i][2^0] = s_i dp[i][2^{j+1}]=dp[dp[i][2^j]][2^j]

求解答案

首先按照前面的方法特殊处理第一步,然后跳到新的等效起点,这个等效起点符合 \text{Lemma 2} 的前提,可以使用 dp 加速跳跃。

接下来用倍增跳跃用最少步数 j 到达 r,方法和树上倍增求 LCA 的第二阶段类似,如果从当前点 i 出发走 2^j 不能到达目标,则走 2^j 步,并移动到 dp[i][2^j] 上。
这里仍然使用等效起点的思路,将起点转移到 dp[i][2^j] 上,然后答案增加 2^j\times(r-l+1)

做完上面的跳跃后,一定可以只花一步就能到达 r

如果 dp[x][0]\le l,即从当前等效起点出发可以一步到达 [l, r] 的所有点,那么答案增加 r-l+1,然后直接退出计算即可。

如果 dp[x][0]>l,答案增加 r-dp[x][0]+1,将等效起点转移到 dp[x][0] 上,答案再增加 dp[x][0]-l,合起来还是增加 r-l+1

但是接下来的部分怎么处理呢?由于接下来的连续的点全部都要到达恰好一次,我们可以定义 sum[i][j] 表示从 i 出发到达所有 j 步可达的点的总最短距离(包不包括 i 都没事,因为到自己的最短距离为 0),同样是在 \text{Lemma 2} 的状态下。

转移方程如下:

sum[i][1] = i-dp[i][1] sum[i][j+j'] = sum[i][j]+j'\times (dp[i][j]-dp[i][j+j'])+sum[dp[i][j]][j'], j, j'>1

中间的 j'\times (dp[i][j]-dp[i][j+j']) 是根据等效起点法得到的,请自行思考。

倍增优化后的转移方程如下;

sum[i][2^0] = i-dp[i][2^0] sum[i][2^{j+1}]=sum[i][2^j]+2^j\times(dp[i][2^j]-dp[i][2^{j+1}])+sum[dp[i][2^j]][2^j], j>1

这样,我们可以继续套用同样的倍增方式:如果走 2^j 步仍未到达 i,则走下这 2^j 步,然后答案增加 sum[x][2^j],等效移动起点到 dp[x][2^j],答案再增加 2^j\times(dp[x][2^j]-l)

最后还剩下一片散块,散块内的点全部可以一步从当前等效起点 x 到达,答案增加 x-l,终于结束了!

代码

警示:\sum\limits_{i=l}^rdist(x, i) 可能超出 \text{int32} 的范围,不仅仅是 sum 的定义,相关计算过程的每一项可能超出 \text{int32} 的表达式都要注意类型正确。

云剪贴板存档

AC 记录

#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;
}