如何新建JavaFX專案並打成可執行 Jar包
圖論
儲存結構
- 鄰接矩陣
無腦存
int[][] g = new int[n][m]; for (int i = 0;i < n;i++) for (int j = 0;j < m;j++) ...
- 鄰接表
List<Integer>[] g = new List[; //List<int[]>[] g;可以存邊權等附加資訊。 for (int i = 0;i < n; i++) g[i] = new ArrayList<>(); //千萬別忘 for (int i = 0;i < n;i++){ for (int v : g[i]) ... }
- 鏈式前向星
class E{ // 邊的終點 下一條邊在edge中的編號 邊的權值 int to, ne, wt; E(int t, int n, int w){to = t;ne = n;wt = w;} E(int t, int n){to = t;ne = n;} } E[] edge = new E[m]; //如果是無向圖 new E[m*2]要開兩倍 int[] hd = new int[n]; //hd[i]:頂點i為起點的第一條邊 void add(int u, int v, int idx){ edge[idx] = new E(v, hd[v]);// 存邊u->v hd[u] = idx; } for (int i = hd[v];i != 0;i = edge[i].ne){ int u = edge[i].to; ... }
圖的遍歷
- 沒啥好說的 與樹遍歷差別在於圖遍歷需要visited[]陣列
考慮到圖可能不連通,需要對每一個點進行掃描
for (int i = 0;i < n;i++){ if (!vis[i]) DFS(i) //或BFS(i) }
DFS
void dfs(int u) { vis[u] = 1; for (int i = head[u]; i; i = e[i].x) { if (!vis[e[i].t]) { dfs(v); } } }
BFS
void bfs(int u) { Q.push(u); vis[u] = 1; while (!Q.empty()) { u = Q.front();Q.pop(); for (int i = head[u]; i; i = e[i].next) { if (!vis[e[i].to]) { Q.push(e[i].to); vis[e[i].to] = 1; } } } }
拓撲排序
前提:有向無環圖(DAG)
核心:維護入度為0的頂點集合
List<Integer>[] g = new List[N]; int[] indeg = new int[N]; //入度 List<Integer> res; //拓撲排序結果 for (int i = 0;i < m;i++){ int u, v; indeg[v]++; g[u].add(v); } Deque<Integer> q = new LinkedList<>(); for (int i = 1;i <= n;i++) if (indeg[i] == 0) q.offerLast(i); while (!q.isEmpty()){ int u = q.pollFirst(); res.add(u); for (int v : g[u]){ if (--indeg[v] == 0) q.offerLast(v); } } if (res.size() == n) // DAG 可以拓撲排序 else // 無法拓撲排序
- 求字典序最大/最小的拓撲排序
將演算法中的佇列替換成最大堆/最小堆實現的優先佇列即可,此時總的時間複雜度為 \(O(E+V \log V)\) 。
最小生成樹
Kruskal(優先使用)
public int kruskal(int n, int[][] edges){ //用堆代替邊集陣列排序 PriorityQueue<int[]> g = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]); Collections.addAll(g, edges); int ans = 0, cnt = 0; while (cnt != n-1){ int[] e = g.poll(); int v = find(e[0]), u = find(e[1]); if ( v != u ){ union(u, v); ans += e[2]; cnt++; } } return ans; //最小總權值 } // ...省略並查集操作
- \(O(m\log m)\)
Prim
public int primHeap(int n, int[][] edges) { int res = 0; //鄰接表存圖(適用邊較少,稀疏圖) <頂點v,[]{頂點u, 距離}> List<List<int[]>> g = new ArrayList<>(); for (int i = 0;i < n;i++) g.add(new ArrayList<>()); for (int[] e : edges){ g.get(e[0]).add(new int[]{e[1], e[2]}); g.get(e[1]).add(new int[]{e[0], e[2]}); } int[] vis = new int[n]; PriorityQueue<int[]> q = new PriorityQueue<>((o1,o2)->o1[1]-o2[1]); q.offer(new int[]{0,0}); //從第0個節點開始構造 while (n > 0){ int[] cur = q.poll(); if (vis[cur[0]] == 1) continue; vis[cur[0]] = 1; res += cur[1]; --n; for (int[] p : g.get(cur[0])){ q.offer(new int[]{p[0], p[1]}); } } return res; }
- \(O((m+n)\log n)\)
最短路
- 性質
對於邊權為正的圖,任意兩個結點之間的最短路,不會經過重複的結點。
對於邊權為正的圖,任意兩個結點之間的最短路,不會經過重複的邊。
對於邊權為正的圖,任意兩個結點之間的最短路,任意一條的結點數 \(<=n\),邊數 \(<=n-1\)。
Floyd
求任意兩個結點之間的最短路的。
適用於任何圖,不管有向無向,邊權正負,但是最短路必須存在。(不能有個負環)
for (k = 1; k <= n; k++) { for (x = 1; x <= n; x++) { for (y = 1; y <= n; y++) { f[x][y] = min(f[x][y], f[x][k] + f[k][y]); //每次以k為中轉點(動態規劃) } } }
f[x][k]+f[k][y]
可能會爆int。
Dijkstra
對於邊 \((u, v)\) ,鬆弛操作對應下面的式子: \(\operatorname{dis}(v)=\min (\operatorname{dis}(v), \operatorname{dis}(u)+w(u, v))\) 。
非負權圖 上單源最短路徑的演算法。
int[] dijkstra (int n, int k){ int[] vis = new int[n+1], dis = new int[n+1]; Arrays.fill(dis,0x3f3f3f3f); dis[k] = 0; //k為源點 for (int t = 1;t <= n;t++){ int minidx = -1; for (int i = 1;i <= n;i++){ if (vis[i]==0 && (minidx==-1||dis[i]<dis[minidx])) minidx = i; } vis[minidx] = 1; for (int[] e : g[minidx]){ int v = e[0], w = e[1]; dis[v] = Math.min(dis[v], dis[minidx]+w); } } return dis; }
int[] dijkstraHeap (int n, int k){ int[] dis = new int[n+1], vis = new int[n+1]; Arrays.fill(dis,0x3f3f3f3f); dis[k] = 0; PriorityQueue<int[]> q = new PriorityQueue<>((o1,o2)->o1[1]-o2[1]); q.offer(new int[]{k, 0}); while (!q.isEmpty()){ int u = q.poll()[0]; //可能有一個點被鬆弛兩次的情況 後彈出的距離肯定比先彈出的小,所以直接跳過 if (vis[u] == 1) continue; vis[u] = 1; for (int[] e : g[u]){ int v = e[0], w = e[1]; if (dis[v] > dis[u]+w){ dis[v] = dis[u]+w; q.offer(new int[]{v, dis[v]}); } } } return dis; }
時間複雜度
- 暴力:\(O(n^2 + m) = O(n^2)\)
- 優先佇列:\(O(m \log m)\)
在稀疏圖中,\(m = O(n)\),優先佇列;
在稠密圖中,\(m = O(n^2)\),暴力。
Bellman-Ford
Bellman-Ford 演算法所做的,就是不斷嘗試對圖上每一條邊進行鬆弛。我們每進行一輪迴圈,就對圖上所有的邊都嘗試進行一次鬆弛操作,當一次迴圈中沒有成功的鬆弛操作時,演算法停止。
在最短路存在的情況下,由於一次鬆弛操作會使最短路的邊數至少\(+1\) ,而最短路的邊數最多為\(n-1\) ,因此整個演算法最多執行\(n-1\) 輪鬆弛操作。故總時間複雜度為\(O(mn)\) 。
但還有一種情況,如果從\(S\) 點出發,抵達一個負環時,鬆弛操作會無休止地進行下去。因此如果第 \(n\)輪迴圈時仍然存在能鬆弛的邊,說明從 \(S\)點出發,能夠抵達一個負環。需要注意的是,以 點為源點跑 Bellman-Ford 演算法時,如果沒有給出存在負環的結果,只能說明從 \(S\)點出發不能抵達一個負環,而不能說明圖上不存在負環。
因此如果需要判斷整個圖上是否存在負環,最嚴謹的做法是建立一個超級源點,向圖上每個節點連一條權值為 0 的邊,然後以超級源點為起點執行 Bellman-Ford 演算法。
int[] bellman(int n, int k){ int[] dis = new int[n+1]; Arrays.fill(dis, 0x3f3f3f3f) dis[k] = 0; for (int t = 1 ; t <= n-1;t++){ int flag = 1;// 如果此次迴圈沒有邊更新 說明鬆弛提前結束 //對每條邊進行遍歷更新 for (int[] e : g){ int u = e[0], v = e[1], w = e[2]; if (dis[v] > dis[u]+w) { dis[v] = dis[u]+w; flag = 0; } }if (flag == 1) break; } boolean exist = true;//判斷是否有負環 for (int[] e : g){ int u = e[0], v = e[1], w = e[2]; if (dis[v] > dis[u]+w) { exist = false;break; } } return exist ? dis:new int[]{}; }
SPFA
public int[] spfa(int n, int k) { boolean exist = true; int[] cnt = new int[n+1];//用於判斷負環(一個節點至多加入佇列n-1次 反之存在負環) //vis判斷該節點是否在佇列中(訪問過後出佇列 vis[i]=0) //跟其他演算法不同(其他演算法大都表示有沒有訪問過該元素) for (int i = 1;i <= n;i++) h[i] = INF; Deque<Integer> q = new LinkedList<>(); q.offerLast(k);cnt[k]++;vis[k] = 1;h[k] = while (!q.isEmpty() && !exist){ int v = q.pollFirst();vis[v] = 0; for (int i = hd[v];i != 0;i = edge[i].ne){ int u = edge[i].to, w = edge[i].wt; if (h[u] < h[v]+w){ h[u] = h[v]+w; if (vis[u] == 1) continue; q.offerLast(u);cnt[u]++;vis[u]=1; if (cnt[u]==n+1) exist = true; } } } return exist?dis:new int[]{}; }
Johnson 全源最短路徑
我們新建一個虛擬節點(在這裡我們就設它的編號為 0 )。從這個點向其他所有點連一條邊權為 0 的邊。
接下來用 Bellman-Ford(SPFA) 演算法求出從 0 號點到其他所有點的最短路,記為 \(h_{i}\) 。
假如存在一條從 \(u\) 點到 \(v\) 點,邊權為 \(w\) 的邊,則我們將該邊的邊權重新設定為 \(w+h_{u}-h_{v}\) 。
接下來以每個點為起點,跑 \(n\) 輪 Dijkstra 演算法即可求出任意兩點間的最短路了。
一開始的 Bellman-Ford 演算法並不是時間上的瓶頸,若使用 priority_queue 實現 Dijkstra 演算法, 該演算法的時間複雜度是 \(O(n m \log m)\) 。
public class Main{ static int n, m, N = 5010, M = 9010, INF = 0x3f3f3f3f; static E[] edge = new E[M];// 鏈式前向星存圖 static int[] hd = new int[N], vis = new int[N]; static int[] dis = new int[N],h = new int[N]; public static void main(String[] args) throws IOException { for (int i = 1;i <= m;i++) add(i, u, v, w); for (int i = 1;i <= n;i++) add(m+i, 0, i, 0); //超級源點0 到其他點距離都為0 int tp = SPFA(0); //此時圖裡一共有n+1個點 要鬆弛n次(不是n-1) if (tp == -1){ System.out.println(-1); return; //有負環 寄 } for (int i= 1;i <= n;i++) for (int k = hd[i];k !=0;k = edge[k].ne) edge[k].wt += h[i]-h[edge[k].to]; //w+h[u]-h[v] 更新權值 for (int i = 1;i <= n;i++){ dijkstra(i);// 每個點跑一遍最短路 for (int k = 1;k <= n;k++){ if (dis[k] == INF) System.out.println(INF); //把之前加的權值減去就是實際最短路長度 else System.out.println(dis[k]-h[i]+h[k]); } } } private static void dijkstra(int k) {...} private static int SPFA(int k) {...} }
不同方法的比較
最短路演算法 Floyd Bellman-Ford Dijkstra Johnson 最短路型別 每對結點間的最短路 單源最短路 單源最短路 每對結點間的最短路 作用於 任意圖 任意圖 非負權圖 任意圖 能否檢測負環? 能 能 不能 能 作用圖的大小 小 中/小 大/中 大/中 時間複雜度 \(O(N^3)\) \(O(NM)\) \(O(M\log M)\) \(O(NM\log M)\) 注:表中的 Dijkstra 演算法在計算複雜度時均用
priority_queue
實現。
差分約束(不等式)
差分約束系統 是一種特殊的 \(n\) 元一次不等式組,它包含 \(n\) 個變數 \(x_{1}, x_{2}, \ldots, x_{n}\) 以及 \(m\) 個約束條件,每個約束條件是由兩個其中的變數做差構成的,形如 \(x_{i}-x_{j} \leq c_{k}\) ,其中\(1 \leq i, j \leq n, i \neq j, 1 \leq k \leq m\) 並且 \(c_{k}\) 是常數(可以是非負數,也可以是負數)。
我們要解決的問題是: 求解 \(x_{1}=a_{1}, x_{2}=a_{2}, \ldots, x_{n}=a_{n}\) ,使得所有約束條件得到滿足,否則無解。
差分約束系統中的每個約束條件 \(x_{i}-x_{j} \leq c_{k}\) 都可以變形成 \(x_{i} \leq x_{j}+c_{k}\) ,這與單源最短路中 的三角形不等式 \(\operatorname{dist}[y] \leq \operatorname{dist}[x]+z\) 非常相似。因此,我們可以把每個變數 \(x_{i}\) 看做圖中的一 個結點,對於每個約束條件 \(x_{i}-x_{j} \leq c_{k}\) ,從結點 \(j\) 向結點 \(i\) 連一條長度為 \(c_{k}\) 的有向邊。
注意到,如果 \(\left\{a_{1}, a_{2}, \ldots, a_{n}\right\}\) 是該差分約束系統的一組解,那麼對於任意的常數 \(d\) , \(\left\{a_{1}+d, a_{2}+d, \ldots, a_{n}+d\right\}\) 顯然也是該差分約束系統的一組解,因為這樣做差後 \(d\) 剛好被消掉。
設 \(\operatorname{dist}[0]=0\) 並向每一個點連一條權重為 0 邊,跑單源最短路,若圖中存在負環,則給定的差分約束系統無解;否則, \(x_{i}=\operatorname{dist}[i]\) 為該差分約束系統的一組解。即建立超級源點\(0\)後跑SPFA.
- 判斷是否有解
題目大意: 求解差分約束系統,有 \(m\) 條約束條件,每條都為形如 \(x_{a}-x_{b} \geq c_{k}\) , \(x_{a}-x_{b} \leq c_{k}\) 或 \(x_{a}=x_{b}\) 的形式,判斷該差分約束系統有沒有解。
題意 轉化 連邊 \(x_{a}-x_{b} \geq c_{k}\) \(x_{b}-x_{a} \leq-c\) add(a, b, -c);
\(x_{a}-x_{b} \leq c_{k}\) \(x_{a}-x_{b} \leq c_{k}\) add(b, a, c);
\(x_{a}=x_{b}\) \(x_{a}-x_{b} \leq 0, x_{b}-x_{a} \leq 0\) add(b, a, 0), add(a, b, 0);
- 求最值解
差分約束問題可以轉化為最短路或最長路問題,所以兩種轉化也就形成了兩種不同的連邊方法。
- 連邊後求最短路
將 \(x_{j}-x_{i} \leq k\) 形式不動,即從 \(i\) 到 \(j\) 連一條邊權為 \(k\) 的邊。加入超級源點後求最短路,得到 \(x_{i} \leq 0\) 所有 \(x\) 最大解。- 連邊後求最長路
將 \(x_{j}-x_{i} \leq k\) 變形為 \(x_{i}-x_{j} \geq -k\) ,即從 \(j\) 到 \(i\) 連一條邊權為 \(-k\) 的邊。加入超級源點後求最長 路,得到 \(x_{i} \geq 0\) 所有 \(x\) 最小解。
- 最長路
最長路問題即為在給定的圖中,計算從源點到所有頂點的最長路。保證圖中沒有正環。
其中一種實現方法為若 \(d_u+w>d_v\),則將 \(d_v\) 更新為 \(d_u+w\)(實際上就是把最短路中的大於號改成小於號),並在初始化時將 \(dis\)陣列全部初始化為一個極小值(\(-INF\)),其餘部分和用 SPFA 求最短路一樣。
二分圖
- 定義
節點由兩個集合組成,且兩個集合內部沒有邊的圖。
- 性質
如果兩個集合中的點分別染成黑色和白色,可以發現二分圖中的每一條邊都一定是連線一個黑色點和一個白色點。
二分圖不存在長度為奇數的環
- 二分圖判斷
染色法:首先將任意的一個頂點染成紅色,再把這個點相鄰的頂點染成藍色,如果按照這種染色方式可以將所有的頂點全部著色,並且相鄰的頂點的顏色不同,那麼該圖就是一個二分圖。
static boolean solve() { for (int i = 1;i <= n;i++){// 可能圖不連通 要對每個點都判斷一遍 if (color[i] == 0) { if (!dfs(i, 1)) return false; } } return true; } static boolean dfs(int v, int c){ // 0-未染色 1-白色 -1-黑色 color[v] = c; for (int u : g[v]){ if (color[u] == c) return false; if (color[u] == 0 && !dfs(u, -c)) return false; } return true; }
- 二分圖最大匹配
求一個二分圖中最大匹配的邊數
static int[] match = new int[N]; //match[u] = v 表示右集合中u點匹配的是左集合中的v點 static int[] vis = new int[N]; public static void main(String[] args) throws Exception{ // 對左集合中每個點進行匹配 for (int i = 1;i <= n;i++) { Arrays.fill(vis, 0); //每一次尋找中 每個點只能訪問一次 if (find(i)) ++ans; } System.out.println(ans); } private static boolean find(int u) { for (int v : g[u]) { if (vis[v]==1) continue; vis[v] = 1; // 右集合中的點v沒有匹配 || v匹配的左集合中的點match[v]可以再找到一個新的匹配 // 就可以讓v成為u的匹配 if (match[v]==0 || find(match[v])) { match[v] = u; return true; } } return false; }
LCA
LCA(Least Common Ancestors),是指在有根樹中,找出某兩個結點u和v最近的公共祖先。
- 核心:倍增
首先我們要記錄各個點的深度和他們\(2^j\)級的的祖先,用陣列\(deep[i]\)表示每個節點的深度,\(f[i][j]\)表示節點\(i\)的\(2^j\)級祖先。(例如上圖17的1級祖先是14, 2級是10, 4級是3, 以此來推)
fa[u][i] = fa[fa[u][i-1]][i-1]
:\(f[i][j]\) 就是\(i\)的\(2^j\)級祖先,那\(f[i][j-1]\)就是\(i\)的\(2^{j-1}\) 級祖先。
又因為 \(2^{j-1}+2^{j-1}=2^{j}\),所以\(i\)的 \(2^{j-1}\) 級祖先的\(2^{j-1}\)級祖先 就是\(i\)的\(2^{j}\) 級祖先。static void dfs(int u, int f) { //u當前節點 f為父節點 搜尋從根節點開始即dfs(root, 0) deep[u] = deep[f]+1; fa[u][0] = f; //u的2^0級祖先 即 u的1級祖先是其父節點 for (int i = 1;(1 << i) <= deep[u];i++){ fa[u][i] = fa[fa[u][i-1]][i-1]; } for (int v : g[u]) if (v != f) dfs(v, u); //父節點不再訪問 }
static int LCA(int u, int v) { if (deep[u] > deep[v]) {int tp = v;v = u;u = tp;} //讓v的深度始終是大於u的 int dis = deep[v]-deep[u]; //深度差 //將u的深度變得與v相同 for (int i = 0;(1<<i) <= dis;i++){ //快速加的意思 任意正整數都可以表示為幾個2的冪次的和 if (((dis>>i) & 1) == 1) v = fa[v][i]; } if (u == v) return u; for (int i = 19;i >= 0;i--){ if (fa[u][i] != fa[v][i]){ u = fa[u][i]; v = fa[v][i]; } } return fa[u][0]; }
例如求解上圖中LCA(13,18):
讓13和18的深度相同(變成小的那個),所以13不變 18變成12, 此時深度都為6.
13和12 第\(2^3\)級祖先(也就是深度比他們少8的,6-8=-2<0相當於根節點)都為1。相同,繼續往下。
13和12 第\(2^2\)級祖先(深度少4,為6-4=2)都為2。相同,繼續往下。
13和12 第\(2^1\)級祖先(深度少2,為6-2=4)分別是7和5。不同,都往上爬2個高度變成7和5。
7和5 第\(2^0\)級祖先(深度少2,為4-1=3)都是3。此時迴圈到了最後一次,說明3就是LCA。
返回\(f[7][0]或者f[5][0]\),結果都是3。所以13和18的LCA是3