最短路

最短路

Floyd

适用于无负环的图,主要思路是枚举所有点对 \((i, j)\) 以及中转点 \(k\) ,再对邻接矩阵进行松弛操作。

时间复杂度 \(O(n^3)\) ,可以求解全源最短路,代码简单好写。

inline void Floyd() {
    for (int k = 1; k <= n; ++k)
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}

用 Floyd 转递闭包时可以用 bitset 优化,时间复杂度 \(O(\frac{n^{3}}{\omega})\)

for (int k = 1; k <= n; ++k)
    for (int i = 1; i <= n; ++i)
        if (f[i][k])
            f[i] = f[i] | f[k];

P1119 灾后重建

给出一张无向图,第 \(i\) 个点在 \(t_i\) 时刻被修复,若 \(t_i = 0\)\(i\) 未损坏。

\(q\) 次询问,每次询问 \(t\) 时刻 \(x\)\(y\) 的最短路,保证给出的 \(t\) 不降。

\(n \le 200\)

用 Floyd 求最短路,按修复时间枚举中转点松弛即可,时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e2 + 7;

int dis[N][N], t[N];

int n, m, q;

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 0; i < n; ++i)
        scanf("%d", t + i);

    memset(dis, inf, sizeof(dis));

    for (int i = 0; i < n; ++i)
        dis[i][i] = 0;

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        dis[u][v] = dis[v][u] = w;
    }

    scanf("%d", &q);
    int k = 0;

    while (q--) {
        int x, y, w;
        scanf("%d%d%d", &x, &y, &w);

        while (t[k] <= w && k < n) {
            for (int i = 0; i < n; ++i)
                for (int j = 0; j < n; ++j)
                    dis[i][j] = dis[j][i] = min(dis[i][j], dis[i][k] + dis[k][j]);
            ++k;
        }
        
        printf("%d\n", t[x] > w || t[y] > w || dis[x][y] == inf ? -1 : dis[x][y]);
    }

    return 0;
}

P6175 无向图的最小环问题

给一个正权无向图,找一个最小权值和的环,或报告无解。

\(n \le 100\)

枚举中转点 \(k\) 时,不难发现此时前 \(k-1\) 个点的最短路径已经求得。而 \(x \to y \to k \to x\) 连接起来就得到了一个经过 \(x , y , k\) 的最小环,因此只要 Floyd 的时候顺带求出即可,时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 1e2 + 7;

int e[N][N], dis[N][N];

int n, m;

signed main() {
    scanf("%d%d", &n, &m);
    memset(e, inf, sizeof(e));

    for (int i = 1; i <= n; i++)
        e[i][i] = 0;

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        e[u][v] = e[v][u] = min(e[u][v], w);
    }

    memcpy(dis, e, sizeof(e));
    int ans = inf;
    
    for (int k = 1; k <= n; ++k) {
        for (int i = 1; i < k; ++i)
            for (int j = i + 1; j < k; ++j)
                if (dis[i][j] != inf && e[i][k] != inf && e[k][j] != inf)
                    ans = min(ans, dis[i][j] + e[i][k] + e[k][j]);

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                dis[j][i] = dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
    }

    if (ans == inf)
        printf("No solution.");
    else
        printf("%lld", ans);

    return 0;
}

Bellman–Ford

定义松弛操作为 \(dis_v \gets \min(dis_v, dis_u + w(u, v))\)

Bellman–Ford 算法不断尝试对图上每一条边进行松弛,循环 \(n - 1\) 次即可求出最短路。时间复杂度 \(O(nm)\)

若第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(S\) 点出发能够抵达一个负环。

SPFA

即队列优化的 Bellman-Ford。

事实上 Bellman-Ford 中会进行很多次无效松弛操作,只有上一次被松弛的结点所连接的边才有可能引起下一次松弛操作。

于是考虑用队列来维护哪些结点可能会引起松弛操作,就能只访问必要的边了。

若要判负环,则记录一下每个点的松弛次数即可,若被松弛超过 \(n\) 次则说明走到了负环。

SPFA 算法在随机图上时间复杂度为 \(O(km)\)\(k\) 为常数),但是可以被卡到 \(O(nm)\)

inline bool SPFA(int S) {
    memset(dis + 1, inf, sizeof(int) * n);
    memset(cnt + 1, 0, sizeof(int) * n);
    queue<int> q;
    dis[S] = 0, q.emplace(S), inque[S] = true, cnt[S] = 1;

    while (!q.empty()) {
        int u = q.front();
        q.pop(), inque[u] = false;
        
        for (int it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w, ++cnt[v];
                    
                if (cnt[v] >= n)
                    return false;

                if (!inque[v])
                    q.emplace(v), inque[v] = true;
            }
        }
    }

    return true;
}

一般来说判负环的时候用 dfs 版的 SPFA 更快

bool SPFA(int u) {
    vis[u] = true;
    
    for (auto it : G.e[u]) {
        int v = it.first, w = it.second;
        
        if (dis[v] > dis[u] + w) {
            dis[v] = dis[u] + w;
            
            if (vis[v] || !SPFA(v))
                return false;
        }
    }
    
    return vis[u] = false, true;
}

常用的一些优化:

  • LLL 优化:使用双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则从队头插入。

  • SLF 优化:使用双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则从队头插入。

  • SLF 带容错:每次将入队结点距离和队首比较,如果比队首大超过一定值则插入至队尾,否则从队头插入。

  • mcfx 优化:定义区间 \([l, r]\) ,当入队点入队次数属于这个区间时从队首插入,否则从队头插入。通常取 \([2, \sqrt{n}]\)

  • SLF + swap:每当队列改变时,如果队首距离大于队尾,则交换首尾。

  • 较为玄学的优化。

    • 随机打乱边。
    • 以一定概率从队首/队尾插入。
    • 入队次数一定周期就随机打乱队列。

Dijkstra

将点分成两个集合:已确定最短路长度的点集 \(S\) 的和未确定最短路长度的点集 \(T\)

初始时所有的点都属于 \(T\) ,令 \(dis_s = 0\) ,其它点的 \(dis\) 均为 \(+ \infty\)

重复操作直到 \(T\) 为空:从 \(T\) 中选一个 \(dis\) 最小的点移到 \(S\) 中,并用该点松弛其它点。

Dijkstra 算法只能解决正权图上的最短路问题问题。

具体实现:

  • 暴力:每次暴力找到 \(dis\) 最小的点松弛其它点,时间复杂度 \(O(n^2 + m) = O(n^2)\)
  • 优先队列:每次松弛 \((u, v)\) 后将 \(v\) 插入优先队列中,每次从优先队列中选 \(dis\) 最小的点松弛其它点。由于不能在优先队列中删除元素,所以取出时要判重,时间复杂度 \(O(m \log n)\)
  • 线段树:基本不用,将上面的操作改为单点修改和全局查询最小值,时间复杂度 \(O(m \log n)\)

需要权衡 \(O(n^2)\)\(O(m \log n)\) 两种实现方式的优劣,一般稠密图使用 \(O(n^2)\) ,稀疏图用 \(O(m \log n)\)

\(O(n^2)\) 的实现:

inline void Dijkstra(int S) {
    memset(dis + 1, inf, sizeof(int) * n);
    dis[S] = 0;

    for (;;) {
        int u = -1;
        
        for (int i = 1; i <= n; ++i)
            if (!vis[i] && (u == -1 || dis[i] < dis[u]))
                u = i;
        
        if (u == -1)
            break;

        vis[u] = true;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w;
        }
    }
}

优先队列优化的 Dijkstra 的实现:

