OI-Wiki 學習筆記

来源:https://www.cnblogs.com/So-noSlack/p/18314825
-Advertisement-
Play Games

演算法基礎 \(\text{Update: 2024 - 07 - 22}\) 複雜度 定義 衡量一個演算法的快慢,一定要考慮數據規模的大小。 一般來說,數據規模越大,演算法的用時就越長。 而在演算法競賽中,我們衡量一個演算法的效率時,最重要的不是看它在某個數據規模下的用時,而是看它的用時隨數據規模而增長的趨 ...


演算法基礎

\(\text{Update: 2024 - 07 - 22}\)

複雜度

定義

衡量一個演算法的快慢,一定要考慮數據規模的大小。

一般來說,數據規模越大,演算法的用時就越長。

而在演算法競賽中,我們衡量一個演算法的效率時,最重要的不是看它在某個數據規模下的用時,而是看它的用時隨數據規模而增長的趨勢,即時間複雜度

符號定義

相等是 \(\Theta\),小於是 \(O\),大於是 \(\Omega\)

\(\Theta\) 符號

對於函數 \(f(n)\)\(g(n)\)\(f(n)=\Theta(g(n))\),當且僅當 \(\exists c_1,c_2,n_0>0\),使得 \(\forall n \ge n_0, 0\le c_1\cdot g(n)\le f(n) \le c_2\cdot g(n)\)

\(O\) 符號

\(\Theta\) 符號同時給了我們一個函數的上下界,如果只知道一個函數的漸進上界而不知道其漸進下界,可以使用 \(O\) 符號。\(f(n) = O(g(n))\),當且僅當 \(\exists c,n_0\),使得 \(\forall n \ge n_0,0\le f(n)\le c\cdot g(n)\)

研究時間複雜度時通常會使用 \(O\) 符號,因為我們關註的通常是程式用時的上界,而不關心其用時的下界。

\(\Omega\) 符號

同樣的,我們使用 \(\Omega\) 符號來描述一個函數的漸進下界。\(f(n) = \Omega(g(n))\),當且僅當 \(\exists c, n_0\),使得 \(\forall n \ge n_0, 0 \le c \cdot g(n) \le f(n)\)

主定理

建議背下來,不是很好理解。

\[T(n) = aT(\frac{n}{b}) + f(n) \qquad \forall n > b \]

那麼

\[T(n) = \begin{cases} \Theta(n^{\log_b a}) & f(n) = O(n^{\log_b (a) - \epsilon}), \epsilon > 0 \\ \Theta(f(n)) & f(n) = \Omega(n^{\log_b (a) + \epsilon}), \epsilon\ge 0\\ \Theta(n^{\log_b a} \log^{k+1} n) & f(n) = \Theta(n^{\log_b a} \log^k n), k \ge 0 \end{cases} \]

需要註意的是,這裡的第二種情況還需要滿足 \(\text{regularity condition}\), 即 \(a f(n/b) \leq c f(n)\)\(\text{for some constant}\) \(c < 1\) \(\text{and sufficiently large}\) \(n\)

幾個例子:

  1. \(T(n) = 2T(\frac{n}{2}) + 1\),那麼 \(a = 2, b = 2, {\log_2 2} = 1\),那麼 \(\epsilon\) 可以取值在 \((0, 1]\) 之間,從而滿足第一種情況,所以 \(T(n) = \Theta(n)\)

  2. \(T(n) = T(\frac{n}{2}) + n\),那麼 \(a = 1, b = 2, {\log_2 1} = 0\),那麼 \(\epsilon\) 可以取值在 \((0, 1]\) 之間,從而滿足第二種情況,所以 \(T(n) = \Theta(n)\)

  3. \(T(n) = T(\frac{n}{2}) + {\log n}\),那麼 \(a = 1, b = 2, {\log_2 1} = 0\),那麼 \(k\) 可以取值為 \(1\),從而滿足第三種情況,所以 \(T(n) = \Theta(\log^2 n)\)

  4. \(T(n) = T(\frac{n}{2}) + 1\),那麼 \(a = 1, b = 2, {\log_2 1} = 0\),那麼 \(k\) 可以取值為 \(0\),從而滿足第三種情況,所以 \(T(n) = \Theta(\log n)\)

空間複雜度

類似地,演算法所使用的空間隨輸入規模變化的趨勢可以用空間複雜度來衡量。

枚舉

實際上一些題的正解就是枚舉,但需要很多優化。

要點:

  1. 建立簡潔的數學模型。

  2. 減少不必要的枚舉空間。

  3. 選擇合適的枚舉順序。

模擬

模擬即為將題目描述中的操作用代碼實現,碼量一般比較大,寫錯比較難調,相當浪費時間。

