OI學習筆記(C++)

Adorable_hly發表於2024-08-09

筆記完整版連結(洛谷)——暫無,正在維護

筆記完整版連結(部落格)

參照 oi.wiki 整理的一些筆記:

學習筆記+模板(Adorable_hly)

(自己結合網路和做題經驗總結的,dalao勿噴)

第一大板塊:DP

動態規劃適用場景:

1. 最最佳化原理:若該問題所包含的子問題解是最優的,就稱該問題具有最優子結構,滿足最最佳化原理。

2. 無後效性:指某一階段的狀態一旦確定,就不受以後決策的影響,換而言之,某狀態不會影響之前的狀態,只與當前狀態有關。

3. 有重疊子問題:子問題之間不獨立,一次決策可能會在往後的決策中多次使用(這一條是動規相較於其他演算法的最大優勢,是dp的必要條件)。


動態規劃五大要素:

1. 狀態

2. 狀態轉移方程

3. 普遍決策

4. 初始狀態

5. 邊界條件


揹包dp

一,0-1揹包

例題(模板):

題意概要:有 \(n\) 個物品和一個容量為 \(W\) 的揹包,每個物品有重量 \(w_{i}\) 和價值 \(v_{i}\) 兩種屬性,要求選若干物品放入揹包使揹包中物品的總價值最大且揹包中物品的總重量不超過揹包的容量。

對於每種物品,我們有取(1)與不取(0)兩種策略,所以稱為01揹包問題。

狀態:設一個 $dp[i][j] → dp_{i,j} $ 陣列,表示只考慮前 \(i\) 個物品的情況下(考慮,不一定放,表示最優解),容量為 \(j\) 的揹包可以獲得的最大總價值。

狀態轉移方程:對於第i個物品,考慮兩種決策:

  1. 不放入揹包,揹包總量保持上一步不變,即 \(dp_{i,j} = dp_{i-1,j}\)

  2. 放入揹包,揹包容量減少 \(w_i\) ,加入新物品的價值 \(v_i\) ,即 \(dp_{i,j} = dp_{i-1,j-w_i} + v_i\)
    綜上,可以得出狀態轉移方程(考慮最優,所以取最大值)

\[dp_{i,j} = max(dp_{i-1,j},dp_{i-1,j-w_i}+v_i) \]

當然,還要加上判斷,當 \(j \ge w_i\) 時,才能取決策二,否則 \(dp\) 的第一維可能會變成負數。

但是,如果直接用二維陣列表示狀態,會 MLE(即爆記憶體),應考慮用滾動陣列的形式來最佳化(減少一維)。

因為在本題中,狀態陣列中只有上一次的決策被使用,所以
不用把每次的 \(dp_{i-1,j}\) 都記錄下來,可以減少一維,直接用 \(dp_{i}\) 來表示處理到當前物品時揹包容量為 \(i\) 的最大價值,得到:

\[dp_{j} = max(dp_{j-1},dp_{j-w_i}+v_i) \]

模板:

for (int i = 1;i<=n;++i)
  for (int j = W;j>=w[i];--j) //不要寫遞增的
    f[j] = max(f[j],f[j - w[i]] + v[i]);

二,完全揹包

與01揹包類似,不同點在於完全揹包每件物品有無限個,即可以選無限次,二01揹包每件物品只能選一次。

狀態:設 \(dp_{i,j}\) 為只能選前 i 個物品時,容量為 j 的揹包可以達到的最大價值。

最暴力的 \(dp\) 就是和0-1揹包思路差不多的, \(k\) 為拿的數量,一個個列舉來轉移,方程如下:

\[dp_{i,j}=\max_{k=0}^{+\infty}(dp_{i-1,j-k\times w_i}+v_i\times k) \]

這個做法的複雜度為:\(O(n^3)\)


↑ 理解了, ↓ 沒理解


考慮一下最佳化,引用自 oi.wki-完全揹包 (略微改動)

沒理解最佳化,但是還是背下來吧:


考慮做一個簡單的最佳化。可以發現,對於 \(f_{i,j}\),只要透過 \(f_{i,j-w_i}\) 轉移就可以了。因此狀態轉移方程為:

