同余最短路问题在ACM比赛里面考察的较少,属于冷门的知识点,当我们遇到类似“给定n个整数,求这n个整数能组成多少种不同的数”等等问题的时候,可以考虑这种方法。
以P3403作为一个简单的示例:
Description
有一个高为h的大楼,每次可以向上移动x、y、z层,问可以到达的楼层数.
SOLUTION
简单的说,这道题就是让求x*x1+y*x2+z*x3能组成多少个范围在[1,h]内的数.
看到这道题的时候很容易想到无限背包的方法,但是因为数据范围巨大,所以不能使用背包,于是就有人提出了使用同余最短路的方法来解决这道题,使用同余最短路并不是用同余关系来运行最短路,而是我们找到一些状态(通过同余)来进行优化,通常这些状态就是最短路中的点。
对于这道题,我们先把问题简化为只有x和y两个条件。
下面我直接说出解决的方法~~~~~~~~~
首先我们需要使用y来找到x的剩余系.
所谓的剩余系就是对于某一个特定的正整数n,一个整数集中的数模n所得的余数域(转载自百度百科)对于剩余系,举个例子:模5的一个剩余系:0,5,10,15
假设i是num%x的余数(num%x==i),那么我们现在需要找到一个最小的num,使得dist[i]=num.
对于y能凑出来的最小高度num,我们能找到(h-num)/x+1个符合条件的结果(加一是因为当前的num也要计算在内).
那么我们只需要把所有的dist[i]加起来就就是最后的结果了。
现在我们分析这一道题
我们把y和z看作上面分析中的y,现在只需要用y和z组成dist[i]就行了,重新说一下dist[i]的意义:num%x==i的最小num就是dist[i],并且有下面两个条件满足:dist[i+y]=dist[i]+y,dist[i+z]=dist[i]+z.
在最短路差分约束的问题中就有类似的式子,现在我们可以直接通过上面的两个式子建图,然后跑最短路。
注:这里的初始化与普通的最短路还不同,初始化dist[1]=1,因为最开始就在一楼。
#include<algorithm>
#include<map>
#include<iostream>
#include<queue>
#include<vector>
#include<set>
#include<string>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int maxn=2e5+100;
const int maxm=2e5+100;
const int inf=0x3f3f3f3f;
const ll INF=0x3f3f3f3f3f3f3f3f;
int head[maxm],l;
struct qnode{
int v;
ll c;
qnode(int _v=0,ll _c=0):v(_v),c(_c){}
bool operator <(const qnode &r) const{
return c>r.c;
}
};
struct Edge{
int v,next;
ll cost;
Edge(int _v=0,int _cost=0):v(_v),cost(_cost){}
};
Edge E[maxm];
bool vis[maxm];
ll dist[maxm];
void debug(){
cout<<"_____five_____\n";
}
void init(){
memset(head,-1,sizeof(head));
l=0;
}
void Dijkstra(int n,int start)
{
memset(vis,false,sizeof(vis));
for(int i=0;i<n;++i) dist[i]=0x3f3f3f3f3f3f3f3f;
priority_queue<qnode>que;
dist[start]=1;
que.push(qnode(start,1));
qnode tmp;
while(!que.empty())
{
tmp=que.top();
que.pop();
int u=tmp.v;
if(vis[u])continue;
vis[u]=true;
for(int i=head[u];i!=-1;i=E[i].next){
int v=E[i].v%n;
ll cost=E[i].cost;
if(!vis[v]&&dist[v]>dist[u]+cost){
dist[v]=dist[u]+cost;
que.push(qnode(v,dist[v]));
}
}
}
}
void addedge(int u,int v,int w){
E[l].v=v;
E[l].cost=w;
E[l].next=head[u];
head[u]=l++;
}
int main()
{
ll h;
int x,y,z;
cin>>h>>x>>y>>z;
/*
f(i+y)=f(i)+y
其中f(i)的意思就是让我们求一个y,对于这个y满足f(i)=(y|y%x==i)
*/
init();
for(int i=0;i<x;++i){
addedge(i,(i+y)%x,y);
addedge(i,(i+z)%x,z);
}
Dijkstra(x,0);
ll num_puts=0;
for(int i=0;i<x;++i){
if(dist[i]<=h)
num_puts+=(h-dist[i])/x+1;
}
cout<<num_puts<<endl;
return 0;
}
下面再给出一个再复杂一点的题目:P2371
对于这道题,与上面的例子不同(上面只有三个给定的数x,y,z,下面这道题给定了n个).
Description
对于方程a1x1+a2y2+…+anxn=B,给定B的取值范围、n的大小以及{ai}.
让我们计算有多少B可以使得我们的等式存在非负整数解(可以理解成不存在负数个数量的物品).
SOLUTION
问题就是说对于a1,a2,.....an,能组成多少个范围在[Bmin,Bmax]内的数.
刚看到这道题的时候,就想到了无限背包:
但是这道题的数据范围是1≤BMin≤BMax≤10^12,使用无限背包会TLE.
那么就要寻求一种更为高效的方法了,这个时候同余最短路就应该进入我们的视线了!
按照前面只有x和y两个条件和有x,y,z三个条件的方法,我们扩展到有n个条件上面来。
相较于前面的问题,这个问题多了一个区间范围,对于端点值的计算需要特别注意:
如果说dist[i]<=r的时候,可以直接加上(r-dist[i])/a[1]+1,但是如果dist[i]<l的时候,就要把多计算的值给删去,减去(r-dist[i])/a[1]+1。
#include<algorithm>
#include<map>
#include<iostream>
#include<queue>
#include<vector>
#include<set>
#include<string>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int maxn=5e5+100;
const int maxm=6e6+100;
const int inf=0x3f3f3f3f;
const ll INF=(ll)1<<62;
int head[maxn],l;
struct qnode{
int v;
ll c;
qnode(int _v=0,ll _c=0):v(_v),c(_c){}
bool operator <(const qnode &r) const{
return c>r.c;
}
};
struct Edge{
int v,next;
ll cost;
Edge(ll _v=0,ll _cost=0):v(_v),cost(_cost){}
};
Edge E[maxm];
bool vis[maxn];
ll dist[maxn];
void init(){
memset(head,-1,sizeof(head));
l=0;
}
void Dijkstra(int n,int start)
{
memset(vis,false,sizeof(vis));
for(int i=0;i<n;++i) dist[i]=0x3f3f3f3f3f3f3f3f;
priority_queue<qnode>que;
dist[start]=0;
que.push(qnode(start,0));
qnode tmp;
while(!que.empty())
{
tmp=que.top();
que.pop();
int u=tmp.v;
if(vis[u])continue;
vis[u]=true;
for(int i=head[u];i!=-1;i=E[i].next){
int v=E[i].v;
ll cost=E[i].cost;
if(!vis[v]&&dist[v]>dist[u]+cost){
dist[v]=dist[u]+cost;
que.push(qnode(v,dist[v]));
}
}
}
}
void addedge(int u,int v,int w){
E[l].v=v;
E[l].cost=w;
E[l].next=head[u];
head[u]=l++;
}
int a[30];
int main()
{
int n;
ll l,r;
cin>>n>>l>>r;
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
sort(a+1,a+1+n);
init();
for(int i=0;i<a[1];++i){
for(int j=2;j<=n;++j){
if(a[j]==0) continue;
addedge(i,(i+a[j])%a[1],a[j]);
}
}
Dijkstra(a[1],0);
ll num_puts=0;
for(int i=0;i<a[1];++i){
if(dist[i]<=r) num_puts+=(r-dist[i])/a[1]+1;
if(dist[i]<l) num_puts-=(l-1-dist[i])/a[1]+1;
}
cout<<num_puts<<endl;
return 0;
}