同余最短路学习笔记.Part3/题解:P3403 跳楼机

· · 题解

Upd 24.10.17:修改了部分笔误。

Upd 25.10.03:修改了部分笔误。

包括 OI Wiki 在内的同余最短路教程感觉都过于抽象,本蒟蒻学了很久才理解……于是写下了这篇算是比较详细的文章。

前置知识:单源最短路、同余。

在此之前可以去 OI Wiki 了解一下同余最短路的用途,这里不多赘述。

注意:本文章语言更偏向口语化,没有过多专业术语,如果您需要更加严谨的文章建议转向其他文章,感谢。

图论部分

(凡是提到类似“跑一遍最短路”的话可以认为是 Dijkstra 算法或 SPFA 算法,两者无异。)

(如果您曾经接触过差分约束并且已经理解掌握,那么您可以跳过该部分。)

先来看这么一张图:

这是以 1 号节点出发,跑一遍最短路会得到 dis 数组,dis[x] 表示从 1 号节点到 x 节点的最短路径。

我会暂时把这类跑一遍最短路的图称为最短路图

在跑最短路时,我们总是会不断更新 dis 数组(即松弛操作),更新的条件是满足:dis[x]+edge(x,y)<dis[y]

其中 edge(x,y) 表示从 xy 的一条有向边的边权,如上图的 edge(5,3)=6

但是最短路图有一个显然的性质:对于任意两个点 x,y,若 xy 有有向边,那么一定满足不等式:

dis[x]+edge(x,y)\ge dis[y]

证明比较容易:如果没有能够进行松弛操作的一对点,就会停止跑最短路,相当于没有任意两个点满足:dis[x]+edge(x,y)<dis[y],那反过来就得到了 dis[x]+edge(x,y)\ge dis[y]

这个性质非常重要,这将会是解决同余最短路和差分约束的关键。

算法部分

引入一个问题:

给定四个正整数 x,y,z,H,求有多少个整数 d \in [0,H] 满足 ax+by+cz=d,其中 a,b,c 且都是非负整数。

(接下来的 a,b,c 均为非负整数,称一个数合法当且仅当其可以表示成 ax+by+cz 的形式,但不一定小于等于 H。)

一个重要的性质,若一个整数 k 能被若干个 y,z 拼成(k=by+cz),那么 k+xk+2xk+3x\cdots 都是合法的,当然 k 本身也是合法的。

我们令函数 f(i)by+cz \bmod x =i 时最小的 by+cz。也可以理解为使用若干个 yz 能够拼的数在模 x 意义下等于 i 的最小的值。

该函数有一个显而易见的性质:

f(i)+y \ge f((i+y)\bmod x)\\ f(i)+z \ge f((i+z)\bmod x)

因为如果存在一个 i 使得 f(i)+y < f((i+y)\bmod x) 就不符合定义,因为让 f((i+y)\bmod x) = f(i)+y 是更优的。z 的情况也同理。

我们发现这个性质与我们上面提到的最短路图的性质(dis[x]+edge(x,y)\ge dis[y])相似。

这样我们可以建边:

edge(i,(i+y)\bmod x)=y\\ edge(i,(i+z)\bmod x)=z

即:

