Ethan Yeh
INFOR 38th — 學術
INFOR38 學術 葉倚誠
年次:116
社團:建中資訊
幹位:學術
大括號不下放
My IG
INFOR38th IG
報名表單!
離散數學中的圖,和日常生活中常聽到的圖不太一樣。
定義上, 圖是由若干頂點及連接兩頂點的邊所構成的圖形
圖可以解決的問題包括但不限於路徑、網路流、社群分析等
P.S. 圖論也是 APCS 檢定重要的命題範圍之一
G = (V(G), E(G))
點(Vertex):又稱節點(Node)、頂點
邊(Edge):連結兩個點
V(G):點集,集合中有G中每一個點
E(G):邊集,V(G) 各點之間邊的集合
以 (u, v) 數組表示,其中 u, v 為圖中的點
相鄰:若 (u, v) 存在,則 u, v 為相鄰
邊可以帶有方向、權重
邊權:邊上所帶的權重
有向圖:邊有方向;無向圖:邊沒有方向
重邊:有兩條邊的 (u, v) 相同
自環:u = v
簡單圖:沒有自環或重邊的圖
度數:與一個點 v 相鄰的邊數量稱為度(Degree),記作 d(v)
有向圖的度數,分為出度、入度
出度:箭頭指離點;入度:箭頭指向點
點權:點上所帶的權重
{V, E, V, E, V, ... , V}
由一連串點與邊所構成的集合,稱為路徑
簡單路徑:不重複經過同一個點的路徑
迴路:起點 = 終點的路徑
環:既是簡單路徑又是迴路
無向圖的連通:
對於一張無向圖,如果有一條路徑可以從 u 連接到 v,我們就稱作 u, v 連通
連通圖:在圖中任取兩個點,這兩個點都是連通的
有向圖的連通:
強連通:在圖中任取兩個點,這兩個點都可以互相通往對方
弱連通:不論方向,在圖中任取兩個點,這兩個點都是連通的
完全圖:在無向簡單圖中,任兩個節點皆有邊相連
有向完全圖:在有向圖中,任意不同兩點都有兩條方向不同的邊
如果今天有 n 個人,兩兩握手數次
他們最後總共握手了 x 次
握手定理告訴我們,x 必定為偶數
因為當兩個人握手之後,總次數增加 2
所以當所有握手結束後,握手次數必定是 2 的倍數
我們可以利用這個邏輯發現對於一個無向簡單圖
各節點度數加總必定是偶數
1. 將度數陣列由大到小排序
2. 將度數最大的人消除,其他項的度數減一
3. 重複上述步驟
4. 若最後所有人的度數都是零,代表成功了
5. 若出現負度數,或最大度數大於其餘節點數,代表失敗了
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
bool f(const int &n, vector<int>& d) {
int sum = 0;
for (const int &x : d) {
sum += x;
if (x >= n) return false;
}
if (sum % 2 != 0) return false;
for (int i = 0; i < n; ++i) {
sort(d.begin() + i, d.end(), greater<int>());
int d1 = d[i];
if (d1 == 0) return true;
if (d1 > (n - 1 - i)) return false;
for (int j = 1; j <= d1; ++j) {
d[i + j]--;
if (d[i + j] < 0) return false;
}
}
return true;
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int n;
while (cin >> n && n) {
vector<int> d(n);
for (int i = 0; i < n; ++i) {
cin >> d[i];
}
if (f(n, d)) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
}
return 0;
}存圖,顧名思義,就是把圖存下來
常用的做法有以下兩種:
鄰接矩陣(Adjacency Matrix)
鄰接陣列(Adjacency List)
開一個二維陣列,例如 adj[u][v]
若為無邊權的圖,則 adj[u][v] = 1 or 0,代表有沒有邊
若為有邊權的圖,則 adj[u][v] 存邊權
如果是無向圖要存兩次(adj[u][v] 及 adj[v][u])
| 0 | 1 | 2 | 3 | |
| 0 | 0 | 0 | 1 | 0 |
| 1 | 0 | 0 | 0 | 1 |
| 2 | 1 | 0 | 0 | 1 |
| 3 | 0 | 1 | 1 | 0 |
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n, m;
cin >> n >> m;
vector<vector<bool>> adj(n, vector<bool>(n, 0));
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u][v] = 1;
adj[v][u] = 1;
}
return 0;
}
開一個陣列,索引值代表起點,元素存該點走向的所有點
無向圖要存兩次(做法同鄰接矩陣)
Graph = {
{2},
{3},
{0, 3},
{1, 2}
}#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n, m;
cin >> n >> m;
vector<vector<int>> adj(n);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
return 0;
}
Depth First Search,中文為深度優先搜尋
這是一種用來遍歷或搜尋圖的演算法
深度優先代表會一條路走到底再換下一條走
可以用遞迴或是 Stack 來實作
0
4
6
5
1
2
3
1. 從起始節點出發,例如 0
2. 不停往當前節點的相鄰節點走
3. 記錄走過的節點
4. 若已經沒有任何未走過的相鄰節點則折返
如此一來,可以走過所有與起點連通的節點
遍歷方式為走完一條路再折返
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
vector<vector<int>> adj;
vector<bool> visited;
void dfs(const int &cur) {
cout << cur << endl;
visited[cur] = true;
for (int nxt : adj[cur]) {
if (!visited[nxt]) {
dfs(nxt);
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n, m;
cin >> n >> m;
adj.assign(n, vector<int>());
visited.assign(n, false);
for (int i = 0; i < m; i++) {
int u, v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(0);
return 0;
}vector<vector<int>> adj;
vector<bool> visited;
void dfs(const int &start) {
stack<int> st;
st.push(start);
while (!st.empty()) {
int cur = st.top();
st.pop();
if (visited[cur]) continue;
cout << cur << endl;
visited[cur] = true;
for (const auto &node : adj[cur]) {
if (!visited[node]) {
st.push(node);
}
}
}
}
Breadth First Search,中文為廣度優先搜尋
與 DFS 最大的不同是,離起點越近的先走
每次都會嘗試走訪同一層的節點,直到全部走完
可以用 Queue 來實作
0
4
6
5
1
2
3
1. 從起始節點出發,例如 0
2. 記錄所有相鄰節點
3. 往所有相鄰節點走(繼續記錄相鄰節點)
如此一來,可以走過所有與起點連通的節點
遍歷方式為先走同一層的節點優先
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
int n;
vector<vector<int>> adj;
vector<bool> visited;
void bfs(const int &start) {
queue<int> q;
visited[start] = 1;
q.push(start);
while (!q.empty()) {
int cur = q.front();
q.pop();
for (const int &v : adj[cur]) {
if (!visited[v]) {
visited[v] = 1;
q.push(v);
}
}
}
}
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n, m;
cin >> n >> m;
adj.resize(n);
visited.assign(n, 0);
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
bfs(0);
return 0;
}先建立鄰接陣列,存圖
照前面的做法 BFS 搜索這張圖(起點為 1)
需要加上兩個陣列
一個存起點到各個電腦的路徑
(順便檢查起點與目標電腦是否連通)
如果連通,從目標電腦開始沿著原路徑走回去
開一個陣列存路徑
最後把陣列反轉(因為找路徑時倒著走),輸出
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
vector<vector<int>> adj;
vector<int> parent;
vector<int> dist;
int main() {
int n, m;
cin >> n >> m;
adj.resize(n + 1);
parent.resize(n + 1);
dist.resize(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (int i = 1; i <= n; ++i) dist[i] = -1;
queue<int> q;
q.push(1);
dist[1] = 1;
while (!q.empty()) {
int curr = q.front();
q.pop();
if (curr == n) break;
for (int neighbor : adj[curr]) {
if (dist[neighbor] == -1) {
dist[neighbor] = dist[curr] + 1;
parent[neighbor] = curr;
q.push(neighbor);
}
}
}
if (dist[n] == -1) {
cout << "IMPOSSIBLE" << endl;
} else {
cout << dist[n] << endl;
vector<int> path;
for (int curr = n; curr != 0; curr = parent[curr]) {
path.push_back(curr);
if (curr == 1) break;
}
reverse(path.begin(), path.end());
for (int i = 0; i < path.size(); i++) {
cout << path[i] << (i == path.size() - 1 ? "" : " ");
}
cout << endl;
}
return 0;
}演算法放課回饋表單
By Ethan Yeh