圖論的一些建圖小技巧

Brilliant11001發表於2024-07-23

\(\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;
}

相關文章