同余最短路

基础理解:

  • 不是一种算法,而是一种思路,一种优化方式。用同余来构造某些状态,来优化时间空间复杂度(多用来优化dp)
  • 最短路常用spfa,不会被卡,因为同余的特殊构造,可保证spfa的时间复杂度(听大佬说的,我不会证)

  • 主要用于给定n个整数,求这n个整数能拼凑出多少的其他整数(n个整数可以重复取),以及给定n个整数,求这n个整数不能拼凑出的最小(最大)的整数,或者至少要拼几次才能拼出模k余p的数的问题。

难度:简单(易理解易实现)

例题引入:https://www.luogu.com.cn/problem/P3403

  1. 简要题意:给你4个数h,x,y,z。求满足ax+by+cz=jg中a,b,c都是非负整数且小于h的jg的个数。
  2. 考虑bfs,每出队列一个数就加x或加y或加z,再重新加入队列,只要不大于h且没有被标记。一看数据范围h<2^63-1,直接炸开。
  3. 考虑选定x,每一个符合条件的数%x在0到x之间。则每一个符合条件的数可以写成kx+r(r为%x的余数),对于每一个余数i,只用求出用x,y,z凑出最小的%x==i的数s,答案+=(h-s)/x+1,表示统计s,s+x,s+2*x,s+3*x.....
  4. 正确性显然,每个%x==i的都统计完了,且不同i之间一定不重复。
  5. 考虑实现,设dp[i]存凑出的最小的%x==i的数,显然有dp[(i+y)%x]=min(dp[(i+y)%x],dp[i]+y)和dp[(i+z)%x]=min(dp[(i+z)%x],dp[i]+z)。观察式子,发现与求最短路相同极为相似,再要从i向(i+y)%x和(i+z)%x连边跑最短即可。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
#define ll long long
ll h,dp[N],ans,nxt[2*N],go[2*N],jz[2*N],hd[N],x,y,z;
int tot;
bool vis[N];
queue<int> q;
void add(int x,int y,int z)
{
    nxt[++tot]=hd[x];go[tot]=y;jz[tot]=z;hd[x]=tot;
    return ;
}
void spfa()
{
    q.push(1%x);
    vis[1%x]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();vis[u]=0;
        for(int i=hd[u];i;i=nxt[i])
        { 
            int v=go[i];
            if(dp[v]>dp[u]+jz[i])
            {
                dp[v]=dp[u]+jz[i];
                if(!vis[v])vis[v]=1,q.push(v);
            }
        }
    }
}
int main()
{
    memset(dp,0x3f,sizeof(dp));
    scanf("%lld",&h);
    scanf("%lld%lld%lld",&x,&y,&z);
    if(x==1||y==1||z==1)
    {
        printf("%lld",h);
        return 0;
    }
    for(int i=0;i<x;i++)
    {
        add(i,(i+y)%x,y);
        add(i,(i+z)%x,z);
    }
    dp[1%x]=1;
    spfa();
    for(int i=0;i<x;i++)if(dp[i]<=h)ans+=(h-dp[i])/x+1;
    printf("%lld\n",ans);
    return 0;
}

举一反三:

https://www.luogu.com.cn/problem/P2371

发现上面的问题只是从3个数变成n个数,仿造做即可