\[dp_{i,j}=\max(dp_{i-1,j},dp_{i,j-w_i}+v_i) \]

理由是當我們這樣轉移時,\(dp_{i,j-w_i}\) 已經由 \(dp_{i,j-2\times w_i}\) 更新過,那麼 \(dp_{i,j-w_i}\) 就是充分考慮了第 i 件物品所選次

例題(模板):

題意概要:有 \(n\) 種物品和一個容量為 \(W\) 的揹包,每種物品有重量 \(w_{i}\) 和價值 \(v_{i}\) 兩種屬性,要求選若干個物品放入揹包使揹包中物品的總價值最大且揹包中物品的總重量不超過揹包的容量。

例題程式碼
#include<bits/stdc++.h>
using namespace std;

const int maxn = 1e4+5,maxm = 1e7+5;
int n, W, w[maxn], v[maxn];
long long dp[maxm];

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>W>>n;
	for(int i = 1; i<=n;++i) cin>>w[i]>>v[i];
	for(int i = 1; i<=n;++i)
		for(int j = w[i];j<=W;j++)
			if(dp[j-w[i]]+v[i]>dp[j]) dp[j]=dp[j-w[i]]+v[i];
	cout<<dp[W]<<endl;
	return 0;
}

三,多重揹包

從多重揹包是0-1揹包的變式,多重揹包每種物品有 \(k_i\) 個,不是一個。

樸素的想法:把「每種物品選 \(k_i\) 次」等價轉換為「有 \(k_i\) 個相同的物品,每個物品選一次」。就成了01揹包的板子
狀態轉移方程:

\[dp_{i,j}=\max_{k=0}^{k_i}(dp_{i-1,j-k\times w_i}+v_i\times k) \]

時間複雜度為 $ O(W\sum_{i=1}^nk_i)$ 。

模板:

for(int i = 1;i<=n;++i)
    for(int weight = W;weight>=w[i];--weight)
        for(int k = 1;k*w[i]<=weight && k<=cnt[i];++k)
            dp[weight] = max(dp[weight],dp[weight-k*w[i]]+v[i]*k);

這個方法的複雜度還是不低的,我們考慮最佳化:
我看不懂,粘一下 oi.wiki 上的解釋:

二進位制分組最佳化:

考慮最佳化。我們仍考慮把多重揹包轉化成 0-1 揹包模型來求解。

顯然,複雜度中的 \(O(nW)\) 部分無法再最佳化了,我們只能從 \(O(\sum k_i)\) 處入手。為了表述方便,我們用 \(A_{i,j}\) 代表第 i 種物品拆分出的第 \(j\) 個物品。

在樸素的做法中,\(\forall j\le k_i,A_{i,j}\) 均表示相同物品。那麼我們效率低的原因主要在於我們進行了大量重複性的工作。舉例來說,我們考慮了「同時選 \(A_{i,1},A_{i,2}\)」與「同時選 \(A_{i,2},A_{i,3}\)」這兩個完全等效的情況。這樣的重複性工作我們進行了許多次。那麼最佳化拆分方式就成為了解決問題的突破口。

過程
我們可以透過「二進位制分組」的方式使拆分方式更加優美。

具體地說就是令 \(A_{i,j}\left(j\in\left[0,\lfloor \log_2(k_i+1)\rfloor-1\right]\right)\) 分別表示由 2\(^{j}\) 個單個物品「捆綁」而成的大物品。特殊地,若 \(k_i+1\) 不是 \(2\) 的整數次冪,則需要在最後新增一個由 \(k_i-2^{\lfloor \log_2(k_i+1)\rfloor-1}\) 個單個物品「捆綁」而成的大物品用於補足。

舉幾個例子:

\(6 =1+2+3\)
\(8 = 1+2+4+1\)
\(18 = 1+2+4+8+3\)
\(31 = 1+2+4+8+16\)

顯然,透過上述拆分方式,可以表示任意 \(\le k_i\) 個物品的等效選擇方式。將每種物品按照上述方式拆分後,使用 0-1 揹包的方法解決即可。

