同余最短路
同余最短路
定义:
出现:
- 给定 \(n\) 个整数,求这 \(n\) 个整数能拼凑成多少的其他整数(可重)。
- 给定 \(n\) 个整数,求这 \(n\) 个整数能不能拼凑出最小/大的整数。
- 至少拼凑几次才能凑出来模 \(K\) 余 \(p\) 的数。
方法:
同余最短路利用同余来构造出一些状态,从而优化时间复杂度。
利用 差分约束,利用同余构造的状态看成最短路中的点,则状态转移为 \(dp[i+y]=dp[i]+y\) ,感觉也挺像最短路的转移方程。
例题:
题意:
求给你 \(a,b,c\),求 \(ax+by+cz\) 在 \([1,h]\) 区间内能表示多少个数(整数)。
分析:
为什么 \(\text {OI-WIKI}\) 和 \(\text{LUOGU}\) 的题解都对整个过程描写的那么简单!!!我理解了很长时间才弄懂。
假设 \(a<b<c\) ,则对于每一个 \(i \in [1,n]\) ,都可以表示成 \(i=ax+k\),其中,\(a\) 是除数,\(x\) 是商,\(k\) 是余数。
当余数不等于零,对于这个数,我们只通过 \(a\) 累加是不能表示出来的。
此时,我们就可以在之前基础上通过 \(b,c\) 进行累加,直到余数等于 \(k\) 。
考虑每一个余数 \(k\in [0,a-1]\) , 我们对每一个余数建立一个点。接下来就是同余最短路最关键的操作:
如果从余数 \(k\) , 通过累加一次 \(b\) 能移动到余数 \(j\),那么一定有:
余数一定小于 \(a\),而且 \(k\) 也是从之前累加 \(b,c\) 转移而来,不用考虑累加多次 \(b,c\) 的情况。
所以,我们可以说:从余数 \(i\rightarrow j\) ,需要走过一次 \(b\) 长度的路径
拓展一下情况:枚举所有的余数 \(k\in [0,a-1]\) :
- 从 \(k\rightarrow (k+b)\mod a\) 建立一条边权为 \(b\) 的边
- 从 \(k\rightarrow (k+c)\mod a\) 建立一条边权为 \(c\) 的边。
则从 \(0\) 到 \(k\) 的 最短路 则是 得到余数 \(k\) 需要走过的最短路径。
得到最短路后,对于每一个余数的路径长度,再除以 \(a\) 后 \(+1\) , 就是再通过 \(a\) 能弄出来的数字 和 加上其本身。有公式:
这个式子表示在 \([dis_k,h]\) 区间内:余数为 \(k\),除数为 \(a\) 的商的个数,就是是余数为 \(k\) 情况下能表示的数字的个数。
然后就是代码:
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define mk make_pair
using namespace std;
const int N=5e5+5;
int vis[N],dis[N];
int nxt[N],ver[N],head[N],edge[N],tot;
priority_queue<pii> q;
void add(int x,int y,int z){
ver[++tot]=y; edge[tot]=z; nxt[tot]=head[x]; head[x]=tot;
}
int ans;
int h,x,y,z;
void dijkstra(){
memset(dis,0x3f,sizeof(dis));
dis[0]=1;//本身也算一次
q.push(mk(-1,0));
while(!q.empty()){
int x=q.top().second; q.pop();
if(vis[x]) continue;
vis[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i],z=edge[i];
if(dis[y]>dis[x]+z){
dis[y]=dis[x]+z; q.push(mk(-dis[y],y));
}
}
}
}
signed main(){
cin>>h>>x>>y>>z;
if(x==1||y==1||z==1) {cout<<h<<endl; return 0;}
for(int i=0;i<x;i++){//使剩余系最小
add(i,(y+i)%x,y); add(i,(z+i)%x,z);
}
dijkstra();
for(int i=0;i<x;i++){
if(h>=dis[i]) ans+=(h-dis[i])/x+1;
}
cout<<ans<<endl;
system("pause");
return 0;
}
题意:
给一个 \(n\) ,求 \(n\) 的倍数的数位之和最小的值。
分析:
有一个性质:
\(i\times 10\) 数位和没有变,\(i+1\) 数位和 \(+1\) 。
因此我们在模 \(n\) 意义下进行建边:设 \(sum[i]\) 表示 \(i\) 数位和
设 \(dis[i]\) 表示 \(\text{min} \ f(x)(x\equiv i(mod\ n))\)
在 \(x(x\equiv i(mod\ n))\) 后加上 \(y\),那么余数为 \((x\times 10+y) \ mod n\)
因此,从 \(i \rightarrow (10i+y)\) 的路径长度为 \(y\)。
就这样依次建边即可。
for(int i=0;i<n;i++) for(int j=0;j<=9;j++) add(i,(i*10+j)%n,j);
for(int i=1;i<=9;i++) add(k,i%k,i);//虚边,第一位不为0,因此跑到最大值进入最一开始初始阶段,贡献即为初始阶段的值
答案即为 \(dis[0]\)(模数为零)。
剩下的都是最短路板子,直接写即可。