inline void Dijkstra(int S) {
    memset(dis + 1, inf, sizeof(int) * n);
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        int u = q.top().second;
        q.pop();

        if (vis[u])
            continue;

        vis[u] = true;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

另一种写法(取消了 \(vis\) 数组):

inline void Dijkstra(int S) {
    memset(dis + 1, inf, sizeof(int) * n);
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (dis[c.second] != -c.first)
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

P11131 【MX-X5-T3】「GFOI Round 1」Cthugha

给定一张 \(n \times m\) 的网格图,每个格子都有一个权值。

给定 \(q\) 个点,找到一个点,最小化其到这 \(q\) 个点的路径权值的最大值,若距离可以无限小则输出 No 。其中路径可以重复经过格子,多次经过同一个格子时权值重复计算。

\(n \times m \le 10^5\)\(q \le 50\)

首先若相邻的两个格子加起来值 \(< 0\) 则无解,因为可以反复横跳。

此时把点权放在边上即可规避掉负权的限制,然后以每个人为源点跑 Dijkstra 即可。

由于 Dijkstra 的正确性基于第一次取到这个点是就是最优答案,而在这个图中如果出去绕一下回来距离变小了,证明不存在最小值。如果存在最小值,绕回来之后一定距离变大,满足条件。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int dx[] = {1, -1, 0, 0};
const int dy[] = {0, 0, 1, -1};
const int N = 1e5 + 7;

int n, m, q;

signed main() {
    scanf("%d%d%d", &n, &m, &q);
    vector<vector<int> > a(n, vector<int>(m));

    for (int i = 0; i < n; ++i)
        for (int &it : a[i])
            scanf("%d", &it);

    for (int i = 0; i < n; ++i)
        for (int j = 0; j < m; ++j) {
            if (i + 1 < n && a[i][j] + a[i + 1][j] < 0)
                return puts("No"), 0;
            else if (j + 1 < m && a[i][j] + a[i][j + 1] < 0)
                return puts("No"), 0;
        }

    vector<vector<ll> > res(n, vector<ll>(m, -1e18));

    auto Dijkstra = [&](int bx, int by) {
        vector<vector<ll> > dis(n, vector<ll>(m, 1e18));
        priority_queue<tuple<ll, int, int> > q;
        dis[bx][by] = 0, q.emplace(-dis[bx][by], bx, by);

        while (!q.empty()) {
            auto c = q.top();
            q.pop();

            if (dis[get<1>(c)][get<2>(c)] != -get<0>(c))
                continue;

            int x = get<1>(c), y = get<2>(c);
            res[x][y] = max(res[x][y], (dis[x][y] + a[x][y] + a[bx][by]) / 2);

            for (int i = 0; i < 4; ++i) {
                int nx = x + dx[i], ny = y + dy[i];

                if (0 <= nx && nx < n && 0 <= ny && ny < m && dis[nx][ny] > dis[x][y] + a[x][y] + a[nx][ny])
                    dis[nx][ny] = dis[x][y] + a[x][y] + a[nx][ny], q.emplace(-dis[nx][ny], nx, ny);
            }
        }
    };

    while (q--) {
        int x, y;
        scanf("%d%d", &x, &y);
        Dijkstra(x - 1, y - 1);
    }

    ll ans = 1e18;

    for (int i = 0; i < n; ++i)
        ans = min(ans, *min_element(res[i].begin(), res[i].end()));

    printf("%lld", ans);
    return 0;
}

P5304 [GXOI/GZOI2019] 旅行者

给定一张有向图和 \(k\) 个关键点,求关键点两两之间最短路的最小值。

\(k \le n \le 10^5\)\(m \le 5 \times 10^5\)

考虑两个关键点 \(x, y\) 之间的最短路,先不考虑 \(x, y\) 直接连边的情况。记路径上非端点的一个点为 \(z\) ,则路径形如 \(x \to z \to y\)

于是考虑对正图和反图各跑一遍以关键点为源点的最短路,则对于一个非关键点 \(z\) ,关键点之间经过 \(z\) 的最短路径即为正反两次 \(dis_z\) 的和,对所有 \(z\) 的贡献取 \(\min\) 即可求出答案。

但是这样是假的,因为可能出现 \(x = y\) 的情况。一个简单的想法是跑次短路,但是并不优美。

考虑不枚举路径上的点,转而枚举路径上的边,再用两端正反图上起点不同的边更新即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 1e5 + 7;

struct Graph {
    vector<pair<int, int> > e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G, rG;

vector<int> kp;

ll dis1[N], dis2[N];
int st1[N], st2[N];

int n, m, k;

inline void Dijkstra(Graph &G, ll *dis, int *st) {
    memset(dis + 1, inf, sizeof(ll) * n);
    memset(st + 1, 0, sizeof(int) * n);
    priority_queue<pair<ll, int> > q;

    for (int it : kp)
        dis[it] = 0, st[it] = it, q.emplace(-dis[it], it);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, st[v] = st[u], q.emplace(-dis[v], v);
        }
    }
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &n, &m, &k);
        kp.resize(k), G.clear(n), rG.clear(n);

        for (int i = 1; i <= m; ++i) {
            int u, v, w;
            scanf("%d%d%d", &u, &v, &w);

            if (u != v)
                G.insert(u, v, w), rG.insert(v, u, w);
        }

        for (int &it : kp)
            scanf("%d", &it);

        Dijkstra(G, dis1, st1), Dijkstra(rG, dis2, st2);

        ll ans = inf;

        for (int u = 1; u <= n; ++u)
            for (auto it : G.e[u]) {
                int v = it.first, w = it.second;

                if (st1[u] && st2[v] && st1[u] != st2[v])
                    ans = min(ans, dis1[u] + w + dis2[v]);
            }

        printf("%lld\n", ans);
    }

    return 0;
}

P7407 [JOI 2021 Final] 机器人 / Robot

给出一张无向图,每条边有颜色 \(c\) 和代价 \(w\)

定义 \(u\) 的一条出边是好的,当且仅当其不与 \(u\) 的其他任意出边颜色相同。

对于一条边,可以花其代价修改颜色。求 \(1\) 只走好边能到 \(n\) 的最小代价,或报告无解。

\(n \le 10^5\)\(m \le 2 \times 10^5\)

需要走 \((u, v, c, w)\) 这条边时,若其为 \(u\) 的好边,则直接走就行。否则要么修改它的颜色,要么修改 \(u\) 其余同色边的颜色。记 \(sum_{u, c}\) 表示 \(u\) 所有颜色 \(c\) 的出边的代价和,则将这条边的边权设为 \(\min(w, sum_{u, c} - w)\) 。称前者为操作一,后者为操作二。

但是这样没有考虑到一种情况:若 \(x \to y \to z\) 两边颜色相同,\(x \to y\) 使用操作一,\(y \to z\) 使用操作二,则只要付出 \(sum_{y, c} - w_{y \to z}\) 的代价。

考虑建虚点处理这种情况。对于 \(u = y\) 所有颜色为 \(c\) 的出边 \((u, v, c, w)\)(数量 \(\ge 2\) ),新建一个虚点 \(u_c\) ,将 \(v\) 向该 \(u_c\) 连一条边权为 \(0\) 的边,而 \(u_c\)\(v\) 连一条边权为 \(sum_{u, c} - w\) 的边,则原先 \(x \to y \to z\) 的决策还可以被 \(x \to y_c \to z\) 的决策替代。

Dijkstra 跑最短路即可,由于虚点的数量上界为 \(2m\) ,时间复杂度 \(O((n + m) \log (n + m))\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e5 + 7;

struct Graph {
    vector<pair<int, ll> > e[N];

    inline void insert(int u, int v, ll w) {
        e[u].emplace_back(v, w);
    }
} G;

map<int, vector<pair<int, ll> > > mp[N];
map<int, ll> sum[N];

ll dis[N];

int n, m, tot;

inline void Dijkstra(int S) {
    memset(dis + 1, 0x3f, sizeof(ll) * tot);
    priority_queue<pair<ll, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first;
            ll w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v, c, w;
        scanf("%d%d%d%d", &u, &v, &c, &w);
        mp[u][c].emplace_back(v, w), sum[u][c] += w;
        mp[v][c].emplace_back(u, w), sum[v][c] += w;
    }

    tot = n;

    for (int u = 1; u <= n; ++u)
        for (auto it : mp[u]) {
            ++tot;

            for (auto x : it.second) {
                int c = it.first, v = x.first;
                ll w = x.second;
                G.insert(u, v, it.second.size() == 1 ? 0 : min(w, sum[u][c] - w));
                G.insert(v, tot, 0), G.insert(tot, v, sum[u][c] - w);
            }
        }

    Dijkstra(1);
    printf("%lld", dis[n] == inf ? -1 : dis[n]);
    return 0;
}

Johnson 全源最短路

P5905 【模板】Johnson 全源最短路

如果没有负权边,那直接跑 \(n\) 次 Dijkstra 即可做到一个较优秀的复杂度,下面考虑怎么处理负权边。

建一个超级源点,所有点与其连一条边权为 \(0\) 的边。先用 SPFA 求每个点与超级源点的最短路径长度 \(h_i\) ,然后将每条边 \(u \to v\) 的边权增加 \(h_u-h_v\) ,最后统计 \(i \to j\) 的最短路时减去 \(h_i - h_j\) 即可,于是就能直接跑 \(n\) 次 Dijkstra 了。

时间复杂度 \(O(km + nm \log m)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 3e3 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(const int u, const int v, const int w) {
        e[u].emplace_back(v, w);
    }
} G;

int dis[N][N], h[N], cnt[N];
bool inque[N];

int n, m;

inline bool SPFA() {
    memset(h + 1, inf, sizeof(int) * n);
    queue<int> q;
    q.emplace(0), inque[0] = true, ++cnt[0];

    while (!q.empty()) {
        int u = q.front();
        q.pop(), inque[u] = false;

        if (cnt[u] == n - 1)
            return false;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (h[v] > h[u] + w) {
                h[v] = h[u] + w;

                if (!inque[v])
                    q.emplace(v), inque[v] = true, ++cnt[v];
            }
        }
    }

    return true;
}

inline void Dijkstra(int S, int *dis) {
    memset(dis + 1, inf, sizeof(int) * n);
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second + h[u] - h[v];

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

inline bool Johnson() {
    for (int i = 1; i <= n; ++i)
        G.insert(0, i, 0);

    if (!SPFA())
        return false;

    for (int i = 1; i <= n; ++i) {
        Dijkstra(i, dis[i]);

        for (int j = 1; j <= n; ++j)
            if (dis[i][j] != inf)
                dis[i][j] -= h[i] - h[j];
    }

    return true;
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, & w);
        G.insert(u, v, w);
    }

    if (!Johnson())
        return puts("-1"), 0;

    for (int i = 1; i <= n; ++i) {
        ll res = 0;

        for (int j = 1; j <= n; ++j)
            res += 1ll * j * (dis[i][j] == inf ? 1e9 : dis[i][j]);

        printf("%lld\n", res);
    }

    return 0;
}

BFS 相关

在一些特殊的图上,可以用 BFS 求解最短路做到 \(O(n + m)\) 的时间复杂度。

  • 无权图最短路:直接 BFS 即可。
  • 01BFS:若边权只有 \(0\)\(1\) ,考虑用 deque 维护 BFS ,若走的边权为 \(0\) 则从队首入队,若走的边权为 \(1\) 则从队尾入队。

CF173B Chamber of Secrets

一个 \(n \times m\) 的图,现在有一束激光从左上角往右边射出,每遇到 # ,你可以选择光线往四个方向射出,或者什么都不做。

问最少需要多少个 # 往四个方向射出才能使光线在第 \(n\) 行往右边射出。

\(n, m \le 1000\)

将柱子改为 # 后,一条光线经过的时候实际效果是该行该列都会有光线。于是视该操作代价为 \(1\) 跑 BFS 即可。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e3 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

queue<int> q;

int dis[N];
char str[N];
bool vis[N];

int n, m;

inline void bfs() {
    memset(dis + 1, inf, sizeof(int) * (n + m));
    dis[1] = 0, q.emplace(1), vis[1] = true;

    while (!q.empty()) {
        int u = q.front();
        q.pop(), vis[u] = true;

        for (int v : G.e[u])
            if (!vis[v])
                dis[v] = dis[u] + 1, q.emplace(v), vis[v] = true;
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i) {
        scanf("%s", str + 1);

        for (int j = 1; j <= m; ++j)
            if (str[j] == '#')
                G.insert(i, j + n), G.insert(j + n, i);
    }

    bfs();
    printf("%d", dis[n] == inf ? -1 : dis[n]);
    return 0;
}

P9351 [JOI 2023 Final] 迷宫 / Maze

给出一张 \(n \times m\) 的网格图,其中一些格子为障碍。一次操作可以清空一个 \(k \times k\) 的矩阵内所有障碍,求使得\((x_1, y_1)\)\((x_2, y_2)\) 联通的最少操作数。

