同余最短路

\(\text{luogu-3403}\)

Srwudi 的家是一幢 \(h\) 层的摩天大楼。由于前来学习的蒟蒻越来越多,srwudi 改造了一个跳楼机,使得访客可以更方便的上楼。

经过改造,srwudi 的跳楼机可以采用以下四种方式移动:

  1. 向上移动 \(x\) 层;
  2. 向上移动 \(y\) 层;
  3. 向上移动 \(z\) 层;
  4. 回到第一层。

一个月黑风高的大中午,DJL 来到了 srwudi 的家,现在他在 srwudi 家的第一层,碰巧跳楼机也在第一层。DJL 想知道,他可以乘坐跳楼机前往的楼层数。

\(1 \le h \le 2^{63}-1\)\(1 \le x,y,z \le 10^5\)


引入一个问题:

给定四个正整数 \(x,y,z,H\),求有多少个整数 \(d \in [0,H]\) 满足 \(ax+by+cz=d\),其中 \(a,b,c\) 均为非负整数。

(接下来的 \(a,b,c\) 均为非负整数,称一个数合法当且仅当其可以表示成 \(ax+by+cz\) 的形式,但不一定小于等于 \(H\)。)

一个重要的性质,若一个整数 \(k\) 能被若干个 \(y,z\) 拼成(\(k=by+cz\)),那么 \(k+x\)\(k+2x\)\(k+3x\cdots\) 都是合法的,当然 \(k\) 本身也是合法的。

我们令函数 \(f(i)\)\(by+cz \bmod x =i\) 时最小的 \(ay+bz\)。也可以理解为使用若干个 \(x\)\(y\) 能够拼成与 \(i\) 在模 \(x\) 意义下最小的值。

该函数有一个显而易见的性质:

\[\begin{align} f(i)+y \ge f((i+y)\bmod x)\\ f(i)+z \ge f((i+z)\bmod x) \end{align} \]

我们发现这个性质与我们上面提到的最短路图的性质(\(dis[x]+edge(x,y)\ge dis[y]\))相似。

这样我们可以建边:

  • \(i\)\((i+y)\bmod x\) 建一条边权为 \(y\) 的有向边。
  • \(i\)\((i+z)\bmod x\) 建一条边权为 \(z\) 的有向边。

\(i\) 为大于等于 \(0\) 小于 \(x\) 的整数。

建完边之后,跑一遍最短路,得到的 \(dis[i]\) 就相当于 \(f(i)\)

那么有了 \(f(i)\),如何计算答案。

根据我们刚开始得出的一个性质,若一个整数 \(k=by+cz\),那么对于 \(k+x\)\(k+2x\)\(k+3x\cdots\) 小于等于 \(H\) 的数共有 \(\lfloor\frac{H-k}{x}\rfloor\) 个。

这个公式其实很简单,大于 \(k\) 小于等于 \(H\) 的数有 \(H-k\) 个,在这些数中每 \(x\) 个数就有 \(1\) 个,一共就是 \(\lfloor\frac{H-k}{x}\rfloor\) 个。

那么本题的答案就是:

\[\sum_{i=0}^{x-1} \left \lfloor \frac{H-f(i)}{x} \right \rfloor +1 \]

加一是因为 \(f(i)\) 本身也是一个合法数。

\(f(i)\) 尽可能小是为了防止漏解。

根据定义,\(f(0)\) 一定等于 \(0\),那么跑最短路时源点也就确定了是 \(0\)


其实这题就跟上面的题一样,但是楼层从 \(1\) 开始,我们可以统计 \(0 \sim h-1\) 的答案,一样的。

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
#define MAXN 100005
#define pii pair<long long, long long>
#define fi first
#define se second

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long H, a, b, c, dis[MAXN];
vector<pii > v[MAXN];
bool vis[MAXN];

void dijkstra(long long s) {
	memset(dis, 0x3f, sizeof dis);
	priority_queue<pii > q;
	q.push({0, 0}), dis[0] = 0;
	while(!q.empty()) {
		long long x = q.top().se; q.pop(); 
		if(vis[x]) continue; vis[x] = 1;
		for(auto it : v[x]) {
			long long y = it.fi, w = it.se;
			if(dis[y] > dis[x] + w) 
				dis[y] = dis[x] + w, q.push({-dis[y], y});
		} 
	}
	return;
}

int main() {
	H = read() - 1, a = read(), b = read(), c = read();
	if(a == 1 || b == 1 || c == 1) { cout << H + 1 << "\n"; return 0; }
	for(int i = 0; i < a; i ++) 
		v[i].push_back({(i + b) % a, b}), 
		v[i].push_back({(i + c) % a, c});
	dijkstra(0); long long ans = 0;
	for(int i = 0; i < a; i ++) if(H >= dis[i])
		ans += (H - dis[i]) / a + 1;
	cout << ans << "\n";
	return 0;
}

\(\text{luogu-2662}\)

给出一个有 \(n\) 个数的序列 \(a_i\),将 \(a_i - m \sim a_i\) 的所有数加入序列 \(A\)

定义,当一个数可由该序列中的可重复的元素相加得到时,该数可以被表示。

