同余最短路
定義:
出現:
- 給定 \(n\) 個整數,求這 \(n\) 個整數能拼湊成多少的其他整數(可重)。
- 給定 \(n\) 個整數,求這 \(n\) 個整數能不能拼湊出最小/大的整數。
- 至少拼湊幾次才能湊出來模 \(K\) 余 \(p\) 的數。
方法:
同余最短路利用同余來構造出一些狀態,從而優化時間復雜度。
利用 差分約束,利用同余構造的狀態看成最短路中的點,則狀態轉移為 \(dp[i+y]=dp[i]+y\) ,感覺也挺像最短路的轉移方程。
例題:
題意:
求給你 \(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\),那么一定有:
余數一定小於 \(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\) 能弄出來的數字 和 加上其本身。有公式:
這個式子表示在 \([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;
}
題意:
給一個 \(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]\)(模數為零)。
剩下的都是最短路板子,直接寫即可。