图论杂题

Trick

  1. 类最短路问题,可以在 dij 上跑 dp 处理。(Minimum Path)

  2. 图上 dp,考虑用最短路去转移。(Game on Graph)

  3. 对于边数与点数差距不大的图论问题,可以考虑建立搜索树。(The Shortest Statement)

  4. 当出现求总距离最小时,可以想一下最小生成树。(青蛙图 (frog))

题目

Minimum Path

一个路径的价值是边权和减去极差。

分析一下这个价值,其实就是不算边权最大的那条边,然后最小的边多加一条。

考虑转化成任选一条边使得权值为 \(0\),任选一条边使得权值翻倍。

拿这个东西在 dij 上跑即可,因为最优的情况一定是选择最大和最小的边去变化。

\(f_{u,0/1,0/1}\) 表示 \(1\to u\) 的最短路,且是否进行了上述的两个操作的最短路。

当然上述情况只对路径有大于等于两条边时有用,所以特判只选一条边的情况,其实也就是 \(f_{u,0,0}\)

写法比较多,可以分层图,可以拆点,但是我是直接开了四个优先队列存对应的值,常数稍微大一点。

一个要注意的细节,有些路径会重复走一条边来使得最优,所以不能用 vis 数组来判断是否能用当前点来更新。

代码
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int N = 2e5 + 10;

int n, m;
ll f[N][2][2];

priority_queue< pair< ll, int> > q00, q01, q10, q11;
vector< pair< int, ll> > G[N];

signed main() {
    ios :: sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;
    
    for ( int i = 1; i <= m; i ++) {
        int u, v; ll w;
        cin >> u >> v >> w;
        G[u].push_back({v, w}), G[v].push_back({u, w});
    }

    memset(f, 127, sizeof f);

    f[1][0][0] = 0;
    q00.push({-f[1][0][0], 1});

    while (! (q00.empty() & q01.empty() & q10.empty() & q11.empty())) {
        if (! q00.empty()) {
            int u = q00.top().second, d = -q00.top().first;
            q00.pop();

            if (f[u][0][0] >= d) {
                for ( auto [v, w] : G[u]) {
                    if (f[v][0][0] > f[u][0][0] + w) {
                        f[v][0][0] = f[u][0][0] + w;
                        q00.push({-f[v][0][0], v});
                    }

                    if (f[v][1][0] > f[u][0][0]) {
                        f[v][1][0] = f[u][0][0];
                        q10.push({-f[v][1][0], v});
                    }

                    if (f[v][0][1] > f[u][0][0] + 2 * w) {
                        f[v][0][1] = f[u][0][0] + 2 * w;
                        q01.push({-f[v][0][1], v});
                    }
                }
            }            
        }

        if (! q01.empty()) {
            int u = q01.top().second, d = -q01.top().first;
            q01.pop();

            if (f[u][0][1] >= d) {
                for ( auto [v, w] : G[u]) {
                    if (f[v][1][1] > f[u][0][1]) {
                        f[v][1][1] = f[u][0][1];
                        q11.push({-f[v][1][1], v});
                    }

                    if (f[v][0][1] > f[u][0][1] + w) {
                        f[v][0][1] = f[u][0][1] + w;
                        q01.push({-f[v][0][1], v});
                    }
                }
            }
        }

        if (! q10.empty()) {
            int u = q10.top().second, d = -q10.top().first;
            q10.pop();

            if (f[u][1][0] >= d) {
                for ( auto [v, w] : G[u]) {
                    if (f[v][1][1] > f[u][1][0] + 2 * w) {
                        f[v][1][1] = f[u][1][0] + 2 * w;
                        q11.push({-f[v][1][1], v});
                    }

                    if (f[v][1][0] > f[u][1][0] + w) {
                        f[v][1][0] = f[u][1][0] + w;
                        q10.push({-f[v][1][0], v});
                    }
                }
            }
        }

        if (! q11.empty()) {
            int u = q11.top().second, d = -q11.top().first;
            q11.pop();

            if (f[u][1][1] >= d) {
                for ( auto [v, w] : G[u]) {
                    if (f[v][1][1] > f[u][1][1] + w) {
                        f[v][1][1] = f[u][1][1] + w;
                        q11.push({-f[v][1][1], v});
                    }
                }
            }
        }
    }

    for ( int i = 2; i <= n; i ++) {
        cout << min(f[i][0][0], f[i][1][1]) << ' ';
    }

    return 0;
}

Game on Graph

考虑倒着做,考虑从出度为 \(0\) 的所有点到起始点。

那么建反向边,就成了从入度为 \(0\) 的点到起始点。

