图论的一些建图小技巧
\(\texttt{just some tips……}\)
\(\texttt{0x00 Lead in}\)
我们知道,图论的难点一般都不在算法的模板和原理,而在于对于题意的抽象,也就是:建图。
所以,如何建图在很大程度上影响了你能否做出这道题。
\(\texttt{0x01 tip1}\):虚点
在一些题目中(比如最短路),会有多个可能的起点,如果对这些起点都跑一次最短路算法,极其容易 TLE。
在这个时候,就可以考虑使用第一个技巧:超级源点。
例题:AcWing1137. 选择最佳线路
题目大意:
给定一张点数为 \(n\),边数为 \(m\) 的有向图,有 \(s\) 个起点,求从这 \(s\) 个起点出发到终点 \(t\) 的最短距离。
这道题也可以建反图做,这里讲一下超级源点。
想象一下将整张图竖过来,起点都在最上方,终点在最下方,素朴做法就是拿若干个杯子分别往每个起点处注水,注 \(s\) 次,十分麻烦。
其实我们只需要在所有起点的上方放一个大漏斗,连接所有的起点,我们就只需要向这个大漏斗里注水就行了。
这个大漏斗,类比的就是超级源点。
我们建立一个超级源点,向每个起点连一条长度为 \(0\) 的边,然后对这个超级源点跑一遍 dijkstra 就行了。
\(\texttt{Code:}\)
#include <queue>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1010, M = 20010;
typedef pair<int, int> PII;
int n, m, cnt, t;
int ori[N];
int h[N], e[M + N], w[M + N], ne[M + N], idx;
int dist[N];
bool st[N];
inline void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dij(int s) {
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
dist[s] = 0;
priority_queue<PII, vector<PII>, greater<PII> > q;
q.push({0, s});
while(q.size()) {
int ver = q.top().second;
q.pop();
if(st[ver]) continue;
st[ver] = true;
for(int i = h[ver]; i != -1; i = ne[i]) {
int j = e[i];
if(dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
q.push({dist[j], j});
}
}
}
}
int main() {
while(scanf("%d%d%d", &n, &m, &t) != EOF) {
memset(h, -1, sizeof h);
idx = 0;
int a, b, w;
for(int i = 1; i <= m; i++) {
scanf("%d%d%d", &a, &b, &w);
add(a, b, w);
}
scanf("%d", &cnt);
for(int i = 1; i <= cnt; i++) {
scanf("%d", &ori[i]);
add(0, ori[i], 0);
}
dij(0);
if(dist[t] == 0x3f3f3f3f) puts("-1");
else printf("%d\n", dist[t]);
}
return 0;
}
注意:有超级源点要注意存边的数组有没有开够!
拓展:P3393 逃离僵尸岛
题目大意:
有 \(k\) 个点不能通行,与这 \(k\) 个点相距小于等于 \(s\) 的点权为 \(q\),其他点为 \(p\)。
这道题要稍微复杂一点,但只要将它层层剥离开来分析,也是很简单的。
我们将题目分成两个部分:
-
一次建图,求出所有与被控制城市距离小于等于 \(s\) 的点;
-
二次建图,求出最小花费。
先来考虑 \(1\)。
先将图的边权都赋值为 \(1\),跟上面的方法一样,建立一个超级源点,向这 \(k\) 个点连一条边权为 \(0\) 的无向边,然后对这个超级源点跑一遍 dijkstra(其实可以 bfs),即可得出危险城市。
然后直接点权转边权,重新建图,再从起点跑一遍 dijkstra 就行了。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<long long, int> PII;
const int N = 100010, M = 500010;
int h[M], e[M], w[M], ne[M], idx;
int n, m, k, s, p, q;
int a[M], b[M];
int mark[N];
long long dist[N];
bool vis[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dij(int s) {
priority_queue<PII, vector<PII>, greater<PII> > q;
memset(dist, 0x3f, sizeof dist);
memset(vis, 0, sizeof vis);
dist[s] = 0;
q.push({0, s});
while(!q.empty()) {
int ver = q.top().second;
q.pop();
if(vis[ver]) continue;
vis[ver] = true;
for(int i = h[ver]; i != -1; i = ne[i]) {
int j = e[i];
if(dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
q.push({dist[j], j});
}
}
}
}
int main() {
scanf("%d%d%d%d%d%d", &n, &m, &k, &s, &p, &q);
memset(h, -1, sizeof h);
for(int i = 1; i <= k; i++) {
int x;
scanf("%d", &x);
mark[x] = 2; //被占领的城市
add(0, x, 0); //建立虚点
add(x, 0, 0);
}
for(int i = 1; i <= m; i++) {
scanf("%d%d", &a[i], &b[i]);
add(a[i], b[i], 1);
add(b[i], a[i], 1);
}
dij(0);
for(int i = 1; i <= n; i++) {
if(dist[i] <= s && mark[i] != 2) mark[i] = 1; //确定危险城市
}
idx = 0;
memset(h, -1, sizeof h);
for(int i = 1; i <= m; i++) {
if(mark[a[i]] == 2 || mark[b[i]] == 2) continue;
//点权转边权
if(mark[b[i]] == 1) add(a[i], b[i], q);
else add(a[i], b[i], p);
if(mark[a[i]] == 1) add(b[i], a[i], q);
else add(b[i], a[i], p);
}
dij(1);
if(mark[n] == 1) printf("%lld", dist[n] - q);
else printf("%lld", dist[n] - p);
return 0;
}
不光是最短路,在最小生成树的题中也有运用。
例题:AcWing 1146. 新的开始
题目大意:
有若干个点,第 \(i\) 个点可以花费 \(v[i]\) 的费用使它加入集合 \(S\);也可以将 \(i\) 与 \(j\) 之间连边,费用为 \(p[i][j]\),让所有点加入集合 \(S\),求最小费用。
我们发现这是一道很明显的最小生成树问题,但是这个点权很讨厌,不像上一道题可以直接转换为边权。
同样的,可以建立一个超级源点,向第 \(i\) 个点连一条长度为 \(v[i]\) 的边就行了。
\(\texttt{Code:}\)
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n;
int g[N][N];
int dist[N];
bool vis[N];
int prim() {
memset(dist, 0x3f, sizeof dist);
dist[0] = 0;
int res = 0;
for(int i = 0; i < n; i++) {
int t = -1;
for(int j = 0; j <= n; j++)
if(!vis[j] && (t == -1 || dist[t] > dist[j])) t = j;
vis[t] = true;
for(int j = 0; j <= n; j++)
if(!vis[j]) dist[j] = min(dist[j], g[t][j]);
}
for(int i = 1; i <= n; i++) res += dist[i];
return res;
}
int main() {
scanf("%d", &n);
int v;
for(int i = 1; i <= n; i++) {
scanf("%d", &v);
g[0][i] = g[i][0] = v;
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
scanf("%d", &g[i][j]);
printf("%d\n", prim());
return 0;
}