同余最短路
lichenyu_ac · · 个人记录
同余最短路
引入
先介绍一下大家熟知的差分约束问题,通过建图将线性规划问题转化为图论问题。
给定多个形如
这种问题有一种很巧妙的构造方法,就是将这类问题抽象成一个图论问题来解决。
具体来说,就是将不等式移项,变为
在每条边都松弛完成之后,在最短路中每条边都满足
如此,发现和原来的不等式很像,于是连接
这个东西叫做三角不等式,将一类线性规划问题通过三角不等式转换为图论问题解决,成功在多项式时间复杂度之内解决了这个难题。
我们发现通过建图将数学问题转化为图论问题的思路很好,尝试推广一下。
例题1
题目传送门:P3403 跳楼机。
题意简述:给定一栋大楼,从一楼开始,每次可以向上走
数据范围:楼高
提示:这是道图论题,由博客标题可知。
Solution
我们发现
然后你发现你只能固定数论做法卒。
我们站在先人的肩膀上,考虑建图转化为图论问题。
我们发现每次执行一次操作,都是
在最短路松弛结束后,每个点都至少有一个入边满足
但是有一个新的问题,就是
于是,我们考虑一个神奇的运算,取模!
发现我们只需将
这种将余数相同的数一起考虑,再建图转化为图论问题的思想被称为同余最短路!
具体来说,我们记
P.S记最小楼层是为了方便求出方案数。
写出状态转移方程式:
将
这样点和边的规模都变为和
让我们再想想方案数怎么统计。
显然,对于每个
所以
代码如下:
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
题意简述:给定一个整数
数据范围:
Solution
这题有非常好的性质,对于每个
于是这样就可做了,将每个
但是
直接保存还挺直接的。
边权只有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 [国家集训队] 墨墨的等式
题意简述:求出
数据范围:$
Solution
好熟悉,定睛一看,不是加强版的跳楼机吗?
差分一下,变为求
又水一道紫题。
代码如下:
#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;
}
总结
学会同余最短路又可以水一堆题。
思路大概是:数论做法
注意模数的选取,不然会出问题。