【學習筆記】同余最短路


【學習筆記】同余最短路

例題一:洛谷P3404 跳樓機

題目鏈接

題目大意

有一幢 \(h\) 層的摩天大樓,你在第一層,你可以采用如下四種方式移動:

  1. 向上移動 \(x\) 層。
  2. 向上移動 \(y\) 層。
  3. 向上移動 \(z\) 層。
  4. 回到第一層。

問有多少樓層可以通過若干次移動到達。

數據范圍:\(1\leq h\leq 2^{63} - 1\)\(1\leq x,y,z\leq 10^5\)


注意到一個重要性質:如果能到達樓層 \(i\),那么樓層 \(i + x, i + 2x, i + 3x,\dots\) 也都是可以到達的。形式化地說,所有 \(j \geq i\)\(j\equiv i\pmod{x}\) 的樓層 \(j\) 都是可以到達的。

所以我們考慮對所有 \(k\in[0, x)\),求出可以到達的最小樓層 \(i\),滿足 \(i\equiv k\pmod{x}\)。記這樣的 \(i\)\(f(k)\),那么答案就是:

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

如何求出所有 \(f(k)\)?用類似 DP 的方法,考慮 \(k\) 的轉移。有如下兩種:

  1. \(f(k) + y \to f((k + y)\bmod x)\)
  2. \(f(k) + z\to f((k + z)\bmod x)\)

其中 \(a\to b\) 表示用 \(a\) 的值來更新 \(b\)。也就是 \(b := \min\{a, b\}\)

這個轉移不同於一般的 DP,它可能帶有環。

我們可以用最短路算法來實現這個 DP。具體來說,從所有 \(k\),分別向 \((k + y)\bmod x\)\((k + z)\bmod x\) 連邊,邊權分別為 \(y\)\(z\)。每一次轉移,就和最短路中“松弛”的過程是一樣的。起點是 \(f(1\bmod x) = 1\)

最短路可以用 SPFA 算法實現,在本題的建圖方式下,可以證明它不會被卡。時間復雜度 \(\mathcal{O}(x)\)


參考代碼:

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

#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;

template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }

const int MAXN = 1e5, MAXM = MAXN * 2;
const ll LL_INF = 3e18;

ll h;
int x, y, z;

struct EDGE {
	int nxt, to, w;
} edge[MAXM + 5];
int head[MAXN + 5], tot;
void add_edge(int u, int v, int w) {
	edge[++tot].nxt = head[u];
	edge[tot].to = v;
	edge[tot].w = w;
	head[u] = tot;
}

ll dis[MAXN + 5];
bool inq[MAXN + 5];

int main() {
	cin >> h;
	cin >> x >> y >> z;
	
	for (int i = 0; i < x; ++i) {
		add_edge(i, (i + y) % x, y);
		add_edge(i, (i + z) % x, z);
	}
	
	for (int i = 0; i < x; ++i) {
		dis[i] = LL_INF;
	}
	dis[1 % x] = 1;
	queue<int> q;
	q.push(1);
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		
		inq[u] = false;
		
		for (int i = head[u]; i; i = edge[i].nxt) {
			int v = edge[i].to;
			int w = edge[i].w;
			
			if (dis[v] > dis[u] + w) {
				dis[v] = dis[u] + w;
				if (!inq[v]) {
					inq[v] = true;
					q.push(v);
				}
			}
		}
	}
	
	ll ans = 0;
	for (int i = 0; i < x; ++i) {
		if (dis[i] <= h) {
			ans += (h - dis[i]) / x + 1;
		}
	}
	cout << ans << endl;
	return 0;
}

例題二:洛谷P2371 [國家集訓隊]墨墨的等式

題目鏈接

題目大意

給定 \(n, a_{1\dots n}, l, r\),請求出有多少整數 \(b\in[l,r]\) 可以使關於 \(x_{1\dots n}\) 的方程 \(\sum_{i = 1}^{n}a_i x_i = b\) 存在非負整數解。

