两题切入——同余最短路

同余最短路问题在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;
}

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值