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