图论的一些建图小技巧

\(\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\)

这道题要稍微复杂一点,但只要将它层层剥离开来分析,也是很简单的。

我们将题目分成两个部分:

  1. 一次建图,求出所有与被控制城市距离小于等于 \(s\) 的点;

  2. 二次建图,求出最小花费。

先来考虑 \(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;
}
posted @ 2024-07-23 14:41  Brilliant11001  阅读(15)  评论(0编辑  收藏  举报