\(n \times m \le 6 \times 10^6\)\(k \le \min(n, m)\)

考虑建立最短路模型,将 \((x, y)\) 向四联通的非障碍格子连边权为 \(0\) 的边,将 \((x, y)\) 向以其为中心、边长为 \(2k + 1\) 的格子(挖掉四个角)连边权为 \(1\) 的边,跑 01-bfs 即可。但是该做法边权为 \(1\) 的边数量为 \(O(nmk^2)\) 级别,无法接受。

考虑将第二类操作转化为走一步四联通后,接下来 \(k - 1\) 步可以走八连通。那么一个点的状态需要记录距离和接下来能走八连通的步数,以前者作为第一关键字(从小到大)、后者作为第二关键字(从大到小)跑 01-bfs 即可做到 \(O(nm)\)

#include <bits/stdc++.h>
using namespace std;
const int dx[] = {1, -1, 0, 0, 1, -1, 1, -1};
const int dy[] = {0, 0, 1, -1, 1, -1, -1, 1};
const int N = 6e6 + 7;

char buf1[N], *str[N];
bool buf2[N], *vis[N];

int n, m, k, bx, by, ex, ey;

signed main() {
    scanf("%d%d%d%d%d%d%d", &n, &m, &k, &bx, &by, &ex, &ey);
    --bx, --by, --ex, --ey;
    str[0] = buf1, vis[0] = buf2;

    for (int i = 0; i < n; ++i)
        scanf("%s", str[i]), str[i + 1] = str[i] + m, vis[i + 1] = vis[i] + m;

    deque<tuple<int, int, int, int> > q;
    q.emplace_back(bx, by, 0, 0);

    while (!q.empty()) {
        int x = get<0>(q.front()), y = get<1>(q.front()), h = get<2>(q.front()), d = get<3>(q.front());
        q.pop_front();

        if (vis[x][y])
            continue;

        vis[x][y] = true;

        if (x == ex && y == ey)
            return printf("%d", d), 0;

        if (h) {
            for (int i = 0; i < 8; ++i) {
                int nx = x + dx[i], ny = y + dy[i];

                if (0 <= nx && nx < n && 0 <= ny && ny < m && !vis[nx][ny])
                    q.emplace_back(nx, ny, h - 1, d);
            }
        } else {
            for (int i = 0; i < 4; ++i) {
                int nx = x + dx[i], ny = y + dy[i];

                if (0 <= nx && nx < n && 0 <= ny && ny < m && !vis[nx][ny]) {
                    if (str[nx][ny] == '#')
                        q.emplace_back(nx, ny, k - 1, d + 1);
                    else
                        q.emplace_front(nx, ny, 0, d);
                }
            }
        }
    }

    return 0;
}

P3547 [POI 2013] CEN-Price List

给定一张无向连通图,初始有 \(m\) 条边权为 \(a\) 的无向边。对于 \(u, v, w\) ,若存在边 \((u, v, a), (v, w, a)\) 但不存在边 \((u, w, a)\) ,那么加入边 \((u, w, b)\)

求起点 \(k\) 到所有点的最短路。

\(n, m \le 10^5\)

考虑可能的最短路构成:

  • 全走 \(a\) :直接 bfs 处理即可。

  • 一部分走 \(a\) ,一部分走 \(b\) :不难发现该部分一定形如一条 \(a\) 和若干条 \(b\) (否则可以调整更优,或者不如全走 \(a\) ),记全走 \(a\) 的最短路为 \(d\) ,则该部分答案为 \(\lfloor \frac{d}{2} \rfloor \times b + [d \bmod 2] \times a\)

  • 全走 \(b\) :此时需要求出 \(k\) 到所有点路径长度为偶数的最短路。一个想法是在 bfs 中直接枚举 \((u, v), (v, w)\) ,并尝试松弛 \(w\) 。注意 \(w\) 不能为 \(u\) 的邻域,可以打标记实现。时间复杂度 \(O(\sum deg^2) = O(n^2)\) ,无法接受。

    注意到 bfs 每次取出 \(dis_u\) 更新 \(v\) 时,\(v\) 一定不会被其他的点更新。

    考虑存两个边集,分别作为奇数边和偶数边。每次取出 \((u, v), (v, w)\) 松弛 \(w\) 时,若 \(w\) 被松弛成功,那么其他 \(u\) 就不会再松弛 \(w\) 了,因此 \((v, w)\) 作为偶数边就没有用了,可以直接删去。

    分析一下复杂度,对于成功删去偶数边的部分,复杂度是 \(O(m)\) 的;对于未删去偶数边的部分,其形如一个三元环,且该三元环会被遍历到三次,复杂度是 \(O(m \sqrt{m})\) 的。

总时间复杂度 \(O(m \sqrt{m})\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, nG;

int dis1[N], dis2[N], tag[N];

int n, m, s, a, b;

inline void bfs1(int S) {
    memset(dis1 + 1, inf, sizeof(int) * n);
    queue<int> q;
    dis1[S] = 0, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int v : G.e[u])
            if (dis1[v] == inf)
                dis1[v] = dis1[u] + 1, q.emplace(v);
    }
}

inline void bfs2(int S) {
    memset(dis2 + 1, inf, sizeof(int) * n);
    queue<int> q;
    dis2[S] = 0, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int v : G.e[u])
            tag[v] = u;

        for (int v : G.e[u]) {
            vector<int> edg;

            for (int w : nG.e[v]) {
                if (tag[w] != u && dis2[w] == inf)
                    dis2[w] = dis2[u] + 1, q.emplace(w);
                else
                    edg.emplace_back(w);
            }

            nG.e[v] = edg;
        }
    }
}

signed main() {
    scanf("%d%d%d%d%d", &n, &m, &s, &a, &b);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
        nG.insert(u, v), nG.insert(v, u);
    }

    bfs1(s), bfs2(s);

    for (int i = 1; i <= n; ++i) {
        int ans = min(dis1[i] * a, dis1[i] / 2 * b + dis1[i] % 2 * a);

        if (dis2[i] != inf)
            ans = min(ans, dis2[i] * b);

        printf("%d\n", ans);
    }

    return 0;
}

次短路

考虑每一条非最短路上的边 \(u \to v\) ,答案即为:

\[\min (dis_{1, u} + w(u, v) + dis_{v, n}) \]

\(dis_{1, u}, dis_{v, n}\) 建立正反图跑两次 Dijkstra 即可求得。

求严格次短路时不必记录最短路的路径,只需枚举每条边,若路径长度严格小于最短路时更新答案即可。

另一种方式是对于每个点都记录一下最短路与次短路,只要被更新就去松弛别的点。

最短路图

即求出所有最短路(多条也算)组成的 DAG,只需将 \(dis_v = dis_u + w\) 的边连边即可。

P2149 [SDOI2009] Elaxia的路线

给出一张无向图和两对点,求图中两对点间最短路的最长公共路径,注意同一条边走的方向不同也算公共路径。

\(n \le 1.5 \times 10^3\)\(m \le 3 \times 10^5\)

一个显然的事实是最长公共路径一定是连续的一段。

考虑建立 \(s_1 \to t_1\) 的最短路图,仅保留 \(dis_{s \to u} + w(u, v) + dis_{v \to t_1} = dis_{s \to t}\) 的边即可。

然后在最短路图上拓扑排序,需要正反各跑一次。一个简单的实现是记 \(f_u, g_u\) 表示正反以 \(u\) 为端点的最长公共路径长度,转移时只要考虑 \(u \to v\) 这条边能不能在 \(s_2 \to t_2\)\(t_2 \to s_2\) 的最短路上即可。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1.5e3 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

int dis[4][N], indeg[N], f[N], g[N];
bool flag[N];

int n, m, s1, t1, s2, t2;

inline void Dijkstra(int S, int *dis) {
    memset(dis + 1, inf, sizeof(int) * n);
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (dis[c.second] != -c.first)
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

signed main() {
    scanf("%d%d%d%d%d%d", &n, &m, &s1, &t1, &s2, &t2);

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        G.insert(u, v, w), G.insert(v, u, w);
    }

    Dijkstra(s1, dis[0]), Dijkstra(t1, dis[1]), Dijkstra(s2, dis[2]), Dijkstra(t2, dis[3]);

    for (int u = 1; u <= n; ++u)
        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[0][u] + w + dis[1][v] == dis[0][t1])
                flag[v] = true, ++indeg[v];
        }

    queue<int> q;
    q.emplace(s1);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (!flag[v])
                continue;

            if (dis[2][u] + w + dis[3][v] == dis[2][t2])
                f[v] = max(f[v], f[u] + w);

            if (dis[3][u] + w + dis[2][v] == dis[2][t2])
                g[v] = max(g[v], g[u] + w);

            --indeg[v];

            if (!indeg[v])
                q.emplace(v);
        }
    }

    printf("%d", max(*max_element(f + 1, f + n + 1), *max_element(g + 1, g + n + 1)));
    return 0;
}

「ROI 2017 Day 1」前往大都会

某国有 \(n\) 座城市与 \(m\) 条单向铁路线,构成一张连通图。第 \(i\) 条单向铁路线由 \(v_{i, 1}, v_{i, 2}, \cdots, v_{i, s_i + 1}\) 城市组成,城市 \(v_{i, j}\) 通过该线路到城市 \(v_{i, j + 1}\) 花费的时间为 \(t_{i, j}\)

\(1\)\(n\) 花费时间最少的情况下,经过任意两个相邻城市所花费时间的平方和的最大值。

\(n, m \le 10^6\)

首先求出最短路图,那么只要在最短路图上找到平方和最大的路径。

这里的最短路图是 DAG, 于是可以按拓扑序设计 DP 。

\(dp_x\) 表示以 \(x\) 为终点的最大权值,枚举上一个换乘点,有:

\[f_x = \max_y \{ f_y + (d_x - d_y)^2 \} = d_x^2 + \max_y \{ d_y^2 - 2d_x d_y + f_y \} \]

斜率优化即可,复杂度瓶颈为最短路。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e6 + 7, S = 2e6 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G, nG;

vector<pair<int, int> > belong[N];
vector<int> City[N], Time[N], ts[N], sta[S];