寫模擬題時有以下註意的點:

  • 在寫代碼之前,儘量在演草紙上自習分析題目的實現過程。

  • 在代碼中儘量把每個部分抽離出來寫成函數,模塊化。

  • 將題目中的信息轉化,不要殘留多種表達。

  • 如果一次沒過大樣例,分塊調試。

  • 實現代碼時按照演草紙上的思路一步步實現。

遞歸

定義

我們可以用以下代碼理解遞歸:

long long f(傳入數值) {
    if(終止條件) return 最小子問題解;
    return f(縮小問題規模);
}

遞歸的優點

  • 結構清晰、可讀性強,可以參考歸併排序的兩種實現方式。
  • 練習分析為題的結構,當發現問題可以分解成相同結構的小問題時,遞歸寫多了就能敏銳發現這個特點,進而高效解決問題。

遞歸的缺點

在程式執行中,遞歸是利用堆棧來實現的。每當進入一個函數調用,棧就會增加一層棧幀,每次函數返回,棧就會減少一層棧幀。而棧不是無限大的,當遞歸層數過多時,就會造成棧溢出 的後果。

顯然有時候遞歸處理是高效的,比如歸併排序;有時候是低效的,比如數孫悟空身上的毛,因為堆棧會消耗額外空間,而簡單的遞推不會消耗空間。比如這個例子,給一個鏈表頭,計算它的長度:

// 典型的遞推遍歷框架
int size(Node *head) {
  int size = 0;
  for (Node *p = head; p != nullptr; p = p->next) size++;
  return size;
}

// 我就是要寫遞歸,遞歸天下第一
int size_recursion(Node *head) {
  if (head == nullptr) return 0;
  return size_recursion(head->next) + 1;
}

二者的對比,compiler 設為 Clang 10.0,優化設為 O1

遞歸優化見後文搜索優化記憶化搜索

要點

明白一個函數的作用並相信它能完成這個任務,千萬不要跳進這個函數裡面企圖探究更多細節,否則就會陷入無窮的細節無法自拔,人腦能壓幾個棧啊。

遞歸與枚舉的區別

枚舉是橫向地把問題劃分,然後依次求解子問題;而遞歸是把問題逐級分解,是縱向的拆分。

遞歸與分治的區別

遞歸是一種編程技巧,一種解決問題的思維方式;分治演算法很大程度上是基於遞歸的,解決更具體問題的演算法思想。

分治

定義

就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。

過程

分治演算法的核心思想就是「分而治之」。

大概的流程可以分為三步:分解 \(\to\) 解決 \(\to\) 合併。

  1. 分解原問題為結構相同的子問題。

  2. 分解到某個容易求解的邊界之後,進行遞歸求解。

  3. 將子問題的解合併成原問題的解。

分治法能解決的問題一般有如下特征:

  • 該問題的規模縮小到一定的程度就可以容易地解決。

  • 該問題可以分解為若幹個規模較小的相同問題,即該問題具有最優子結構性質,利用該問題分解出的子問題的解可以合併為該問題的解。

  • 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子問題。

註意:如果各子問題是不獨立的,則分治法要重覆地解公共的子問題,也就做了許多不必要的工作。此時雖然也可用分治法,但一般用動態規劃較好。

貪心

適用範圍

貪心演算法在有最優子結構的問題中尤為有效。

最優子結構的意思是問題能夠分解成子問題來解決,子問題的最優解能遞推到最終問題的最優解。

證明

貪心演算法有兩種證明方法:反證法和歸納法。

一般情況下,一道題只會用到其中的一種方法來證明。

  1. 反證法:如果交換方案中任意兩個元素/相鄰的兩個元素後,答案不會變得更好,那麼可以推定目前的解已經是最優解了。

  2. 歸納法:先算得出邊界情況(例如 \(n = 1\))的最優解 \(F_1\),然後再證明:對於每個 \(n\)\(F_{n + 1}\) 都可以由 \(F_{n}\) 推導出結果。

常見題型

在提高組難度以下的題目中,最常見的貪心有兩種。

  • 「我們將 \(XXX\) 按照某某順序排序,然後按某種順序(例如從小到大)選擇。」。

  • 「我們每次都取 \(XXX\) 中最大/小的東西,並更新 \(XXX\)。」(有時「\(XXX\) 中最大/小的東西」可以優化,比如用優先隊列維護)

二者的區別在於一種是離線的,先處理後選擇;一種是線上的,邊處理邊選擇。

排序解法

用排序法常見的情況是輸入一個包含幾個(一般一到兩個)權值的數組,通過排序然後遍歷模擬計算的方法求出最優值。

後悔解法

思路是無論當前的選項是否最優都接受,然後進行比較,如果選擇之後不是最優了,則反悔,捨棄掉這個選項;否則,正式接受。如此往複。

排序

幾種排序演算法的比較

一張表格速通

