算法学习笔记(13):同余最短路
同余最短路
是一种通过同余把状态分类, 再通过建图跑最短路解决问题的算法。可以高效率解决一些特定的问题。 非常的奇妙。
算法
鉴于学不懂, 所以直接搬 \(oi-wiki\) 的题吧。 呜呜呜。
P3403 跳楼机
有一栋高为 \(h\) 的楼, 初始在一楼, 每次可以向上移动 \(x\), \(y\), \(z\) 层, 也可以回到一楼, 询问可以到达楼层的数量。
转化问题, 可以看成有多少个 \(i\), 满足 \(i = ax + by + cz\), 考虑如果可以跳到1, 那么 \(1 + x\), \(1 + x + x\)...,
都可以跳到, 可以跳到 \(2\), 那么 \(2 + x\), \(2 + x + x\)...都可以跳到, 并且答案不重复, 所以考虑按 \(mod \ x\) 的剩余系来, 分段统计答案, 我们只需要计算 \(p \ mod \ x = i\)能跳到的最低楼层, 因为有些跳不到。
如果这里懂了, 那么建边也就懂了。
- \(i -> (i + y) \ mod \ x\) 边权为 \(y\)。
- \(i -> (i + z) \ mod \ z\) 边权为 \(z\)。
最后从 \(1\) 开始跑最短路。 再统计答案就可以了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5 + 10;
const ll INF = LONG_LONG_MAX;
int X, Y, Z, head[N], tot, vis[N];
ll H, d[N];
struct Edge{ int to, dis, nxt; }edge[N << 3];
void add(int x, int y, int z) { edge[++tot] = {y, z, head[x]}; head[x] = tot; }
#define fi first
#define se second
priority_queue< pair<ll, int> > q;
void dijk(int s) {
fill(d, d + X, INF);
d[s] = 1; q.push({-d[s], s});
while (!q.empty()) {
int u = q.top().se; q.pop();
if (vis[u]) continue;
vis[u] = 1;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (!vis[v] && d[u] + edge[i].dis < d[v]) {
d[v] = d[u] + edge[i].dis;
q.push({-d[v], v});
}
}
}
}
int main() {
scanf("%lld%d%d%d", &H, &X, &Y, &Z);
if (X == 1 || Y == 1 || Z == 1) {
printf("%lld\n", H);
return 0;
}
for (int i = 0; i < X; i++) {
add(i, (i + Y) % X, Y);
add(i, (i + Z) % X, Z);
}
dijk(1);
ll ans = 0;
for (int i = 0; i < X; i++) if (H >= d[i]) ans += (H - d[i]) / X + 1;
printf("%lld", ans);
return 0;
}
[ABC077D] Small Multiple
输出 \(K\) 的正整数倍的最小数位累加和。
考虑很难办, 最质朴的做法就是枚举左右的倍数, 显然不行。 但是我们把所有数按数位和分类, \(sum = 1\) 的有\(1, 10, 100, 1000...\), \(sum = 2\) 的有 \(2, 11, 101, 110, 1001...\), 这样就会很难发现一个性质, 一个数一定可以由 若干\(+1\), \(\times 10\)操作过来, 且 \(+1\)操作产生1的代价, \(\times 10\) 不产生代价, 这时候就可以跑同余最短路, 并且这还是01图, 直接01BFS就可以做到时间复杂度 \(O(E)\)。 答案就是 \(1\) 到 \(0\) 的最短路, 严谨的你会发现, 如果连续跑10次边权为\(1\)的边, 得出来的答案不是错的?这种情况显然不合法, 但是我们跑的是最短路, 跑10次边权为1的边, 肯定不优。 所以不影响答案。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int K, d[N], vis[N];
#define pf push_front
#define pb push_back
#define ls ((u * 10) % K)
#define rs ((u + 1) % K)
deque<int> q;
void bfs(int s) {
memset(d, 0x3f, sizeof d);
q.pb(s); d[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop_front();
if (u == 0) printf("%d", d[u]), exit(0);
vis[u] = 1;
if (!vis[ls] && d[ls] > d[u]) d[ls] = d[u], q.pf(ls);
if (!vis[rs] && d[rs] > d[u] + 1) d[rs] = d[u] + 1, q.pb(rs);
}
}
int main() {
scanf("%d", &K);
bfs(1);
return 0;
}
牛场围栏
考虑和跳楼机十分相似, 我们钦定一个最小的木棍为模数 \(Min\) (因为这样边数最少), 然后\(O(N\times M\times MIN)\)求建图跑同余最短路, 就可以找到每一个同余类的最小的可以围成的围栏, \(-Min\)就是最大的不能围成的围栏。
墨墨的等式
考虑是牛场围栏和跳楼机的整合版, 答案统计的时候魔改一下就行了。

浙公网安备 33010602011771号