ll f[N];
int s[N], dis[N], id[N];

int n, m;

inline void Dijkstra(int S) {
    memset(dis + 1, inf, sizeof(int) * n);
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (dis[c.second] != -c.first)
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

inline void prework() {
    int cnt = 0;
    
    for (int i = 1; i <= m; ++i) {
        ts[i].resize(s[i] + 1);
        
        for (int j = 0; j <= s[i]; ++j) {
            if (!j) {
                ts[i][j] = ++cnt;
                continue;
            }
            
            int u = City[i][j - 1], v = City[i][j], w = Time[i][j - 1];
            ts[i][j] = (dis[u] + w > dis[v] ? ++cnt : ts[i][j - 1]);
        }
    }
}

inline ll slope(int x, int d) {
    return -2ll * dis[x] * d + 1ll * dis[x] * dis[x] + f[x];
}

inline bool check(int a, int b, int c) {
    ll ka = -2ll * dis[a], kb = -2ll * dis[b], kc = -2ll * dis[c], ta = 1ll * dis[a] * dis[a] + f[a], 
        tb = 1ll * dis[b] * dis[b] + f[b], tc = 1ll * dis[c] * dis[c] + f[c];
    return (__int128) (tc - ta) * (ka - kb) >= (__int128) (tb - ta) * (ka - kc);
}

signed main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d", s + i, &u);
        City[i].emplace_back(u), belong[u].emplace_back(i, 0);
        
        for (int j = 1; j <= s[i]; ++j) {
            scanf("%d%d", &w, &v);
            City[i].emplace_back(v), Time[i].emplace_back(w);
            G.insert(u, v, w), belong[v].emplace_back(i, j), u = v;
        }
    }
    
    Dijkstra(1), prework();
    iota(id + 1, id + n + 1, 1);
    
    sort(id + 1, id + 1 + n, [](const int &a, const int &b) {
        return dis[a] < dis[b];
    });
    
    for (auto it : belong[1])
        sta[ts[it.first][it.second]].emplace_back(1);
    
    for (int i = 2; i <= n; ++i) {
        int x = id[i];

        if (dis[x] == inf)
            break;
            
        for (auto it : belong[x]) {
            int ns = ts[it.first][it.second];
            
            if (sta[ns].empty())
                continue;
            
            while (sta[ns].size() >= 2 && slope(sta[ns][sta[ns].size() - 2], dis[x]) >= 
                slope(sta[ns][sta[ns].size() - 1], dis[x]))
                sta[ns].pop_back();
            
            f[x] = max(f[x], slope(sta[ns][sta[ns].size() - 1], dis[x]) + 1ll * dis[x] * dis[x]);
        }
        
        for (auto it : belong[x]) {
            int ns = ts[it.first][it.second];
            
            if (!sta[ns].empty() && slope(sta[ns][sta[ns].size() - 1], dis[x]) >= slope(x, dis[x]))
                continue;
            
            while (sta[ns].size() >= 2 && check(sta[ns][sta[ns].size() - 2], sta[ns][sta[ns].size() - 1], x))
                sta[ns].pop_back();
                
            sta[ns].emplace_back(x);
        }
    }
    
    printf("%d %lld", dis[n], f[n]);
    return 0;
}

最短路径树(SPT)

即由最短路径组成的树,和最短路图的区别就是少了几条边。可以通过求解最短路时记录每个点的前驱更新节点求得。

CF545E Paths and Trees

给出一张无向图,给定源点,求边权和最小的 SPT。

\(n, m \le 3 \times 10^5\)

要求边权和最小,可以考虑贪心,在松弛时若遇到松弛前后边权相等时取边权较小者即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 3e5 + 7;

struct Graph {
    struct Edge {
        int nxt, v, w;
        bool tag;
    } e[N << 1];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v, int w) {
        e[++tot] = (Edge){head[u], v, w, false}, head[u] = tot;
    }
} G;

ll dis[N];
int pre[N];

int n, m, s;

inline void Dijkstra(int S) {
    memset(dis + 1, 0x3f, sizeof(ll) * n);
    priority_queue<pair<ll, int> > q;
    dis[S] = 0, q.emplace(-dis[S], S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (dis[v] > dis[u] + w) 
                dis[v] = dis[u] + w, pre[v] = i, q.emplace(-dis[v], v);
            else if (dis[v] == dis[u] + w && w < G.e[pre[v]].w)
                pre[v] = i;
        }
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        G.insert(u, v, w), G.insert(v, u, w);
    }

    scanf("%d", &s);
    Dijkstra(s);

    for (int i = 1; i <= n; ++i)
        G.e[pre[i]].tag = true;

    ll ans = 0;

    for (int i = 1; i <= m; ++i)
        if (G.e[i << 1].tag || G.e[i << 1 | 1].tag)
            ans += G.e[i << 1].w;

    printf("%lld\n", ans);

    for (int i = 1; i <= m; ++i)
        if (G.e[i << 1].tag || G.e[i << 1 | 1].tag)
            printf("%d ", i);

    return 0;
}

CF1005F Berland and the Shortest Paths

给出一张无向无边权简单连通图,求 SPT 方案数并给出 \(k\) 个方案(若超过 \(k\) 种则只取 \(k\) 种即可)。

\(n, m \le 2 \times 10^5\)\(mk \le 10^6\)

对每个点维护可能成为前驱节点的集合,总方案数就是所有集合大小的乘积,求解方案直接暴力从每个集合中选一个元素组合即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;

struct Graph {
    struct Edge {
        int nxt, v;
    } e[N << 1];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v) {
        e[++tot] = (Edge) {head[u], v}, head[u] = tot;
    }
} G;

vector<int> pre[N];

int dis[N];
bool vis[N];

int n, m, k, ans = 1;

inline void bfs(int S) {
    memset(dis + 1, -1, sizeof(int) * n);
    queue<int> q;
    dis[S] = 0, q.emplace(S);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v;

            if (dis[v] == -1)
                dis[v] = dis[u] + 1, pre[v].emplace_back(i >> 1), q.emplace(v);
            else if (dis[v] == dis[u] + 1)
                pre[v].emplace_back(i >> 1);
        }
    }
}

void dfs(int u) {
    if (u > n) {
        for (int i = 1; i <= m; ++i)
            putchar(vis[i] ? '1' : '0');

        puts("");

        if (!--ans)
            exit(0);

        return;
    }

    for (int it : pre[u])
        vis[it] = true, dfs(u + 1), vis[it] = false;

    return;
}

signed main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    bfs(1);

    for (int i = 2; i <= n; ++i) {
        if (ans * pre[i].size() > k) {
            ans = k;
            break;
        } else
            ans *= pre[i].size();
    }

    printf("%d\n", ans);
    dfs(2);
    return 0;
}

P6880 [JOI 2020 Final] 奥运公交 / Olympic Bus

给定一个有向图,每条边从 \(u_i\) 指向 \(v_i\),经过这条边的代价为 \(c_i\) ,反转这条边方向的额外代价为 \(d_i\)

可以反转一条边(或不反转),求 \(1 \to n \to 1\) 的最小代价和。

\(n \le 200\)\(m \le 5 \times 10^4\)

考虑求翻转一条边 \((u, v)\)\(1 \to n\) 的最短路,\(n \to 1\) 是类似的。

一个显然的暴力是暴力每次都跑一次 Dijkstra,时间复杂度 \(O(mn^2)\)

注意到只有 SPT 上的边才需要跑 Dijkstra,其余情况只要用强制经过这条边的最短路与原最短路取较小者即可。

