Arvin Liu
考驗你的學的多寡的東西
還有你的實作的能力...
就是樹狀圖的感覺
就是一堆點連來連去
內容 | 快速連結 | APCS範圍 |
---|---|---|
前言 | Link | |
Disjoint Set | Link | ❓ |
搜尋法 | Link | ⭕ |
- 廣度優先搜尋法 Breadth First Search (BFS) | Link | ⭕ |
- 深度優先搜尋法 Depth First Search (DFS) | Link | ⭕ |
- BFS 變種 - Uniform Cost Search | Link | ❌ |
- DFS 變種 - 回溯法 (Backtracking) | Link | ⭕ |
樹論 (Tree) |
Link | ⭕ |
- 二元搜尋樹 Binary Search Tree (BST) | Link | ❓ |
圖論 (Graph) |
Link | ⭕ |
- 單源最短路徑 Dijkstra's Algorithm | Link | ❌ |
- 環檢測 (Cycle Detection) + 拓樸排序 Topological Sort | Link | ⭕ |
有些可能會稍稍偏離 APCS 的範圍,
但還是希望大家可以看一下 :)
or Union Find Tree, 併查集
or 簡稱 DSU (Disjoint Set Union)
現在有N個人,M個關係,Q筆詢問。
每個關係會給兩個人,表示這兩人是朋友。
我們定義朋友的朋友也是朋友。
每筆詢問會問兩個人,請輸出他們是不是朋友。
5 3 2
1 2
2 5
3 4
1 5
1 3
舉例來說:
1
2
5
3
4
輸出:)
輸出:(
關係圖
給定關係圖,接著詢問任兩人有沒有關係。
你會怎麼做?
關係
1 2
2 5
3 4
誰 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
初始 | 1 | 2 | 3 | 4 | 5 |
1-2 | 1 | 1 | 3 | 4 | 5 |
2-5 | 1 | 1 | 3 | 4 | 2 |
3-4 | 1 | 1 | 3 | 3 | 2 |
查詢時看B可不可以while到A就好了!
嗎?
下一個關係是 2-4 怎麼辦?
2 4
給定關係圖,接著詢問任兩人有沒有關係。
最簡單的解法:讓每個朋友群有個老大
關係
1 2
2 5
3 4
誰 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
老大 | 1 | 2 | 3 | 4 | 5 |
1-2 | 1 | 1 | 3 | 4 | 5 |
2-5 | 1 | 1 | 3 | 4 | 1 |
3-4 | 1 | 1 | 3 | 3 | 1 |
2-4 | 1 | 1 | 1 | 3 | 1 |
A跟B變成朋友 = (A老大) 跟 (B老大) 變成朋友!
2 4
這樣就不會不知道要跟誰,因為老大的老大只會是自己。
給定關係圖,接著詢問任兩人有沒有關係。
最簡單的解法:讓每個朋友群有個老大
關係
1 2
2 5
3 4
2 4
A跟B變成朋友 = (A老大) 跟 (B老大) 變成朋友!
這樣就不會不知道要跟誰,因為老大的老大只會是自己。
1
2
5
3
4
給定關係圖,接著詢問任兩人有沒有關係。
最簡單的解法:讓每個朋友群有個老大
A跟B變成朋友 = (A老大) 跟 (B老大) 變成朋友!
int boss[N];
void init() {
for (int i=0; i<N; i++)
boss[i] = i;
}
int find_boss(int x) {
if (x == boss[x])
return x;
return find_boss(boss[x]);
}
int merge(int x, int y) {
boss[find_boss(y)] = find_boss(x);
}
boss = list(range(n))
def find_boss(x):
if x == boss[x]:
return x
return find_boss(boss[x])
def merge(x, y):
boss[find_boss(y)] = find_boss(x)
C++
Python
給定關係圖,接著詢問任兩人有沒有關係。
int boss[N];
void init() {
for (int i=0; i<N; i++)
boss[i] = i;
}
int find_boss(int x) {
if (x == boss[x])
return x;
return find_boss(boss[x]);
}
int merge(int x, int y) {
boss[find_boss(y)] = find_boss(x);
}
boss = list(range(n))
def find_boss(x):
if x == boss[x]:
return x
return find_boss(boss[x])
def merge(x, y):
boss[find_boss(y)] = find_boss(x)
最差複雜度?
關係
4 5
3 4
2 3
1 2
1
2
5
3
4
路徑壓縮 Path Compression
給定關係圖,接著詢問任兩人有沒有關係。
int boss[N];
void init() {
for (int i=0; i<N; i++)
boss[i] = i;
}
int find_boss(int x) {
if (x == boss[x])
return x;
return find_boss(boss[x]);
}
int merge(int x, int y) {
boss[find_boss(y)] = find_boss(x);
}
boss = list(range(n))
def find_boss(x):
if x == boss[x]:
return x
return find_boss(boss[x])
def merge(x, y):
boss[find_boss(y)] = find_boss(x)
1
2
5
3
4
每問一次都更新到最大的boss?
C++
Python
給定關係圖,接著詢問任兩人有沒有關係。
int boss[N];
void init() {
for (int i=0; i<N; i++)
boss[i] = i;
}
int find_boss(int x) {
if (x == boss[x])
return x;
return boss[x] = find_boss(boss[x]);
}
int merge(int x, int y) {
boss[find_boss(y)] = find_boss(x);
}
boss = list(range(n))
def find_boss(x):
if x == boss[x]:
return x
boss[x] = find_boss(boss[x])
return boss[x]
def merge(x, y):
boss[find_boss(y)] = find_boss(x)
啊?這樣就可以比較快喔?
不只快,他的最差複雜度是:
(均攤複雜度)
這題好難QQ,我也不會
啟發式合併 Union by Rank
1
2
在我們合併的時候,
其實應該是有個「輩分」在的。
.
.
.
.
.
?
那我們應該要誰當老大呢?
體感上來說:感覺小併大好一點。
那麼怎樣叫小,怎樣叫大呢?
兩個實作完的複雜度都是
Rank = 2
Rank = 1
給定關係圖,接著詢問任兩人有沒有關係。
我們以 Union by Rank 做說明:
Case - 1:
兩邊 Rank 不同
Case - 2:
兩邊 Rank 相同
小指到大,
Rank不變。
隨便指,
但 Rank + 1。
X 4
1
2
Rank = 2
Rank = 3
1
2
Rank = 3
Rank = 3
(與最遠的屬下距離)
給定關係圖,接著詢問任兩人有沒有關係。
int boss[N], rank[N]={};
void init() {
for (int i=0; i<N; i++)
boss[i] = i;
}
int find_boss(int x) {
if (x == boss[x])
return x;
return find_boss(boss[x]);
}
void merge(int x, int y) {
int boss_x = find_boss(x);
int boss_y = find_boss(y);
if (boss_x == boss_y)
return ;
if (rank[boss_x] > rank[boss_y]) {
boss[boss_y] = boss_x;
} else {
boss[boss_x] = boss_y;
if (rank[boss_x] == rank[boss_y])
rank[boss_y] ++;
}
}
boss = list(range(N))
rank = [0] * N
def find_boss(x):
if x == boss[x]:
return x
return find_boss(boss[x])
def merge(x, y):
boss_x = find_boss(x)
boss_y = find_boss(y)
if boss_x == boss_y:
return
if rank[boss_x] > rank[boss_y]:
boss[boss_y] = boss_x
else:
boss[boss_x] = boss_y
if rank[boss_x] == rank[boss_y]:
rank[boss_y] += 1
可以自己想想看
Union by Size 要怎麼寫!
C++
Python
Path Compression
Union by Rank/Size
Union by Rank/Size with Path Compression
給定關係圖,接著詢問任兩人有沒有關係。
int boss[N], rank[N]={};
void init() {
for (int i=0; i<N; i++)
boss[i] = i;
}
int find_boss(int x) {
if (x == boss[x])
return x;
return boss[x] = find_boss(boss[x]);
}
void merge(int x, int y) {
int boss_x = find_boss(x);
int boss_y = find_boss(y);
if (boss_x == boss_y)
return ;
if (rank[boss_x] > rank[boss_y]) {
boss[boss_y] = boss_x;
} else {
boss[boss_x] = boss_y;
if (rank[boss_x] == rank[boss_y])
rank[boss_y] ++;
}
}
boss = list(range(N))
rank = [0] * N
def find_boss(x):
if x != boss[x]:
boss[x] = find_boss(boss[x])
return boss[x]
def merge(x, y):
boss_x = find_boss(x)
boss_y = find_boss(y)
if boss_x == boss_y:
return
if rank[boss_x] > rank[boss_y]:
boss[boss_y] = boss_x
else:
boss[boss_x] = boss_y
if rank[boss_x] == rank[boss_y]:
rank[boss_y] += 1
C++
Python
WTF is ?
首先我們要介紹阿克曼函數 A(m, n):
數字膨脹
非常誇張的函數!
跟手遊戰力一樣
WTF is ?
阿克曼函數:成長超級快速
阿克曼反函數:成長超級緩慢
一般來說,我們可以假設這個數字 < 5
#include <stdio.h>
#define N 10001
int boss[N], rank[N] = {};
// DSU 模板 code
int main()
{
int n, m, q, x, y;
scanf("%d%d%d", &n, &m, &q);
init();
while (m--) {
scanf("%d%d", &x, &y);
merge(x, y);
}
while (q--) {
scanf("%d%d", &x, &y);
printf(":%c\n", "()"[find_boss(x) == find_boss(y)]);
}
return 0;
}
N, m, q = map(int, input().split())
boss = list(range(N+1))
rank = [0] * (N+1)
# DSU 模板 code
for _ in range(m):
x, y = map(int, input().split())
merge(x, y)
for _ in range(q):
x, y = map(int, input().split())
if find_boss(x) == find_boss(y):
print(":)")
else:
print(":(")
其實只需要寫 Path Compression,
就已經足夠快了!
Accounts Merge (Leetcode 721)
現在有N筆資料,每筆資料有一個人名,多個信箱。
人名可能會重複,但信箱的持有人只會有一個。
現在請你將資料重新整理,把該合併的資料合併。
舉例來說:
人名 | 持有信箱 |
---|---|
John | johnsmith@mail.com, john_newyork@mail.com, john00@mail.com |
John | johnnybravo@mail.com |
人名 | 持有信箱 |
---|---|
John | johnsmith@mail.com, john_newyork@mail.com |
John | johnsmith@mail.com, john00@mail.com |
John | johnnybravo@mail.com |
輸出應該是:
現在有N筆資料,每筆資料有一個人名,多個信箱。
人名可能會重複,但信箱的持有人只會有一個。
現在請你將資料重新整理,把該合併的資料合併。
你覺得該用什麼當作 DSU 的單位呢?
現在有N筆資料,每筆資料有一個人名,多個信箱。
人名可能會重複,但信箱的持有人只會有一個。
現在請你將資料重新整理,把該合併的資料合併。
你覺得該用什麼當作 DSU 的單位呢?
A
B
B
C
A
D
John
E
John
John
John
A B 應該指到同個 boss
A D 應該指到同個 boss
這樣你會寫了嗎?
你寫這題需要知道的注意事項:
class Solution {
public:
unordered_map<string, string> boss;
unordered_map<string, int> rank;
unordered_map<string, int> account_to_idx;
// DSU 模板
string find_boss(string x) {
if (boss[x] == x)
return x;
return boss[x] = find_boss(boss[x]);
}
void merge(string x, string y) {
string bossX = find_boss(x);
string bossY = find_boss(y);
if (bossX != bossY) {
if (rank[bossX] > rank[bossY]) {
boss[bossY] = bossX;
} else{
boss[bossX] = bossY;
if (rank[bossX] == rank[bossY])
rank[bossY]++;
}
}
}
// .....
C++ Solution - Page 1
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
// 初始化,以及開始合併
for (int i = 0; i < accounts.size(); i++) {
auto &account_list = accounts[i];
for (int j = 1; j < account_list.size(); j++) {
string &account = account_list[j];
if (boss.find(account) == boss.end()) {
boss[account] = account;
rank[account] = 0;
}
account_to_idx[account] = i;
merge(account, account_list[1]);
}
}
// 蒐集答案
unordered_map<string, set<string>> pre_answer;
for (int i = 0; i < accounts.size(); i++) {
auto &account_list = accounts[i];
for (int j = 1; j < account_list.size(); j++) {
string &account = account_list[j];
pre_answer[find_boss(account)].insert(account);
}
}
// 輸出答案
vector<vector<string>> answer;
for (auto &[root, account_set] : pre_answer) {
vector<string> merged_account;
merged_account.push_back(accounts[account_to_idx[root]][0]);
// Set 會自己排序。
merged_account.insert(merged_account.end(), account_set.begin(), account_set.end());
answer.push_back(merged_account);
}
return answer;
}
};
C++ Solution - Page 2
# email 對應到第幾個資料
account_to_idx = {}
boss = {}
rank = defaultdict(lambda: 0)
# 這裡插入 DSU 模板 code
# 開始合併。
for i, account_list in enumerate(accounts):
for account in account_list[1:]:
account_to_idx[account] = i
if account not in boss:
boss[account] = account
merge(account_list[1], account)
pre_answer = defaultdict(lambda: set())
# 開始統整答案
for i, account_list in enumerate(accounts):
for account in account_list[1:]:
boss_name_idx = account_to_idx[find_boss(account)]
pre_answer[boss_name_idx].add(account)
return [ [accounts[i][0]] + sorted(list(l)) for i, l in pre_answer.items()]
Python Solution
題目名稱 | 來源 | 備註 |
---|---|---|
感染風險 | CB / C 班圖論 II - 5 | |
真假子圖 | Zerojudge g598 |
APCS 2021 / 4 |
11987 - Almost Union-Find | Zerojudge f292 | Hint: 別真的刪點。 |
... Make Network Connected | Leetcode 1319 |
Searching Algorithms
大概就是你找到「解答」需要一些「過程」
而你找到「解答」的方法,就叫做搜尋法
大概就是你找到「解答」需要一些「過程」
而你找到「解答」的方法,就叫做搜尋法
給你兩個座標,
請問 A 走到 B 最快怎麼走?
過程:中間可能經過的城市
給你老鼠跟起司的位置,
請問老鼠到起司怎麼走最快?
過程:中間可能經過的格子
大概就是你找到「解答」需要一些「過程」
而你找到「解答」的方法,就叫做搜尋法
給你目前的棋局,
下步怎麼走最可以贏?
過程:中間可能的所有棋盤可能
給你目前的數獨,
怎樣可以填完所有格子?
過程:中間經過所有可能的數獨
我只能說,差超級多。
但在這裡我們只介紹兩種:
廣度優先搜尋 (BFS)
深度優先搜尋 (DFS)
Breadth-first Search (二維平面版本)
給定一個二維迷宮,求從左上 (2, 2) 走到右下 (n-1, n-1) 的最短路徑長度
舉例來說:
#########
#.......#
#.#####.#
#.......#
##.#.####
#..#.#..#
#.##.##.#
#.......#
#########
#########
#1......#
#2#####.#
#3456...#
##.#7####
#..#8#..#
#.##9##.#
#...ABCD#
#########
最短路徑為:
給定一個二維迷宮,求從左上 (2, 2) 走到右下 (n-1, n-1) 的最短路徑長度
起點
2, 2
2, 3
2, 4
3, 2
3, 4
4, 2
4, 3
4, 4
2, 5
...
我們要怎麼知道哪一條路比較快呢?
#########
#.......#
#.#.###.#
#...###.#
#######.#
#######.#
#######.#
#######.#
#########
跟你玩 minecraft
的水 87% 像
從中間開始擴散,
一開始擴散到距離中心為 1 的格子,
再擴散到距離中心為 2 的格子,
最大擴散到距離 7 的格子。
給定一個二維迷宮,求從左上 (2, 2) 走到右下 (n-1, n-1) 的最短路徑長度
起點
2, 2
2, 3
2, 4
3, 2
3, 4
4, 2
4, 3
4, 4
2, 5
...
t=1
t=1
t=2
t=2
t=3
t=4
t=3
t=5 ?
t=3
走過不要再走了!
理論通過,開始實踐
給定一個二維迷宮,求從左上 (2, 2) 走到右下 (n-1, n-1) 的最短路徑長度
理論通過,開始實踐
起點
t=1
的所有點
t=2
的所有點
t=3
的所有點
...
大致上的流程就是這樣
怎麼做呢?你大概可以這麼做:
起點
t=1 的所有點
t=2 的所有點
t=3 的所有點
BFS (?) 大致上的流程是這樣
2, 2
2, 3
3, 2
一個可以裝很多東西的容器。
2, 4
4, 2
3, 4
2, 5
4, 3
需要注意的實作細節
int dx[]={1, 0, -1, 0}, dy[]={0, 1, 0, -1};
DIRS = [(1, 0), (0, 1), (-1, 0), (0, -1)]
C++
Python
所以整個流程會是:
#include <iostream>
#include <vector>
using namespace std;
char table[101][101];
int ix[] = {1, 0, -1, 0};
int iy[] = {0, 1, 0, -1};
vector<pair<int, int>> Q[10000];
int main(){
int n, ans=-1;
scanf("%d", &n);
for(int i=0; i<n; i++)
scanf("%s", table[i]);
Q[0].push_back({1, 1});
for(int cur_cost=0; !Q[cur_cost].empty(); cur_cost++){
for (auto &[x, y] : Q[cur_cost]) {
if(x == n-2 && y == n-2) {
printf("%d\n", cur_cost+1);
return 0;
}
for(int k=0; k<4; k++){
int nx = x + ix[k], ny = y + iy[k];
if(table[nx][ny] == '.'){
Q[cur_cost+1].push_back({nx, ny});
table[nx][ny] = '#';
}
}
}
}
printf("No solution!");
return 0;
}
C++ Solution
from collections import defaultdict
DIRS = [[0, 1], [0, -1], [1, 0], [-1, 0]]
n = int(input())
maze = [input() for _ in range(n)]
Q = defaultdict(list)
Q[0].append((1, 1))
cur_cost = 0
visit = set()
answer = None
while Q[cur_cost] and answer is None:
for x, y in Q[cur_cost]:
# If it's answer, break
if x == n-2 and y == n-2:
answer = cur_cost
break
# If visit, skip
if (x, y) in visit:
continue
visit.add((x, y))
# Search next states
for dx, dy in DIRS:
nx, ny = x + dx, y + dy
if maze[nx][ny] == '.':
Q[cur_cost+1].append((nx, ny))
cur_cost += 1
if answer is None:
print("No solution!")
else:
print(answer + 1)
Python Solution
Breadth-first Search (二維平面版本)
起點
t=1 的所有點
t=2 的所有點
t=3 的所有點
BFS 的流程
2, 2
2, 3
3, 2
一個可以裝很多東西的容器。
2, 4
4, 2
3, 4
2, 5
4, 3
好像每一次都只會用到兩排...?
有辦法可以只開兩排嗎?
t=1 的所有點
t=2 的所有點
t=3 的所有點
2, 3
3, 2
2, 4
4, 2
3, 4
2, 5
4, 3
有辦法可以只開兩排嗎?
你可能會回答:動態規劃中的滾動法
(pst=0, cur=1)
(pst=1, cur=0)
(pst=0, cur=1)
那...有辦法可以只開一排嗎?
t=1 的所有點
t=2 的所有點
t=3 的所有點
那...有辦法可以只開一排嗎?
起點
生成
生成
生成
在 t 生成 t + 1 的時候,不存在 t / t + 1 以外的狀態(點)存在。
在 t 生成 t + 1 的時候,不會再新增 t 的狀態(點)存在。
Q: 有什麼 (線性的) 資料結構,可以一邊生成狀態,
一邊消耗狀態,並保證先生成的先消耗呢?
怕你聽不懂,講個人話:
Q: 有什麼資料結構,是先進先出呢?
Queue
所以這個時候,好像可以把 t+1的點放在 t 的狀態點後面..?
BFS 大致上的流程是這樣
2, 2
2, 3
3, 2
Queue
2, 4
4, 2
3, 4
2, 5
4, 3
起點
t=1 的所有點
t=2 的所有點
t=3 的所有點
❌
❌
❌
❌
❌
需要注意的實作細節
所以整個流程會是:
#include <iostream>
#include <queue>
using namespace std;
char table[101][101];
int cost[101][101];
int ix[] = {1, 0, -1, 0};
int iy[] = {0, 1, 0, -1};
int main(){
int n, ans=-1;
scanf("%d", &n);
for(int i=0; i<n; i++)
scanf("%s", table[i]);
queue<pair<int, int> > Q({{1, 1}});
while(!Q.empty()){
auto now = Q.front();
Q.pop();
if(now.first == n-2 && now.second == n-2){
printf("%d\n", cost[n-2][n-2]+1);
return 0;
}
for(int k=0; k<4; k++){
int nx = now.first + ix[k];
int ny = now.second + iy[k];
if(table[nx][ny] == '.'){
cost[nx][ny] = cost[now.first][now.second] + 1;
Q.push({nx, ny});
table[nx][ny] = '#';
}
}
}
printf("No solution!");
return 0;
}
C++ Solution (BFS with queue)
#include <iostream>
#include <queue>
using namespace std;
char table[101][101];
int ix[] = {1, 0, -1, 0};
int iy[] = {0, 1, 0, -1};
int main(){
int n, ans=-1;
scanf("%d", &n);
for(int i=0; i<n; i++)
scanf("%s", table[i]);
queue<vector<int> > Q({{1, 1, 1}});
while(!Q.empty()){
auto tmp = Q.front();
int x = tmp[0], y = tmp[1], cost = tmp[2];
Q.pop();
if(x == n-2 && y == n-2){
printf("%d\n", cost);
return 0;
}
for(int k=0; k<4; k++){
int nx = x + ix[k], ny = y + iy[k];
if(table[nx][ny] == '.'){
Q.push({nx, ny, cost+1});
table[nx][ny] = '#';
}
}
}
printf("No solution!");
return 0;
}
解1: 把 cost 塞到狀態內
解2: 開 cost 陣列紀錄
from collections import deque
DIRS = [[0, 1], [0, -1], [1, 0], [-1, 0]]
n = int(input())
maze = [input() for _ in range(n)]
# Starts at (1, 1) with cost 1
Q = deque([(1, 1, 1)])
visit = set()
answer = None
while Q:
x, y, cost = Q.popleft()
# If gets answer, break
if x == n-2 and y == n-2:
answer = cost
break
# If visit, skip
if (x, y) in visit:
continue
visit.add((x, y))
# Search next states
for dx, dy in DIRS:
nx, ny = x + dx, y + dy
if maze[nx][ny] == '.':
Q.append((nx, ny, cost+1))
if answer is None:
print("No solution!")
else:
print(answer)
Python Solution (BFS + queue)
你在這章節學到了兩種實作BFS的方法,
來找到二維圖單源最短路徑。
你去外面上課所講的 BFS,
以及大家公認的 BFS 都是第二種方法。
(它也比較好寫)
但第一個方法對你之後學真正的最短路徑演算法有幫助,敬請期待吧 :)。
這兩個的時間複雜度都是...
O(地圖格子數量)
Depth-first Search (二維平面版本)
給定一個二維圖片,請你實作油漆桶功能。
舉例來說:
對這格使用顏色 2 到油漆桶
小樣!這不就是BFS裸題嗎?
給定一個二維圖片,請你實作油漆桶功能。
小樣!這不就是BFS裸題嗎?
論功能性,BFS 跟 DFS 幾乎一樣,
但 BFS 跟 DFS 各有優劣。
DFS 其實就是用遞迴的概念擴散
所以先想想看這題
怎麼用遞迴吧!
給定一個二維圖片,請你實作油漆桶功能。
所以先想想看這題怎麼用遞迴吧!
0, 0
0, 1
0, 2
1, 0
1, 1
2, 0
2, 2
1, 2
2, 1
在遞迴的課程中,我們講過:
遞迴就是一個定義遊戲。
的定義是在這個位置倒油漆桶。
這個遞迴式可以做什麼呢?
做題目所說的事情。
sol(位置)
(可能還有顏色跟地圖,看個人實作)
給定一個二維圖片,請你實作油漆桶功能。
所以先想想看這題怎麼用遞迴吧!
0, 0
0, 1
0, 2
1, 0
1, 1
2, 0
2, 2
1, 2
2, 1
在遞迴的課程中,我們講過:
遞迴就是一個定義遊戲。
sol(位置) = 在這個位置到油漆桶
sol(x, y) - 在 x, y 倒油漆桶
看起來好像跟
BFS一樣?別急
sol(x, y) - 在 x, y 倒油漆桶
0, 0
0, 1
0, 2
1, 0
1, 1
2, 0
2, 2
1, 2
2, 1
想想看遞迴順序吧!
0, 0
0, 1
0, 2
1, 0
1, 1
2, 0
是不是跟 BFS 很不一樣呢?
class Solution {
public:
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
int original_color = image[sr][sc];
if (color == original_color)
return image;
image[sr][sc] = color;
for (int k=0; k<4; k++) {
int nx = sr + dx[k], ny = sc + dy[k];
if (nx < 0 || nx >= image.size() || ny < 0 || ny >= image[nx].size())
continue;
if (image[nx][ny] == original_color)
floodFill(image, nx, ny, color);
}
return image;
}
};
一些實作細節
C++ Solution
class Solution:
def floodFill(self, image: List[List[int]], sr: int, sc: int, color: int) -> List[List[int]]:
original_color = image[sr][sc]
if color == original_color:
return image
image[sr][sc] = color
for dx, dy in [[1, 0], [0, 1], [-1, 0], [0, -1]]:
nx, ny = sr + dx, sc + dy
if not (0 <= nx < len(image) and 0 <= ny < len(image[nx])):
continue
if image[nx][ny] == original_color:
self.floodFill(image, nx, ny, color)
return image
一些實作細節
Python Solution
急完了,你來自己做做看這題 BFS 怎麼寫吧!
BFS 就像是水源擴散一樣,
會先把深度淺的都搜尋完才會進下一層。
DFS 就像是強力水柱一樣,
會往一個方向全搜尋完才會搜尋另一個方向。
Breath-first Search (BFS)
Depth-first Search (DFS)
關於 C++ 的 Stack Overflow,請看 遞迴-StackOverflow
關於 Python 的 Stack Overflow 會有這樣的錯誤訊息
這個時候你可以打這幾行指令放鬆限制
import sys
sys.setrecursionlimit(1000000)
這是因為 python 直接加了硬限制,
遞迴呼叫鍊只能 1000 次 (stack frame只能疊1000次)
其實...
你已經會了你知道嗎?
寫出一個 BFS,
把 queue 改成 stack 就好。
想想看為什麼?
給定一個二維圖,判斷有幾個島。
舉例來說:
11000
11000
00100
00011
三個島
11110
11010
11000
00000
一個島
請嘗試寫出三個解法:
Enumerate by BFS / DFS
我們會介紹兩種進階用法
什麼是唯一解?
就是只在乎能不能夠解出來,
不在乎過程怎麼走的。
對於走迷宮來說:
Enumerate by DFS - Backtracking
給你一個未完成的數獨,完成他。
舉例來說:
在此之前,先完成一個東西...
給你一個未完成的數獨,給定位置,
回傳這個位置的所有可能。
vector<char> getPossibilities(vector<vector<char>>& board, int x, int y) {
bool visit[256] = {};
for (int i=0; i<9; i++) {
visit[board[i][y]] = true;
visit[board[x][i]] = true;
visit[board[x/3*3 + i/3][y/3*3 + i%3]] = true;
}
vector<char> V;
for (char c='1'; c<='9'; c++)
if (!visit[c])
V.push_back(c);
return V;
}
def getPossibilities(self, board: List[List[str]], x: int, y: int) -> bool:
possibles = set('123456789')
for i in range(9):
possibles.discard(board[i][y])
possibles.discard(board[x][i])
possibles.discard(board[x//3*3 + i//3][y//3*3 + i%3])
return possibles
給你一個未完成的數獨,完成他。
我們嘗試 DFS 它吧!
怎麼定義遞迴式呢?
sol (board) = 完成數獨
好像有點寫不出來過程 (拆成更小的步驟)...
硬要寫的話就會變成這樣
求數獨解 -> 找第一個洞 -> 嘗試答案 -> 再求數獨解
這樣寫也不是不行,但有個更好的寫法。
給你一個未完成的數獨,完成他。
我們嘗試 DFS 它吧!
怎麼定義遞迴式呢?
sol (board, pos) = 從第pos個格子完成數獨
這樣好像就比較好遞迴了!
因為我們要先解決完第 pos 格填什麼,再去解決第 pos + 1 格填什麼。
Base Case: 當你填滿的時候
所以大致上,遞迴(猜)的順序大概會是這樣...
Start!
猜第一格
猜1
猜2
猜3
猜8
...
猜第二格
猜第三格
猜1
猜2
猜1
...
...
填完了:)
...
...
這種錯了就回去
重試其他答案
的DFS,
就叫做回溯法
(Backtracking)
沒其他可能了:(
沒其他可能了:(
沒其他可能了:(
沒其他可能了:(
給你一個未完成的數獨,完成他。
再想得細一點吧!
sol (board, pos) = 從第pos個格子完成數獨
sol(board, pos)
先解決完第 pos 格填什麼,再解決第 pos + 1 格填什麼。
給你一個未完成的數獨,完成他。
bool solveSudoku(vector<vector<char>>& board, int start=0) {
if (start == 81)
return true;
int x = start / 9, y = start % 9;
if (board[x][y] != '.')
return solveSudoku(board, start+1);
for (char guess : getPossibilities(board, x, y)) {
board[x][y] = guess;
if (solveSudoku(board, start+1))
return true;
board[x][y] = '.';
}
return false;
}
def solveSudoku(self, board: List[List[str]], start=0) -> None:
if start == 81:
return True
x, y = start // 9, start % 9
if board[x][y] != '.':
return self.solveSudoku(board, start+1)
else:
for guess in self.getPossibilities(board, x, y):
board[x][y] = guess
if self.solveSudoku(board, start+1):
return True
board[x][y] = '.'
return False
C++
Solution
Python
Solution
記得清理狀態
記得清理狀態
Start!
猜第一格
猜1
猜2
猜第二格
猜第三格
猜1
猜2
猜1
...
沒其他可能了:(
沒其他可能了:(
記得清理狀態
這個時候第二格不是 '.'
而是上一輪的2
Start!
猜第一格
猜1
猜2
猜第二格
猜第三格
猜1
猜2
猜1
...
沒其他可能了:(
沒其他可能了:(
重新想想看倒油漆跟猜數獨的關係吧!
1, 1
0, 1
0, 0
1, 0
2, 0
0, 2
從 (1, 1) 開始倒
重新想想看倒油漆跟猜數獨的關係吧!
Start!
猜第一格
猜1
猜2
猜第二格
猜1
猜2
1, 1
0, 1
0, 0
0, 2
從 (1, 1) 開始倒
在數獨的 Backtracking 中,
是以每一個 (遊戲) 狀態為單位。
在倒油漆的 DFS 中
是以每一個格子為單位。
所以其實 Backtracking 就是把抽象的遊戲狀態當成是格子在做 DFS !
Enumerate by BFS - Uniform Cost Search (UCS)
給你一個 2 x 3 的拼盤,
求最優解的步數
舉例來說:
解答: 5
解答: -1 (無解)
在此之前,先完成一些東西...
給你一個 2 x 3 的拼盤,
vector<vector<int>> endBoard{{1, 2, 3}, {4, 5, 0}};
bool isEnd(vector<vector<int>> &board) {
return board == endBoard;
}
vector<vector<vector<int>>> getPossibilites(vector<vector<int>> &board) {
vector<vector<vector<int>>> possibilities;
// Find zero
int x, y;
for (int i = 0; i < 2; i++)
for (int j = 0; j < 3; j++)
if (board[i][j] == 0)
x = i, y = j;
// Get Possibilities
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
for (int k = 0; k < 4; k++) {
int nx = x + dx[k], ny = y + dy[k];
if (nx < 0 || nx > 1 || ny < 0 || ny > 2)
continue;
vector<vector<int>> nxt_board(board);
swap(nxt_board[nx][ny], nxt_board[x][y]);
possibilities.push_back(nxt_board);
}
return possibilities;
}
vector可以接直接==,但陣列不行喔
給你一個 2 x 3 的拼盤,
def isEnd(self, board):
return board == [[1, 2, 3], [4, 5, 0]]
def getPossibilities(self, board):
# Find Zero
x, y = [(i, j) for i in range(2) for j in range(3) if board[i][j] == 0][0]
possibilities = []
for dx, dy in [[1, 0], [-1, 0], [0, 1], [0, -1]]:
nx, ny = x + dx, y + dy
if not (0 <= nx <= 1 and 0 <= ny <= 2):
continue
nxt_board = deepcopy(board)
nxt_board[x][y], nxt_board[nx][ny] = nxt_board[nx][ny], nxt_board[x][y]
possibilities.append(nxt_board)
return possibilities
list / tuple 都可以接直接==
要使用 copy.deepcopy 進行深度複製
要是沒有使用 deepcopy,
你會改到原本的list。
接下來呢?我們先畫遊戲樹給你看。
(game tree)
4
1
2
5
-
3
4
1
2
5
3
-
4
1
2
-
5
3
4
-
2
5
1
3
4
1
-
5
3
2
4
1
2
5
-
3
4
2
-
5
1
3
-
4
2
5
1
3
4
1
2
5
-
3
4
1
2
5
-
3
-
1
2
4
5
3
(順序: 右上左下)
重複
重複
重複
所以其實 Backtracking 就是把抽象的遊戲狀態當成是格子在做 DFS !
在解數獨中,我們說:
在解 sliding puzzle 也是一樣!
Uniform Cost Search 其實就是把抽象的遊戲狀態當成是格子在做 BFS !
所以整個流程會是:
表示跟之前的BFS不同的地方
Uniform Cost Search 其實就是把抽象的遊戲狀態當成是格子在做 BFS !
int slidingPuzzle(vector<vector<int>>& board) {
queue<pair<vector<vector<int>>, int>> Q;
Q.push({board, 0});
unordered_set<string> visit;
while (!Q.empty()) {
auto [cur_board, cost] = Q.front();
Q.pop();
if (isEnd(cur_board))
return cost;
auto key = compress(cur_board);
if (visit.find(key) != visit.end())
continue;
visit.insert(key);
for (auto nxt_board : getPossibilites(cur_board))
Q.push({nxt_board, cost + 1});
}
return -1;
}
string compress(
vector<vector<int>>& board) {
string result;
for (const auto& row : board)
for (int val : row)
result += to_string(val);
return result;
}
將版面壓縮成 str 的函數。
為什麼要這一步之後會提。
Uniform Cost Search 其實就是把抽象的遊戲狀態當成是格子在做 BFS !
def slidingPuzzle(self, board: List[List[int]]) -> int:
Q = deque([(board, 0)])
visit = set()
while Q:
cur_board, cost = Q.popleft()
if self.isEnd(cur_board):
return cost
key = str(cur_board)
if key in visit:
continue
visit.add(key)
for nxt_board in self.getPossibilities(cur_board):
Q.append((nxt_board, cost+1))
return -1
將版面壓縮成 str。
為什麼要這一步之後會提。
我們說...
C++ 的 set / map
Python 的 set / dict
是一個存放 的資料結構
(包括 unordered)
key / key-value
這個key
是唯一的。
這個 key 至少要能夠判斷兩個東西是不是一樣的。
我們先前提到這樣的一句話
那麼請你想想看:
#include <iostream>
#include <unordered_set>
#include <set>
using namespace std;
struct A {
int x;
bool operator <(const A &other) const {
return x < other.x;
}
};
struct B {
int x;
bool operator ==(const B &other) const {
return x == other.x;
}
};
namespace std {
template<> struct hash<B> {
size_t operator()(const B& p) const noexcept {
return p.x;
}
};
}
int main() {
set<A> S1;
S1.insert(A());
unordered_set<B> S2;
S2.insert(B());
}
class A:
def __init__(self):
self.x = 0
def __eq__(self, value: object) -> bool:
return self.x == value.x
def __hash__(self) -> int:
return self.x
print(len({A(), A()}))
具體 set / map / dict 怎麼實作的,
以及什麼是hash,
這個實在是太複雜了,
所以我們暫時不提。
題目名稱 | 來源 | 備註 | CodingBar |
---|---|---|---|
傳送點 | Zerojudge f166 | APCS 2019/10 - 4 | C 班圖論 2-2 |
開啟寶盒 | Zerojudge k734 |
APCS 2023/6 - 4 其實比較像 Kahn |
C 班圖論 2-1 |
搬家 | Zerojudge m372 | APCS 2023/10 - 3 | |
靈犬尋寶 | Zerojudge b059 | 95年學科競賽 - 4 | C 班圖論 1-5 |
Numbers of Islands | Leetcode 200 | C 班圖論 1-1 | |
暴雨降臨 | C 班圖論 3-1 | ||
星際旅行 | C 班圖論 3-4 | ||
Find a safe walk through a grid |
Leetcode 787 | 二維圖最短路, 但不太一樣 |
遊戲樹
遞迴樹
分析樹
決策樹
(Decision Tree)
(Parsing Tree)
點裡面是一份資料的。
Terminology of Tree
A
B
C
D
E
F
G
H
I
A
(Root Node)
(Leaf / Outer / Terminal Node)
D
F
G
(Edge)
(Node)
C
H
I
以 C 為準:
(Parent Node)
A
B
C
D
E
F
G
H
I
A
(Children Node)
E
F
G
(Sibling Node)
B
(Ancestors)
(Descendants)
(Neighbor)
E
F
G
H
I
A
A
E
F
G
(Degree)
1
2
3
A
B
C
D
E
F
G
H
I
(Height)
(Size)
(Degree)
高
度
為
3
* 有些人的高度定義是4,你爽就好。
大小為 9
度為 3
(Binary Tree)
(Complete Binary Tree)
(Perfect / Full Binary Tree)
* 3 就 三元樹,同理。
A
B
C
D
E
G
H
I
J
K
L
M
N
O
P
中文 | 英文 | 意思 |
---|---|---|
樹根 | Root Node | 沒有父節點的點 (孤兒) |
樹葉 / 葉節點 | Leaf / Outer / Terminal Node | 沒有小孩的節點 |
父節點 | Parent Node | 上游的那個點 |
子節點 | Children Node | 下游的點們 |
兄弟節點 | Sibling Node | 父節點的子節點扣掉自己 |
祖先 | Ancestors | 上游的所有點 |
子孫 | Descendants | 下游的所有點 |
鄰居 | Neighbor | 父節點+子節點 (= 距離為 1 的點) |
節點的度 | Degree of Node | 有幾個子節點 |
樹的度 | Degree of Tree | 樹中所有節點中最大的度 |
樹的大小 | Size | 總共有幾個節點 |
樹的高度 | Height | 從樹根到樹葉的最大距離 |
二元樹 | Binary Tree | 樹的度為 2 |
完全二元樹 | Complete Binary Tree | 排列緊湊的二元樹 |
完美 / 滿二元樹 | Full / Perfect Binary Tree | 樹葉都在同深度的完全二元樹 |
(僅列舉比較重要的)
A
B
C
D
How to build a tree?
完全樹 -> 陣列
不是完全樹 -> 開 struct / class
如果這棵樹是一顆完全二元樹
A
B
E
D
C
G
F
我們來嘗試對其編號:
如果一個點的編號為 x :
0
1
2
3
4
5
6
char tree[] = "ABEDFCG";
int x = 1;
printf("Node %d: %c", x, tree[x]);
int x_l = 2*x+1;
printf("L %d: %c", x_l, tree[x_l]);
int x_r = 2*x+2;
printf("R %d: %c", x_r, tree[x_r]);
tree = "ABEDFCG"
x = 1
print(f"Node {x}: {tree[x]}")
x_l = 2*x+1
print(f"L {x_l}: {tree[x_l]}")
x_r = 2*x+2
print(f"R {x_r}: {tree[x_r]}")
A
B
E
D
C
G
F
0
1
2
3
4
5
6
C++
Python
struct Tree {
char data;
Tree *l, *r;
Tree (char d, Tree *l, Tree *r):
data(d), l(l), r(r) {}
};
A
B
E
D
C
G
F
class Tree:
def __init__(self, d, l, r):
self.data = d
self.l = l
self.r = r
C++
Python
#include <stdio.h>
struct Tree {
char data;
Tree *l, *r;
Tree (char d, Tree *l, Tree *r):
data(d), l(l), r(r) {}
};
int main() {
Tree *root = new Tree('A',
new Tree('B',
new Tree('D', NULL, NULL),
new Tree('F', NULL, NULL)
),
new Tree('E',
new Tree('C', NULL, NULL),
new Tree('G', NULL, NULL)
)
);
printf("%c\n", root->l->r->data); // F
printf("%c\n", root->r->l->data); // C
}
A
B
E
D
C
G
F
C++
class Tree:
def __init__(self, d, l, r):
self.data = d
self.l = l
self.r = r
root = Tree('A',
Tree('B',
Tree('D', None, None),
Tree('F', None, None)),
Tree('E',
Tree('C', None, None),
Tree('G', None, None))
)
print(root.l.r.data) # F
print(root.r.l.data) # C
A
B
E
D
C
G
F
Python
struct Tree {
char data;
Tree l, r;
Tree (char d, Tree l, Tree r):
data(d), l(l), r(r) {}
};
struct Tree {
char data;
int l, r;
Tree (char d, int l, int r):
data(d), l(l), r(r) {}
};
5
3
6
1
9
6. 請嘗試做出這樣的一棵樹,使用 struct / class
BFS / DFS a tree
給你一個 struct / class 建構
出來的二元樹,輸出所有點的資訊
A
B
E
D
C
G
F
想想看 BFS 怎麼做!
t=1
t=1
t=2
t=2
t=2
t=2
如果在 Root 倒水
所以跟之前只差在
擴散的點是由樹決定的,
並且 BFS 的單位是一顆節點!
(而且不會回流)
給你一個 struct / class 建構
出來的二元樹,輸出所有點的資訊
A
B
E
D
C
G
F
t=1
t=1
t=2
t=2
t=2
t=2
queue<Tree *> Q({root});
while (!Q.empty()) {
Tree *cur = Q.front();
Q.pop();
printf("-> %c ", cur->data);
if (cur->l) Q.push(cur->l);
if (cur->r) Q.push(cur->r);
}
-> A -> B -> E -> D -> F -> C -> G
Q = deque([root])
while Q:
cur = Q.popleft()
print(" ->", cur.data, end='')
if cur.l: Q.append(cur.l)
if cur.r: Q.append(cur.r)
C++
Python
輸出
給你一個 struct / class 建構
出來的二元樹,輸出所有點的資訊
A
B
E
D
C
G
F
想想看 DFS 怎麼做!
遞迴定義:輸出整棵樹
rec(root):
給你一個 struct / class 建構
出來的二元樹,輸出所有點的資訊
A
B
E
D
C
G
F
void dfs(Tree *root) {
if (!root) return;
printf("-> %c ", root->data);
dfs(root->l);
dfs(root->r);
}
-> A -> B -> D -> F -> E -> C -> G
def dfs(root):
if root:
print(" ->", root.data, end='')
dfs(root.l)
dfs(root.r)
C++
Python
輸出
1
2
3
4
5
6
7
給你一個 struct / class 建構
出來的二元樹,輸出所有點的資訊
A
B
E
D
C
G
F
rec(root):
這個順序很重要嗎?
你能夠講出其他兩種的輸出嗎?
給你一個 struct / class 建構
出來的二元樹,輸出所有點的資訊
A
B
E
D
C
G
F
Preorder: ABDFECG
Inorder: DBFACEG
Postorder: DFBCGEA
Level Order: ABEDFCG
題目名稱 | 來源 |
---|---|
Binary Tree Level Order Traversal | Leetcode 102 |
Binary Tree Level Order Traversal II | Leetcode 107 |
Binary Tree Preorder Traversal | Leetcode 144 |
Binary Tree Inorder Traversal | Leetcode 94 |
Binary Tree Postorder Traversal | Leetcode 145 |
基礎走訪
題目名稱 | 來源 |
---|---|
Same Tree | Leetcode 100 |
Binary Tree Zigzag Level Order Traversal | Leetcode 103 |
Serialize and Deserialize Binary Tree | Leetcode 297 |
進階
嘗試用遞迴去寫吧!
給你一個 struct / class 建構
出來的二元樹,求樹的深度 (1個點算深度1)
A
B
E
D
F
想想看 DFS 怎麼做!
遞迴定義:輸出整棵樹的高度
rec(root):
一般情況下,
樹題我們都會用 DFS 去做
Base case 呢?
rec(NULL / None) = 0
給你一個 struct / class 建構
出來的二元樹,求樹的深度 (1個點算深度1)
A
B
E
D
F
int maxDepth(TreeNode* root) {
if (!root) return 0;
return 1 + max(
maxDepth(root->left),
maxDepth(root->right));
}
def maxDepth(self, root):
if root is None:
return 0
return 1 + max(
self.maxDepth(root.left),
self.maxDepth(root.right))
C++
Python
給你一個 struct / class 建構出來的二元樹,
輸出樹的直徑長度
A
B
D
F
I
J
K
F
J
樹的直徑定義:
一棵樹距離最遠的兩個點的連線
直徑為5
無腦下遞迴定義:
rec(root) = 樹的直徑
好像有點困難?
像 DP 一樣分個 Case 吧!
給你一個 struct / class 建構出來的二元樹,
輸出樹的直徑長度
A
B
D
F
I
J
K
F
J
樹的直徑定義:
一棵樹距離最遠的兩個點的連線
直徑為5
rec(root) = 樹的直徑
像 DP 一樣分個 Case 吧!
有經過 Root
沒經過 Root
rec(root.l)
rec(root.r)
1
+ height(root.l)
+ height(root.r)
(自己)
所以你還要順便算 height!
(每次重算複雜度會炸)
給你一個 struct / class 建構出來的二元樹,
輸出樹的直徑長度
class Solution {
public:
// Return height, diameter
pair<int, int> height_and_diameter(TreeNode *root) {
if (!root) return {0, 0};
auto [L_h, L_d] = height_and_diameter(root->left);
auto [R_h, R_d] = height_and_diameter(root->right);
return {1 + max(L_h, R_h), max({L_d, R_d, 1+L_h+R_h})};
}
int diameterOfBinaryTree(TreeNode* root) {
return height_and_diameter(root).second - 1;
}
};
class Solution:
def diameterOfBinaryTree(self, root):
def rec(root):
if root is None:
return 0, 0
L_h, L_d = rec(root.left)
R_h, R_d = rec(root.right)
return 1 + max(L_h, R_h), max(L_h + R_h + 1, L_d, R_d)
return rec(root)[1]-1
C++
Python
L_h: L的height
L_d: L的diameter
題目名稱 | 來源 | 備註 |
---|---|---|
子樹大小 | C 班 圖論 I - 4 | 算每個節點的大小 |
Balanced Binary Tree | Leetcode 110 | |
Path Sum | Leetcode 112 | |
Path Sum II | Leetcode 113 | 好像是經典題 |
Binary Tree Maximum Path Sum | Leetcode 124 | 處理很像算直徑 |
血緣關係 | Zerojudge b967 |
APCS 2016/3 - 4 拓樸更直觀一點 |
樹狀圖分析 (Tree Analyses) | Zerojudge c463 | APCS 2017/10 - 3 |
貨物分配 | Zerojudge h029 | APCS 2020/1 - 4 |
Binary Search Tree
來介紹一種資料結構吧!
給你一個 struct / class 建構出來的二元搜尋樹,
給你一個值,請回傳擁有這個值的節點。
7
4
9
1
6
二元搜尋樹的定義:
對於每個點,滿足下列條件:
如果這個點改8,那麼這就不是BST
x
l
r
怎麼找到題目要求的值?
(Binary Search Tree, BST)
給你一個 struct / class 建構出來的二元搜尋樹,
給你一個值,請回傳擁有這個值的節點。
7
4
9
1
6
二元搜尋樹的定義:
(Binary Search Tree, BST)
如果現在要搜尋 2 這個數字
可以通過跟現在的
root.val 比較,
知道 2 會在哪裡。
find 2
find 2
find 2
7
<7
>7
Base Case:
找到沒東西 (NULL / None),
就是不存在
給你一個 struct / class 建構出來的二元搜尋樹,
給你一個值,請回傳擁有這個值的節點。
(Binary Search Tree, BST)
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if (!root) return NULL;
if (val == root->val) return root;
if (val < root->val) return searchBST(root->left, val);
return searchBST(root->right, val);
}
};
class Solution:
def searchBST(self, root, val):
if root is None: return None
if root.val == val: return root
if val < root.val: return self.searchBST(root.left, val)
return self.searchBST(root.right, val)
C++
Python
給你一個 struct / class 建構出來的二元樹,
判斷其是否為二元搜尋樹 (Binary Search Tree)
7
4
9
1
6
先來個無腦定義
rec(root) = 判斷這棵樹是不是BST
好像不好下手,
仔細想想看每個點的限制
(考慮祖先們的限制就好,為什麼?)
對於每個點,都有其上下界。
只要都遵循,就是BST
把上下界加入遞迴參數內!
給你一個 struct / class 建構出來的二元樹,
判斷其是否為二元搜尋樹 (Binary Search Tree)
7
4
9
1
6
rec(root, L, R) = 判斷是不是BST,
並限定之後的數字只能 > L 且 < R
祖先的數字
會限制他的子孫們
怎麼個限制?
遞迴的時候順便
檢查自己的數值就好了!
給你一個 struct / class 建構出來的二元樹,
判斷其是否為二元搜尋樹 (Binary Search Tree)
class Solution {
public:
bool isValidBST(
TreeNode* root,
long long L_bound=LLONG_MIN,
long long R_bound=LLONG_MAX) {
if (!root)
return true;
if (root->val <= L_bound || R_bound <= root->val)
return false;
return
isValidBST(root -> left, L_bound, root->val) && \
isValidBST(root -> right, root->val, R_bound);
}
};
class Solution:
def isValidBST(self, root, L=-2**32, R=2**32) -> bool:
if root is None:
return True
if root.val <= L or root.val >= R:
return False
return self.isValidBST(root.left, L, root.val) and \
self.isValidBST(root.right, root.val, R)
C++
Python
平衡的定義:
任一棵節點 x 的
Hint:
只是我們先教點 Graph 再說吧!
還有APCS的樹也不會考很難
其實 18 世紀就在玩免洗手遊
柯尼斯堡七橋問題 (1735)
Seven Bridges of Königsberg
現在有四個區域,七座橋。
請問可以在每個橋
都只走一遍的情況下,
走完所有七個橋嗎?
柯尼斯堡七橋問題 (1735)
Seven Bridges of Königsberg
簡化一下圖
A
B
C
D
A
B
C
D
樹 就是一種特殊的圖
森林 就是一種特殊的圖
很多棵樹
地圖 也是一種圖
關係圖 也是一種圖
(很混亂的)
電路設計 也是一種圖
知識圖 也是一種圖
(Knowledge Graph)
這張圖是一種CPU的電路架構
就是畫出各種東西的關係鍊
點裡面是一份資料的。
A
B
C
D
Terminology of Graph
A
B
C
D
(Directed Graph / Digraph)
(Edge)
(Vertex)
A
B
C
D
A
A
Undirected Graph
有向圖
無向圖
通常無向圖會當作有向圖做。
<B, C> 無向圖 = <B, C>, <C, B> 有向圖
我們會用 G = (V, E) 來定義一個圖。
Graph = (Vertex, Edge)
點的性質,以 B 為準:
(Adjacent)
(in-degree)
(out-degree)
A
B
C
D
邊的性質:
(self-edge / loop)
(multiple / parallel edges)
(simple graph)
C
入度=3
出度=1
(circuit)
A
B
C
D
(Path / Simple Path)
(cycle)
ABCDBCDADAD
ADCB
ADCBDE
ABCDA
ABEDCBDA
E
A
B
C
D
A
B
C
D
A
B
C
D
(弱)連通圖
(weakly) connected graph
每個點都是
被連起來的。
強連通圖
strongly connected graph
任兩個節點都
有一條路可以到。
不是個連通圖
圖中連通的塊稱做元件 (component)
A
B
C
D
A
B
C
D
有向無環圖
Directed Acyclic Graph, DAG
沒有任何環的
有向圖
無向無環圖
Undirected Acyclic Graph
沒有任何環的
無向圖
沒有任何環的無向圖,並且連通
A
B
C
D
森林
Forest
樹
Tree
吸收一下吧!
Vertex 的個數
Edge 的個數
這也是一個平面圖
A
B
C
D
A
B
C
D
因為它可以
變成這樣
A
B
C
(僅列舉比較重要的)
中文 | 英文 | 意思 |
---|---|---|
(節)點 | Vertex | 就...點? |
邊 | Edge | 連結兩個點的箭頭 |
圖 | Graph | 由點跟邊定義的一個關係圖 |
相鄰 | Adjacent | 一個點往外箭頭指誰 |
入度 | in-degree | 有多少個箭頭指向這個點 |
出度 | out-degree | 有多少個箭頭從這個點指出去 |
自環 | Self-edge / loop | 有這樣的邊 <u, u> |
重邊 | multiple / parallel edges | 有兩個一模一樣的箭頭 |
(簡單) 路徑 | (Simple) Path | 沒有經過重複 V 的 walk |
環 | Cycle | 起終點一樣的 path |
簡單圖 | Simple Graph | 沒有自環,重邊的圖 |
(弱)連通圖 | (Weakly) Connected Graph | 每個點都跟其他人連著的圖 |
強連通圖 | Strongly Connected Graph | 每兩個點都有 Path 可以到達 |
元件 | Component | 連通的子圖 (subgraph) |
How to build a graph?
0
1
2
3
0
1
2
3
紀錄一下每個點
的 adjacent 有誰吧!
1
3
2
1
1
2
這種叫做 相鄰串列 (Adjacency List)
0
1
2
3
0
1
2
3
1
3
2
1
1
2
相鄰串列 (Adjacency List)
vector<vector<int>> G {
{1, 3},
{2},
{1},
{1, 2}
};
G = {
0: [1, 3],
1: [2],
2: [1],
3: [1, 2]
}
C++
Python
用 map/dict 存還是用
set/map 存其實都可以。
0
1
2
3
相鄰串列 (Adjacency List)
vector<vector<int>> G(n);
cin >> n;
for (int x=0; x<n; x++) {
cin >> m;
while (m--) {
cin >> y;
G[x].push_back(y);
}
}
C++ 相鄰串列
4 6
0 1
0 3
1 2
2 1
3 1
3 2
4
2 1 3
1 2
1 1
2 1 2
題目常見的兩種給法:
相鄰串列
只給邊
vector<vector<int>> G(n);
cin >> n >> m;
while (m--) {
cin >> x >> y;
G[x].push_back(y);
}
4個點
6條邊
4個點
C++ 只給邊
* 如果是無向圖
要push <x,y> 跟 <y,x>
0
1
2
3
相鄰串列 (Adjacency List)
n = int(input())
G = {i:[] for i in range(n)}
for i in range(n):
G[i] = list(map(int, input().split()))[1:]
Python 相鄰串列
4 6
0 1
0 3
1 2
2 1
3 1
3 2
4
2 1 3
1 2
1 1
2 1 2
題目常見的兩種給法:
相鄰串列
只給邊
n, m = map(int, input().split())
G = {i:[] for i in range(n)}
for _ in range(m):
x, y = map(int, input().split())
G[x].append(y)
4個點
6條邊
4個點
Python 只給邊
* 如果是無向圖
要push <x,y> 跟 <y,x>
0
1
2
3
相鄰矩陣 (Adjacency Matrix)
vector<vector<int>> G {
{0, 1, 0, 1},
{0, 0, 1, 0},
{0, 1, 0, 0},
{0, 1, 1, 0}
};
G = [
[0, 1, 0, 1],
[0, 0, 1, 0],
[0, 1, 0, 0],
[0, 1, 1, 0],
]
C++
Python
對於解題向
比較少在用
0
1
2
3
0
1
2
3
BFS / DFS a Graph
給你一個圖 G 的 Adjacency List,
輸出從 0 開始可以碰到的所有點
想想看 BFS 怎麼做!
t=2
如果在 0 倒水
所以跟之前只差在
擴散的點是由鄰居決定的,
並且 BFS 的單位是一顆節點!
0
1
2
3
t=1
t=1
t=2
走過不要
再走了!
給你一個圖 G 的 Adjacency List,
輸出從 0 開始可以碰到的所有點
t=2
0
1
2
3
t=1
t=1
t=2
走過不要
再走了!
vector<bool> visit(4, false);
queue<int> Q({0});
while (!Q.empty()) {
int cur = Q.front();
Q.pop();
if (visit[cur])
continue;
visit[cur] = true;
printf("-> %d ", cur);
for (auto neighbor: G[cur])
Q.push(neighbor);
}
-> A -> B -> E -> D -> F -> C -> G
Q = deque([0])
visit = set()
while Q:
cur = Q.popleft()
if cur in visit:
continue
visit.add(cur)
print(" ->", cur, end='')
for neighbor in G[cur]:
Q.append(neighbor)
C++
Python
-> 0 -> 3 -> 1 -> 2
輸出
想想看 DFS 怎麼做!
遞迴定義:
輸出從0開始碰的到的點
rec(cur=0):
給你一個圖 G 的 Adjacency List,
輸出從 0 開始可以碰到的所有點
0
1
2
3
給你一個圖 G 的 Adjacency List,
輸出從 0 開始可以碰到的所有點
0
1
2
3
vector<bool> visit(4, false);
void dfs(int cur) {
if (visit[cur])
return ;
visit[cur] = true;
printf(" -> %d", cur);
for (int neighbor : G[cur])
dfs(neighbor);
}
def dfs(cur, visit):
if cur in visit:
return
visit.add(cur)
print(" ->", cur, end='')
for neighbor in G[cur]:
dfs(neighbor, visit)
C++
Python
-> 0 -> 1 -> 2 -> 3
輸出
給你一個圖 G 的 E,
問 source 可不可以到 destination
嘗試自己做做看吧!
給你一個 簡單圖 G,
請判斷 G 是不是個二分圖 (Bipartite)
什麼是二分圖?
顧名思義就是可以把圖二分的意思
A
B
C
D
E
F
完全二分圖
A
B
C
非二分圖的
最小例子
可以把點分成兩群,
每群之間沒有邊,
就叫做二分圖
A
B
C
D
給你一個 簡單圖 G,
請判斷 G 是不是個二分圖 (Bipartite)
A
B
C
D
集合 1
集合 2
如果是二分圖,則
可以將點分成 集合 1 和 集合 2
先對第一個點標成集合 1,
接著 1 2 1 2 的往外擴散。
如果任何一個點的標記衝突,
則這就不是二分圖
給你一個 簡單圖 G,
請判斷 G 是不是個二分圖 (Bipartite)
A
B
C
D
如果任何一個點的標記衝突,
則這就不是二分圖
1
2
2
1
先對第一個點標成集合 1,
接著 1 2 1 2 的往外擴散。
使用 DFS 擴散
要怎麼定義 dfs()?
(給編號2也是可以啦)
dfs (起點, 這個點的編號)
= 判斷這個點的元件是不是二分圖
給你一個 簡單圖 G,
請判斷 G 是不是個二分圖 (Bipartite)
class Solution {
public:
bool dfs(vector<vector<int>>& graph,
vector<int>& group, int start, int group_num) {
if (group[start])
return group[start] == group_num;
group[start] = group_num;
for(auto nxt: graph[start])
if (!dfs(graph, group, nxt, 3-group_num))
return false;
return true;
}
bool isBipartite(vector<vector<int>>& graph) {
vector<int> group(graph.size(), 0);
for (int i=0; i<graph.size(); i++)
if (!group[i] && !dfs(graph, group, i, 1))
return false;
return true;
}
};
group = 0 表示沒走過
group = 1 表示編號1
group = 2 表示編號 2
如果沒走過 (group[i] == 0),那麼就對 i 開始跑檢測,並幫 i 給編號1
如果 group != 0,
檢查編號有沒有衝突,
並且回傳,結束 (不回流)
1 的鄰居是 2
2 的鄰居是 1
如果任何一個檢測是 false
就回傳 false
C++
給你一個 簡單圖 G,
請判斷 G 是不是個二分圖 (Bipartite)
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
def dfs(start, group_num):
if group[start]:
return group[start] == group_num
group[start] = group_num
for nxt in graph[start]:
if not dfs(nxt, 3 - group_num):
return False
return True
group = [0] * len(graph)
for i in range(len(graph)):
if group[i] == 0:
if not dfs(i, 1):
return False
return True
Python
group = 0 表示沒走過
group = 1 表示編號1
group = 2 表示編號 2
如果沒走過 (group[i] == 0),那麼就對 i 開始跑檢測,並幫 i 給編號1
如果 group != 0,
檢查編號有沒有衝突,
並且回傳,結束 (不回流)
1 的鄰居是 2
2 的鄰居是 1
如果任何一個檢測是 false
就回傳 false
題目名稱 | 來源 | 備註 |
---|---|---|
Clone Graph | Leetcode 133 | 用 struct / class 建圖 |
Dijkstra's Algorithm (偽)
給你一個 帶邊權簡單圖 G,邊權 = 擴散所需時間
從第 k 點擴散,需要多久才可以擴散所有點?
1
3
2
4
5
5
2
2
1
1
1
3
t=2
t=4
t=3
t=5
答案 = 5
我們要怎麼存帶邊權圖 ?
(也有帶點權圖)
Adjacency List
G[u].push_back(v)
G[u].push_back({v, w})
G[u].append(v)
G[u].append((v, w))
給你一個 帶邊權簡單圖 G,邊權 = 擴散所需時間
從第 k 點擴散,需要多久才可以擴散所有點?
1
3
2
4
5
5
2
2
1
1
1
3
t=2
t=4
t=3
t=5
答案 = 5
使用 BFS 會有什麼問題?
1
3
2
5
2
2
你會不知道哪條路比較好
給你一個 帶邊權簡單圖 G,邊權 = 擴散所需時間
從第 k 點擴散,需要多久才可以擴散所有點?
1
3
2
4
5
5
2
2
1
1
1
3
t=2
t=4
t=3
t=5
答案 = 5
使用 BFS 會有什麼問題?
你會不知道哪條路比較好
學習水的思路!
1
3
2
5
2
2
等 t = 5
擴散
等 t = 2
擴散
t=0
/ 1
/ 2
/ 3
/ 4
x 4
給你一個 帶邊權簡單圖 G,邊權 = 擴散所需時間
從第 k 點擴散,需要多久才可以擴散所有點?
1
3
2
4
5
5
2
2
1
1
1
3
t=2
t=4
t=3
t=5
答案 = 5
學習水的思路!
1
3
2
5
2
2
等 t = 5
擴散
等 t = 2
擴散
t=0
/ 1
/ 2
/ 3
/ 4
x 4
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<int> delay(n+1, -1);
// Build Graph
vector<vector<pair<int, int>>> G(n+1);
for (auto edge : times) {
int u = edge[0], v = edge[1], w = edge[2];
G[u].push_back({v, w});
}
// BFS
vector<vector<int>> Q(10001);
Q[0].push_back({k});
for (int t=0; t<Q.size(); t++) {
for (int i=0; i<Q[t].size(); i++) {
int cur = Q[t][i];
if (delay[cur] != -1)
continue;
delay[cur] = t;
for (auto [nxt, w] : G[cur]) {
Q[t+w].push_back(nxt);
}
}
}
// Get answer
// There is a node not being touched
if (*min_element(delay.begin()+1, delay.end()) == -1)
return -1;
// return max delay
return *max_element(delay.begin()+1, delay.end());
}
};
不寫
是因為 w 有可能為 0
for (int cur : Q[t])
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
# Build Graph
G = defaultdict(list)
for u, v, w in times:
G[u].append((v, w))
# BFS
delay = [-1] * (n+1)
Q = defaultdict(list)
Q[0].append((k))
for t in range(10001):
while Q[t]:
cur = Q[t].pop()
if delay[cur] != -1:
continue
delay[cur] = t
for nxt, w in G[cur]:
Q[t+w].append(nxt)
if min(delay[1:]) == -1:
return -1
return max(delay)
不寫
是因為 w 有可能為 0
for cur in Q[t]:
嘗試分析時間複雜度吧!
不重複會跑幾個點?
每個點都會擴散最多多少個點?
最多會有多少的 queue / vector ?
等等, 呢?
嘗試分析時間複雜度吧!
不重複會跑幾個點?
最多擴散多少次?
最多會有多少的 queue / vector ?
Dijkstra's Algorithm
我們在上 BFS 單人解謎 - 最優解 (UCS) 時...
這個資料結構,就是
priority_queue / heapq
你只要每次拿最小時間點,
且還沒看過的點就可以了!
C++ 實作細節:
// Dijkstra's Algorithm
priority_queue<
pair<int, int>,
vector<pair<int, int>>,
greater<pair<int, int>> > pq;
pq.push({0, k});
while (!pq.empty()) {
auto [cur_delay, cur] = pq.top();
pq.pop();
if (delay[cur] != -1)
continue;
delay[cur] = cur_delay;
for (auto [nxt, w] : G[cur])
pq.push({cur_delay + w, nxt});
}
2. 使用 greater
// Dijkstra's Algorithm
priority_queue<pair<int, int>> pq;
pq.push({0, k});
while (!pq.empty()) {
auto [cur_delay, cur] = pq.top();
pq.pop();
if (delay[cur] != -1)
continue;
delay[cur] = -cur_delay;
for (auto [nxt, w] : G[cur])
pq.push({cur_delay - w, nxt});
}
1. 乘以 -1
C++ 實作細節:
// Dijkstra's Algorithm
// Lambda函數。跟 sort 的 cmp 是反的
auto cmp = [](
pair<int, int>& a,
pair<int, int>& b) {
return a.first > b.first;
};
priority_queue< pair<int, int>,
vector<pair<int, int>>,
decltype(cmp) > pq;
pq.push({0, k});
while (!pq.empty()) {
auto [cur_delay, cur] = pq.top();
pq.pop();
if (delay[cur] != -1)
continue;
delay[cur] = cur_delay;
for (auto [nxt, w] : G[cur])
pq.push({cur_delay + w, nxt});
}
4. 使用 declytpe
struct Compare {
// 方向跟 sort 的 cmp 是反的。
bool operator()(
pair<int, int>& a,
pair<int, int>& b) {
return a.first > b.first;
}
};
...
// Dijkstra's Algorithm
priority_queue< pair<int, int>,
vector<pair<int, int>>, Compare > pq;
pq.push({0, k});
while (!pq.empty()) {
auto [cur_delay, cur] = pq.top();
pq.pop();
if (delay[cur] != -1)
continue;
delay[cur] = cur_delay;
for (auto [nxt, w] : G[cur])
pq.push({cur_delay + w, nxt});
}
3. 寫 comparator class / struct
Python 實作細節:
heapify(Q) # 讓 Q 變成 heap
heappush(Q, x) # 將 x 插入進 Q
heappop(Q) # 將 Q pop 最小值
Q[0] # 拿最小值
什麼是 heap?
heap是一種完全二元樹
但想知道更多請參見 heap 章節
Q = [(0, k)] # 一個點不用 heapify
while Q:
cur_delay, cur = heappop(Q)
if delay[cur] != -1:
continue
delay[cur] = cur_delay
for nxt, w in G[cur]:
heappush(Q, (cur_delay+w, nxt))
嘗試分析時間複雜度吧!
不重複會跑幾個點?
每個點都會擴散最多多少個點?
等等,heapq / pq 的 push 複雜度呢?
還有更快的實作,目前是
題目名稱 | 來源 | 備註 |
---|---|---|
最短路徑 | C 班 圖論 I - 2 | |
黑白城市 | C 班 圖論 III - 2 | 邊權只有 0 / 1 |
長途跋涉 | C 班 圖論 III - 3 | 變形題,需要做三次 |
Path with Maximum Probability | Leetcode 1514 | 換個運算符號而已 |
Minimum cost to make at least one valid path | Leetcode 1368 | 我覺得很有趣 |
Cheapest Flights within k stops | Leetcode 787 | 只能走k次的最短路 可能算是對圖做DP |
BFS-based Topological Algorithm / Kahn's Algorithm
給你一個 有向簡單圖 G,每個節點表示一堂課,
<u, v> 表示 v 的擋修是 u (要完成 u 才可以修 v)。
請判斷你能不能夠修完所有課?
0
1
2
0
1
2
可以,2 -> 1 -> 0
不行,死結 (deadlock)
給你一個 有向簡單圖 G,每個節點表示一堂課,
<u, v> 表示 v 的擋修是 u (要完成 u 才可以修 v)。
請判斷你能不能夠修完所有課?
什麼樣的情況下才會有死結?
有環的時候。
這題就是要請你判斷一個圖是不是
有向無環圖 (DAG)
也就是請你做環的檢測
給你一個 有向簡單圖 G,每個節點表示一堂課,
<u, v> 表示 v 的擋修是 u (要完成 u 才可以修 v)。
請判斷你能不能夠修完所有課?
0
1
2
可以,2 -> 1 -> 0
很簡單的貪心策略:
把已經可以修的課
全部修完不就好了嗎?
沒有擋修的課
有什麼性質?
給你一個 有向簡單圖 G,每個節點表示一堂課,
<u, v> 表示 v 的擋修是 u (要完成 u 才可以修 v)。
請判斷你能不能夠修完所有課?
0
1
2
很簡單的貪心策略:
把已經可以修的課
全部修完不就好了嗎?
沒有擋修的課
有什麼性質?
in_deg = 2
in_deg = 1
in_deg = 0
2
修課順序:
-> 1
-> 0
/ 0
/ 1
/ 0
這個算法習慣用BFS做!
給你一個 有向簡單圖 G,每個節點表示一堂課,
<u, v> 表示 v 的擋修是 u (要完成 u 才可以修 v)。
請判斷你能不能夠修完所有課?
很簡單的貪心策略:
把已經可以修的課
全部修完不就好了嗎?
沒有擋修的課
有什麼性質?
這個算法習慣用BFS做!
Kahn's Algorithm 流程
Kahn's Algorithm 流程
class Solution {
public:
bool canFinish(int numCourses,
vector<vector<int>>& prerequisites) {
// Build Graph
vector<vector<int>> G(numCourses);
vector<int> deg_in(numCourses);
for (auto &tmp : prerequisites) {
int u=tmp[0], v=tmp[1];
G[v].push_back(u);
deg_in[u]++;
}
// Find course with in-degree = 0
queue<int> Q;
for (int i=0; i<numCourses; i++) {
if (deg_in[i] == 0)
Q.push(i);
}
// Iteratively delete node
while (!Q.empty()) {
int cur = Q.front();
Q.pop();
for (int nxt : G[cur]) {
// Update in_deg and find next course.
deg_in[nxt]--;
if (deg_in[nxt] == 0) {
Q.push(nxt);
}
}
}
// All in_deg = 0 means you finished all.
return *max_element(deg_in.begin(),
deg_in.end()) == 0;
}
};
C++
1
1
2
3
Kahn's Algorithm 流程
class Solution:
def canFinish(self, numCourses, prerequisites):
G = defaultdict(list)
# Build Graph
deg_in = [0] * numCourses
for u, v in prerequisites:
deg_in[u] += 1
G[v].append(u)
# Find course with in-degree = 0
Q = deque(i for i, deg
in enumerate(deg_in) if deg == 0)
# Iteratively delete node
while Q:
cur = Q.popleft()
for nxt in G[cur]:
# Update in_deg and find next course.
deg_in[nxt] -= 1
if deg_in[nxt] == 0:
Q.append(nxt)
# All courses in_deg = 0
# means you finished all.
return max(deg_in) == 0
Python
1
1
2
3
給你一個 有向簡單圖 G,每個節點表示一堂課,
<u, v> 表示 v 的擋修是 u (要完成 u 才可以修 v)。
請找出一種可以修完課的順序,
如果不可能的話回傳空陣列。
0
1
2
0
1
2
回傳 [2, 1, 0]
無解,回傳 []
這種順序,稱作
Topological Order
給你一個 有向簡單圖 G,找出拓樸順序
Topological Order
0
1
2
in_deg = 2
in_deg = 1
in_deg = 0
2
修課順序:
-> 1
-> 0
/ 0
/ 1
/ 0
還記得剛剛的概念嗎?
把你 BFS 的順序印出來
就是拓樸順序了。
White-Gray-Black Cycle Detection
DFS-based Topological Sort
可能有點難理解,但我們來試試看吧
給你一個 有向簡單圖 G,請檢測有沒有環
0
1
2
3
4
5
我們來用尤拉導覽來分析吧!
(Euler tour technique, ETT)
Euler Tour 就是將 DFS 的順序
寫出來分析的一個技巧。
Euler Tour
0
1
2
3
3
4
4
5
5
2
1
0
1
2
3
4
5
6
7
8
9
10
11
12
來看看怎麼實作吧!
(Euler Tour ≠ Euler Circuit,別搞混 )
給你一個 有向簡單圖 G,請檢測有沒有環
0
1
2
3
4
5
Euler Tour
1
2
3
4
5
6
7
8
9
10
11
12
0
1
2
3
3
4
5
5
4
2
1
0
來看看怎麼實作吧!
其實也就是 DFS 的頭跟尾加一些程式
vector<int> tour;
void dfs(int cur) {
if (visit[cur])
return;
visit[cur]=true;
tour.push_back(cur);
for (int nxt : G[cur])
dfs(nxt);
tour.push_back(cur);
}
tour = []
def dfs(cur):
if cur in visit[cur]:
return
visit.add(cur)
tour.append(cur)
for nxt in G[cur]:
dfs(nxt)
tour.append(cur)
C++
Python
給你一個 有向簡單圖 G,請檢測有沒有環
0
1
2
3
4
5
Euler Tour
1
2
3
4
5
6
7
8
9
10
11
12
0
1
2
3
3
4
5
5
4
2
1
0
怎麼檢測迴路呢?
如果碰到一個點:
1
5
底下所有點都被走完了
給你一個 有向簡單圖 G,請檢測有沒有環
0
1
2
3
4
5
Euler Tour
1
2
3
4
5
6
7
8
9
10
11
12
0
1
2
3
3
4
5
5
4
2
1
0
如果碰到一個點,在 Euler Tour...
沒藍色
有藍色
沒橘色
有藍色
有橘色
沒橘色
用陣列 / Dict 去紀錄每個點是哪種狀態!
怎麼檢測迴路呢?
class Solution {
public:
int euler_top = 0;
bool dfs(vector<vector<int>>& G, vector<vector<int>>& euler_idx, int cur) {
if (euler_idx[cur].size())
return euler_idx[cur].size() == 2;
euler_idx[cur].push_back(euler_top++);
for (int nxt : G[cur]) {
if (!dfs(G, euler_idx, nxt))
return false;
}
euler_idx[cur].push_back(euler_top++);
return true;
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> G(numCourses);
vector<vector<int>> euler_idx(numCourses);
int euler_top = 0;
// Build Graph
for (auto &tmp : prerequisites) {
int u=tmp[0], v=tmp[1];
G[v].push_back(u);
}
for (int i=0; i<numCourses; i++) {
if (!dfs(G, euler_idx, i))
return false;
}
return true;
}
};
C++ Solution
class Solution:
def canFinish(self, numCourses, prerequisites):
G = defaultdict(list)
# Build Graph
for u, v in prerequisites:
G[v].append(u)
euler_top = 1
euler_idx = defaultdict(list)
def dfs(cur):
nonlocal euler_top
if euler_idx[cur]:
return len(euler_idx[cur]) == 2
euler_idx[cur].append(euler_top)
euler_top += 1
for nxt in G[cur]:
if not dfs(nxt):
return False
euler_idx[cur].append(euler_top)
euler_top += 1
return True
return all(dfs(i) for i in range(numCourses))
Python Solution
給你一個 有向簡單圖 G,請檢測有沒有環
如果碰到一個點,在 Euler Tour...
沒藍色
有藍色
沒橘色
有藍色
有橘色
沒橘色
這樣的算法其實叫做:
White-Gray-Black Cycle Detection
(還沒走過)
(正在走)
(走過了)
不過你改成 White-Gray-Black
就沒辦法做下一題了 D:
給你一個 有向簡單圖 G,找出拓樸順序
Topological Order
還記得剛剛的概念嗎?
把橘色的順序反著看就是拓樸順序。
4
9
10
0
1
2
3
1
6
3
4
2
5
7
8
Euler Tour
0
1
2
3
3
4
4
2
1
0
如果 A -> B,那麼一定是 B 先 結束 dfs ,
才會是 A 結束 dfs。
B 的 橘色數字 一定比 A 的橘色數字前面。
finish[B]
給你一個有向無環的電路圖,並給定輸入信號。
問輸出信號,並且最大延遲為何?
4 5 4 13
1 0 1 0
1 2 3 4 1
1 5
2 5
2 6
3 6
3 7
4 7
4 8
5 10
6 9
6 11
7 9
8 13
9 12
4輸入,5閘, 4輸出, 13線
編號: 1~4 , 5~9 , 10~13
輸入信號
閘的型態
型態1: &
型態2: |
型態3: ^
型態4: !
型態1: &
線的輸入
線的輸出
答案: 延遲 2,輸出 0 1 1 1
t=0
t=0
t=0
t=0
t=0
t=1
t=1
t=1
t=1
t=2
t=1
t=1
t=1
t=2
給你一個有向無環的電路圖,並給定輸入信號。
問輸出信號,並且最大延遲為何?
最重要的就是決定順序!
我們每次處理的閘道都必須先算好輸入
-> 照著拓樸順序
給你一個有向無環的電路圖,並給定輸入信號。
問輸出信號,並且最大延遲為何?
在這之前,我們先建圖
struct Gate{
// type: 如果是邏輯閘,他是哪類。in_deg: 入度。
// out: 輸出的結果。delay: 延遲。 inp: 邏輯閘的輸入,至多兩個。
// out_idx: 輸出到哪些邏輯閘或者輸出端口上。
int type, in_deg, out, delay;
vector<int> inp, out_idx;
};
Gate gate[100000];
int main() {
int p,q,r,m,max_delay=0;
queue<int> Q;
scanf("%d%d%d%d", &p, &q, &r, &m);
for (int i=1; i<=p; i++) {
scanf("%d", &gate[i].out);
// 輸入端口一定入度為零,所以放進 queue。
Q.push(i);
}
for (int i=p+1; i<=p+q; i++) {
scanf("%d", &gate[i].type);
}
for (int i=0; i<m; i++) {
int u, v;
scanf("%d%d", &u, &v);
// 紀錄入度以及該點的下游是誰。
gate[u].out_idx.push_back(v);
gate[v].in_deg ++;
}
因為變數很多,
所以就開 struct / class吧!
每個Gate需要:
給你一個有向無環的電路圖,並給定輸入信號。
問輸出信號,並且最大延遲為何?
在這之前,我們先建圖
class Gate:
def __init__(self):
self.in_deg = 0 # 入度
self.type = None # 邏輯閘的種類
self.out = None # 該gate的輸出結果
self.delay = 0 # 該gate的延遲
self.inp = [] # 輸入
self.nxt = [] # 下游
p, q, r, m = map(int, input().split())
# 要加 1 是因為編號從 1 開始,所以第零號是空的。
gate = [Gate() for _ in range(p+q+r+1)]
# 處理輸入的訊號
for i, in_signal in enumerate(list(map(int, input().split()))):
gate[i+1].out = in_signal
# 處理邏輯閘的種類
for i, gate_type in enumerate(list(map(int, input().split()))):
gate[p+i+1].type = gate_type
# 處理入度並記錄下游
for _ in range(m):
u, v = map(int, input().split())
gate[v].in_deg += 1
gate[u].nxt.append(gate[v])
# 輸入 [1 ~ p]
Q = [gate[i] for i in range(1, p+1)]
因為變數很多,
所以就開 struct / class吧!
每個Gate需要:
給你一個有向無環的電路圖,並給定輸入信號。
問輸出信號,並且最大延遲為何?
And
Gate
對於每個 Gate:
剩下就是實作細節了,可以參考我寫的解析
BFS
一路下來,我們都上了什麼?
DFS
二維地圖
BFS
DFS
樹
抽象地圖
(遊戲搜尋)
Uniform Cost Search
Backtracking
圖
Dijkstra's Algorithm
Kahn's Algorithm
White-Gray-Black Cycle Detection
DFS
BFS
Traverse
Binary Search Tree
Disjoint Set
Verify Bipartite
後面還有什麼有趣的?
各式各樣的樹:
各式各樣的技巧
後面還有什麼有趣的?
各式各樣的圖技巧:
歡迎參考這個
圖 / 樹有很多要學!
不過 APCS 通常的圖論都只會
考 DFS / BFS 的衍生 + 拓樸路徑而已,
不用太擔心!
還有記得有些圖看起來不是圖論,
但其實也是圖論題喔!
題目名稱 | 來源 | 備註 |
---|---|---|
最短路徑 | C 班 圖論 I - 2 | |
題目名稱 | 來源 | 備註 |
---|---|---|
病毒演化 | Zerojudge f582 | APCS 2020/7 - 4 很像樹DP但其實偏DP |
Redundant Connection | Leetcode 684 | 無向圖環檢測 也可以 disjoint set |
Longest Cycle in a Graph | Leetcode 2360 |
7
4
2
1
3
Heap的定義:
對於每個點,滿足下列條件:
x
l
r
請實作一個完全二元樹,包含三種操作:
1. 查看最大值,2. 刪除最大值,3. 插入數值
(Complete Binary Tree)
Segment Tree
Segment Tree - Lazy Tag
Strongly Connected Component, SCC
Bellman-Ford Algorithm
Floyd-Warshall Algorithm
Minimum Spanning Tree