數據范圍:\(1\leq n\leq 12\)\(0\leq a_i\leq 5\times10^5\)\(1\leq l\leq r\leq 10^{12}\)


首先特判所有 \(a_i\) 都等於 \(0\) 情況。

差分。求出 \(\leq r\)\(b\) 的數量,和 \(\leq l - 1\)\(b\) 的數量,相減得到答案。問題轉化為對 \(r\),求有多少 \(b\in[1, r]\),使方程 \(\sum_{i = 1}^{n}a_i x_i = b\) 有解。

與上一題類似,任選一個 \(a_i\neq 0\),記為 \(p\)。考慮在 \(\bmod p\) 意義下跑同余最短路。

具體來說,對於所有 \(0\leq i < p\), \(1\leq j\leq n\),從點 \(i\) 向點 \((i + a_j)\bmod p\) 連一條邊權為 \(a_j\) 的邊。含義是,如果存在一個使方程有解的 \(b\),滿足 \(b\equiv i\pmod{p}\),那么 \(b' = b + a_j\) 也能使方程有解,且 \(b'\equiv i + a_j\pmod{p}\)

起點是 \(0\)\(f(0) = 0\)。最終答案的計算方式與上題類似。

同樣可以跑 SPFA。時間復雜度 \(\mathcal{O}(n\cdot p) = \mathcal{O}(n\cdot \min\{a_i\})\)


參考代碼:

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

#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;

template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }

const int MAXN = 5e5, MAXM = 5e5 * 12;
const ll LL_INF = 1e18;

int n, a[15], p;
ll L, R;

struct EDGE {
	int nxt, to, w;
} edge[MAXM + 5];
int head[MAXN + 5], tot;
void add_edge(int u, int v, int w) {
	edge[++tot].nxt = head[u];
	edge[tot].to = v;
	edge[tot].w = w;
	head[u] = tot;
}

ll dis[MAXN + 5];
bool inq[MAXN + 5];

int main() {
	cin >> n >> L >> R;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
		if (a[i] == 0) {
			--i, --n;
		}
	}
	if (!n) {
		cout << 0 << endl;
		return 0;
	}
	sort(a + 1, a + n + 1);
	p = a[1];
	for (int i = 2; i <= n; ++i) {
		if (a[i] == a[i - 1])
			continue;
		for (int j = 0; j < p; ++j) {
			add_edge(j, (j + a[i]) % p, a[i]);
		}
	}
	
	for (int i = 0; i < p; ++i) {
		dis[i] = LL_INF;
	}
	dis[0] = 0;
	queue<int> q;
	q.push(0);
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		
		inq[u] = false;
		
		for (int i = head[u]; i; i = edge[i].nxt) {
			int v = edge[i].to;
			int w = edge[i].w;
			
			if (dis[v] > dis[u] + w) {
				dis[v] = dis[u] + w;
				if (!inq[v]) {
					inq[v] = true;
					q.push(v);
				}
			}
		}
	}
	
	ll ans = 0;
	--L;
	for (int i = 0; i < p; ++i) {
		if (dis[i] <= R) {
			ans += ((R - dis[i]) / p + 1);
		}
		if (dis[i] <= L) {
			ans -= ((L - dis[i]) / p + 1);
		}
	}
	cout << ans << endl;
	return 0;
}

小總結

同余最短路問題的關鍵,是找到一個數 \(x\),滿足:如果 \(i\) 合法,那么所有 \(j \geq i\)\(j\equiv i\pmod{x}\)\(j\) 也合法。

這樣,計數問題(求有多少合法的數),就轉化為了最小值問題:對每個 \(k\in[0, x)\),求出 \(i\equiv k\pmod{x}\) 的、最小的、合法的 \(i\)

我們建圖就直接在 \([0, x)\)\(x\) 個點上建就好了,轉移也是描述的這 \(x\) 個點之間的關系。

例題三:【正睿聯賽特訓】巡回

