例题 \(1\):P3403 跳楼机:给定正整数 \(h,x,y,z\),求有多少 \(d\in[1,h]\) 满足 \(ax+by+cz=d-1\),其中 \(a,b,c\) 为非负整数。
这道题第一眼给我的印象是一道数论题,但仔细想了想发现做不了。注意到 \(x,y,z\) 的范围较小,尝试从这个条件入手。
为方便起见,不妨将 \(h\) 减 \(1\),那么初始楼层变为第 \(0\) 层,\(d\) 的范围改为 \([0,h-1]\)。
注意到如果设 \(\mu=by+cz\),那么 \(\lambda x+\mu\)(\(\lambda\in\mathbf{N}\))一定能够通过跳楼机到达。
因此我们考虑这样一个函数 \(f(i)\),其表示跳若干次 \(y\) 和 \(z\) 后到达满足层数模 \(x\) 等于 \(i\) 的最小楼层,更数学一点表述就是 \(f(i)\) 等于满足 \((by+cz)\bmod x=i\) 的最小的 \(by+cz\)。根据定义,我们可以得出 \(f(i)\) 的两条性质:\(f(i)+y\ge f((i+y)\bmod x),f(i)+z\ge f((i+z)\bmod x)\)。
这使我们联想到最短路中的 \(dist\) 函数。对于一张我们跑完一遍 Dijkstra 算法后的图,其每条边 \((u,v,w)\) 都应满足 \(dist(u)+w\ge dist(v)\)。这个式子的形式与上面的 \(f\) 函数是一致的。
因此我们可以考虑建图,按照对应关系,我们建一张包含 \(x\) 个点的有向带权图,点的编号为 \(0\sim x-1\),对于每个 \(i\in[0,x-1]\),我们建立有向边 \((i,(i+y)\bmod x,y)\) 和 \((i,(i+z)\bmod x,z)\)。
建完图后,跑一遍 Dijkstra 求得 \(dist\),这里的 \(dist\) 实际上就是上文的 \(f\) 函数。
那么对于每个点 \(i\),其对答案的贡献即为 \(\left\lfloor \dfrac{h-dist(i)}{x} \right\rfloor+1\)。所以答案即为 \(\displaystyle \sum^{x-1}_{i=0}\left\lfloor \dfrac{h-dist(i)}{x} \right\rfloor+1\)。
代码实现如下:
#include<bits/stdc++.h>
#define int long long
#define PII pair<int, int>using namespace std;const int N = 1e5 + 10, INF = LLONG_MAX;int h, x, y, z, ans = 0;
vector<PII> e[N];int dist[N];
bool st[N];void dijkstra(int s)
{priority_queue<PII> q;for(int i = 0; i < N; i ++){dist[i] = INF;}dist[s] = 0;q.push({0, s});while(q.size()){int u = q.top().second;q.pop();if(st[u]){continue;}st[u] = true;for(auto i : e[u]){int v = i.second, w = i.first;if(dist[v] > dist[u] + w){dist[v] = dist[u] + w;q.push({-dist[v], v});}}}
}signed main()
{cin >> h >> x >> y >> z;h --;for(int i = 0; i < x; i ++){e[i].push_back({y, (i + y) % x});e[i].push_back({z, (i + z) % x});}dijkstra(0);for(int i = 0; i < x; i ++){if(h >= dist[i]){ans += (h - dist[i]) / x + 1;}}cout << ans;return 0;
}
例题 \(2\):P2371 [国家集训队] 墨墨的等式。
本题其实只是把上一题的 \(3\) 个数改为了 \(n\) 个,所以建图方式是一致的。
在统计答案时,考虑前缀和的思想,单点贡献为从 \(r\) 中扣掉 \(l-1\) 的部分。
注意 \(a_i=0\) 的数据要去掉。
#include<bits/stdc++.h>
#define int long long
#define PII pair<int, int>using namespace std;const int N = 5e5 + 10, M = 20, INF = LLONG_MAX;int n, m, mi = INF, id, ans = 0;
int l, r;
int a[M];
vector<PII> e[N];int dist[N];
bool st[N];void dijkstra(int s)
{priority_queue<PII> q;for(int i = 0; i < N; i ++){dist[i] = INF;}dist[s] = 0;q.push({0, s});while(q.size()){int u = q.top().second;q.pop();if(st[u]){continue;}st[u] = true;for(auto i : e[u]){int v = i.second, w = i.first;if(dist[v] > dist[u] + w){dist[v] = dist[u] + w;q.push({-dist[v], v});}}}
}signed main()
{cin >> n >> l >> r;l --;for(int i = 1; i <= n; i ++){int x;scanf("%lld", &x);if(x){a[++ m] = x;if(mi > x){mi = x;id = m;}}}for(int i = 0; i < mi; i ++){for(int j = 1; j <= m; j ++){if(j == id){continue;}e[i].push_back({a[j], (i + a[j]) % mi});}}dijkstra(0);for(int i = 0; i < mi; i ++){if(r >= dist[i]){ans += (r - dist[i]) / mi + 1;}if(l >= dist[i]){ans -= (l - dist[i]) / mi + 1;}}cout << ans;return 0;
}
例题 \(3\):Small Multiple:给定正整数 \(K\),求 \(K\) 的所有倍数的最小数位和。
每一个正整数都可以由 \(1\) 出发,仅经过若干次两种变换得到:
- 把数 \(+1\);
- 把数 \(\times 10\)。
因为两种操作保证了可以任意决定数字的位数与每位上的数字是什么。
那么我们可以用每个数字代表一个点,点与点之间的连边表示两种操作。因为只有操作一会对数位和产生 \(1\) 的贡献,因此可以根据两种操作的类型给边权赋为 \(1/0\)。但这样做会有点小问题,如果对于每个数都建一个点空间不够,解决方案是由于同余,所以我们只需要开 \(0\sim K-1\) 共 \(K\) 个点即可。那么初始点为 \(1\),我们要到达的 \(K\) 的倍数对应的点为 \(0\)。
由于边权仅有 \(0\) 和 \(1\),因此我们只需要跑一个 01BFS 即可。
#include<bits/stdc++.h>
#define PII pair<int, int>using namespace std;const int N = 1e5 + 10, INF = 1e9;int K;
vector<PII> e[N];int dist[N];
bool st[N];void bfs(int s)
{deque<int> q;for(int i = 0; i < N; i ++){dist[i] = INF;}dist[s] = 1;q.push_back(s);while(q.size()){int u = q.front();q.pop_front();if(st[u]){continue;}st[u] = true;for(auto i : e[u]){int v = i.second, w = i.first;if(dist[v] > dist[u] + w){dist[v] = dist[u] + w;if(st[v] == false){if(w){q.push_back(v);}else{q.push_front(v);}}}}}
}int main()
{cin >> K;for(int i = 0; i < K; i ++){e[i].push_back({1, (i + 1) % K});e[i].push_back({0, (i * 10) % K});}bfs(1);cout << dist[0];return 0;
}