排序方法 平均情況 最好情況 最壞情況 空間 穩定性
選擇排序 \(O(n^2)\) \(O(n^2)\) \(O(n^2)\) \(O(1)\) 不穩定
冒泡排序 \(O(n^2)\) \(O(n)\) \(O(n^2)\) \(O(1)\) 穩定
插入排序 \(O(n^2)\) \(O(n)\) \(O(n^2)\) \(O(n)\) 穩定
計數排序 \(O(n + w)^1\) \(O(n + w)\) \(O(n + w)\) \(O(n + w)\) 穩定
基數排序 \(O(kn + \sum\limits_{i = 1}^{k} w_i)^2\) \(O(kn + \sum\limits_{i = 1}^{k} w_i)\) \(O(kn + \sum\limits_{i = 1}^{k} w_i)\) \(O(n + k)\) 穩定
快速排序 \(O(n \log n)\) \(O(n \log n)\) \(O(n^2)\) \(O(\log n) \sim O(n)\) 不穩定
歸併排序 \(O(n \log n)\) \(O(n \log n)\) \(O(n \log n)\) \(O(n)\) 穩定
堆排序 \(O(n \log n)\) \(O(n \log n)\) \(O(n \log n)\) \(O(1)\) 不穩定
桶排序 \(O(n + \frac{n^2}{k} + k)^3\) \(O(n)\) \(O(n^2)\) \(O(n + m)^4\) 穩定
希爾排序 \(O(n \log n) \sim O(n^2)\) \(O(n^{1.3})\) \(O(n^2)\) \(O(1)\) 不穩定
錦標賽排序 \(O(n \log n)\) \(O(n \log n)\) \(O(n \log n)\) \(O(n)\) 穩定
\(\text{tim}\) 排序 \(O(n \log n)\) \(O(n)\) \(O(n \log n)\) \(O(n)\) 穩定

\(^1\):其中 \(w\) 為排序元素的值域。

\(^2\):其中 \(k\) 為排序元素的最大位數,\(w_i\) 為第 \(i\) 個關鍵字的值域。

\(^3\):其中 \(k\) 表示將 \(n\) 個元素放進 \(m\) 個桶中,每個桶的數據為 \(k = \frac{n}{m}\)

\(^4\):其中 \(m\) 表示將 \(n\) 個元素放進的 \(m\) 個桶。

首碼和

定義

首碼和可以簡單理解為「數列的前 \(\text{i}\) 項的和」,是一種重要的預處理方式,能大大降低查詢的時間複雜度。

一維首碼和

預處理遞推式為:

\[p_i = p_{i - 1} + a_i \]

查詢 \([l, r]\) 的數值:

\[ans = p_r - p_{l - 1} \]

二維首碼和

預處理遞推式為:

\[p_{i, j} = p_{i - 1, j} + p_{i, j - 1} - p_{i - 1, j - 1} + a_{i, j} \]

查詢 \((i, j) \sim (k, w)\) 的數值:

\[ans = p_{k, w} - p_{k, j - 1} - p_{i - 1, w} + p_{i - 1, j - 1} \]

樹上首碼和

\(s_i\) 表示結點 \(i\) 到根節點的權值總和。

若是點權,\(x, y\) 路徑上的和為 \(s_x + s_y - s_{lca} - s_{fa_{lca}}\)

若是邊權,\(x, y\) 路徑上的和為 \(s_x + s_y - 2 \times s_{lca}\)

差分

定義

差分是一種和首碼和相對的策略,可以當作是求和的逆運算。

這種策略的定義是令 \(b_i = \begin{cases} a_i - a_{i - 1} \, &i \in[2, n] \\ a_1 \, &i = 1 \end{cases}\)

性質

  • \(a_i\) 的值是 \(b_i\) 的首碼和,即 \(a_n = \sum\limits_{i = 1}^n b_i\)

  • 計算 \(a_i\) 的首碼和 \(sum = \sum\limits_{i = 1}^n a_i = \sum\limits_{i = 1}^n \sum\limits_{j = 1}^{i} b_j = \sum\limits_{i = 1}^n (n - i + 1) b_i\)

它可以維護多次對序列的一個區間加上一個數,併在最後詢問某一位的數或是多次詢問某一位的數。註意修改操作一定要在查詢操作之前。

樹上差分

樹上差分可以理解為對樹上的某一段路徑進行差分操作,這裡的路徑可以類比一維數組的區間進行理解。例如在對樹上的一些路徑進行頻繁操作,並且詢問某條邊或者某個點在經過操作後的值的時候,就可以運用樹上差分思想了。

樹上差分通常會結合樹基礎和最近公共祖先來進行考察。樹上差分又分為點差分邊差分,在實現上會稍有不同。

點差分