建完边之后,跑一遍最短路,得到的 $dis[i]$ 就相当于 $f(i)$。 那么有了 $f(i)$,如何计算答案。 根据我们刚开始得出的一个性质,若一个整数 $k=by+cz$,那么对于 $k+x$、$k+2x$、$k+3x\cdots$ 小于等于 $H$ 的数共有 $\lfloor\frac{H-k}{x}\rfloor$ 个。 这个公式其实很简单,大于 $k$ 小于等于 $H$ 的数有 $H-k$ 个,在这些数中每 $x$ 个数就有 $1$ 个,一共就是 $\lfloor\frac{H-k}{x}\rfloor$ 个。 那么本题的答案就是: $$ \sum_{i=0}^{x-1} ( \lfloor\frac{H-f(i)}{x}\rfloor+1 ) $$ 加一是因为 $f(i)$ 本身也是一个合法数。 让 $f(i)$ 尽可能小是为了防止漏解。 根据定义,$f(0)$ 一定等于 $0$,那么跑最短路时源点也就确定了是 $0$。 ## 例题1 [P3403 跳楼机](https://www.luogu.com.cn/problem/P3403) 其实就是上面的那道题。 但是楼层是从 $1$ 开始的,我们可以假设从 $0$ 开始,统计到 $H-1$ 即可。 ```cpp #include <stdio.h> #include <queue> using namespace std; #define int long long const int N=1e5+5; const int inf=9223372036854775807; int H,x,y,z,idx=0; int dis[N],vis[N],h[N]; struct node{int to,c,ne;}e[N<<1]; void add(int a,int b,int c){ e[++idx]={b,c,h[a]}; h[a]=idx; } void SPFA(){//最短路模板 for(int i=0;i<x;++i) dis[i]=inf; queue<int> q; q.push(0),dis[0]=0; while(!q.empty()){ int x=q.front(); q.pop(); vis[x]=0; for(int i=h[x],y,w;i;i=e[i].ne){ y=e[i].to; w=e[i].c; if(dis[x]+w<dis[y]){ dis[y]=dis[x]+w; if(!vis[y]){ vis[y]=1; q.push(y); } } } } } int main(){ scanf("%lld%lld%lld%lld",&H,&x,&y,&z); if(x==1||y==1||z==1){printf("%lld\n",H);return 0;}//特判 --H; for(int i=0;i<x;++i){ add(i,(i+y)%x,y); add(i,(i+z)%x,z); } SPFA(); int res=0; for(int i=0;i<x;++i) if(H>=dis[i]) res+=(H-dis[i])/x+1;//这里特判一下是可能会有大于H的情况,导致出现负数 printf("%lld\n",res); } ``` ## 例题2 [P2371 [国家集训队] 墨墨的等式](https://www.luogu.com.cn/problem/P2371) 只是从 3 个的情况变成了 $n$ 个的情况。 统计时只需要先统计小于等于 $r$ 的情况,把答案减去小于等于 $l-1$ 的情况即可。 注意要额外特判等于 $0$ 的情况。 ```cpp int main(){ scanf("%lld%lld%lld",&n,&l,&r);qwq=-1; for(int i=1;i<=n;++i){ scanf("%lld",&a[i]); if(a[i]==1){ printf("%lld\n",r-l+1); return 0; } if(a[i]!=0&&qwq==-1) qwq=i;//记录第一个不为0的位置 } if(qwq==-1){printf("0\n");return 0;} --l; for(int i=0;i<a[qwq];++i) for(int j=1;j<=n;++j) if(j!=qwq) add(i,(i+a[j])%a[1],a[j]); SPFA(); int res=0; for(int i=0;i<a[qwq];++i){ if(r>=dis[i]) res+=((r-dis[i])/a[qwq]+1); if(l>=dis[i]) res-=((l-dis[i])/a[qwq]+1); } printf("%lld\n",res); } ``` ## 例题3 [AT_arc084_b Small Multiple](https://www.luogu.com.cn/problem/AT_arc084_b) 令函数为 $f(i)$ 为一个数 $x \bmod n =i$ 时位数和的最小值。 那么有: - $f(i) \ge f(10i\bmod n)

i10i \bmod n 建一条边权为 0 的边、i(i+1)\bmod n 建一条边权为 1 的边。

注意这里应该让 f(1)=1 且以 1 为源点跑最短路,答案为 f(0),因为我们是要求出 n 的正整数倍的答案,其在模 n 意义下为 0。而且 f(1) 显然等于 1

其他注意事项

不难发现这个同余最短路是需要选出一个数作为模数,我们暂时令这个数为 x

我看很多讲解是要求 x 尽可能小,但意义不是很大。跑这个最短路需要 x 个点,边的个数与点数相关,因此,选出的 x 实际上与整个程序效率有关,并不影响正确性。

感谢您能看到这里!如果有什么不懂的地方\有错误需要指正的地方就私信我,感谢!!qwq