\(f_{i,0}\) 表示先手到点 \(i\) 的最小代价,\(f_{i,1}\) 表示后手到点 \(i\) 的最大代价,转移就是:

\[f_{u,0}=\min_{(u,v)\in E} f_{v,1}+w(u,v) \\ f_{u,1}=\max_{(u,v)\in E} f_{v,0}+w(u,v) \]

因为有环,所以要放在 dij 上转移。

考虑把 \(f_{u,0}\) 的值当作主元进行转移,这样每次从堆里取出的 \(f_{u,0}\) 都是最小值,就可以直接拿去更新 \(f_{u,1}\)

\(f_{u,1}\) 是辅助元,为了让 \(f_{u,1}\) 取到最大值,就必须让它入边的值全部把它更新后,才能入堆。

后续做到这种题要注意一下 dij 转移时的主元与辅助元之分。

代码
#include <bits/stdc++.h>
#define int long long

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e18, mod = 998244353;

int n, m, V;

priority_queue< tuple< int, int, int> > q;
vector< pair< int, int> > G[N];

int in[N];

int f[N][2];
int vis[N][2];

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> m >> V;

    for ( int i = 1; i <= m; i ++) {
        int u, v, w; cin >> u >> v >> w;
        G[v].push_back({u, w});
        in[u] ++;
    }

    for ( int i = 1; i <= n; i ++) {
        if (! in[i]) q.push({0, i, 0}), q.push({0, i, 1});
        else f[i][0] = inf;
    }

    while (q.size()) {
        int u = get<1>(q.top()), op = get<2>(q.top()); q.pop();

        if (vis[u][op]) continue ;
        vis[u][op] = 1;

        for ( auto [v, w] : G[u]) {
            if (op) {
                if (f[v][0] > f[u][1] + w)
                    f[v][0] = f[u][1] + w, q.push({-f[v][0], v, 0});
            } else {
                f[v][1] = max(f[v][1], f[u][0] + w);
                if (! -- in[v]) q.push({-f[v][1], v, 1});
            }
        }
    }

    if (f[V][0] == inf) cout << "INFINITY\n";
    else cout << f[V][0] << '\n';

    return 0;
}

The Shortest Statement

边数比点数多不超过 \(20\),那么肯定要考虑 dfs 树了。

只有 \(20\) 条反祖边,那么只有 \(40\) 个特殊点。

考虑预处理出每个特殊点的单元最短路,统计答案就是所有特殊点中 \(dis_{x,u}+dis_{x,v}\) 的最小值。

正确性能够保证,因为至少会经过一个特殊点,那么枚举所有特殊点就肯定是包含所有情况的。

最终答案就是原树路径距离和上述值中的最小值。

代码
#include <bits/stdc++.h>

using namespace std;
using ll = long long;

const int N = 1e5 + 10;
const ll inf = 1e18;

int n, m;
vector< pair< int, ll> > G[N], E[N];

int vis[N];
int ind[N], len;

void dfs( int u, int pre) {
    vis[u] = 1;

    for ( auto [v, w] : G[u]) {
        if (v == pre) continue ;

        if (vis[v]) ind[++ len] = u, ind[++ len] = v;
        else E[u].push_back({v, w}), dfs(v, u);
    }
}

int fa[N], dep[N], siz[N], son[N];
ll dis[N];

void dfs1( int u, int fu) {
    fa[u] = fu, dep[u] = dep[fu] + 1, siz[u] = 1;

    for ( auto [v, w] : E[u]) {
        dis[v] = dis[u] + w;
        dfs1(v, u);
        siz[u] += siz[v];
        son[u] = (siz[v] > siz[son[u]] ? v : son[u]);
    }
}

int top[N];

void dfs2( int u, int topt) {
    top[u] = topt;
    if (son[u]) dfs2(son[u], topt);

    for ( auto [v, w] : E[u])
        if (v != son[u])
            dfs2(v, v);
}

int lca( int u, int v) {
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        u = fa[top[u]];
    }

    return dep[u] < dep[v] ? u : v;
}

priority_queue< pair< ll, int> > q;
ll f[45][N];