舉例:對樹上的一些路徑 \(\delta(s_1, t_1), \delta(s_2, t_2), \delta(s_3, t_3)\dots\) 進行訪問,問一條路徑 \(\delta(s, t)\) 上的點被訪問的次數。

對於一次 \(\delta(s, t)\) 的訪問,需要找到 \(s\)\(t\) 的公共祖先,然後對這條路徑上的點進行訪問(點的權值加一),若採用 \(\text{DFS}\) 演算法對每個點進行訪問,由於有太多的路徑需要訪問,時間上承受不了。這裡進行差分操作:

\[\begin{aligned} &d_s\leftarrow d_s + 1\\ &d_{lca}\leftarrow d_{\textit{lca}} - 1\\ &d_t\leftarrow d_t + 1\\ &d_{f(\textit{lca})}\leftarrow d_{f(\textit{lca})} - 1\\ \end{aligned} \]

其中 \(f(x)\) 表示 \(x\) 的父親節點,\(d_i\) 為點權 \(a_i\) 的差分數組。

可以認為公式中的前兩條是對藍色方框內的路徑進行操作,後兩條是對紅色方框內的路徑進行操作。不妨令 \(\textit{lca}\) 左側的直系子節點為 \(\textit{left}\)。那麼有 \(d_{\textit{lca}} - 1 = a_{\textit{lca}} - (a_{\textit{left}} + 1)\)\(d_{f(\textit{lca})} - 1 = a_{f(\textit{lca})} - (a_{\textit{lca}} + 1)\)。可以發現實際上點差分的操作和上文一維數組的差分操作是類似的。

邊差分

若是對路徑中的邊進行訪問,就需要採用邊差分策略了,使用以下公式:

\[\begin{aligned} &d_s\leftarrow d_s + 1\\ &d_t\leftarrow d_t + 1\\ &d_{\textit{lca}}\leftarrow d_{\textit{lca}} - 2\\ \end{aligned} \]

由於在邊上直接進行差分比較困難,所以將本來應當累加到紅色邊上的值向下移動到附近的點里,那麼操作起來也就方便了。對於公式,有了點差分的理解基礎後也不難推導,同樣是對兩段區間進行差分。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 定義 工廠方法模式是一種創建型設計模式,它定義了一個用於創建對象的介面,但由子類來決定實例化哪一個類。工廠方法使得類的實例化延遲到子類,這樣可以讓客戶端在不需要知道具體類的情況下創建對象。工廠方法模式通過使用繼承和多態性,允許子類來控制對象的創建方式,能夠更好地應對對象創建的複雜性和變化性。 為什麼 ...
  • 本文主要介紹了Python中創建自定義類時如何使用多重繼承、菱形繼承的概念和易錯點,同時講解瞭如何使用PyQtGraph庫對串口接收的數據進行繪圖。 ...
  • 定義 簡單工廠模式(Simple Factory Pattern)是一種創建型設計模式,它定義一個用於創建對象的介面,但由一個單獨的類來實現實際創建的工作。簡單工廠模式通過在一個類中集中管理對象的創建過程,可以減少客戶端與具體類之間的耦合,使得代碼結構更加清晰和易於維護。通過專門定義一個類來負責創建 ...
  • 需求場景 按著慣例,還是以一個應用場景作為代理模式的切入點。現在有一個訂單系統,要求是:一旦訂單被創建,只有訂單的創建人才可以修改訂單中的數據,其他人則不能修改。 基本實現思路 按著最直白的思路,就是查詢資料庫中訂單的創建人和當前Session中的登錄賬號ID是否一致。 class Order { ...
  • 本文主要介紹了在使用Python面向對象編程時,異常的使用場景、定義和特點,錯誤的類型和特點,並舉出實際例子來輔助講解。 ...
  • notepad++下載安裝 找到瀏覽器輸入:notepad或者 https://notepad-plus-plus.org/downloads/ 官網下載即可使用 如果官網崩了,可以在微信公眾號:A軟體安裝管家,找到安裝notepad++,下載安裝即可。 typora下載安裝 找到瀏覽器輸入:typ ...
  • 指針 定義 指針是一個變數,存儲另一個變數的記憶體地址,它允許直接訪問和操作記憶體中的數據,使得程式能夠以更靈活和高效的方式處理數據和記憶體。 獲取變數地址:使用取地址符 &。 訪問地址上的數據:使用解引用符 *。 例子1 指針是存儲另一個變數地址的變數。通過使用取地址符 & 和解引用符 *,我們可以靈活 ...
  • **06 訪問修飾符 封裝 繼承 多態 ** 訪問修飾符 public 公開級別,對外公開 protected 受保護級別,對子類和同一個包中的類公開 default 預設級別,無修飾符,向同一個包的類公開 private 私有級別,只有類本身可以訪問,不對外公開 修飾符可以用來修飾類中的屬性,成員 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...