由于边数较多,用朴素 Dijkstra 即可做到 \(O(n^3)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e2 + 7, M = 5e4 + 7;

struct Edge {
    int u, v, w1, w2;
} edg[M];

pair<ll, ll> e[N][N];

ll dis[5][N];
int pre[5][N];
bool vis[N];

int n, m;

inline void Dijkstra(int S, ll *dis, int *pre) {
    memset(dis + 1, inf, sizeof(ll) * n);
    memset(pre + 1, -1, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    dis[S] = 0;

    for (;;) {
        int u = -1;

        for (int i = 1; i <= n; ++i)
            if (!vis[i] && (u == -1 || dis[i] < dis[u]))
                u = i;
        
        if (u == -1)
            break;

        vis[u] = true;

        for (int v = 1; v <= n; ++v)
            if (dis[v] > dis[u] + e[u][v].first)
                dis[v] = dis[u] + e[u][v].first, pre[v] = u;
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            e[i][j] = make_pair(inf, inf);

    for (int i = 1; i <= m; ++i) {
        int u, v, w1, w2;
        scanf("%d%d%d%d", &u, &v, &w1, &w2);
        edg[i] = (Edge){u, v, w1, w2};

        if (w1 <= e[u][v].first)
            e[u][v].second = e[u][v].first, e[u][v].first = w1;
        else if (w1 <= e[u][v].second)
            e[u][v].second = w1;
    }

    Dijkstra(1, dis[0], pre[0]), Dijkstra(n, dis[1], pre[1]);

    for (int i = 1; i <= n; ++i)
        for (int j = i + 1; j <= n; ++j)
            swap(e[i][j], e[j][i]);

    Dijkstra(1, dis[2], pre[2]), Dijkstra(n, dis[3], pre[3]);

    for (int i = 1; i <= n; ++i)
        for (int j = i + 1; j <= n; ++j)
            swap(e[i][j], e[j][i]);

    ll ans = dis[0][n] + dis[1][1];

    for (int i = 1; i <= m; ++i) {
        int u = edg[i].u, v = edg[i].v, w1 = edg[i].w1, w2 = edg[i].w2;
        auto pe = e[u][v], pe2 = e[v][u];
        e[u][v].first = e[u][v].second, e[v][u].first = min(e[v][u].first, (ll)w1);
        ll res = w2;

        if (pre[0][v] != u || dis[0][u] + w1 != dis[0][v])
            res += min(dis[0][n], dis[0][v] + w1 + dis[3][u]);
        else
            Dijkstra(1, dis[4], pre[4]), res += dis[4][n];

        if (pre[1][v] != u || dis[1][u] + w1 != dis[1][v])
            res += min(dis[1][1], dis[1][v] + w1 + dis[2][u]);
        else
            Dijkstra(n, dis[4], pre[4]), res += dis[4][1];

        ans = min(ans, res), e[u][v] = pe, e[v][u] = pe2;
    }

    printf("%lld", ans >= inf ? -1 : ans);
    return 0;
}

差分约束系统

差分约束系统用于求解 \(n\) 元一次不等式组。每个不等式都形如 \(x_i - x_j \le c_k\) ,其中 \(c_k\) 为常数且 \(i \not = j\)

将每个不等式都转化为 \(x_i \le x_j + c_k\) ,这与三角形不等式 \(dis_v \le dis_u + w\) 十分相似。那么对于一组不等式 \(x_v - x_u \le w\) ,建边 \((u, v, w)\)

从超级源点向每个点连一条边权为 \(0\) 的边,若建图后图中有负环则方程组无解,否则 \(x_i = dis_i\) 就是方程组的一组解。

一些技巧:

  • \(x_i - x_j < c_k\) 可以转化为 \(x_i - x_j \le c_k - 1\)
  • \(x_i = x_j\) 可以转化为 \(x_i - x_j \le 0\)\(x_j - x_i \le 0\)

差分约束系统可以用于处理字典序极值的解,这基于变量有界的基础上。

不妨设希望求出当限制 \(x_i \le 0\) 时整个差分约束系统的字典序的最大值,考虑将 \(x_i \le 0\) 视为 \((x_0 = 0) + 0 \ge x_i\) ,这样跑出来的解就是字典序最大的。这是因为最短路上的边均有 \(x_u + w(u, v) = x_v\) ,若将 \(x_i\) 增大 \(1\) 则最短路上至少有一条边的限制无法被满足。

对于字典序最小解,考虑限制 \(x_i \ge 0\) ,字典序最小即字典序最大时的相反数。

注意下界可以动态调整,给整体加上 \(d\) 即可调整。

[AGC056C] 01 Balanced

构造长度为 \(n\) 的字典序最小的 \(01\) 字符串,满足 \(m\) 组子串 \([l_i, r_i]\) 含相同数量的 \(0\)\(1\)

\(n \le 10^6\)\(m \le 2 \times 10^5\) ,保证 \(r- l + 1\) 是偶数

考虑将 \(0\) 当作 \(1\)\(1\) 当作 \(-1\) 。因为要让答案的字典序最小,即 \(s_i\) 尽可能大,即需要求出字典序最大的一组解,便于差分约束系统求解。

考虑相邻两个位置的限制从 \(|s_i - s_{i - 1}| = 1\) 弱化为 \(|s_i - s_{i - 1}| \le 1\) ,因为不可能存在 \(s_{i - 1} = s_i\) (若存在可以构造出 \(\pm 1\) 交错的 \(s\) 使得字典序更大)。

对于一组限制,转化为 \(s_{l - 1} = s_r\) ,于是 01bfs 即可求解。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e6 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

int dis[N];

int n, m;

inline void bfs() {
    memset(dis + 1, inf, sizeof(int) * n);
    deque<int> q;
    dis[0] = 0, q.emplace_back(0);

    while (!q.empty()) {
        int u = q.front();
        q.pop_front();

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;

                if (w)
                    q.emplace_back(v);
                else
                    q.emplace_front(v);
            }
        }
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        G.insert(i - 1, i, 1), G.insert(i, i - 1, 1);

    for (int i = 1; i <= m; ++i) {
        int l, r;
        scanf("%d%d", &l, &r);
        G.insert(l - 1, r, 0), G.insert(r, l - 1, 0);
    }

    bfs();

    for (int i = 1; i <= n; ++i)
        putchar(dis[i] < dis[i - 1] ? '1' : '0');
    
    return 0;
}

[AGC036D] Negative Cycle

有一张 \(n\) 个点的有向图,形如:

  • \(n - 1\) 条形如 \(i \to i + 1\) 边权为 \(0\) 的边,不可删去。

  • 对于每一对 \(i < j\) ,存在边权为 \(-1\) 的边。

  • 对于每一对 \(i > j\) ,存在边权为 \(1\) 的边。

删去边 \((i, j)\) 花费 \(a_{i, j}\) 的代价,求图中不存在负环的最小删边代价。

\(n \le 500\)

考虑差分约束系统,要求图上没有负环,等价于存在一组差分约束的合法解,那么可以把图上的边都写成不等式。

设差分约束系统的合法解为 \(x_{1 \sim n}\) ,记 \(x\) 的差分数组为 \(c_i = x_i - x_{i + 1}\) ,则:

  • 对于边 \(i \to i + 1\) ,其等价于 \(x_i - x_{i + 1} = c_i \ge 0\)

  • 对于边 \(i \to j\)\(i < j\) ),其等价于 \(x_i - x_j = \sum_{k = i}^{j - 1} c_k \ge 1\) ,即 \([i, j - 1]\) 的区间和非 \(0\)

  • 对于边 \(i \to j\)\(i > j\) ),其等价于 \(x_j - x_i = \sum_{k = j}^{i - 1} c_k \le 1\)

可以发现,若 \(c_i \ge 2\) ,则可以调整为 \(c_i = 1\) ,答案不会变劣,因此仅需考虑 \(c_i \in \{ 0, 1 \}\) 的情况。

\(f_{i, j}\) 表示考虑到 \(i\)\(c_i = 1\) ,上一个 \(c\)\(1\) 的位置为 \(j\) 的最小代价,考虑如何从 \(f_{j, k}\) 转移到 \(f_{i, j}\)

  • 左右端点都在 \([j + 1, i]\) 中的第二类边需要删去。
  • 左端点 \(\in [k + 1, j]\) ,右端点 \(\in [i + 1, n]\) 的第三类边需要删去。

不难用二维前缀和统计,时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e2 + 7;

ll f[N][N], s2[N][N], s3[N][N];
int a[N][N];

int n;

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j) {
            if (i < j)
                scanf("%lld", s2[i] + (j - 1));
            else if (i > j)
                scanf("%lld", s3[j] + (i - 1));
        }

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j) {
            s2[i][j] += s2[i - 1][j] + s2[i][j - 1] - s2[i - 1][j - 1];
            s3[i][j] += s3[i - 1][j] + s3[i][j - 1] - s3[i - 1][j - 1];
        }

    ll ans = 1e18;

    auto calc = [](int k, int j, int i) {
        return s2[i - 1][i - 1] - s2[j][i - 1] - s2[i - 1][j] + s2[j][j] +
            s3[j][n] - s3[k][n] - s3[j][i - 1] + s3[k][i - 1];
    };

    for (int i = 1; i < n; ++i) {
        f[i][0] = calc(0, 0, i);

        for (int j = 1; j < i; ++j) {
            f[i][j] = 1e18;

            for (int k = 0; k < j; ++k)
                f[i][j] = min(f[i][j], f[j][k] + calc(k, j, i));
        }

        for (int j = 0; j < i; ++j)
            ans = min(ans, f[i][j] + calc(j, i, n));
    }

    printf("%lld", ans);
    return 0;
}

P3971 [TJOI2014] Alice and Bob

对于序列 \(x_{1 \sim n}\) ,记 \(a_i\) 表示以 \(i\) 结尾的 LIS 的长度,\(b_i\) 表示以 \(i\) 开头的 LDS 的长度。

给出 \(a_{1 \sim n}\) ,求所有 \(x_{1 \sim n}\) 的解中 \(\sum b_i\) 的最大值。

\(n \le 10^5\)

首先有结论,\(x_{1 \sim n}\) 一定可以取到一个排列,因为若 \(x_i = x_j\) ,则可以调整为 \(x_i > x_j\) 使得 \(\sum b_i\) 更大。

考虑 \(a_{1 \sim n}\)\(x_{1 \sim n}\) 有何限制,记 \(lst_x\) 表示上一个 \(a_i = x\)\(i\) ,则 \(x_{lst_{a_i}} > x_i\)\(x_{lst_{a_i - 1}} < x_i\) ,因为 \(a\) 相同时靠后的一定更小。

由此考虑建立差分约束系统,对于 \(x_i < x_j\) 的限制,连 \(i \to j\) 的有向边,则该图的拓扑序即为一组解。

为了最大化 \(\sum b_i\) ,显然在多个可以填的位置中选最靠后的最优,因此只要将拓扑排序中的队列换成优先队列即可。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    int indeg[N];

    inline void insert(int u, int v) {
        e[u].emplace_back(v), ++indeg[v];
    }
} G;

int a[N], lst[N], b[N], f[N];

int n;

inline void Toposort() {
    priority_queue<int> q;

    for (int i = 1; i <= n; ++i)
        if (!G.indeg[i])
            q.emplace(i);

    int cnt = 0;

    while (!q.empty()) {
        int u = q.top();
        q.pop(), b[u] = ++cnt;

        for (int v : G.e[u])
            if (!--G.indeg[v])
                q.emplace(v);
    }
}

namespace BIT {
int c[N];

inline void update(int x, int k) {
    for (; x <= n; x += x & -x)
        c[x] = max(c[x], k);
}

inline int query(int x) {
    int res = 0;

    for (; x; x -= x & -x)
        res = max(res, c[x]);

    return res;
}
} // namespace BIT

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i) {
        scanf("%d", a + i);

        if (lst[a[i]])
            G.insert(i, lst[a[i]]);

        if (lst[a[i] - 1])
            G.insert(lst[a[i] - 1], i);

        lst[a[i]] = i;
    }

    Toposort();
    ll ans = 0;

    for (int i = n; i; --i)
        BIT::update(b[i], f[i] = BIT::query(b[i] - 1) + 1), ans += f[i];
    
    printf("%lld", ans);
    return 0;
}

P7515 [省选联考 2021 A 卷] 矩阵游戏

有一个 \(n \times m\) 的矩阵 \(a\) ,其中 \(a_{i, j} \le 10^6\) 。定义一个 \((n - 1) \times (m - 1)\) 的矩阵 \(b\) ,其中 \(b_{i, j} = a_{i, j} + a_{i, j + 1} + a_{i + 1, j} + a_{i + 1, j + 1}\)

给定矩阵 \(b\) ,构造一组合法的 \(a\) ,或判定无解。

\(n, m \le 300\)

