同余最短路
同余最短路
引入
先介绍一下大家熟知的差分约束问题,通过建图将线性规划问题转化为图论问题。
给定多个形如\(a_i-a_j\ge c_{ij}\)的不定式,找出一种可行解。
这种问题有一种很巧妙的构造方法,就是将这类问题抽象成一个图论问题来解决。
具体来说,就是将不等式移项,变为\(a_i\ge a_j+c_{ij}\),发现很像最短(长)路中的式子。
在每条边都松弛完成之后,在最短路中每条边都满足\(d_y\le d_x+z\)(不存在负环),反之可以继续松弛。同理,在最长路中每条边都满足\(d_y\ge d_x+z\)(不存在正环)。
如此,发现和原来的不等式很像,于是连接\(j\)到\(i\),边权为\(c_{ij}\)的边,跑一边最长路,就得到了原问题的一种可行解。
这个东西叫做三角不等式,将一类线性规划问题通过三角不等式转换为图论问题解决,成功在多项式时间复杂度之内解决了这个难题。
我们发现通过建图将数学问题转化为图论问题的思路很好,尝试推广一下。
例题1
题目传送门:P3403 跳楼机。
题意简述:给定一栋大楼,从一楼开始,每次可以向上走\(x,y,z\)层楼,问可以到达的楼层数。
数据范围:楼高\(1 \le h\le 2^{63}-1\),每次移动的距离\(1\le x,y,z\le10^5\)。
提示:这是道图论题,由博客标题可知。
Solution
我们发现\(h\)很大,设最后到达的楼层数为\(D\),有\(D=k_xx+k_yy+k_zz\)且满足\(k_x,k_y,k_z\in \text{N}\)。
然后你发现你只能固定\(k_x,k_y\)算出方案数,但\(h\)太大了,数论做法卒。
我们站在先人的肩膀上,考虑建图转化为图论问题。
我们发现每次执行一次操作,都是\(D_1=D_2+edge,edge\in{x,y,z}\)。这不是最短路问题吗?
在最短路松弛结束后,每个点都至少有一个入边满足\(d_y=d_x+z\),于是可以建图最短路,找到所有点。
但是有一个新的问题,就是\(h\)的范围太大,最短路的时空复杂度都承担不下。
于是,我们考虑一个神奇的运算,取模!
发现我们只需将\(\text{mod }x\)后的值保存下来,在统计答案时在统计\(\text{mod } x\)同余的楼层一起计算即可。
这种将余数相同的数一起考虑,再建图转化为图论问题的思想被称为同余最短路!
具体来说,我们记\(f(i)\)为只通过若干加\(y\)和加\(z\)最后到达的楼层中\(\text{mod }x=i\)的最小楼层。
P.S记最小楼层是为了方便求出方案数。
写出状态转移方程式:
将\(i,(i+y)\%x,(i+z)\%x\)视为点跑最短路即可,初始状态\(f(1)=1\)。
这样点和边的规模都变为和\(x,y,z\)有关,设\(x,y,z\)的值域为\(n\),所以时间复杂度为\(O(nlogn)\)。
让我们再想想方案数怎么统计。
显然,对于每个\(f(i)\)我们最后只需考虑填\(x\)的方案数。
所以\(ans=\sum_{i=0}^x(\left \lfloor \frac{h-f(i)}{x} \right \rfloor +1)\),\(O(n)\)计算即可。
代码如下:
SPFA已经死了无数次了,虽然这题不卡但还是用dijkstra吧。
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=1e5+10,M=2e5+10;
int head[N],ver[M],edge[M],nxt[M],tot=1;
void add(int x,int y,int z){
ver[++tot]=y,edge[tot]=z,nxt[tot]=head[x],head[x]=tot;
}
ll f[N],ans,h,X,Y,Z;
bool v[N];
priority_queue<pair<ll,int>>q;
void dijkstra(){
memset(f,0x3f,sizeof(f));
f[1]=1;q.push({1,1});
while(q.size()){
int x=q.top().second;q.pop();
if(v[x])continue;
v[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(f[y]>f[x]+edge[i]){
f[y]=f[x]+edge[i];
q.push({-f[y],y});
}
}
}
}
int main(){
scanf("%lld%lld%lld%lld",&h,&X,&Y,&Z);
if(X==1||Y==1||Z==1)printf("%lld\n",h),exit(0);
for(int i=0;i<X;i++){
add(i,(i+Y)%X,Y);
add(i,(i+Z)%X,Z);
}
dijkstra();
for(int i=0;i<X;i++)
if(f[i]<=h)ans+=(h-f[i])/X+1;
printf("%lld\n",ans);
return 0;
}
例题2
题目传送门:[ABC077D] Small Multiple
题意简述:给定一个整数\(K\),求一个\(K\)的倍数\(S\),使得\(S\)在十进制下的数位和最小,输出最小数位和。
数据范围:\(2\le K \le 10^5\)。
Solution
这题有非常好的性质,对于每个\(S\),每次\(\times 10\)对答案没有贡献;每次\(+1\),答案\(+1\)。
于是这样就可做了,将每个\(S\)看作一个点,建图跑最短路,判断是否是\(K\)的倍数。
但是\(S\)可以很大,于是我们可以像上题一样,用取模压缩状态。
直接保存\(S\%K\)的值即可,还挺直接的。
边权只有\(0\)和\(1\),01bfs也可做。
代码如下:
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=1e5+10,M=2e5+10;
int head[N],ver[M],edge[M],nxt[M],tot=1;
void add(int x,int y,int z){
ver[++tot]=y,edge[tot]=z,nxt[tot]=head[x],head[x]=tot;
}
ll f[N],n;
bool v[N];
priority_queue<pair<ll,int>>q;
void dijkstra(){
memset(f,0x3f,sizeof(f));
f[1]=1;q.push({1,1});
while(q.size()){
int x=q.top().second;q.pop();
if(v[x])continue;
v[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(f[y]>f[x]+edge[i]){
f[y]=f[x]+edge[i];
q.push({-f[y],y});
}
}
}
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++)
add(i,(i+1)%n,1),add(i,(i*10)%n,0);
dijkstra();
printf("%lld\n",f[0]);
return 0;
}
做过的最水紫题之一。
例题3
题目传送门:P2371 [国家集训队] 墨墨的等式
题意简述:求出\([l,r]\)内有多少个\(K\)满足\(\sum_{i=1}^na_ix_i=K\)。
数据范围:$$n \le 12$,\(0 \le a_i \le 5\times 10^5\),\(1 \le l \le r \le 10^{12}\)。
Solution
好熟悉,定睛一看,不是加强版的跳楼机吗?
差分一下,变为求\([0,l-1]\)和\([0,r]\)的贡献。做法如出一辙。
又水一道紫题。
代码如下:
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=5e5+10,M=6e6+10;
int head[N],ver[M],edge[M],nxt[M],tot=1;
void add(int x,int y,int z){
ver[++tot]=y,edge[tot]=z,nxt[tot]=head[x],head[x]=tot;
}
ll f[N],ans,n,l,r,a[N],m;
bool v[N];
priority_queue<pair<ll,int>>q;
void dijkstra(){
memset(f,0x3f,sizeof(f));
f[0]=0;q.push({0,0});
while(q.size()){
int x=q.top().second;q.pop();
if(v[x])continue;
v[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(f[y]>f[x]+edge[i]){
f[y]=f[x]+edge[i];
q.push({-f[y],y});
}
}
}
}
ll solve(ll x){
ll ans=0;
for(int i=0;i<a[1];i++)
if(f[i]<=x)ans+=(x-f[i])/a[1]+1;
return ans;
}
int main(){
scanf("%lld%lld%lld",&n,&l,&r);
for(int i=1,x;i<=n;i++){
scanf("%d",&x);
if(x)a[++m]=x;
}
for(int i=0;i<a[1];i++)
for(int j=2;j<=m;j++)
if(a[j]!=a[1])add(i,(i+a[j])%a[1],a[j]);
dijkstra();
printf("%lld\n",solve(r)-solve(l-1));
return 0;
}
总结
学会同余最短路又可以水一堆题。
思路大概是:数论做法\(\Rightarrow\)建图等价转换\(\Rightarrow\)发现在\(\%K\)下等价,缩小数据范围。
注意模数的选取,不然会出问题。

浙公网安备 33010602011771号