图上算法学习笔记(一):图论基础知识、最短路相关、生成树相关
基础知识
最短路相关
最短路算法
次短路 / \(k\) 短路
最短路图 / 树
同余最短路
算是一种图论建模的方法,利用最短路求解一些诸如“给定
\(n\) 个整数,求这 \(n\) 个整数能拼凑出多少的其他整数”的问题。一般这些问题都可以用背包做,但是空间复杂度可能会特别高,因此需要利用同余来优化。
经典例题
P3403 跳楼机
首先我们可以将 \(h\) 减去 \(1\),转化成从第 \(0\) 层开始往上移动。此时我们需要求出在 \([1, h]\) 中,所有可以被表示成 \(ax + by + cz\) 的数字的数量(其中 \(a, b, c \geq 0\))。
朴素的方法可以考虑 DP,设 \(dp_i\) 表示能否到达第 \(i\) 层,转移就是 \(dp_i = dp_{i - x} \operatorname{or} dp_{i - y} \operatorname{or} dp_{i - z}\),但是这样做肯定爆炸。
我们注意到如果 \(x\) 能被表示出,那么 \(x + k_1 a + k_2 b + k_3 c(k_1, k_2, k_3 > 0)\) 都可以被表示出。我们选出 \(a, b, c\) 中最小的数,假设是 \(a\),那么我们可以求出每一个模 \(a\) 的同余类 \(K_i\) 中最小能被表示出的数 \(f_i\),那么 \(x\) 能被表示出,当且仅当 \(x \geq f_{x \bmod a_i}\)。
具体地,我们一开始所有值全为 \(0\)。加入 \(b\) 的时候,我们对于每个同余类的 \(K_i\),将 \(f_i\) 转移到 \(f_{(i + b) \bmod a}\),\(c\) 同理。
我们发现这类似一个最短路的形式。我们将一个同余类设成一个点。对于一个同余类 \(K_i\),从 \(i\) 向 \(i + b \bmod a, i + c \bmod a\) 连长度分别为 \(b, c\) 的边,求从源点 \(0\) 开始到每个点的最短路。
求出 \(f_i\) 后容易求答案 \(= \displaystyle\sum_{i = 0}^{a - 1} \max \left(0, \left\lfloor \frac{h - f_i}{a}\right\rfloor \right)\)。
:::info[完整代码]
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 9;
struct Edge{
int v, w, nex;
} e[N << 1];
int head[N], ecnt;
void addEdge(int u, int v, int w){
e[++ecnt] = Edge{v, w, head[u]};
head[u] = ecnt;
}
struct Node{
int id, dis;
bool operator < (const Node& b) const{
return dis > b.dis;
}
};
int dis[N];
bool vis[N];
priority_queue <Node> q;
void dijkstra(){
memset(dis, 0x3f, sizeof(dis));
dis[0] = 0;
q.push(Node{0, 0});
while(!q.empty()){
Node u = q.top();
q.pop();
if(vis[u.id])
continue;
vis[u.id] = true;
for(int i = head[u.id]; i; i = e[i].nex){
int v = e[i].v, w = e[i].w;
if(dis[u.id] + w < dis[v]){
dis[v] = dis[u.id] + w;
q.push(Node{v, dis[v]});
}
}
}
}
int h, x, y, z, ans;
signed main(){
scanf("%lld%lld%lld%lld", &h, &x, &y, &z);
h--;
for(int i = 0; i < x; i++){
addEdge(i, (i + y) % x, y);
addEdge(i, (i + z) % x, z);
}
dijkstra();
for(int i = 0; i < x; i++)
if(h >= dis[i])
ans += max(0ll, (h - dis[i]) / x + 1);
printf("%lld", ans);
return 0;
}
:::
P2371 [国家集训队] 墨墨的等式
上一道题目的加强版,从 \(3\) 个数变成了 \(n\) 个数。
:::info[完整代码]
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e5 + 9;
struct Edge{
int v, w, nex;
} e[N * 12];
int head[N], ecnt;
void addEdge(int u, int v, int w){
e[++ecnt] = Edge{v, w, head[u]};
head[u] = ecnt;
}
struct Node{
int id, dis;
bool operator < (const Node& b) const{
return dis > b.dis;
}
};
int dis[N];
bool vis[N];
priority_queue <Node> q;
void dijkstra(){
memset(dis, 0x3f, sizeof(dis));
dis[0] = 0;
q.push(Node{0, 0});
while(!q.empty()){
Node u = q.top();
q.pop();
if(vis[u.id])
continue;
vis[u.id] = true;
for(int i = head[u.id]; i; i = e[i].nex){
int v = e[i].v, w = e[i].w;
if(dis[u.id] + w < dis[v]){
dis[v] = dis[u.id] + w;
q.push(Node{v, dis[v]});
}
}
}
}
int a[N], n, l, r, ans1, ans2;
signed main(){
scanf("%lld%lld%lld", &n, &l, &r);
for(int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
for(int i = 0; i < a[1]; i++)
for(int j = 2; j <= n; j++)
addEdge(i, (i + a[j]) % a[1], a[j]);
dijkstra();
for(int i = 0; i < a[1]; i++){
if(r >= dis[i])
ans1 += max(0ll, (r - dis[i]) / a[1] + 1);
if(l - 1 >= dis[i])
ans2 += max(0ll, (l - 1 - dis[i]) / a[1] + 1);
}
printf("%lld", ans1 - ans2);
return 0;
}
:::
P2662 [WC2002] 牛场围栏
这道题变成了不能凑出的最大的数,但做法是类似的。
首先判断无解的情况,那一定是存在一类木料,它原本的长度,或者削短后的长度为 \(1\),此时就可以凑出任何一种长度的围栏。
否则我们还是按照之前的思路得到 \(f_i\),那么 \(f_i - a_1\) 就是同余系 \(K_i\) 中最大的不能被表示的数字,再对所有同余系中最大的不能被表示的数字取 \(\max\),就是答案。注意如果存在 \(f_i = \infty\),就表示不存在不能被表示出的最大值,此时也输出 \(-1\)。
:::info[完整代码]
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 9, INF = 0x3f3f3f3f;
struct Node{
int id, dis;
bool operator < (const Node& b) const{
return dis > b.dis;
}
};
int dis[N], a[N], n, m;
bool vis[N];
priority_queue <Node> q;
void dijkstra(){
memset(dis, 0x3f, sizeof(dis));
dis[0] = 0;
q.push(Node{0, 0});
while(!q.empty()){
Node u = q.top();
q.pop();
if(vis[u.id])
continue;
vis[u.id] = true;
for(int i = 2; i <= n; i++){
int v = (u.id + a[i]) % a[1], w = a[i];
if(dis[u.id] + w < dis[v]){
dis[v] = dis[u.id] + w;
q.push(Node{v, dis[v]});
}
}
}
}
int cnt, ans;
signed main(){
scanf("%lld%lld", &n, &m);
cnt = n;
for(int i = 1; i <= n; i++){
scanf("%lld", &a[i]);
for(int j = 1; j <= min(a[i], m); j++)
a[++cnt] = a[i] - j;
}
sort(a + 1, a + cnt + 1);
n = unique(a + 1, a + cnt + 1) - a - 1;
if(a[1] <= 1){
printf("-1");
return 0;
}
dijkstra();
for(int i = 0; i < a[1]; i++){
if(dis[i] == INF){
printf("-1");
return 0;
}
ans = max(ans, max(0ll, dis[i] - a[1]));
}
printf("%lld", ans);
return 0;
}
:::
转圈技巧
我们发现同余最短路解决的是一个模 \(m\) 意义下的完全背包问题,也就是 \(dp_{(i + w_j) \bmod m} = \min(dp_{(i + w_j) \bmod m}, dp_i + w_j)\)。这样的 DP 转移会形成若干个环。
此时我们发现,从一个状态出发,不可能转移一圈再转移回到该点,因为最短路不会经过同一个点两次,否则存在负环。因此,我们往背包里加入重量为 \(w_i\) 的物品,最多只会加入 \(\displaystyle\frac{m}{\gcd(m, w_i)} - 1\) 个。于是,我们找到状态转移形成的环,绕着这个环转两圈,即可考虑到所有转移,因为每个点都转移到了该环上其它所有点。
此时我们将时间复杂度降低到了严格的 \(\mathcal O(nm)\),不需要带上 dijkstra 的 \(\log\),也不会像 SPFA 一样有可能被卡。
拿P2371 [国家集训队] 墨墨的等式举例,完整代码如下:
:::info[完整代码]
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e5 + 9;
int a[N], f[N], n, l, r, ans1, ans2;
signed main(){
scanf("%lld%lld%lld", &n, &l, &r);
for(int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
memset(f, 0x3f, sizeof(f));
f[0] = 0;
for(int i = 2; i <= n; i++){
int cnt = __gcd(a[i], a[1]);
for(int j = 0; j < cnt; j++){
for(int k = j, c = 1; c <= 2; c += k == j){
int nex = (k + a[i]) % a[1];
f[nex] = min(f[nex], f[k] + a[i]);
k = nex;
}
}
}
for(int i = 0; i < a[1]; i++){
if(r >= f[i])
ans1 += max(0ll, (r - f[i]) / a[1] + 1);
if(l - 1 >= f[i])
ans2 += max(0ll, (l - 1 - f[i]) / a[1] + 1);
}
printf("%lld", ans1 - ans2);
return 0;
}
:::
应用:大容量完全背包问题
在DP 学习笔记(一):DP 基础知识,基础 DP 类型中,我们给出了大容量背包的一般性解法,但是针对完全背包,我们有更简单的 DP 方法。
定理 2.4.3.1
设性价比最高的物品 \(x\) 的重量为 \(w_x\),那么其它物品最多选择 \(w_x - 1\) 个。
证明:非常简单,假设有一个物品 \(y\) 选了大于等于 \(w_x\) 个,那么它一定会占据 \(w_x w_y\) 的重量,此时我们不如将这 \(w_x\) 个 \(y\) 物品换成 \(w_y\) 个 \(x\) 物品,这样在重量不变的情况下,性价比提高了。
算法流程
考虑到 \(V\) 巨大,于是我们现在开始设计同余最短路。设 \(dp_i\) 表示已装入的物品的总重量 \(\bmod \, w_x = i\) 的所有情况中,总价值的最大值。此时的状态转移方程就是 \(dp_{(i + w_j) \bmod w_x} = \max(dp_{(i + w_j) \bmod w_x}, dp_i + v_j)\)。和之前一样转圈去更新即可。
注意到此时不需要限定性价比不是最高的物品选择至多 \(w_x\) 个,因为错解不优,不会更新到答案中。
:::info[完整代码]
:::
生成树相关
参考资料
-
蒋 sh、夏 wb 的课件
-
浅谈 OI 中的大容量背包问题 方心童
-
图论 I Alex_Wei
-
同余最短路的转圈技巧 Alex_Wei
-
同余最短路 exCat
本文来自博客园,作者:Orange_new,转载请注明原文链接:https://www.cnblogs.com/JPGOJCZX/p/18422840

浙公网安备 33010602011771号