考虑构造一组特解然后调整。构造一组特解是简单的,直接令 \(a_{n, i} = a_{i, m} = 0\) 然后递推即可,接下来考虑调整使其满足 \(a_{i, j} \le 10^6\) 的限制。可以发现存在一种调整方式为:

\[\begin{bmatrix} A_{1, 1} + c_1 + d_1 & A_{1, 2} - c_1 + d_2 & A_{1, 3} + c_1 + d_3 & A_{1, 4} - c_1 + d_4 & \cdots \\ A_{2, 1} + c_2 - d_1 & A_{2, 2} - c_2 - d_2 & A_{2, 3} + c_2 - d_3 & A_{2, 4} - c_2 - d_4 & \cdots \\ A_{3, 1} + c_3 + d_1 & A_{3, 2} - c_3 + d_2 & A_{3, 3} + c_3 + d_3 & A_{3, 4} - c_3 + d_4 & \cdots \\ A_{4, 1} + c_4 - d_1 & A_{4, 2} - c_4 - d_2 & A_{4, 3} + c_4 - d_3 & A_{4, 4} - c_4 - d_4 & \cdots \\ \vdots & \vdots & \vdots & \vdots & \ddots \end{bmatrix} \]

考虑差分约束:

  • \(2 \mid i\)\(2 \mid j\)\(0 \le A_{i, j} - c_i - d_j \le 10^6\)
  • \(2 \mid i\)\(2 \nmid j\)\(0 \le A_{i, j} + c_i - d_j \le 10^6\)
  • \(2 \nmid i\)\(2 \mid j\)\(0 \le A_{i, j} - c_i + d_j \le 10^6\)
  • \(2 \nmid i\)\(2 \nmid j\)\(0 \leq A_{i, j} + c_i + d_j \le 10^6\)

可以发现 \([2 \mid i] \ne [2 \mid j]\) 的形式均为 \(c - d \le k\)\(d - c \le k\) ,已经可以表示为差分约束的形式,但是 \([2 \mid i] = [2 \mid j]\) 的情况并不能表示为差分约束系统。

考虑记 \(x_i = (-1)^i \times c_i, y_i = (-1)^{i + 1} \times d_i\) ,则:

  • \(2 \mid i\)\(2 \mid j\)\(0 \le A_{i, j} -x_i + y_j \le 10^6\)
  • \(2 \mid i\)\(2 \nmid j\)\(0 \le A_{i, j} + x_i + y_j \le 10^6\)
  • \(2 \nmid i\)\(2 \mid j\)\(0 \le A_{i, j} + x_i - y_j \le 10^6\)
  • \(2 \nmid i\)\(2 \nmid j\)\(0 \leq A_{i, j} -x_i + y_j \le 10^6\)

直接跑差分约束系统即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 6e2 + 7;

struct Graph {
    vector<pair<int, int> > e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

ll dis[N];
int b[N][N], a[N][N], cnt[N];
bool inque[N];

int n, m;

inline bool SPFA() {
    queue<int> q;

    for (int i = 1; i <= n + m; ++i)
        dis[i] = cnt[i] = 0, q.emplace(i), inque[i] = true;

    while (!q.empty()) {
        int u = q.front();
        q.pop(), inque[u] = false;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w, ++cnt[v];

                if (cnt[v] >= n + m)
                    return false;

                if (!inque[v])
                    q.emplace(v), inque[v] = true;
            }
        }
    }

    return true;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &m);

        for (int i = 1; i < n; ++i)
            for (int j = 1; j < m; ++j)
                scanf("%d", b[i] + j);

        memset(a[n] + 1, 0, sizeof(int) * m);

        for (int i = 1; i <= n; ++i)
            a[i][m] = 0;

        for (int i = n - 1; i; --i)
            for (int j = m - 1; j; --j)
                a[i][j] = b[i][j] - a[i + 1][j] - a[i][j + 1] - a[i + 1][j + 1];

        G.clear(n + m);

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j) {
                if ((i + j) & 1)
                    G.insert(i, j + n, a[i][j]), G.insert(j + n, i, 1e6 - a[i][j]);
                else
                    G.insert(j + n, i, a[i][j]), G.insert(i, j + n, 1e6 - a[i][j]);
            }

        if (!SPFA()) {
            puts("NO");
            continue;
        }

        puts("YES");

        for (int i = 1; i <= n; ++i, puts(""))
            for (int j = 1; j <= m; ++j)
                printf("%d ", a[i][j] + ((i + j) & 1 ? dis[i] - dis[j + n] : dis[j + n] - dis[i]));
    }

    return 0;
}

同余最短路

同余最短路利用同余来构造一些状态。设模数为 \(m\) ,考虑将 \(0 \sim m - 1\) 看作单源最短路中的点,然后跑单源最短路求出每个余数的最小解(最小能表示的数),则该解加上若干倍的 \(m\) 均能被表示。

注意到这本质和模意义下的完全背包相同。对于体积为 \(v_i\) 的物品,其会形成 \(d = \gcd(v_i, m)\) 个环。由于至多在环上转一圈,因此只要加入 \(\frac{m}{\gcd(v_i, m)} - 1\) 个。对于每一个环,只要绕着这个环转两圈即可考虑到所有转移,因为每个点都转移到了子环上其它所有点。

P3403 跳楼机

给出 \(x, y, z, h\) ,求有多少 \(k \in [1, h]\) 满足 \(ax + by + cz = k\)

\(x, y, z \le 10^5\)\(h \le 2^{63} - 1\)

不妨设 \(x < y < z\) 。令 \(d_i\) 表示仅通过 \(by + cz\) 后能得到的模 \(x\) 下与 \(i\) 同余的最小数,用来计算该同余类满足条件的数个数。可以建边:\((i, (i + y) \bmod x, y), (i, (i + z) \bmod x, z)\) ,于是跑一次最短路即可求出 \(d_i\)