求最大不可被序列 \(A\) 表示的数。

\(1 < n < 100\)\(0 \le m < 3000\)


先暴力把所有能直接用的数字求出来,并求出其中最小的那个数字 \(p\)

如果 \(p=1\),那么任何长度的围栏都可以修建,输出 \(-1\)

考虑同余优化,如果一个数 \(y\) 能被拼出,那么 \(p+y,2p+y,\cdots,np+y\) 都能被拼出,而 \(p\) 是最小的能直接用的数字,这样能保证正确性和效率,但是不代表 \(y-p,y-2p,\cdots,y-np\) 能被拼出。

我们对于每个不小于 \(0\) 且不大于 \(p\) 的数,抽象成一个点,把它向每一个接一段能到达的数的点连一条长为该段木料的长度的边,然后跑 spfa 最短路,\(dis_i\) 即为 \(i\) 的倍数中最小可以拼出的数。

如果最后有点不可达,说明只要是这个数的倍数,都不能被拼出,这个最大值不存在,输出 \(-1\)

否则由于我们跑的是最短路,所以此时 \(dis_i-p\) 肯定不能被拼出,答案即为 \(\max\limits_{i=1}^{p-1} dis_i-p\)

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
#define MAXN 10005
#define pii pair<long long, long long>
#define fi first
#define se second

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, a[MAXN], p, dis[MAXN];
vector<pii > v[MAXN];
bool vis[MAXN];

void spfa(long long s) {
	memset(dis, 0x3f, sizeof dis);
	queue<long long> q; q.push(s);
	dis[s] = 0, vis[s] = 1;
	while(!q.empty()) {
		long long x = q.front(); q.pop();
		vis[x] = 0;
		for(auto it : v[x]) {
			long long y = it.fi, w = it.se;
			if(dis[y] > dis[x] + w) {
				dis[y] = dis[x] + w;
				if(!vis[y]) q.push(y), vis[y] = 1;
			}
		}
	}
	return;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= n; i ++) a[i] = read();
	sort(a + 1, a + n + 1), p = max(1ll, a[1] - m);
	if(p == 1) { cout << "-1\n"; return 0; }
	for(int i = 1; i <= n; i ++) 
		for(int j = max(a[i - 1] + 1, a[i] - m); j <= a[i]; j ++)
			if(j != p) for(int k = 0; k < p; k ++)
				v[k].push_back({(k + j) % p, j});
	spfa(0); long long ans = 0;
	for(int i = 1; i < p; i ++) {
		if(dis[i] > 1e18) { cout << "-1\n"; return 0; }
		ans = max(ans, dis[i] - p);
	}
	cout << ans << "\n";
	return 0;
}

\(\text{luogu-2371}\)

墨墨突然对等式很感兴趣,他正在研究 \(\sum\limits_{i=1}^n a_ix_i=b\) 存在非负整数解的条件,他要求你编写一个程序,给定 \(n, a_{1\dots n}, l, r\),求出有多少 \(b\in[l,r]\) 可以使等式存在非负整数解。

\(n \le 12\)\(0 \le a_i \le 5\times 10^5\)\(1 \le l \le r \le 10^{12}\)


很容易想到 完全背包,用 \(f_i\) 表示 \(B\) 的值能否为 \(i\),那么转移方程为:

\[\large {f_j = f_j \mid f_{j - a_i}} \]

还可以用 \(\rm bitset\) 优化,时间复杂度为 \(O(\frac{nr}{w})\)

\(l, r\) 很大,上述方法显然行不通。

我们可以分别求出 \(0 \sim r\) 中符合条件的 \(B\) 的数量 和 \(0 \sim l - 1\) 中符合条件的 \(B\) 的数量,前者减去后者即是答案。现在假设 \(p\)\(a_i\) 中的一个数,那么对于 \(a_1x_1 + a_2x_2 + \cdots + a_nx_n = i\),都满足 \(a_1x_1 + a_2x_2 + \cdots + a_nx_n = i + kp\ (k \in \rm N)\)

在这个式子中,显然 \(i\) 越小,符合条件的数就会越多。

我们可以用 \(dis_i\) 表示 \(B\)\(p\) 等于 \(i\) 时的最小值。接下来连有向边 \(i \to (i + a_j) \bmod p\),其中 \(0 \leq i < p\),边权为 \(a_j\),表示从 \(i\) 变为 \(i + a_j\) 所花费的代价是 \(a_j\)

\(0\)\(i\) 的最短路即是 \(B\)\(p\) 等于 \(i\) 时的最小值。假定现在要求 \(0 \sim x\) 中符合条件的 \(B\) 的数量,若这个最小值不大于 \(x\),则所有的 \(i + k \times mn\ (i + kp \leq x,k \in \rm N)\) 都符合条件,一共有 \(\left \lfloor \frac{x - dis_i}{p} \right \rfloor + 1\) 个。

