同余最短路

同余最短路

定义:

出现:

  • 给定 \(n\) 个整数,求这 \(n\) 个整数能拼凑成多少的其他整数(可重)。
  • 给定 \(n\) 个整数,求这 \(n\) 个整数能不能拼凑出最小/大的整数。
  • 至少拼凑几次才能凑出来模 \(K\)\(p\) 的数。

方法:

同余最短路利用同余来构造出一些状态,从而优化时间复杂度。

利用 差分约束,利用同余构造的状态看成最短路中的点,则状态转移为 \(dp[i+y]=dp[i]+y\) ,感觉也挺像最短路的转移方程。

例题:

P3403 跳楼机

题意:

求给你 \(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\),那么一定有:

\[(k+b)\mod a=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\) 能弄出来的数字加上其本身。有公式:

\[ans=\sum_{k=0}^{a-1} (\frac{h-dis_k}{a}+1) \]

这个式子表示在 \([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;
}

[ARC084B] Small Multiple

题意:

给一个 \(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]\)(模数为零)。

剩下的都是最短路板子,直接写即可。

posted @ 2021-11-03 09:43  Evitagen  阅读(2486)  评论(1编辑  收藏  举报