图论杂题
Trick
-
类最短路问题,可以在 dij 上跑 dp 处理。(Minimum Path)
-
图上 dp,考虑用最短路去转移。(Game on Graph)
-
对于边数与点数差距不大的图论问题,可以考虑建立搜索树。(The Shortest Statement)
-
当出现求总距离最小时,可以想一下最小生成树。(青蛙图 (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\) 的最大代价,转移就是:
因为有环,所以要放在 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;
}

浙公网安备 33010602011771号