void dij( int s) {
    for ( int i = 1; i <= n; i ++) f[s][i] = inf;

    f[s][ind[s]] = 0;
    q.push({-f[s][ind[s]], ind[s]});

    while (! q.empty()) {
        auto [d, u] = q.top(); q.pop();
        d = -d;

        if (f[s][u] < d) continue ;

        for ( auto [v, w] : G[u])
            if (f[s][v] > f[s][u] + w)
                f[s][v] = f[s][u] + w, q.push({-f[s][v], v});
    }
}

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;

    for ( int i = 1; i <= m; i ++) {
        int u, v; ll w;
        cin >> u >> v >> w;
        G[u].push_back({v, w}), G[v].push_back({u, w});
    }

    dfs(1, 0);
    dfs1(1, 0), dfs2(1, 1);

    sort(ind + 1, ind + len + 1), len = unique(ind + 1, ind + len + 1) - ind - 1;

    for ( int i = 1; i <= len; i ++) dij(i);

    int q; cin >> q;

    while (q --) {
        int u, v;
        cin >> u >> v;
        ll ans = dis[u] + dis[v] - 2 * dis[lca(u, v)];

        for ( int i = 1; i <= len; i ++)
            ans = min(ans, f[i][u] + f[i][v]);

        cout << ans << '\n';
    }

    return 0;
}

青蛙图 (frog)

因为一条编号为 \(i\) 的边经过 \(n\) 次都比经过一次编号 \(i+1\) 的边强,所以不考虑新添加边的情况,答案就是最小生成树。

考虑加边,显然边的一个端点一定是 \(1\)

那么添加一条 \(1\to x\) 的边,就是在树上把连接 \(fa_x\)\(x\) 的这条边断掉。

肯定贪心地去断掉前 \(k\) 大的边,这样一定优秀。

剩下 \(k+1\) 个连通快,除了包含 \(1\) 的连通块,其他连通块都要找到一个点,使得连通块内所有点到这个点的距离之和最小。

这个很经典,选出每个连通块的重心即可。

代码
#include <bits/stdc++.h>

void Freopen() {
    freopen("frog.in", "r", stdin);
    freopen("frog.out", "w", stdout);
}

using namespace std;
const int N = 1e6 + 10, M = 2e6 + 10, inf = 1e9, mod = 998244353;

int add( int x, int y) {
    x += y;
    return x >= mod ? x - mod : x;
}

int n, m, k;

vector< pair< int, int> > G[N];

int cost[M];

struct edge {
    int u, v, w;
} E[M];

int fa[N], can[M], vis[N], siz[N];
int ans;

int find( int x) {
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

void getsiz( int u, int fu) {
    siz[u] = 1, vis[u] = 1;
    
    for ( auto [v, w] : G[u])
        if (v != fu && ! can[w])
            getsiz(v, u), siz[u] += siz[v];
}

void getrt( int u, int fu, int sum, int & rt) {
    int mx = 0;
    
    for ( auto [v, w] : G[u])
        if (v != fu && ! can[w])
            getrt(v, u, sum, rt), mx = max(mx, siz[v]);
            
    mx = max(mx, sum - siz[u]);
    if (mx <= sum / 2) rt = u;
}

void cal( int u, int fu, int dis) {
    ans = add(ans, dis);

    for ( auto [v, w] : G[u])
        if (v != fu && ! can[w])
            cal(v, u, add(dis, cost[w]));
}

void solve() {
    cin >> n >> m >> k;

    ans = 0;
    for ( int i = 1; i <= n; i ++) G[i].clear(), vis[i] = 0, fa[i] = i;
    for ( int i = 1; i <= m; i ++) can[i] = 0;

    for ( int i = 1; i <= m; i ++) {
        int u, v;
        cin >> u >> v;
        E[i] = {u, v, i};
    }

    if (k >= n - 1) return cout << "0\n", void();

    vector< int> vec;
    int cnt = 0;
    for ( int i = 1; i <= m; i ++) {
        auto [u, v, w] = E[i];
        int fu = find(u), fv = find(v);

        if (fu == fv) continue ;
        fa[fu] = fv;

        cnt ++;
        vec.push_back(i);
        G[u].push_back({v, w});
        G[v].push_back({u, w});

        if (cnt == n - 1) break ;
    }

    reverse(vec.begin(), vec.end());
    for ( int i = 1; i <= min(k, (int)vec.size()); i ++) can[vec[i - 1]] = 1;

    for ( int i = 1; i <= n; i ++) {
        if (! vis[i]) {
            if (i == 1) getsiz(1, 0), cal(1, 0, 0);
            else {
                getsiz(i, 0);
                int rt = 0;
                getrt(i, 0, siz[i], rt);
                cal(rt, 0, 0);
            }
        }
    }

    cout << ans << '\n';
}

signed main() {
    Freopen();

    ios :: sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int id, T;
    cin >> id >> T;

    cost[0] = 1;
    for ( int i = 1; i < M; i ++) cost[i] = 1ll * cost[i - 1] * 1145141 % mod;

    while (T --) solve();

    return 0;
}
posted @ 2025-11-05 11:30  咚咚的锵  阅读(9)  评论(0)    收藏  举报