\(1\) 作为源点,此时 \(dis_1 = 1\) 最小,即可得到最小的一组解,类比差分约束即可得到所有解,答案即为 \(\sum_{i = 0}^{x - 1} (\frac{h - d_i}{x} + 1)\) ,时间复杂度 \(O(x \log x)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = (1ull << 63) - 1;
const int N = 1e5 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

ll dis[N];

ll h, x, y, z;

inline void Dijkstra() {
    fill(dis, dis + x, inf);
    priority_queue<pair<ll, int> > q;
    dis[0] = 0, q.emplace(-dis[0], 0);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

signed main() {
    scanf("%lld%lld%lld%lld", &h, &x, &y, &z);

    if (x == 1 || y == 1 || z == 1)
        return printf("%lld\n", h), 0;

    --h;

    for (int i = 0; i < x; ++i)
        G.insert(i, (i + y) % x, y), G.insert(i, (i + z) % x, z);

    Dijkstra();
    ll ans = 0;

    for (int i = 0; i < x; ++i)
        if (h >= dis[i])
            ans += (h - dis[i]) / x + 1;

    printf("%lld", ans);
    return 0;
}

使用转圈技巧可以做到线性。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = (1ull << 63) - 1;
const int N = 1e5 + 7;

ll dis[N];
int vis[N];

ll h, x, y, z;

signed main() {
    scanf("%lld%lld%lld%lld", &h, &x, &y, &z);

    if (x == 1 || y == 1 || z == 1)
        return printf("%lld\n", h), 0;

    --h, fill(dis, dis + x, inf), dis[0] = 0;

    auto update = [](int m, int k) {
        memset(vis, 0, sizeof(int) * m);

        for (int i = 0; i < m; ++i) {
            if (vis[i])
                continue;

            for (int x = i; vis[x] < 2; x = (x + k) % m) {
                ++vis[x];

                if (dis[x] != inf)
                    dis[(x + k) % m] = min(dis[(x + k) % m], dis[x] + k);
            }
        }
    };

    update(x, y), update(x, z);
    ll ans = 0;

    for (int i = 0; i < x; ++i)
        if (h >= dis[i])
            ans += (h - dis[i]) / x + 1;

    printf("%lld", ans);
    return 0;
}

[ABC077D] Small Multiple

给定一个整数 \(K\)。求一个 \(K\) 的正整数倍 \(S\),使得 \(S\) 的数位累加和最小。

\(2 \le K \le 10^5\)

注意到一个数都可以通过 \(+1\)\(\times 10\) 得到。\(+1\) 时数位累加和增加,\(\times 10\) 时不变。

因为不需要求出具体数值,输出数位累加和即可,所以我们在 \(\bmod k\) 意义下利用同余最短路配合 01BFS 计算即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;

bool vis[N];

int K, ans;

signed main() {
    scanf("%d", &K);
    deque<pair<int, int> > q;
    q.emplace_back(1, 1), vis[1] = true;
    
    while (!q.empty()) {
        int num = q.front().first, w = q.front().second;
        q.pop_front();
        
        if (!num) {
            printf("%d", w);
            break;
        }
        
        if (!vis[num * 10 % K])
            vis[num * 10 % K] = true, q.emplace_front(num * 10 % K, w);
        
        if (!vis[num + 1])
            q.emplace_back(num + 1, w + 1);
    }
    
    return 0;
}

删边最短路

CF1163F Indecisive Taxi Fee

给出一张无向带正权图, \(q\) 次询问,每次询问给出 \(t, x\),求若将 \(t\) 这条边的长度修改为 \(x\)\(1\)\(n\) 的最短路长度。

\(n, m, q \le 2 \times 10^5\)

首先,若这条边不在最短路上,则答案要么为原来的最短路,要么为经过这条边的最短路,即:

\[ans = \min \{ dis_{1, n}, dis_{1, u} + x + dis_{v, n}, dis_{1, v} + k + dis_{u, n} \} \]

否则又分两种情况。若走这条边,答案为 \(dis_{1, u} + w(u, v) + dis_{v, n}\)

若不走这条边,设删掉这条边后找出的最短路为 \(E\),共有 \(k\) 条边分别为 \(e_{1 \sim k}\)

结论:删掉任意一条边后,一定存在一条 \(1\)\(n\) 的最短路有一个前缀(可能为空)和 \(E\) 重合,有一个后缀(也可能为空)和 \(E\) 重合,中间的部分都不在 \(E\) 上。这是因为若有两段不在 \(E\) 上,因为只删掉了一条边,所以将其中一段换为 \(E\) 上的一段一定不劣。

设:

  • \(l_x\) 表示最小的 \(i\) 使得在某条 \(1 \to x\) 的最短路上 \(e_i\) 是第一条 \(E\) 上的不在其中的边。
  • \(r_x\) 表示最大的 \(i\) 使得在某条 \(x\to n\) 的最短路上 \(e_i\) 是最后一条 \(E\) 上的不在其中的边。

考虑求 \(l_x, r_x\) 。首先以 \(1\)\(n\) 为源点分别求一遍最短路,找出一条最短路 \(E\) 。对于 \(E\) 上的第 \(i\) 个点 \(x\),初始化 \(l_x = i, r_x = i - 1\)

\(l\) 为例,\(r\) 同理。若边 \((u,v)\) 满足 \(d_{1,u}+w_{u,v}=d_{1,v}\),则 \(l_v=\min(l_v,l_u)\)。按照 \(dis_{1, i}\) 排序后则可以线性更新。注意此时需要满足 \(1\to x\)\(E\) 只有一个前缀重合,所以不能用 \(E\) 上的边更新。

\(a_i\) 为删掉 \(e_i\) 之后的答案。求出 \(l, r\) 后枚举不在 \(E\) 上的边 \((u,v)\),用 \(d_{1,u}+w_{u,v}+d_{v,n}\) 更新 \([a_{l_u},a_{r_v}]\),用 \(d_{1,v}+w_{u,v}+d_{u,n}\) 更新 \([a_{l_v},a_{r_u}]\)。需要支持区间取 \(\min\) ,最后单点查询,离线用 multiset 做一遍扫描线即可。

时间复杂度 \(O(m \log n + q)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e5 + 7;

struct Graph {
    struct Edge {
        int nxt, v, w;
    } e[N << 1];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v, int w) {
        e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
    }
} G;

struct Edge {
    int u, v, w, id;
} e[N];

vector<ll> ins[N], rmv[N];

ll dis1[N], disn[N], ans[N];
int l[N], r[N];

int n, m, q, Len = 1;

inline void Dijkstra(int S, ll *dis) {
    memset(dis + 1, inf, sizeof(ll) * n);
    priority_queue<pair<ll, int> > q;
    dis[S] = 0, q.emplace(-dis[S], S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

signed main() {
    scanf("%d%d%d", &n, &m, &q);

    for (int i = 1; i <= m; ++i) {
        scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
        G.insert(e[i].u, e[i].v, e[i].w), G.insert(e[i].v, e[i].u, e[i].w);
    }

    Dijkstra(1, dis1), Dijkstra(n, disn);
    fill(l + 1, l + 1 + n, n + 1), fill(r + 1, r + 1 + n, 0);

    for (int u = 1; u != n;) {      
        l[u] = ++Len, r[u] = Len - 1;

        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (disn[v] + w == disn[u]) {
                u = v, e[i / 2].id = Len;
                break;
            }
        }
    }

    l[n] = ++Len, r[n] = Len - 1;
    vector<int> id(n); 
    iota(id.begin(), id.end(), 1);
    sort(id.begin(), id.end(), [](const int &a, const int &b) { return dis1[a] < dis1[b]; });

    for (int u : id)
        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (!e[i / 2].id && dis1[u] + w == dis1[v])
                l[v] = min(l[v], l[u]);
        }

    sort(id.begin(), id.end(), [](const int &a, const int &b) { return disn[a] < disn[b]; });

    for (int u : id)
        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v, w = G.e[i].w;

            if (!e[i / 2].id && disn[u] + w == disn[v])
                r[v] = max(r[v], r[u]);
        }

    for (int i = 1; i <= m; ++i) {
        if (e[i].id)
            continue;

        int u = e[i].u, v = e[i].v, w = e[i].w;

        if (l[u] <= r[v]) {
            ins[l[u]].emplace_back(dis1[u] + w + disn[v]);
            rmv[r[v]].emplace_back(dis1[u] + w + disn[v]);
        }

        if (l[v] <= r[u]) {
            ins[l[v]].emplace_back(dis1[v] + w + disn[u]);
            rmv[r[u]].emplace_back(dis1[v] + w + disn[u]);
        }
    }

    multiset<ll> st;

    for (int i = 1; i <= Len; ++i) {
        for (ll it : ins[i])
            st.insert(it);

        ans[i] = st.empty() ? inf : *st.begin();

        for (ll it : rmv[i])
            st.erase(st.find(it));
    }

    while (q--) {
        int x, k;
        scanf("%d%d", &x, &k);
        int u = e[x].u, v = e[x].v, w = e[x].w;

        if (e[x].id)
            printf("%lld\n", min(dis1[n] + k - w, ans[e[x].id]));
        else
            printf("%lld\n", min(dis1[n], min(dis1[u] + k + disn[v], dis1[v] + k + disn[u])));
    }

    return 0;
}

网格图最短路相关

P5897 [IOI 2013] wombats

给定一个 \(n \times m\) 的网格图,相邻两个格子之间有边,边带边权,移动时 \((x, y)\) 只能移动到 \((x + 1, y), (x, y - 1), (x, y + 1)\) 三者之一且不能移出网格。

\(q\) 次操作,操作有:

  • 修改某条边的边权,共 \(C\) 次。
  • 查询 \((1, x) \to (n, y)\) 的最短路,共 \(Q\) 次。

\(n \le 5000\)\(m \le 200\)\(C \le 500\)\(Q \le 2 \times 10^5\) ,TL = 8s,ML = 250MB

注意到 \(n, m\) 范围差别较大,考虑对 \(n\) 一维开线段树维护,线段树上每个区间 \([l, r]\) 维护矩阵 \(f_{i, j}\) 表示 \((l, i) \to (r, j)\) 的最短路,合并时做 \((\min, +)\) 矩阵乘法。预处理直接做是 \(O(n m^3 \log n)\) 的,无法通过。

考虑优化,观察矩乘的形式 \(f_{i, j} = \min \{ fl_{i, k} + fr_{k, j} \}\) ,可以发现当 \(i < j\)\(i > j\)\(k\) 均有决策单调性。记 \(p_{i, j}\) 为决策点,则 \(p_{i, j - 1} \le p_{i, j} \le p_{i + 1, j}\) ,因此可以用 Knuth's Optimization 优化到 \(O(m^2)\) ,预处理的复杂度降为 \(O(n m^2 \log n)\)

但是此时空间是 \(O(n m^2)\) 的,无法接受。考虑将连续 \(B\) 行的状态压缩到线段树的叶子上,即当 \(r - l + 1 \le B\) 的时候定义该点为叶子,每次更新叶子时暴力 \(O(B m^2)\) 处理。

时间复杂度 \(O(n m^2 \log n + C \times m^2 (B + \log \frac{n}{B}) + Q)\) ,空间复杂度 \(O(\frac{n m^2}{B})\) ,取 \(B = 15\) 即可。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e3 + 7, M = 2e2 + 7, B = 15;

int w1[N][M], w2[N][M];

int n, m, q;

struct Matrix {
    int a[M][M];

    inline Matrix(bool flag = false) {
        memset(a, inf, sizeof(a));

        if (flag) {
            for (int i = 1; i <= m; ++i)
                a[i][i] = 0;
        }
    }
};

inline Matrix getmatrix(int x) {
    vector<int> s(m);

    for (int i = 1; i < m; ++i)
        s[i] = s[i - 1] + w1[x][i];

    Matrix f;

    for (int i = 1; i <= m; ++i)
        for (int j = i; j <= m; ++j)
            f.a[i][j] = f.a[j][i] = s[j - 1] - s[i - 1];

    return f;
}

inline Matrix merge(Matrix fl, int mid, Matrix fr) {
    Matrix f;
    static int g[M][M];

    for (int i = 1; i <= m; ++i)
        for (int j = 1; j <= m; ++j)
            if (fl.a[i][j] + w2[mid][j] + fr.a[j][i] < f.a[i][i])
                f.a[i][i] = fl.a[i][j] + w2[mid][j] + fr.a[j][i], g[i][i] = j;

    for (int len = 2; len <= m; ++len)
        for (int i = 1, j = len; j <= m; ++i, ++j) {
            for (int k = g[i][j - 1]; k <= g[i + 1][j]; ++k)
                if (fl.a[i][k] + w2[mid][k] + fr.a[k][j] < f.a[i][j])
                    f.a[i][j] = fl.a[i][k] + w2[mid][k] + fr.a[k][j], g[i][j] = k;

            for (int k = g[j - 1][i]; k <= g[j][i + 1]; ++k)
                if (fl.a[j][k] + w2[mid][k] + fr.a[k][i] < f.a[j][i])
                    f.a[j][i] = fl.a[j][k] + w2[mid][k] + fr.a[k][i], g[j][i] = k;
        }

    return f;
}

namespace SMT {
Matrix mt[N / B << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void pushup(int x, int l, int r) {
    int mid = (l + r) >> 1;
    mt[x] = merge(mt[ls(x)], mid, mt[rs(x)]);
}

void build(int x, int l, int r) {
    if (r - l + 1 <= B) {
        mt[x] = getmatrix(l);

        for (int i = l; i < r; ++i)
            mt[x] = merge(mt[x], i, getmatrix(i + 1));

        return;
    }

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
    pushup(x, l, r);
}

void update(int x, int nl, int nr, int p) {
    if (nr - nl + 1 <= B) {
        mt[x] = getmatrix(nl);

        for (int i = nl; i < nr; ++i)
            mt[x] = merge(mt[x], i, getmatrix(i + 1));

        return;
    }

    int mid = (nl + nr) >> 1;

    if (p <= mid)
        update(ls(x), nl, mid, p);
    else
        update(rs(x), mid + 1, nr, p);

    pushup(x, nl, nr);
}
} // namespace SMT

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j < m; ++j)
            scanf("%d", w1[i] + j);

    for (int i = 1; i < n; ++i)
        for (int j = 1; j <= m; ++j)
            scanf("%d", w2[i] + j);

    SMT::build(1, 1, n);
    scanf("%d", &q);

    while (q--) {
        int op, x, y;
        scanf("%d%d%d", &op, &x, &y);
        ++x, ++y;

        if (op == 1) {
            int w;
            scanf("%d", &w);
            w1[x][y] = w, SMT::update(1, 1, n, x);
        } else if (op == 2) {
            int w;
            scanf("%d", &w);
            w2[x][y] = w, SMT::update(1, 1, n, x);
        } else
            printf("%d\n", SMT::mt[1].a[x][y]);
    }

    return 0;
}

