七橋問題:
如果上圖每一座橋都只能經過一次,
是否存在一種方法走遍這 7 座橋
B
C
A
D
A
B
A
B
圖論:討論點邊關係的學問
B
C
A
D
A
B
A
B
B
C
A
D
點的 度數 (degree)
一個點 \(v\) 被幾條邊連接
左圖中
\(\deg(A) = 3\)
\(\deg(B) = 5\)
B
C
A
D
點的 有向度數 (directed degree)
出度 Out degree 是一個點 \(v\) 伸出幾條邊
入度 In degree 是一個點 \(v\) 有多少邊指向他
A
B
\(\deg_{in}(A)=0\)
\(\deg_{out}(A)=1\)
\(\deg_{in}(B)=1\)
\(\deg_{out}(B)=0\)
B
C
A
D
給定義一張連通無向圖,從一個頂點開始行走,是否存在一種方法
使得每條邊只經過一次的狀況下,
經過每一條邊
A
B
C
D
E
連通:任兩點都能透過邊互相抵達
A
B
C
D
E
如果頂點 \(v_x\) 不是起點也不是終點
若要能一筆畫
那 \(\deg(v_x)\) 一定要是偶數 (why?)
\(\Rightarrow\) 最多只有兩個點的 degree 是奇數,分別為起點與終點
\(\Rightarrow\) 如果沒有頂點 degree 是奇數,每個點都能當起點 (起點與終點是同一個點)
note. 不存在 只有 \(1\) 個點的 degree 是奇數的圖 (why?)
zerojudge
b924: kevin 愛畫畫
極大部分的圖論題目輸入都長這樣
V E // 幾個點, 幾個邊
// 接下來有 E 行
v1 v2 // v1 v2 之間有邊
...
...
...
vn vm
輸入通常很大,要注意 IO 時間
#include <bits/stdc++.h>
using namespace std;
int deg[10001];
int main() {
int N, M;
scanf("%d%d", &N, &M);
while (M--) {
int a, b;
scanf("%d%d", &a, &b);
deg[a]++;
deg[b]++;
}
int odds = 0;
for(int i=1;i<=N;++i)
if (deg[i]%2==1)
odds ++;
if (odds > 2) cout <<"NO\n";
else cout <<"YES\n";
}
A
B
C
D
E
無向圖中,如果 \(a\) 能到 \(b\),那 \(b\) 也能到 \(a\)
有向圖則不一定
A
B
C
D
A
B
C
D
連通
不連通
A
B
C
D
A
B
C
D
連通
不連通
對於一個無向圖
A
B
C
D
連通
因此檢查一張圖是否連通
只需要選擇隨意的一個起點
看看能不能由起點走到其他所有的頂點就行了
最常使用的為第二種 - 鄰接串列
每種都有不同的使用情境,會在遇到時介紹
使用情境出現位置 : 鄰接矩陣 : 全點對最短路徑、圖論的數學計算 / 邊陣列 : 最小生成樹、網路流
對於每一個點 \(v\),用一個 vector 紀錄他的鄰居有誰
A
B
C
D
vec[A] = {B, C}
vec[B] = {A, C}
vec[C] = {A, B, D}
vec[D] = {C}
vector : C++ 內建可以自動長大的陣列 @
vector<int> V[100];
// 輸入一條邊
cin >> s >> e;
// s 的鄰居們加入 e
V[s].empalce_back(e);
// 無向圖中要記得反過來的路也要蓋!
V[e].empalce_back(s);
Key point : 用一個陣列紀錄有哪一些點我們可以用
vector<int> todo; // 紀錄有那一些點等待我們去走
todo.emplace_back(S); // 一開始只知道起點 !
vector<int> todo; // 紀錄有那一些點等待我們去走
todo.emplace_back(S); // 一開始只知道起點 !
while (!todo.empty()) // 如果還有點沒有去看他
{
int v = todo.back(); // 挑一個的資料出來
todo.pop_back(); // 拿出來的記得刪掉!
for(int u:V[v]) // 找 v 的所有鄰居
todo.emplace_back(u); // 放到清單中
}
透過一個點的鄰居,可以讓我們知道還有哪一些點沒有被走過!
A
B
C
A->B->C->A->B->C...
無窮迴圈 !?
vector<int> todo;
vector<bool> used(V); // 紀錄一個點有沒有被走過
todo.emplace_back(S);
used[S] = true;
while (!todo.empty())
{
int v = todo.back();
todo.pop_back();
for(int u:V[v])
if (!used[u]) { // 如果 u 還沒看過
todo.emplace_back(u);
used[u] = true; // 紀錄為已看過
}
}
一個點只要被看過一次,要再用一個陣列紀錄有沒有看過
不然會有無窮迴圈的問題 !
A
B
C
D
起點 : A
todo = {A}
A
B
C
D
起點 : A
todo = {}
拿出 A
A
B
C
D
起點 : A
todo = {B,C}
拿出 A
把 A 的鄰居放到 todo
A
B
C
D
起點 : A
todo = {B}
拿出 A
把 A 的鄰居放到 todo
拿出 C
Question : 第三步可以改拿 B 嗎 ?
A
B
C
D
起點 : A
todo = {B,D}
拿出 A
把 A 的鄰居放到 todo
拿出 C
把 C 的鄰居放到 todo
A
B
C
D
起點 : A
todo = {B}
拿出 A
把 A 的鄰居放到 todo
拿出 C
把 C 的鄰居放到 todo
拿出 D
A
B
C
D
起點 : A
todo = {B}
拿出 A
把 A 的鄰居放到 todo
拿出 C
把 C 的鄰居放到 todo
拿出 D
把 D 的鄰居放到 todo
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居放到 todo
拿出 C
把 C 的鄰居放到 todo
拿出 D
把 D 的鄰居放到 todo
拿出 B
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居放到 todo
拿出 C
把 C 的鄰居放到 todo
拿出 D
把 D 的鄰居放到 todo
拿出 B
把 B 的鄰居放到 todo
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居放到 todo
拿出 C
把 C 的鄰居放到 todo
拿出 D
把 D 的鄰居放到 todo
拿出 B
把 B 的鄰居放到 todo
todo 沒東西了,演算法結束
我們找出了所有從起點 A 出發能到達的點!
#include <bits/stdc++.h>
using namespace std;
vector<int> V[801];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int N, M;
while (~scanf("%d%d", &N, &M))
{
for(int i=1;i<=N;++i)
V[i].clear();
while (M--)
{
int a, b;
scanf("%d%d", &a, &b);
V[a].emplace_back(b);
}
int S, E;
scanf("%d%d", &S, &E);
vector<int> todo;
vector<int> used(N+1);
todo.emplace_back(S);
used[S] = true;
while (!todo.empty()) {
int v = todo.back();
todo.pop_back();
for(int u:V[v])
if (!used[u]) {
used[u] = true;
todo.emplace_back(u);
}
}
if (used[E]) cout << "Yes!!!\n";
else cout << "No!!!\n";
}
}
透過遞迴不斷的往前行走,來找到所有可以到達的點 !
void dfs(int v) { // 現在走到 v 號點
used[v] = true; // 走過了一樣要標記!
}
透過遞迴不斷的往前行走,來找到所有可以到達的點 !
void dfs(int v) { // 現在走到 v 號點
used[v] = true; // 走過了一樣要標記!
for (int u:V[v]) // 看 v 的所有鄰居
if (!used[u]) // 如果沒走過
dfs(u); // 去看看他
}
試著改寫前面的程式碼~
#include <bits/stdc++.h>
using namespace std;
vector<int> V[801];
bool used[801];
void dfs(int v) {
used[v] = true;
for (int u:V[v])
if (!used[u])
dfs(u);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int N, M;
while (~scanf("%d%d", &N, &M))
{
for(int i=1;i<=N;++i) {
V[i].clear();
used[i] = false;
}
while (M--)
{
int a, b;
scanf("%d%d", &a, &b);
V[a].emplace_back(b);
}
int S, E;
scanf("%d%d", &S, &E);
dfs(S);
if (used[E]) cout << "Yes!!!\n";
else cout << "No!!!\n";
}
}
實務上兩種方法都會用到,都要學
UVa 10004 Bicoloring
給一張圖,問可不可以剛好用兩種顏色圖滿整張圖,使得相鄰不同色
ZJ b689: 2. 棕櫚迷宮
根據題目的變化,圖不一定要是普通的點線
也可能是用陣列 "畫出來" 的
ZJ b689: 2. 棕櫚迷宮
根據題目的變化,圖不一定要是普通的點線
也可能是用陣列 "畫出來" 的
我們通常把這種座標轉換的圖稱之為網格圖
網格圖通常可以利用座標當作點的編號,來做圖論的處理
不需要轉換為一般圖的形式
把之前得程式碼做一點修改 ! 使用座標當作點的編號
char map[50][50]; // 把地圖存在 char 陣列裡面
int H, W; // 地圖長寬
int x, y;
// x,y = 找到的入口座標
vector<tuple<int,int>> todo;
todo.emplace_back(x, y);
Tuple : 合併資料儲存 @
一個座標 (x,y) 的鄰居有誰 ?
vector<tuple<int,int>> todo;
bool used[100][100]; // 直接用二維陣列紀錄有沒有用過
todo.emplace_back(x, y);
used[x][y] = true;
while (!todo.empty()) {
tie(x, y) = todo.back();
todo.pop_back();
// 找 (x, y) 的鄰居
}
(x+1,y) (x-1,y) (x,y+1) (x,y-1)
要怎麼快速地列出鄰居 ?
int dx[] = {1,-1,0,0};
int dy[] = {0,0,1,-1};
{
for(int i=0;i<4;++i) {
int nx = x + dx[i];
int ny = y + dy[i];
}
}
(x+1,y) (x-1,y) (x,y+1) (x,y-1)
千萬不要幹把程式碼複製 * n 的行為
程式碼越多越容易錯!
int dx[] = {-1, 0, 1,
-1, 1,
-1, 0, 1};
int dy[] ={-1,-1,-1,
0, 0,
1, 1, 1};
while (!todo.empty()) {
tie(x, y) = todo.back();
todo.pop_back();
for (int i=0;i<4;++i) {
int nx = x+dx[i];
int ny = y+dy[i];
}
}
不是每一個格子上下左右都是鄰居!
有些是牆壁
有些超出地圖範圍
=> 判斷跳過
檢查是不是牆壁
檢查在不再地圖內
if (nx < 0 || H <= nx ||
ny < 0 || W <= ny)
if (mp[nx][ny] == '#')
哪一個要先判斷 ?
還是都可以 ?
陣列 :
永遠先檢查
要放到中括號裡的資料
檢查是不是牆壁
檢查在不在地圖內
for (int i=0;i<4;++i) {
int nx = x+dx[i];
int ny = y+dy[i];
if (nx < 0 || H <= nx ||
ny < 0 || W <= ny)
/* 這裡放什麼 */
if (mp[nx][ny] == '#')
/* 這裡放什麼 */
}
if 的下面要放什麼 ?
提示 : 如果座標錯了,"跳過" 他
(A). break
(B). continue
(C). return
while (!todo.empty()) {
tie(x, y) = todo.back();
todo.pop_back();
for (int i=0;i<4;++i) {
int nx = x+dx[i];
int ny = y+dy[i];
if (nx < 0 || H <= nx ||
ny < 0 || W <= ny)
continue;
if (mp[nx][ny] == '#')
continue;
if (!used[nx][ny]) {
todo.emplace_back(nx, ny);
used[nx][ny] = true;
}
}
}
// x,y 永遠記錄著最後從 todo 拿出的資料
// 題目座標從 1 開始的
cout << x+1 << ' ' << y+1 << '\n';
幾乎快完成了 !
題目要求 "最深" 的點
那大概就是最後放到 todo 的點吧
7 9
#########
##.....##
##.###.##
##.#...##
##.#.##..
#..#....#
#########
7 9
######.##
##.....##
##.####.#
##.#....#
##.#.##.#
#..#....#
#########
10 10
##########
#....#####
#.##.....#
#...####.#
#######..#
##....#.##
##.##.#..#
##.##.##.#
##..#....#
###.######
Testcases
幫你打好了
#include <bits/stdc++.h>
using namespace std;
int H, W;
char mp[50][50];
bool used[50][50];
tuple<int, int> findEntry(int h, int w) {
for (int i=0;i<h;++i) {
if (mp[i][0] == '.') return make_tuple(i,0);
if (mp[i][w-1] == '.') return make_tuple(i,w-1);
}
for (int i=0;i<w;++i) {
if (mp[0][i] == '.') return make_tuple(0,i);
if (mp[h-1][i] == '.') return make_tuple(h-1, i);
}
assert(false && "testcase error!");
}
int dx[] = {1,-1,0,0};
int dy[] = {0,0,1,-1};
int main()
{
cin >> H >> W;
for(int i=0;i<H;++i)
cin >> mp[i];
int x, y;
tie(x, y) = findEntry(H, W);
vector<tuple<int,int>> todo;
todo.emplace_back(x, y);
used[x][y] = true;
while (!todo.empty()) {
tie(x, y) = todo.back();
todo.pop_back();
for (int i=0;i<4;++i) {
int nx = x+dx[i];
int ny = y+dy[i];
if (nx < 0 || H <= nx ||
ny < 0 || W <= ny)
continue;
if (mp[nx][ny] == '#')
continue;
if (!used[nx][ny]) {
todo.emplace_back(nx, ny);
used[nx][ny] = true;
}
}
}
cout << x+1 << ' ' << y+1 << '\n';
}
int ax, ay;
void dfs(int x, int y) {
used[x][y] = true;
// 暫存答案
ax = x;
ay = y;
for (int i=0;i<4;++i) {
int nx = x+dx[i];
int ny = y+dy[i];
if (nx < 0 || H <= nx ||
ny < 0 || W <= ny)
continue;
if (mp[nx][ny] == '#')
continue;
if (!used[nx][ny])
dfs(nx, ny);
}
}
#include <bits/stdc++.h>
using namespace std;
int H, W;
char mp[50][50];
bool used[50][50];
tuple<int, int> findEntry(int h, int w) {
for (int i=0;i<h;++i) {
if (mp[i][0] == '.') return make_tuple(i,0);
if (mp[i][w-1] == '.') return make_tuple(i,w-1);
}
for (int i=0;i<w;++i) {
if (mp[0][i] == '.') return make_tuple(0,i);
if (mp[h-1][i] == '.') return make_tuple(h-1, i);
}
assert(false && "testcase error!");
}
int dx[] = {1,-1,0,0};
int dy[] = {0,0,1,-1};
int ax, ay;
void dfs(int x, int y) {
used[x][y] = true;
// 暫存答案
ax = x;
ay = y;
for (int i=0;i<4;++i) {
int nx = x+dx[i];
int ny = y+dy[i];
if (nx < 0 || H <= nx ||
ny < 0 || W <= ny)
continue;
if (mp[nx][ny] == '#')
continue;
if (!used[nx][ny])
dfs(nx, ny);
}
}
int main()
{
cin >> H >> W;
for(int i=0;i<H;++i)
cin >> mp[i];
int x, y;
tie(x, y) = findEntry(H, W);
dfs(x, y);
cout << ax+1 << ' ' << ay+1 << '\n';
}
總是拿 "最新發現的鄰居" 來考慮
是一切搜尋最基本的方法
void dfs(int v) {
used[v] = true;
for (int u: V[v]) {
if (!used[u]) {
// 一發現新鄰居,立刻走上去
dfs(u);
}
}
}
while (!todo.empty()) {
v = todo.back(); // 拿最後的東西
for (int u: V[v]) {
if (!used[u]) {
// 一發現新鄰居,放到 todo 最後面
dfs(u);
used[u] = true;
}
}
}
c129: 00572 - Oil Deposits (簡單網格圖)
d768: 10004 - Bicoloring (hint: 邊走路邊圖顏色)
b517 : 是否為樹-商競103 (hint : 一個圖為樹 => 連通 且 沒有環)
A
B
C
D
樹
A
B
C
D
不是樹
(ABC形成循環)
A
B
C
D
不是樹
(不連通)
b517 參考輸入範例
這一題雖然簡單,但細節有點多
輸入也對初學者不太友善
可以複製左邊程式碼來繼續寫
程式碼已經寫到把圖建立好的程度了
#include <bits/stdc++.h>
using namespace std;
vector<int> V[81];
int used[81];
bool appeared[81];
int main() {
int T;
cin >> T; cin.get();
while (T--) {
string buf;
getline(cin, buf);
// 初始化放這裡
memset(used, 0, sizeof(used));
memset(appeared, 0, sizeof(appeared));
for(int i=0;i<81;++i) V[i].clear();
int x, y;
stringstream ss(buf);
while (ss >> buf) {
sscanf(buf.c_str(), "%d,%d", &x, &y);
// 讀取一條邊 x<=>y
appeared[x] = appeared[y] = true;
V[x].emplace_back(y);
V[y].emplace_back(x);
}
// 繼續做你想做的事情
}
}
A
B
C
D
距離 0 : A
距離 1 : B, C
距離 2 : D
// 要把 vector<> todo 改成 deque<> todo
// deque 才能操作 front
while (!todo.empty()) {
v = todo.front(); // 拿前面的東西
todo.pop_front();
for (int u: V[v]) {
if (!used[u]) {
// 一發現新鄰居,放到 todo 最後面
todo.emplace_back(u);
used[u] = true;
}
}
}
A
B
C
D
起點 : A
todo = {A}
0
A
B
C
D
起點 : A
todo = {}
拿出 A
0
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居距離設定為自己+1
0
1
1
A
B
C
D
起點 : A
todo = {B,C}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
0
1
1
A
B
C
D
起點 : A
todo = {C}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
0
1
1
A
B
C
D
起點 : A
todo = {C}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
0
1
1
A
B
C
D
起點 : A
todo = {C}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
把 B 的鄰居放入 todo
0
1
1
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
把 B 的鄰居放入 todo
拿出 C
0
1
1
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
把 B 的鄰居放入 todo
拿出 C
把 C 的鄰居距離設定為自己+1
0
1
1
2
A
B
C
D
起點 : A
todo = {D}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
把 B 的鄰居放入 todo
拿出 C
把 C 的鄰居距離設定為自己+1
把 C 的鄰居放入 todo
0
1
1
2
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
把 B 的鄰居放入 todo
拿出 C
把 C 的鄰居距離設定為自己+1
把 C 的鄰居放入 todo
拿出 D
0
1
1
2
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
把 B 的鄰居放入 todo
拿出 C
把 C 的鄰居距離設定為自己+1
把 C 的鄰居放入 todo
拿出 D
把 D 的鄰居距離設定為自己+1
0
1
1
2
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
把 B 的鄰居放入 todo
拿出 C
把 C 的鄰居距離設定為自己+1
把 C 的鄰居放入 todo
拿出 D
把 D 的鄰居距離設定為自己+1
把 D 的鄰居放入 todo
0
1
1
2
A
B
C
D
起點 : A
todo = {}
拿出 A
把 A 的鄰居距離設定為自己+1
把 A 的鄰居放入 todo
拿出 B
把 B 的鄰居距離設定為自己+1
把 B 的鄰居放入 todo
拿出 C
把 C 的鄰居距離設定為自己+1
把 C 的鄰居放入 todo
拿出 D
把 D 的鄰居距離設定為自己+1
把 D 的鄰居放入 todo
0
1
1
2
todo 沒有資料了
搜尋完成
把每一個連通的點都標上了與起點間的距離!
// 要把 vector<> todo 改成 deque<> todo
// deque 才能操作 front
dist[s] = 0; // 起點距離為 0
while (!todo.empty()) {
v = todo.front(); // 拿前面的東西
todo.pop_front();
for (int u: V[v]) {
if (!used[u]) {
// 一發現新鄰居,放到 todo 最後面
todo.emplace_back(u);
dist[u] = v + 1; // 鄰居距離 = 自己 + 1
used[u] = true;
}
}
}
按照與起點的距離由近到遠搜尋資料!
可以算出每一個點與起點的距離!
Directed Acyclic Graph
顧名思義,一種有向圖,而且沒有環
這種圖是圖論的特例,在有向無環圖上,問題通常會比較容易解決
把一個頂點當作一個工作,一份工作要開始之前,他的前置作業必須完成
是否能給出一個順序,使得每個工作開始前,他的前置作業都完成了
如果有一條路 \(a\) 到 \(b\) ,那 \(a\) 就要排在 \(b\) 前面
拓譜排序的概念與前面的走訪類似
e
我們只在一個點,他的前置工作都完成時,
才把這一個點放到 todo 中
左圖中,如果要把 e 放到 todo 中
那就要先把 a,c,d 放到 todo
為什麼不用考慮 b ?
e
如何算出每一個點的前置工作有幾個 ?
左圖中,如果要把 e 放到 todo 中
那就要先把 a,c,d 放到 todo
前置工作的數量,就是有幾根箭頭指向自己
就是入度 ( In degree )
a | b | c | d | e | |
---|---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
e
如何算出每一個點的前置工作有幾個 ?
前置工作的數量,就是有幾根箭頭指向自己
就是入度 ( In degree )
a | b | c | d | e | |
---|---|---|---|---|---|
tasks | 0 | 1 | 1 | 3 | 3 |
如果一個點一開始就沒有前置作業 (a)
就把他丟到 todo 中!
while (M--) {
int a, b;
cin >> a >> b; // 有向邊 a->b
V[a].emplace_back(b);
tasks[b]++;
}
vector<int> todo;
for(int i=0;i<N;++i)
if (tasks[i]==0)
todo.emplace_back(i);
e
todo 每次拿出來的一個點
他的鄰居能直接放到 todo 嗎 ?
要檢查這個鄰居,他的前置作業都完成了,
才能放入
如何設計一個簡單快速的檢查方法呢?
每拿出一個點,就把他的鄰居 tasks - 1
表示完成了一個工作
如果鄰居變成 0 ,表示完成所有工作 !
vector<int> solution;
while (!todo.empty()) {
int v = todo.back(); // 任意拿一個出來
todo.pop_back();
solution.emplace_back(v);
for (int u:V[v])
{
tasks[u] = tasks[u] - 1;
if (tasks[u] == 0)
todo.emplace_back(u);
}
}
todo 拿出來的資料,依序就是拓譜排序的結果
可以用另一個陣列存起來,或是做題目要求的事情
e
步驟1. 計算所有點的 tasks
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
a |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
a |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
a | b | c | d | e |
---|---|---|---|---|
0 | 1 | 1 | 3 | 3 |
a |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 2 | 2 |
a |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 2 | 2 |
b | c |
---|
a |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 2 | 2 |
c |
---|
a | b |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 1 | 2 |
c |
---|
a | b |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 1 | 2 |
c |
---|
a | b |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 1 | 2 |
a | b | c |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 1 |
a | b | c |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 1 |
d |
---|
a | b | c |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 1 |
a | b | c | d |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
a | b | c | d |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
e |
---|
a | b | c | d |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
a | b | c | d | e |
---|
e
步驟1. 計算所有點的 tasks
步驟2. 把 tasks 為 0 的點放入 todo
while todo 非空
從 todo 拿出一個點 p
檢查 p 的所有鄰居 v
將 v 的 tasks - 1
如果 v 的 tasks 變成 0
就把 v 放到 todo
a | b | c | d | e |
---|
拓譜排序的答案不為一
可能有很多種結果,都滿足拓譜排序
a | c | b | d | e |
---|
e
如果演算法結束後,如果有點沒走過
代表 圖不是有向無環圖
A
B
C
D
ZJ f167: m4a1-社團 Club
#include <bits/stdc++.h>
using namespace std;
vector<int> V[1001];
int main() {
int N, M;
cin >> N >> M;
vector<int> tasks(N+1);
while (M--) {
int a, b;
cin >> a >> b; // 有向邊 a->b
V[a].emplace_back(b);
tasks[b]++;
}
vector<int> todo;
for(int i=1;i<=N;++i)
if (tasks[i]==0)
todo.emplace_back(i);
vector<int> solution;
while (!todo.empty()) {
int v = todo.back(); // 任意拿一個出來
todo.pop_back();
solution.emplace_back(v);
for (int u:V[v])
{
tasks[u] = tasks[u] - 1;
if (tasks[u] == 0)
todo.emplace_back(u);
}
}
if (solution.size() == N) {
cout << "YES\n";
for (int i:solution)
cout << i << '\n';
} else {
cout << "NO\n";
}
}
e
除此方法之外
也能用一次 DFS 找出拓譜序 (的逆序)
方法也十分簡單
// 如果圖不是DAG,直接這樣用會爛掉
void dfs(int v){
used[v] = true;
for (int u:V[v])
if (!used[u])
dfs(u);
solution.emplace_back(v);
}
Q. 上面的程式碼怎麼判斷是不是 DAG ?
bool OnStack[2000];
bool used[2000];
void dfs(int v){
OnStack[v] = true;
used[v] = true;
for (int u:V[v]) {
if (!used[v]) // 沒看過
dfs(u);
else if (OnStack[u])
return ; // 不是 dag!
}
OnStack[v] = false;
solution.emplace_back(v);
}
記得這個 solution 與前面演算法的剛好前後顛倒阿
2021 全國賽 D. 汽車不再繞圈圈 (car)
ZJ a454: TOI2010 第二題:專案時程
TIOJ 1226. H遊戲
TIOJ 1092. 跳格子遊戲
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
因為沒則選擇,只能選擇移動到 "先手必敗" 的狀況 (下一回合先後手交換GG)
Note : 這一個題目是 "後手做移動",如果是 "先手做移動" 結論會有點不一'樣
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
如果只有終點 : 先手必勝
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
只有通向必勝的路,因此必敗
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
終點前兩步 : 有路通向必敗,因此必勝
KEY : 如果目前這一步是 "先手必勝"
表示不論如何移動,都會使得 "先手必敗"
以此推出所有點是必勝還是必敗
按照拓譜排序的逆序進行
Single Source Shortest Path
A
B
C
5
10
vector<tuple<int,int>> V; // (neighbor, length)
int s, t, w;
cin >> s >> t >> w;
V[s].emplace_back(t, w);
// V[t].emplace_back(s, w); // 雙向邊
for(auto [u, w] : V[v]) {
// do something
}
A
B
C
3
2
A
B
C
1
1
1
1
1
A
C
1
1
1
1
1
2
B
1
1
1
1
1
1
頂點 A :
現在 B 距離我 9
現在 C 距離我 2
A
C
1
1
1
1
1
2
B
1
1
1
1
1
1
頂點 A :
現在 B 距離我 9
現在 C 距離我 2
<= 下一個遇到的是 C
從一群東西找最小 : Priority Queue !
int Dijkstra(int s, int e, int N) {
const int INF = INT_MAX / 2;
vector<int> dist(N, INF);
vector<bool> used(N, false);
dist[s] = 0;
}
0x3f3f 大概比最大正整數 0x7fff 一半 0x3fff 小一點
int dist[2000];
int Dijkstra(int s, int e, int N) {
memset(dist, 0x3f, sizeof(dist));
const int INF = dist[0];//0x3f3f3f3f
dist[s] = 0;
}
也可以用 for loop / fill / fill_n 之類的來設定陣列數值
方法 1. 改符號方向
C++ 預設用 小於 (less) 比較,我們換成 大於(greater)
using T = tuple<int,int>;
priority_queue<T, vector<T>, greater<T>> pq;
方法 2. 自訂比較函數 (函數物件)
struct cmp {
bool operator()(int a, int b) {
return a > b;
}
};
priority_queue<T, vector<T>, cmp> pq;
方法 3. 自訂比較函數 (一般函數)
個人較不建議這樣寫
bool cmp(int a, int b) {
return a > b;
}
priority_queue<T, vector<T>, decltype(&cmp)> pq(cmp);
// priority_queue<T, vector<T>, function<bool(int, int)>> pq(cmp);
int Dijkstra(int s, int e, int N) {
const int INF = INT_MAX / 2;
std::vector<int> dist(N, INF);
using T = tuple<int,int>;
priority_queue<T, vector<T>, greater<T>> pq;
dist[s] = 0;
pq.emplace(0, s); // (w, e) 讓 pq 優先用 w 來比較
}
while (!pq.empty()) {
tie(std::ignore, s) = pq.top();
pq.pop();
if ( used[s] ) continue;
used[s] = true; // 每一個點都只看一次
for (auto [e, w] : V[s]) {
if (dist[e] > dist[s] + w) {
dist[e] = dist[s] + w;
pq.emplace(dist[e], e);
}
}
}
存在更好的實作,可以更新 heap 裡的資料,而不是塞垃圾到 heap 裡面,然後再篩選資料
int Dijkstra(int s, int e, int N) {
const int INF = INT_MAX / 2;
vector<int> dist(N, INF);
vector<bool> used(N, false);
using T = tuple<int,int>;
priority_queue<T, vector<T>, greater<T>> pq;
dist[s] = 0;
pq.emplace(0, s); // (w, e) 讓 pq 優先用 w 來比較
while (!pq.empty()) {
tie(std::ignore, s) = pq.top();
pq.pop();
if ( used[s] ) continue;
used[s] = true; // 每一個點都只看一次
for (auto [e, w] : V[s]) {
if (dist[e] > dist[s] + w) {
dist[e] = dist[s] + w;
pq.emplace(dist[e], e);
}
}
}
return dist[e];
}
A
B
C
2
3
-6
A 到 B 的最短路徑是 A->C->B ,答案為 -3 ,但是 dijkstra 會把最先出現的 A->B = 2 當作答案。
大致等於:
找最大值的複雜度 x 次數 + 把資料放入資料結構複雜度 x 次數
找最大值 | 次數 | 更新資料 | 次數 | 總複雜度 | |
---|---|---|---|---|---|
內建的 priority queue | 1 | E | log E | E | E log E |
用陣列暴力做 | V | V | 1 | E | V^2+E |
實作可更新資料的 priority queue | 1 | E | log V | E | E log V |
Fibonacci Heap | log V | V | 1 | E | E+Vlog V |
A
B
C
-5
-3
-6
D
8
對於每一條邊 \(s\stackrel{w}\to e\) ,如果有
$$d(e) > d(s) + w$$
那我們就需要 relax
也就是把 \(d(e)\) 更新成更好的答案 \(d(s)+w\)
vector<tuple<int,int,int>> Edges; // (s, t, w)
vector<tuple<int,int,int>> Edges;
int BellmanFord(int s, int e, int N) {
const int INF = INT_MAX / 2;
vector<int> dist(N, INF);
dist[s] = 0;
bool update;
while (true) {
update = false;
for(auto [v, u, w] : Edges)
{
if (dist[u] > dist[v] + w)
{
dist[u] = dist[v] + w;
update = true;
}
}
if (!update)
break;
}
return dist[e];
}
vector<tuple<int,int,int>> Edges;
int BellmanFord(int s, int e, int N) {
const int INF = INT_MAX / 2;
vector<int> dist(N, INF);
dist[s] = 0;
bool update;
for(int i=1;i<=N;++i) {
update = false;
for(auto [v, u, w] : Edges)
{
if (dist[u] > dist[v] + w)
{
dist[u] = dist[v] + w;
update = true;
}
}
if (!update)
break;
if (i == N) // && update
return -1; // gg !
}
return dist[e];
}
對於任三點 \(a, b, c\) ,如果有
$$d(a, c) > d(a,b) + d(b,c)$$
那我們就需要 relax
也就是把 \(d(a,c)\) 更新程更好的答案 \(d(a,b)+d(b,c)\)
int d[MAXN][MAXN];
int d[100][100];
void FloydWarshall(int N){
for(int k=0;k<N;++k)
for(int i=0;i<N;++i)
for(int j=0;j<N;++j)
if(d[i][j] > d[i][k] + d[k][j])
d[i][j] = d[i][k] + d[k][j];
}
UVa 11463
功能 | 複雜度 | 缺點 | |
---|---|---|---|
Dijkstra | 求單一起點最短路 | O(E log E) | 不能有負邊 |
BellmanFord | 求單一起點最短路 負邊處理 |
O(EV) | 複雜度高 |
SPFA | 求單一起點最短路 負邊處理 |
O(Ek) | 比賽容易被卡,會跟 Bellman Ford 一樣爛 |
FloydWarshall | 任兩點最短路 | O(V^3) | 專解任兩點最短路 |
0
3
1
2
4
5
6
7
8
4
4
2
4
9
4
3
8
1
3
3
10
1
Mininum Spanning Tree
Tree
= 由 \(n\) 個點 \(n-1\) 條邊構成的聯通圖
= 在任兩點加上邊就會有環的無環圖
vector<tuple<int,int,int>> Edges;
int kruskal(int N) {
int cost = 0;
sort(Edges.begin(), Edges.end());
DisjointSet ds(N);
sort(Edges.begin(), Edges.end());
for(auto [w, s, t] : Edges) {
if (!ds.same(s, t)) {
cost += w;
ds.unit(s, t);
}
}
return cost;
}
時間複雜度 : \(O(E\log E)\)
uva 10034-Freckles
ZJ e509: Conscription
uva 10048 - Audiophobia
uva 534 - Frogger
基本解法 : 二分搜、或構造成 MST,最短路問題