#include <bits/stdc++.h>
using namespace std;
const int N=20,M=5e5+10;
const long long INF=1e12+10;
int n,tot,nxt[20*M],hd[M],go[20*M],jz[20*M],a[N],mi=0x3f3f3f3f;
long long ans1,ans2,dp[M],l,r;
bool vis[M];
void add(int x,int y,int z)
{
    nxt[++tot]=hd[x];go[tot]=y;jz[tot]=z;hd[x]=tot;
    return ;
}
queue<int> q;
void spfa()
{
    dp[0]=0;
    q.push(0);
    vis[0]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();vis[u]=0;
        for(int i=hd[u];i;i=nxt[i])
        {
            int v=go[i];
            if(dp[v]>dp[u]+jz[i])
            {
                dp[v]=dp[u]+jz[i];
                if(!vis[v])vis[v]=1,q.push(v);
            }
        }
    }
} 
int main()
{
    scanf("%d%lld%lld",&n,&l,&r);l--;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        if(a[i]<mi)mi=a[i];

    }
    for(int i=0;i<mi;i++)
    {
        dp[i]=INF;
        for(int j=1;j<=n;j++)
        {
            add(i,(i+a[j])%mi,a[j]);
        }
    }
    spfa();
    for(int i=0;i<mi;i++)
    {
        if(l>=dp[i])ans1+=(l-dp[i])/mi+1;
        if(r>=dp[i])ans2+=(r-dp[i])/mi+1;
    }
    printf("%lld\n",ans2-ans1);
    return 0;
}

https://www.luogu.com.cn/problem/P2662

这道题与上面有所不同,求最大的不能被凑成的数,考虑从dp[i]中快速求出,因为定义dp[i]为最小的%x==i的数,所以%x==i最大的不能被凑出的数是dp[i]-x,对每一个i取最大值就是结果。再判一下无解即可。

#include <bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
int n,m,a[5000010],mi=0x3f3f3f3f,cnt;
long long ans1,dp[5000010];
bool vis[5000010];
queue<int> q;
void spfa()
{
    dp[0]=0;
    q.push(0);
    vis[0]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();vis[u]=0;
        for(int j=1;j<=cnt;j++)
        {
            if(a[j]==mi)continue;
            int v=(u+a[j])%mi; 
            if(dp[v]>dp[u]+a[j])
            {
                dp[v]=dp[u]+a[j];
                if(!vis[v])vis[v]=1,q.push(v);
            }
        }
    }
} 
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[++cnt]);
        int x=a[cnt];
        for(int i=1;i<=m;i++)
        {
            if(x>=i)
            {
                a[++cnt]=x-i; 
                if(x-i<mi)mi=x-i;
            }
        }
    }
    if(mi<=1)
    {
        printf("-1\n");
        return 0;
    }
    for(int i=0;i<mi;i++)dp[i]=INF;
    spfa();
    for(int i=0;i<mi;i++)
    {
        if(dp[i]==INF)
        {
            printf("-1\n");
            return 0;
        }
        ans1=max(ans1,dp[i]-mi);
    }
    printf("%lld\n",ans1);
    return 0;
}

https://www.luogu.com.cn/problem/AT_arc084_b

同余思路的借鉴,设dp[i]为各数位上数字之和%k==i的最小值,答案为dp[0]。转移显然dp[(i*10+j)%k]=min(dp[i]+j,dp[(i*10+j)%k]);

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=1e6+10;
int n,tot,nxt[M],hd[N],go[M],jz[M];
long long dp[N];
bool vis[N];
void add(int x,int y,int z)
{
    nxt[++tot]=hd[x];go[tot]=y;jz[tot]=z;hd[x]=tot;
    return ;
}
queue<int> q;
void spfa()
{
    dp[n]=0;
    q.push(n);
    vis[n]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();vis[u]=0;
        for(int i=hd[u];i;i=nxt[i])
        {
            int v=go[i];
            if(dp[v]>dp[u]+jz[i])
            {
                dp[v]=dp[u]+jz[i];
                if(!vis[v])vis[v]=1,q.push(v);
            }
        }
    }
} 
int main()
{
    memset(dp,0x3f,sizeof(dp));
    scanf("%d",&n);
    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(n,i%n,i);
    spfa();
    printf("%lld\n",dp[0]);
    return 0;
}

 

实现上的优化:

  1. 选用所有值中最小的一个做模数会提高时间复杂度
  2. 空间开不下时,不存边,在spfa时枚举使用。
  3. 一定注意赋的极大值和变量的类型。

 

posted @ 2023-10-13 10:37  storms11  阅读(47)  评论(0编辑  收藏  举报