P3350 [ZJOI2016] 旅行者

给定一个 \(n \times m\) 的网格图,相邻两个格子之间有边,边带边权,\(q\) 次询问 \((x_1, y_1), (x_2, y_2)\) 两点间的最短路。

\(n \times m \le 2 \times 10^4\)\(q \le 10^5\)

考虑猫树分治,每次选取区间更长的维度分治,不妨设当前处理 \(x_1, x_2 \in [l_x, r_x]\)\(y_1, y_2 \in [l_y, r_y]\) 的所有询问,当前分割区间为 \([l_x, mid]\)\([mid + 1, r_x]\)

对于 \((mid, l_y \sim r_y)\) 的每个中转点,求出只经过 \(x \in [l_x, r_x], y \in [l_y, r_y]\) 的点时所有 \((x, y)\) 与它的距离,然后用 \(dis_{x_1, y_1} + dis_{x_2, y_2}\) 更新答案。

下面说明该做法的正确性,如果一个询问递归到了左区间,则所有中转点在右区间的路径一定经过 \(mid\) ,因此 \((mid, l_y \sim r_y)\) 同样在路径上,其作为中转点一定更新过答案,因此无需递归右区间更新该询问。

\(A = nm\) ,时间复杂度 \(O(A \sqrt A \log A + q \sqrt{A})\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e4 + 7, B = 1.5e2 + 7, Q = 1e5 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

struct Query {
    int bx, by, ex, ey;
} qry[Q];

int dis[N], ans[Q];

int n, m, q;

inline int getid(int x, int y) {
    return (x - 1) * m + y;
}

inline pair<int, int> getpos(int k) {
    return make_pair(k / m + (k % m ? 1 : 0), k % m ? k % m : m);
}

inline void Dijkstra(int S, int xl, int xr, int yl, int yr) {
    memset(dis + 1, inf, sizeof(int) * (n * m));
    priority_queue<pair<int, int> > q;
    dis[S] = 0, q.emplace(0, S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        for (auto it : G.e[u]) {
            int v = it.first, w = it.second, x = getpos(v).first, y = getpos(v).second;

            if (xl <= x && x <= xr && yl <= y && y <= yr && dis[v] > dis[u] + w)
                dis[v] = dis[u] + w, q.emplace(-dis[v], v);
        }
    }
}

void solve(int xl, int xr, int yl, int yr, vector<int> &id) {
    if (id.empty())
        return;

    if (xl == xr && yl == yr) {
        for (int it : id)
            ans[it] = 0;

        return;
    }

    if (xr - xl + 1 >= yr - yl + 1) {
        int mid = (xl + xr) >> 1;

        for (int i = yl; i <= yr; ++i) {
            Dijkstra(getid(mid, i), xl, xr, yl, yr);

            for (int it : id)
                ans[it] = min(ans[it], dis[getid(qry[it].bx, qry[it].by)] + dis[getid(qry[it].ex, qry[it].ey)]);
        }

        vector<int> ql, qr;

        for (int it : id) {
            if (max(qry[it].bx, qry[it].ex) <= mid)
                ql.emplace_back(it);
            else if (min(qry[it].bx, qry[it].ex) > mid)
                qr.emplace_back(it);
        }

        solve(xl, mid, yl, yr, ql), solve(mid + 1, xr, yl, yr, qr);
    } else {
        int mid = (yl + yr) >> 1;

        for (int i = xl; i <= xr; ++i) {
            Dijkstra(getid(i, mid), xl, xr, yl, yr);

            for (int it : id)
                ans[it] = min(ans[it], dis[getid(qry[it].bx, qry[it].by)] + dis[getid(qry[it].ex, qry[it].ey)]);
        }

        vector<int> ql, qr;

        for (int it : id) {
            if (qry[it].by > qry[it].ey)
                swap(qry[it].bx, qry[it].ex), swap(qry[it].by, qry[it].ey);

            if (max(qry[it].by, qry[it].ey) <= mid)
                ql.emplace_back(it);
            else if (min(qry[it].by, qry[it].ey) > mid)
                qr.emplace_back(it);
        }

        solve(xl, xr, yl, mid, ql), solve(xl, xr, mid + 1, yr, qr);
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j < m; ++j) {
            int w;
            scanf("%d", &w);
            G.insert(getid(i, j), getid(i, j + 1), w);
            G.insert(getid(i, j + 1), getid(i, j), w);
        }

    for (int i = 1; i < n; ++i)
        for (int j = 1; j <= m; ++j) {
            int w;
            scanf("%d", &w);
            G.insert(getid(i, j), getid(i + 1, j), w);
            G.insert(getid(i + 1, j), getid(i, j), w);
        }

    scanf("%d", &q);

    for (int i = 1; i <= q; ++i)
        scanf("%d%d%d%d", &qry[i].bx, &qry[i].by, &qry[i].ex, &qry[i].ey);

    memset(ans + 1, inf, sizeof(int) * q);
    vector<int> id(q);
    iota(id.begin(), id.end(), 1);
    solve(1, n, 1, m, id);

    for (int i = 1; i <= q; ++i)
        printf("%d\n", ans[i]);

    return 0;
}

最短路径

给定一张 \(n \times m\) 的网格图,每个点 \((i, j)\) 可以移动到 \((i - 1, j)\)\((i + 1, j)\)\((i, j + 1)\) 中的一个位置(需满足在网格图内),每条横纵边带边权。

对于 \(i = 1, 2, \cdots, n\) ,求 \(\sum_{j = 1}^n \mathrm{dist}((i, 1), (j, m))\) ,其中 \(\mathrm{dist}\) 表示最短路。

\(n \times m \le 2 \times 10^5\)

考虑起点向下移动时 SPT 的变化,显然每个点的父亲只会不断向下移动。

考虑分治,假设已经求出 \(i = l\)\(i = r\) 的 SPT,若某个点的父亲在这两棵树上相同,则 \(i = l, l + 1, \cdots, r\) 时该点的父亲都相同,可以将其与父亲缩起来。

由于每条边只会在 \(O(\log n)\) 个分治区间出现,使用线性求 SPT 的做法(从后往前做类似前缀和优化的操作),时间复杂度 \(O(nm \log(nm))\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e5 + 7;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

vector<int> a[N], b[N];

ll ans[N];

int n, m;

struct SPT {
    vector<tuple<int, int, ll> > edg;
    vector<pair<int, ll> > ans;
    vector<ll> dis;

    int siz;

    inline SPT() : siz(0) {}

    inline void build(int s) {
        dis.assign(siz + 1, inf), dis[s] = 0;

        for (auto it : edg)
            dis[get<1>(it)] = min(dis[get<1>(it)], dis[get<0>(it)] + get<2>(it));
    }

    inline SPT(int s) {
        siz = n * m, ans.assign(siz + 1, make_pair(0, 0ll));

        auto getid = [&](int x, int y) {
            return (x - 1) * n + y;
        };

        for (int i = 1; i <= n; ++i)
            ans[getid(m, i)].first = 1;

        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j < n; ++j)
                edg.emplace_back(getid(i, j), getid(i, j + 1), b[j][i]);

            for (int j = n - 1; j; --j)
                edg.emplace_back(getid(i, j + 1), getid(i, j), b[j][i]);

            if (i < m) {
                for (int j = 1; j <= n; ++j)
                    edg.emplace_back(getid(i, j), getid(i + 1, j), a[j][i]);
            }
        }

        build(s);
    }

    inline ll calc() {
        ll res = 0;

        for (int i = 1; i <= siz; ++i)
            res += dis[i] * ans[i].first + ans[i].second;

        return res;
    }
};

inline SPT maintain(SPT A, SPT B) {
    SPT C;
    dsu.prework(A.siz);
    auto ans = A.ans;

    for (auto it : A.edg) {
        int u = get<0>(it), v = get<1>(it);
        ll w = get<2>(it);

        if (dsu.find(v) == v && A.dis[u] + w == A.dis[v] && B.dis[u] + w == B.dis[v]) {
            dsu.merge(u = dsu.find(u), v);
            ans[u].first += ans[v].first, ans[u].second += ans[v].second + w * ans[v].first;
        }
    }

    vector<int> id(A.siz + 1, -1);
    C.ans = {make_pair(0, 0)}, C.dis = {0};

    for (int i = 1; i <= A.siz; ++i)
        if (dsu.find(i) == i)
            id[i] = ++C.siz, C.ans.emplace_back(ans[i]), C.dis.emplace_back(A.dis[i]);

    for (auto it : A.edg) {
        int u = get<0>(it), v = get<1>(it);
        ll w = get<2>(it);

        if (dsu.find(v) == v)
            C.edg.emplace_back(id[dsu.find(u)], id[v], w + A.dis[u] - A.dis[dsu.find(u)]);
    }

    return C;
}

void solve(int l, int r, SPT Gl, SPT Gr) {
    if (l + 1 == r) {
        ans[l] = Gl.calc();
        return;
    }

    int mid = (l + r) >> 1;
    SPT Gm = Gl;
    Gm.build(mid - l + 1);
    solve(l, mid, maintain(Gl, Gm), maintain(Gm, Gl));
    solve(mid, r, maintain(Gm, Gr), maintain(Gr, Gm));
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        a[i].resize(m), b[i].resize(m + 1);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j < m; ++j)
            scanf("%d", &a[i][j]);

    for (int i = 1; i < n; ++i)
        for (int j = 1; j <= m; ++j)
            scanf("%d", &b[i][j]);

    if (n == 1)
        return printf("%lld", SPT(1).calc()), 0;

    SPT Gl(1), Gr(n);
    ans[n] = Gr.calc(), solve(1, n, Gl, Gr);

    for (int i = 1; i <= n; ++i)
        printf("%lld\n", ans[i]);

    return 0;
}
posted @ 2024-07-11 22:33  EverythingsGone  阅读(58)  评论(0)    收藏  举报