同余最短路


同余最短路

定義:

出現:

  • 給定 \(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]\)(模數為零)。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM