同余最短路

同余最短路

引入

先介绍一下大家熟知的差分约束问题,通过建图将线性规划问题转化为图论问题。

给定多个形如\(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记最小楼层是为了方便求出方案数。

写出状态转移方程式:

\[f((i+y)\%x)=f(i)+y\\f((i+z)\%x)=f(i)+z \]

\(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\)下等价,缩小数据范围。

注意模数的选取,不然会出问题。

posted @ 2024-10-04 17:54  lichenyu_ac  阅读(576)  评论(0)    收藏  举报