題目大意

給定一張 \(n\) 個點,\(m\) 條邊的無向圖。第 \(i\) 條邊連接兩個點 \(u_i, v_i\),且有一個邊權 \(w_i\),無論從哪個方向經過這條邊用時都是 \(w_i\)。你於 \(0\) 時刻從起點 \(1\) 出發,中途不在任何節點停留,可以經過重復的點和邊(甚至起點和終點),你想要恰好在 \(T\) 時刻到達終點 \(n\),問能否實現。

數據范圍:\(1\leq n,m\leq 50\)\(1\leq T\leq 10^{18}\)\(1\leq w_i\leq 10^4\)


考慮一條以 \(n\) 為端點的邊 \((u, n, w)\)。注意到,如果能夠在 \(i\) 時刻到達 \(n\),那么通過在這條邊上繞一下,也就可以在 \(i + 2w\) 時刻到達點 \(n\)。任取一條這樣的邊,記 \(p = 2w\),考慮在 \(\bmod p\) 意義下跑同余最短路。

因為題目里本來就有一張圖,所以我們考慮一個二維的 DP 狀態:設 \(f(u, k)\) (\(1\leq u\leq n\), \(0\leq k < p\)) 表示最小的時刻 \(i\),滿足 \(i\equiv k\pmod{p}\),且可以恰好在時刻 \(i\) 到達點 \(u\)

轉移是顯然的。因為轉移過程中可能存在環,所以我們同樣用最短路算法來實現這個 DP。

答案就是 \(f(n, T\bmod p)\) 是否 \(\leq T\)

時間復雜度 \(\mathcal{O}((n + m)\cdot w)\)


參考代碼:

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

#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;

template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }

const int MAXN = 50, MAXM = 50, MAXW = 1e4;
const int INF = 1e9;
const ll LL_INF = 3e18;

int n, m, p;
ll T;

ll dis[MAXN + 5][MAXW * 2];
bool inq[MAXN + 5][MAXW * 2];

struct EDGE {
	int nxt, to, w;
} edge[MAXM * 2 + 5];
int head[MAXN + 5], tot;
void add_edge(int u, int v, int w) {
	edge[++tot].nxt = head[u];
	edge[tot].to = v;
	edge[tot].w = w;
	head[u] = tot;
}

void solve_case() {
	cin >> n >> m >> T;
	tot = 0;
	for (int i = 1; i <= n; ++i) {
		head[i] = 0;
	}
	
	p = INF;
	for (int i = 1; i <= m; ++i) {
		int u, v, w;
		cin >> u >> v >> w;
		++u, ++v;
		add_edge(u, v, w);
		add_edge(v, u, w);
		
		if (u == n || v == n) {
			ckmin(p, w * 2);
		}
	}
	
	if (p == INF) {
		cout << "Impossible" << endl;
		return;
	}
	
	queue<pii> q;
	for (int i = 1; i <= n; ++i) {
		for (int j = 0; j < p; ++j) {
			dis[i][j] = LL_INF;
			inq[i][j] = false;
		}
	}
	dis[1][0] = 0;
	q.push(mk(1, 0));
	
	while (!q.empty()) {
		int u = q.front().fi;
		int t = q.front().se;
		q.pop();
		inq[u][t] = false;
		for (int i = head[u]; i; i = edge[i].nxt) {
			int v = edge[i].to;
			int w = edge[i].w;
			int tt = (w + t) % p;
			if (dis[v][tt] > dis[u][t] + w) {
				dis[v][tt] = dis[u][t] + w;
				if (!inq[v][tt]) {
					inq[v][tt] = true;
					q.push(mk(v, tt));
				}
			}
		}
	}
	
	if (dis[n][T % p] <= T) {
		cout << "Possible" << endl;
	} else {
		cout << "Impossible" << endl;
	}
	
}
int main() {
	int T; cin >> T; while (T--) {
		solve_case();
	}
	return 0;
}


免責聲明!

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



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