所以枚举 \(i\),累加就能得到答案。同时 \(p\) 取所有 \(a_i\) 的最小值最优,因为这样边数最少。时间复杂度为 \(O(kn\max\limits_{i = 1}^n a_i )\) 。由于特殊的连边,\(\rm SPFA\) 不会被卡,可以放心使用。

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
#define MAXN 500005
#define pii pair<long long, long long>
#define fi first
#define se second
#define INF 0x3f3f3f3f

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, l, r, a[MAXN], m, p = INF, dis[MAXN];
vector<pii > v[MAXN];
bool vis[MAXN];

void spfa(long long s) {
	memset(dis, 0x3f, sizeof dis);
	queue<long long> q; q.push(s);
	dis[s] = 0, vis[s] = 1;
	while(!q.empty()) {
		long long x = q.front(); q.pop();
		vis[x] = 0;
		for(auto it : v[x]) {
			long long y = it.fi, w = it.se;
			if(dis[y] > dis[x] + w) {
				dis[y] = dis[x] + w;
				if(!vis[y]) q.push(y), vis[y] = 1;
			}
		}
	}
	return;
}

long long query(long long x) {
	long long res = 0;
	for(int i = 0; i < p; i ++) if(dis[i] <= x)
		res += (x - dis[i]) / p + 1;
	return res;
}

int main() {
	n = read(), l = read(), r = read();
	for(int i = 1; i <= n; i ++) {
		long long x = read();
		if(x) a[++ m] = x, p = min(p, x);
	}
	n = m;
	for(int i = 0; i < p; i ++) for(int j = 1; j <= n; j ++)
		if(a[j] != p) v[i].push_back({(i + a[j]) % p, a[j]});
	spfa(0);
	cout << query(r) - query(l - 1) << "\n";
	return 0;
}

\(\text{hdu-6071}\)

在 HDU,你必须在校园内跑 \(24\) 圈,否则你将在体育课上不及格。

根据规定,你必须保持速度,跑步的距离不得少于 \(K\) 米。

校园内有 \(4\) 个检查点,索引为 \(p_1,p_2,p_3\)\(p_4\)。每次经过一个检查点时,你都需要刷卡,然后这个检查点与上一个经过的检查点之间的距离将被加到你的总距离中。

系统将这 \(4\) 个检查点视为一个圆。当你在检查点 \(p_i\) 时,你可以直接跑到 \(p_{i-1}\)\(p_{i+1}\)\(p_1\) 也在 \(p_4\) 附近)。你可以在两个相邻检查点之间跑更长的距离,但系统只会计算节省的距离。

检查点 \(p_2\) 是离宿舍最近的,Little Q 总是从这个检查点开始和结束跑步。请编写一个程序帮助 Little Q 找到总距离不少于 \(K\) 的最短路径。

\(1 \le k \le 10^{18}\)\(1 \le d \le 3 \times 10^4\)


\(w=min(dis_{1,2},dis_{2,3})\),取 \(\min\) 是为了降低时空复杂度。

假设存在一条合法路径,该路径长度为 \(t\),则 \(t+p \times 2w,p \in \mathbb{N}^*\) 也是一条合法路径。

而我们要找的就是最短路径,这些路径可以被分成 \(2w\) 组。

求出每一组同余类 \(i\) 的合法路径长度,最后 \(ans\)\(\min\) 即可。

跑最短路的过程中,\(dis_{i,j}\) 表示 \(2\)\(i\) 的路径中 \(\%w\)\(j\) 的最小值。

更新答案时,若 \(dis_{2,i} \ge k\),则 \(ans\) 直接取 \(\min\);反之则 \(ans=min(ans, dis_{2,i}+p \times 2w)\)

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
#define MAXN 60005
#define pii pair<long long, long long>
#define fi first
#define se second

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long T, k, d12, d23, d34, d41, w, dis[5][MAXN];
vector<pii > v[5];

void dijkstra(long long s) {
	memset(dis, 0x3f, sizeof dis);
	priority_queue<pii > q;
	q.push({0, s}), dis[s][0] = 0;
	while(!q.empty()) {
		long long x = q.top().se, d = -q.top().fi; 
		q.pop(); if(dis[x][d % w] < d) continue;
		for(auto it : v[x]) {
			long long y = it.fi, t = it.se + d;
			if(dis[y][t % w] > t) 
				dis[y][t % w] = t, q.push({-t, y});
		} 
	}
	return;
}

int main() {
	T = read();
	while(T --) {
		k = read(), d12 = read(), d23 = read();
		d34 = read(), d41 = read();
		for(int i = 1; i <= 4; i ++) v[i].clear();
		v[1].push_back({2, d12}), v[2].push_back({1, d12});
		v[2].push_back({3, d23}), v[3].push_back({2, d23});
		v[3].push_back({4, d34}), v[4].push_back({3, d34});
		v[4].push_back({1, d41}), v[1].push_back({4, d41});
		w = 2 * min(d12, d23), dijkstra(2);
		long long ans = 1e18;
		for(int i = 0; i < w; i ++) 
			if(dis[2][i] >= k) ans = min(ans, dis[2][i]);
			else ans = min(ans, dis[2][i] + (k - dis[2][i] + w - 1) / w * w);
		cout << ans << "\n";
	}
	return 0;
}
posted @ 2026-01-11 20:52  So_noSlack  阅读(10)  评论(0)    收藏  举报