同余最短路

同余最短路

与差分约束类似,都是最短路的一些神奇应用。

同余最短路可以解决类似这样的问题:

  • 给定 \(n\) 个数,求给定值域内,用这些数(可以重复取)能够拼出多少其他数。代表题目:P3403、P2371。
  • 给定 \(n\) 个数,求这 \(n\) 个整数不能拼出的最小/最大整数。代表题目:P2662。
  • 给定 \(n\) 个数,至少要拼几次才能拼出模 \(K\)\(p\) 的数。代表题目:Unknown

P3403 跳楼机

为了方便,我们把楼层的值域从 \([1,h]\) 调整为 \([0,h-1]\)

不妨假设 \(x<y<z\),令 \(\text{dis}_i\) 为只通过 \(y\)\(z\),需满足 \(p\bmod x=i\) 的最低楼层 \(p\),也就是只用 \(y\)\(z\) 能拼出的模 \(x\)\(i\) 同余的最小数。可以得到两个状态:

  • \(i\xrightarrow{y}(i+y)\bmod x\)
  • \(i\xrightarrow{z}(i+z)\bmod x\)

就按照这两个状态建边,跑最短路,可以求出我们刚才所说的 \(\text{dis}_i\),其中 \(i\in[0,n)\)。接下来考虑怎么用 \(\text{dis}_i\) 统计答案,因为 \(\text{dis}_i\) 是满足模 \(x\)\(i\) 同余的最低楼层,所以用值域减去 \(\text{dis}_i\),再除以 \(x\) 并向下取整,就是它能提供的贡献。还要 \(+1\),因为 \(d_i\) 所在楼层也算一次。

用公式表示出来如下:

\[\mathit{ans}=\sum_{i=0}^{x-1}\left(\left\lfloor\frac{h-\text{dis}_i}x\right\rfloor+1\right)[h\ge\text{dis}_i] \]

容易发现最后的时空复杂度只和 \(x\) 有关,这也是为什么 \(x\) 越小越好。

然后这题就做完了。注意细节,\(h<2^{63}\),而 long long 的最大范围是 \(2^{63}-1\),所以得开 unsigned long long。

#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;

using pii=pair<int,int>;
constexpr int MAXN=1e5+5;
int h,x,y,z;
vector<pii>g[MAXN];
int dis[MAXN];
bool vis[MAXN];
void dijkstra(int s){
	for(int i=1;i<x;++i) dis[i]=ULONG_LONG_MAX;
	priority_queue<pii,vector<pii>,greater<pii>>q;
	q.emplace(0,s);
	while(!q.empty()){
		int u=q.top().second;
		q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(auto vv:g[u]){
			int v=vv.first,w=vv.second;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				q.emplace(dis[v],v);
			}
		}
	}
}

signed main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>h>>x>>y>>z;
	--h;
	for(int i=0;i<x;++i){
		g[i].emplace_back((i+y)%x,y);
		g[i].emplace_back((i+z)%x,z);
	}
	dijkstra(0);
	int ans=0;
	for(int i=0;i<x;++i)	
		if(h>=dis[i])
			ans+=(h-dis[i])/x+1;
	cout<<ans<<'\n';
	return 0;
}

P2371 [国家集训队] 墨墨的等式

不过是上一道题的拓展。将值域从单一的 \([0,h-1]\) 变成了 \([l,r]\),同时由三个数变成了 \(n\) 个数。

统计是同理的,加上 \([0,r]\) 的答案再减去 \([0,l-1]\) 的答案即可。

signed main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>n>>l>>r>>x;
	--l;
	for(int i=2;i<=n;++i){
		cin>>y;
		for(int j=0;j<x;++j) g[j].emplace_back((j+y)%x,y);
	}
	dijkstra(0);
	int ans=0;
	for(int i=0;i<x;++i){
		if(r>=dis[i]) ans+=(r-dis[i])/x+1;
		if(l>=dis[i]) ans-=(l-dis[i])/x+1;
	}
	cout<<ans<<'\n';
	return 0;
}

AT_arc084_b [ABC077D] Small Multiple

注意到任何一个正整数都可以从 \(1\) 开始,按照某种顺序执行 \(\times10\)\(+1\) 操作得到。最后执行的 \(+1\) 操作个数就是这个数的数位和。往最短路方面考虑。

建边方式为:对于所有 \(0\le k<n\),从 \(k\) 向 \(10k\bmod n\) 连边权为 \(0\) 的边,从 \(k\) 向 \((k+1)\bmod n\) 连边权为 \(1\) 的边。与一般的最短路不同的是,初始令 \(\text{dis}_1=1\),最后求的是 \(\text{dis}_0\) 的值。用 01BFS 的技巧可以做到 \(O(n)\) 复杂度。

#include<bits/stdc++.h>
using namespace std;

constexpr int MAXN=1e5+5;
vector<pair<int,int>>g[MAXN];
int dis[MAXN];
bool vis[MAXN];
void dijkstra(int s){
	memset(dis,0x3f,sizeof(dis));
	dis[s]=1;
	priority_queue<pair<int,int>>q;
	q.emplace(1,s);
	while(!q.empty()){
		int u=q.top().second;
		q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(auto vv:g[u]){
			int v=vv.first,w=vv.second;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				q.emplace(-dis[v],v);
			}
		}
	}
}

int main(){
	int n;
	cin>>n;
	for(int k=0;k<n;++k){
		g[k].emplace_back((k*10)%n,0);
		g[k].emplace_back((k+1)%n,1);
	}
	dijkstra(1);
	cout<<dis[0]<<'\n';
	return 0;
}
posted @ 2025-01-21 16:43  Laoshan_PLUS  阅读(58)  评论(1)    收藏  举报