图上算法学习笔记(一):图论基础知识、最短路相关、生成树相关

基础知识

最短路相关

最短路算法

次短路 / \(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\) 个,因为错解不优,不会更新到答案中。

P9140 [THUPC 2023 初赛] 背包

:::info[完整代码]


:::

生成树相关

参考资料

posted @ 2024-09-20 17:04  Orange_new  阅读(25)  评论(0)    收藏  举报