時間複雜度 \(O(W\sum_{i=1}^n\log_2k_i)\)

二進位制分組程式碼 (看不懂) :

index = 0;
for(int i = 1;i<=m;++i)
{
  int c = 1,p,h,k;
  cin>>p>>h>>k;
  while(k>c)
  {
    k -= c;
    list[++index].w = c * p;
    list[index].v = c * h;
    c *= 2;
  }
  list[++index].w = p*k;
  list[index].v = h*k;
}

dp未完待續……


------------------------華麗的分割線---------------------------

第二大板塊:樹狀陣列

一,概念
手搓了兩張草圖:
1圖一 ↑ ↑ ↑

2圖二 ↑ ↑ ↑(請不要在意顏色,瞎整的)

關於儲存大概就是這個結構,和線段樹的功能有點像。

  • 功能:
  1. 單點修改,單點查詢(這個就不要需要樹狀陣列了)
  2. 區間修改,單點查詢(本版塊重點
  3. 單點修改,區間查詢(本版塊重點
  4. 區間修改,區間查詢(建議線段樹實現)
  5. 等等

優點:相較於線段樹好寫,省時省力(殺雞焉用宰牛刀, 而且我現在也不會線段樹 )。

缺點:擴充套件性弱,換而言之,線段樹能解決的問題用樹狀陣列可以解決大部分,但不是全部。

先記一下 lowbit 的用法:

計算非負整數 n 在二進位制下,從右到左第一個1到最右邊構成的數,等價於刪去從左到右最後一個1到最左邊所有的數(最後一個1不刪)

例如:

a = 1010100;
lowbit(a) = 100;//刪去了左邊的“1010”
  • 實現:對 \((x)_2\) 取反,再與原數 \((x)_2\) 進行按位與運算。

  • 寫法1:

int lowbit(int x)
{ return ((x)&(-x)); }//因為篇幅,稍微壓一下行
  • 寫法2:
#define lowbit(x) ((x)&(-x))

注:帶一堆括號只是為了保險 (雖然我也知道沒必要,但寫上肯定不會錯)

圖2,用t[i]表示以x為根的子數中葉子節點值的和,原陣列為a[]。容易發現:

\[t_4 = t_2+t_3+a_4 = t_1+a_2+t_3+a_4 = a_1+a_2+a_3+a_4 \]

觀察一下二進位制數,發現每一層末尾的0個數是相通的(可能我畫的不太形象,第一層是 \(t_1,t_3,t_5,t_7\),第二層是 \(t_2,t_6\) ,第三層是 \(t_4\) ,第四層是 \(t_8\)

再觀察,樹狀陣列中節點x的父親節點為 x + lowbit(x)
eg:對於 t[2] (父親節點為 t[4] ),

\[t[4] = t[2+lowbit(2)] , 4 = 2+lowbit(2) \]

原理大致介紹完了,記一下例題吧

例題

洛谷P3374 樹狀陣列模板1


  • 大意:輸入n,m(該數列數字的個數和操作的總個數)
    輸入n個數表示第 i 項的初始值。
    接下來 m 行每行包含 3 個整數,表示一個操作:
    1 x k 含義:將第 x 個數加上 k
    2 x y 含義:輸出區間 [x,y] 內每個數的和

對於每次2操作輸出一次區間和。


一道很板的題,要考慮兩個:單點修改和區間查詢

1. 單點修改,區間查詢

1.1 單點修改

單點修改時,可以吧 t[i]理解為字首和,例如,如果我們對 a[1]+k,那麼對於 t[1],t[2],t[4],t[8](即全部祖先)都需要+k更新,此時就可以使用上面關於 lowbit 的結論了,

  • 模板:
int add(int x,int k)//對第x項進行+k操作
{
  for(int i = x;i<=n;i+=lowbit(i))
    t[i]+=k;
}

1.2 區間查詢

我們先找例子,再由一般到特殊:
eg:查詢 1~7的和

還是從圖2,很容易看出:答案就是 \(t[7]+t[6]+t[4]\)

進一步觀察,

\[6 = 7-lowbit(7),4 = 6-lowbit(6) \]

所以可以迴圈不斷 -lowbit(),一直減到最底層來實現

int sumout(x)
{
	int sum = 0;
	for(int i=x;i;i-=lowbit(i))
		sum+=t[i];
	return sum;
}//算了壓行碼風太醜,就不了。。。

這個模板只能求區間 [1,x] 的和,當然求 [l,r] 的區間和基本同理,利用字首和相減的性質就可以了,

\[[l,r] = [1,r]-[1,l-1] \]

  • 實現1:
    利用上述函式
return sumout(1,r)-sumout(1,l-1);
  • 實現2:

重新手搓一個
也是字首和思想,同上

int sumout(int l,int r)
{
	int sum = 0;
	for(int i = r;i;i-=lowbit(i)) sum+=t[i];
	for(int i = l-1;i;i-=lowbit(i)) sum-=t[i];
	return sum;
}

洛谷P3368 樹狀陣列模板2

2. 區間修改,單點查詢

2.1 區間修改

差分的原理,構造一個差分陣列 c,用樹狀陣列維護 c 即可,利用差分陣列的性質,只需要更新 \(add(l,k),add(r+1,-k)\) 即可

  • 模板:
void change(int loc,int k)//把loc及其後面的點+k
{
  for(int i = loc;i<=n;i+=lowbit(i))
    	c[i]+=k;
}
  • 實現:
change(l,k);
change(r+1,-k);

2.2 單點查詢

單點查詢即求出 c 的字首和即可;

\(a[x] = c[1] + c[2] + ... + c[x]的字首和\)(依據差分陣列的性質)

int findd(int loc)
{
	int ans = 0;
  for(int i = loc;i;i-=lowbit(i)) ans+=c[i];
  return ans;
}

lowbit 原理同上

3.區間修改,區間查詢

用樹狀陣列過於複雜,建議使用線段樹 (雖然我不會)
你好我叫郭旭東


------------------------華麗的分割線---------------------------

第三大板塊:圖論

先整理一下基本概念(部分引用自 oi.wiki

只介紹比較簡單的定義:

圖(graph)是一個二元組 \(G = (V(G),E(G))\)。其中 V(G) 是非空集,稱為 點集 (vertex set),對於 V 中的每個元素,我們稱其為 頂點 (vertex) 或 節點 (node),簡稱 點;E(G) 為 V(G) 各結點之間邊的集合,稱為 邊集 (edge set)。

常用 G=(V,E) 表示圖。
當 V,E 都是有限集合時,稱 G 為 有限圖。
當 V 或 E 是無限集合時,稱 G 為 無限圖。

若 G 為無向圖,則 E 中的每個元素為一個無序二元組 (u, v),稱作 無向邊 (undirected edge),簡稱 邊 (edge),其中 u, v \in V。設 e = (u, v),則 u 和 v 稱為 e 的 端點 (endpoint)。

若 G 為有向圖,則 E 中的每一個元素為一個有序二元組 (u, v),有時也寫作 u \(\to\) v,稱作 有向邊 (directed edge) 或 弧 (arc),在不引起混淆的情況下也可以稱作 邊 (edge)。設 e = u \(\to\) v,則此時 u 稱為 e 的 起點 (tail),v 稱為 e 的 終點 (head),起點和終點也稱為 e 的 端點 (endpoint)。並稱 u 是 v 的直接前驅,v 是 u 的直接後繼。

若 G 為混合圖,則 E 中既有 有向邊,又有 無向邊。

若 G 的每條邊 e_k=(u_k,v_k) 都被賦予一個數作為該邊的 權,則稱 G 為 賦權圖。如果這些權都是正實數,就稱 G 為 正權圖。

形象地說,圖是由若干點以及連線點與點的邊構成的。

路徑

\(途徑 (walk):途徑是連線一連串頂點的邊的序列,可以為有限或無限長度。形式化地說,一條有限途徑 w 是一個邊的序列 e_1, e_2, \ldots, e_k,使得存在一個頂點序列 v_0, v_1, \ldots, v_k 滿足 e_i = (v_{i-1}, v_i),其中 i \in [1, k]。這樣的途徑可以簡寫為 v_0 \to v_1 \to v_2 \to \cdots \to v_k。通常來說,邊的數量 k 被稱作這條途徑的 長度(如果邊是帶權的,長度通常指途徑上的邊權之和,題目中也可能另有定義)。\)

\(跡 (trail):對於一條途徑 w,若 e_1, e_2, \ldots, e_k 兩兩互不相同,則稱 w 是一條跡。\)

\(路徑 (path)(又稱 簡單路徑 (simple path)):對於一條跡 w,若其連線的點的序列中點兩兩不同,則稱 w 是一條路徑。迴路 (circuit):對於一條跡 w,若 v_0 = v_k,則稱 w 是一條迴路。\)

\(環/圈 (cycle)(又稱 簡單迴路/簡單環 (simple circuit)):對於一條迴路 w,若 v_0 = v_k 是點序列中唯一重複出現的點對,則稱 w 是一個環。\)

連通

無向圖

對於一張無向圖 G = (V, E),對於 u, v \in V,若存在一條途徑使得 v_0 = u, v_k = v,則稱 u 和 v 是 連通的 (connected)。由定義,任意一個頂點和自身連通,任意一條邊的兩個端點連通。

若無向圖 G = (V, E),滿足其中任意兩個頂點均連通,則稱 G 是 連通圖 (connected graph),G 的這一性質稱作 連通性 (connectivity)。

若 H 是 G 的一個連通子圖,且不存在 F 滿足 H\subsetneq F \subseteq G 且 F 為連通圖,則 H 是 G 的一個 連通塊/連通分量 (connected component)(極大連通子圖)。

有向圖

對於一張有向圖 G = (V, E),對於 u, v \in V,若存在一條途徑使得 v_0 = u, v_k = v,則稱 u 可達 v。由定義,任意一個頂點可達自身,任意一條邊的起點可達終點。(無向圖中的連通也可以視作雙向可達。)

若一張有向圖的節點兩兩互相可達,則稱這張圖是 強連通的 (strongly connected)。

若一張有向圖的邊替換為無向邊後可以得到一張連通圖,則稱原來這張有向圖是 弱連通的 (weakly connected)。

與連通分量類似,也有 弱連通分量 (weakly connected component)(極大弱連通子圖)和 強連通分量 (strongly connected component)(極大強連通子圖)。

相關演算法請參見 強連通分量。

相關演算法請參見 割點和橋 以及 雙連通分量。

在本部分中,有向圖的「連通」一般指「強連通」。

對於連通圖 G = (V, E),若 V'\subseteq V 且 G\left[V\setminus V'\right](即從 G 中刪去 V' 中的點)不是連通圖,則 V' 是圖 G 的一個 點割集 (vertex cut/separating set)。大小為一的點割集又被稱作 割點 (cut vertex)。

對於連通圖 G = (V, E) 和整數 k,若 |V|\ge k+1 且 G 不存在大小為 k-1 的點割集,則稱圖 G 是 k- 點連通的 (k-vertex-connected),而使得上式成立的最大的 k 被稱作圖 G 的 點連通度 (vertex connectivity),記作 \kappa(G)。(對於非完全圖,點連通度即為最小點割集的大小,而完全圖 K_n 的點連通度為 n-1。)

對於圖 G = (V, E) 以及 u, v\in V 滿足 u\ne v,u 和 v 不相鄰,u 可達 v,若 V'\subseteq V,u, v\notin V',且在 G\left[V\setminus V'\right] 中 u 和 v 不連通,則 V' 被稱作 u 到 v 的點割集。u 到 v 的最小點割集的大小被稱作 u 到 v 的 區域性點連通度 (local connectivity),記作 \kappa(u, v)。

還可以在邊上作類似的定義:

\(對於連通圖 G = (V, E),若 E'\subseteq E 且 G' = (V, E\setminus E')(即從 G 中刪去 E' 中的邊)不是連通圖,則 E' 是圖 G 的一個 邊割集 (edge cut)。大小為一的邊割集又被稱作 橋 (bridge)。\)

\(對於連通圖 G = (V, E) 和整數 k,若 G 不存在大小為 k-1 的邊割集,則稱圖 G 是 k- 邊連通的 (k-edge-connected),而使得上式成立的最大的 k 被稱作圖 G 的 邊連通度 (edge connectivity),記作 \lambda(G)。(對於任何圖,邊連通度即為最小邊割集的大小。)\)

\(對於圖 G = (V, E) 以及 u, v\in V 滿足 u\ne v,u 可達 v,若 E'\subseteq E,且在 G'=(V, E\setminus E') 中 u 和 v 不連通,則 E' 被稱作 u 到 v 的邊割集。u 到 v 的最小邊割集的大小被稱作 u 到 v 的 區域性邊連通度 (local edge-connectivity),記作 \lambda(u, v)。\)

點雙連通 (biconnected)

\(幾乎與 2- 點連通完全一致,除了一條邊連線兩個點構成的圖,它是點雙連通的,但不是 2- 點連通的。換句話說,沒有割點的連通圖是點雙連通的。\)

邊雙連通 (2-edge-connected)

\(與 2- 邊雙連通完全一致。換句話說,沒有橋的連通圖是邊雙連通的。\)

\(與連通分量類似,也有 點雙連通分量 (biconnected component)(極大點雙連通子圖)和 邊雙連通分量 (2-edge-connected component)(極大邊雙連通子圖)。\)

Whitney 定理:

\(對任意的圖 G,有 \kappa(G)\le \lambda(G)\le \delta(G)。(不等式中的三項分別為點連通度、邊連通度、最小度。)\)

其他更詳細的 森林 之類的,詳情見 OI.WIKI-圖論相關概念

1.0 圖的儲存

1.1 直接存邊

使用一個陣列來存邊,陣列中的每個元素都包含一條邊的起點與終點(帶邊權的圖還包含邊權)。(或者使用多個陣列分別存起點,終點和邊權。)

模板:

#include<bits/stdc++.h>
using namespace std;

struct Edge
{
  int u, v;
};
int n, m;
vector<Edge> e;
vector<bool> vis;

bool find_edge(int u, int v)
{
	for(int i = 1; i <= m; ++i)
	{
		if(e[i].u == u && e[i].v == v)
		{
			return true;
		}
	}
	return false;
}
void dfs(int u)
{
	if(vis[u]) return;
	vis[u] = true;
	for(int i = 1;i<=m;++i)
		if(e[i].u == u)
			dfs(e[i].v);
}
int main()
{
	cin>>n>>m;
	vis.resize(n + 1,0);
	e.resize(m + 1);
	for(int i = 1; i <= m; ++i) cin>>e[i].u>>e[i].v;
	return 0;
}

複雜度:

查詢是否存在某條邊: \(O(m)\)

遍歷一個點的所有出邊: \(O(m)\)

遍歷整張圖: \(O(nm)\)

空間複雜度: \(O(m)\)

1.2 鄰接矩陣:

用edge[][]來儲存邊,若 uv 存在邊,讓 edge[u][v] = 1 ,反之設為 0 ,若圖存在邊權,可以直接儲存邊權。
模板:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5+5;
bool edge[N][N];
int m;

int main()
{
	cin>>m;
	for (int i = 1;i<=m;++i)
	{
		int u, v;
		cin>>u>>v;
		edge[u][v] = 1;
	}
	return 0;
}
//帶邊權版本 
#include<bits/stdc++.h>
using namespace std;

const int N = 1e5+5;
int edge[N][N];
int m;

int main()
{
	cin>>m;
	for (int i = 1;i<=m;++i)
	{
		int u,v,w;
		cin>>u>>v>>w;
		edge[u][v] = w;
	}
	return 0;
}
/*
遍歷·摘自oi.wiki 
void dfs(int u) {
  if (vis[u]) return;
  vis[u] = true;
  for (int v = 1; v <= n; ++v) {
    if (adj[u][v]) {
      dfs(v);
    }
  }
}
*/

複雜度:

查詢是否存在某條邊: \(O(1)\)

遍歷一個點的所有出邊: \(O(n)\)

遍歷整張圖: \(O(n^2)\)

空間複雜度: \(O(n^2)\)

1.3 鏈式前向星

實質:連結串列

實現:

void add(int u, int v)
{
  nxt[++cnt] = head[u];  // 當前邊的後繼
  head[u] = cnt;         // 起點 u 的第一條邊
  to[cnt] = v;           // 當前邊的終點
}

遍歷 u 的出邊:

for (int i = head[u]; i!=-1; i = nxt[i])
{
  int v = to[i];
}

模板:

#include<bits/stdc++.h> 
using namespace std;

int n,m;
vector<bool> vis;
vector<int> head, nxt, to;

void add(int u,int v)
{
	nxt.push_back(head[u]);
	head[u] = to.size();
	to.push_back(v);
}
bool find_edge(int u,int v)
{
	for(int i = head[u];i!=-1;i = nxt[i])
		if(to[i] == v)
			return true;
	return false;
}
void dfs(int u)
{
	if(vis[u]) return;
	vis[u] = true;
	for(int i = head[u]; ~i; i = nxt[i]) dfs(to[i]); //~i 等效 i!=-1
}

int main()
{
	cin>>n>>m;
	vis.resize(n + 1,false);
	head.resize(n + 1,-1);
	for(int i = 1;i<=m;++i)
	{
		int u, v;
		cin>>u>>v;
		add(u,v);
	}
	return 0;
}

複雜度:

查詢是否存在 u 到 v 的邊: \(O(d^+(u))\)

遍歷點 u 的所有出邊: \(O(d^+(u))\)

遍歷整張圖: \(O(n+m)\)

空間複雜度: \(O(m)\)

2.0 最短路演算法

記號:
為了方便敘述,這裡先給出下文將會用到的一些記號的含義。

  • \(n\) 為圖上點的數目,\(m\) 為圖上邊的數目;

  • \(s\) 為最短路的源點;

  • \(D(u)\)\(s\) 點到 \(u\) 點的 實際 最短路長度;

  • \(dis(u)\)\(s\) 點到 \(u\) 點的估計最短路長度。任何時候都有 \(dis(u) \geq D(u)\)。特別地,當最短路演算法終止時,應有 \(dis(u)=D(u)\)

  • \(w(u,v)\)\((u,v)\) 這一條邊的邊權。

性質:

對於邊權為正的圖,有:

  1. 任意兩個結點之間的最短路,不會經過重複的結點。
  2. 任意兩個結點之間的最短路,不會經過重複的邊。
  3. 任意兩個結點之間的最短路,任意一條的結點數不會超過 \(n\),邊數不會超過 \(n-1\)

2.1 \(Floyd\) 演算法

特點:

複雜度高,常熟小,易實現。

使用場景:

不能有負環,必須存在負環且不被有無向和邊權正負影響

實現:

定義一個陣列:\(f[k][x][y]\),表示只允許經過結點 \(1\)\(k\)(也就是說,在子圖 \(V'={1, 2, \ldots, k}\) 中的路徑,注意,\(x\)\(y\) 不一定在這個子圖中),結點 \(x\) 到結點 \(y\) 的最短路長度。

所以節點 \(x\)\(y\) 的最短距離就是 \(f[n][x][y]\)

f[0][x][y]:\(x\)\(y\) 的邊權,或者 \(0\) 或者 \(+\infty\)
(當 \(x = y\) 的時候為零,因為到本身的距離為零;當 \(x\)\(y\) 沒有直接相連的邊的時候,為 \(+\infty)\)。)

所以公式就是:

\[f[k][x][y] = min(f[k-1][x][y],f[k-1][x][k]+f[k-1][k][y]) \]

因為第一維的引數只會使用到k-1,可以使用滾動陣列最佳化掉一維。

即為:

\[f[x][y] = f[x][k]+f[k][y] \]

時間複雜度為:\(O(n^3)\)

模板:

for(int k = 1;k<=n;k++){
  for(int x = 1;x<=n;x++){
    for(int y = 1;y<=n;y++){
      f[x][y] = min(f[x][y], f[x][k] + f[k][y]);
    }
  }
}
































.

相關文章