每天資訊演算法攻略從零到一(cpp版)

菜單

演算法攻略從零到一(cpp版)

演算法攻略從零到一(cpp版)

1 前言

雖然C++是向物件語,但是演算法題的基本思維方式是

面向過程式

,也就是針對演算法題我們是不需要掌握面向物件部分(繼承、封裝、多型)。只需要掌握刷演算法的時候需要到的部分(

基本輸輸出、STL標準模板庫、string字串等

)。

使用C++刷題有幾大好處:

方便的輸入輸出;C++的

cout

cin

不再像C語言裡面的輸入輸出

scanf

printf

那樣需要自己寫明變數的型別,比如

scanf(“%d”, &n);

printf(“%d”, n);

,直接

cin >> n;

cout << n

更強大的字串處理;C++的

string

字串類提供了強大的字串處理功能,不再像C語言裡面的字元陣列,處理起來比較繁瑣;

STL

模板庫;

STL

的動態陣列

vector

、集合

set

、對映

map

、棧

stack

、佇列

queue

、位運算

bitset

,以及演算法庫

#include

sort

排序演算法函式模板等等,都極大提高了我們做題的效率;

整理自網路,非商業用途,侵權聯絡刪除。

2 演算法基礎之C++

2。1 輸入輸出

leetcode平臺是不需要我們自己寫資料輸入的

,只需要在下圖的紅色文字框中寫自己的解題程式碼就行。

演算法攻略從零到一(cpp版)

然而在其他的OJ平臺上刷題,是需要自己

需要自己編寫輸入輸出。

演算法攻略從零到一(cpp版)

然而在其他的OJ平臺上刷題,是需要自己

然後在網頁編輯框填寫自己的程式碼提交評測。

演算法攻略從零到一(cpp版)

2。1。1 整數輸入問題

最簡單的輸入

//最簡單的輸入,輸入單行

Sample Input 1 2

Sample Output 3

#include

using

namespace

std

int

main

()

{

int

a

b

cin

>>

a

>>

b

cout

<<

a

+

b

<<

endl

//對其他題目,換成要求的複雜處理與輸出

return

0

}

輸入多行數時,直到讀至輸入檔案末尾(

EOF

)為止

輸入多行數時,直到讀至輸入檔案末尾(EOF)為止

說明1:當讀到輸入結束時,cin >> a >> b返回 0,迴圈也結束。

說明2:在除錯程式時,鍵盤輸入的資料,用CTRL-Z(即按住CTRL鍵不放,再按下Z)組合作為輸入結束,此謂鍵盤輸入裝置的“檔案末尾”。

重點掌握

Sample Input

1 5

10 20

400 516

Sample Output

6

30

916

#include

using

namespace

std

int

main

()

{

int

a

b

while

cin

>>

a

>>

b

//當題目輸入行數不確定時使用此方法

{

cout

<<

a

+

b

<<

endl

}

return

0

}

多組由兩個整數(a和b)構成的輸入,a和b之間用空格隔開,每組輸入單獨佔一行。

多組由兩個整數(a和b)構成的輸入,a和b之間用空格隔開,每組輸入單獨佔一行。

當輸入為 0 0 時,輸入結束。

Sample Input

1 5

10 20

0 0

Sample Output

6

30

#include

using

namespace

std

int

main

()

{

int

a

b

while

cin

>>

a

>>

b

&&

a

||

b

))

{

cout

<<

a

+

b

<<

endl

}

return

0

}

第一行是資料的組數N,從第二行是N組由兩個整數(a和b)構成的輸入,a和b之間用空格隔開,每組輸入單獨佔一行

第一行是資料的組數N,

從第二行是N組由兩個整數(a和b)構成的輸入,a和b之間用空格隔開,每組輸入單獨佔一行

重點掌握

Sample Input

2

1 5

10 20

Sample Output

6

30

#include

using

namespace

std

int

main

() {

int

a

b

n

cin

>>

n

for

int

i

=

0

i

<

n

i

++

{

cin

>>

a

>>

b

//cin以空格或者回車作為輸入輸出分隔符

cout

<<

a

+

b

<<

endl

}

return

0

}

利用檔案重定向提高除錯效率

#include

#include

using

namespace

std

int

main

()

{

freopen

“input。txt”

“r”

stdin

);

// 將輸入重定向到檔案 input。txt (注意檔案路徑)

int

a

b

cin

>>

a

>>

b

cout

<<

a

+

b

<<

endl

return

0

}

// 在執行程式前,將本該由鍵盤輸入的資料,寫到檔案 input。txt 中。而在執行程式時,資料將不再需要人去輸入

2。1。2

讀取和解析標點字元(如逗號)分隔資料

讀取以逗號間隔的數字到陣列中

處理輸入問題:讀取以逗號間隔的數字到陣列中

例:

輸入:1,12,123

陣列a:a[0] = 1,a[1] = 12, a[2] = 123

#include

#include

#include

#include

using

namespace

std

int

main

()

{

vector

<

int

>

a

string

s

cin

>>

s

//讀取輸入字串到s

stringstream

input

s

);

//將字串s轉化為流

string

numstr

while

getline

input

numstr

‘,’

))

//按逗號分隔為字串( getline每次讀一個 )

{

a

push_back

stoi

numstr

));

}

return

0

}

思路:使用

getline

stringstream

以 ‘,’ 為分隔符來切分資料 ,然後使用標準庫

string

的數值轉換函式例如字串轉整形

stoi

進行解析。注意: 當資料以空格分隔時,可以直接用

cin

來讀入!

2。2 String類

string

類,使得字串的定義、拼接、輸出、處理都更加簡單。不過

string

只能

cin

cout

處理,法

scanf

printf

處理:

string

s

=

“hello world”

// 賦值字串

string

s2

=

s

string

s3

=

s

+

s2

// 字串拼接直接+號就可以

string

s4

cin

>>

s4

// 讀字串

cout

<<

s

// 輸出字串

cin

讀字串的時候,是以空格為分隔符的,如果想要讀整的字串,就需要

getline

此外

string

的長度可以用

string s; s。length(); s。size()

獲取,這兩個獲取長度的函式功能是一樣的。與C語言的

char []

還要考慮尾部的

\0

字元,

string

裡面是多少字元就是多少,當然也包括

‘’

字元。

string

s

// 定義個空字串s

getline

cin

s

);

// 讀取的字串,包括空格

cout

<<

s

length

();

// 輸出字串s的度

string

中還有個很常的函式叫做

substr

,作是擷取某個字串中的串,法有兩種形式:

string s2 = s。substr(4); // 表示從下標4開始直到結束string s3 = s。substr(5, 3); // 表示從下標5開始,3個字元

2。3 STL

2。3。1 STL之動態陣列vector(量)

之前C語

int arr[]

定義陣列,它的缺點是陣列的度不能隨所欲的改變。而

vector

它能夠在運階段設定陣列的度、在末尾增加新的資料、在中間插新的值、度任意被改變。它在頭件

vector

,也在名稱空間

std

,所以使的時候要引頭件

#include

using namespace std

vector

stack

queue

map

set

這些在C++中都叫做容器,這些容器的都可以

。size()

獲取到,就像

string s

的度

s。length()

獲取樣。只是對於

string

字串我們一般是用

。length()

,而對於容器類我們一般用

。size()

#include #include int main() { vector v1; // 定義個vector v1,定義的時候沒有分配 cout << v1。size(); // 輸出vector v1的,此處應該為0 return 0; }

vector

可以開始不定義,之後

resize

法分配,也可以開始就定義,之後還可以對它插刪除動態改變它的。且不管在

main

函式還是在全域性中定義,它都能夠直接將所有的值初始化為0(不顯式地寫出來,預設就是所有的元素為0)。

vector v(10); // 直接定義度為10的int陣列,預設這10個元素值都為0// 或者vector v1;v1。resize(8); //先定義個vector變數v1,然後將度resize為8,預設這8個元素都是0// 在定義的時候就可以對vector變數進初始化vector v3(100, 9);// 把100度的陣列中所有的值都初始化為9// 訪問的時候像陣列樣直接[]下標訪問即可(也可以迭代器訪問,下會講) v[1] = 2;cout << v[0];

除了可以訪問官網檢視

vector

的所有功能,我們下面列舉一些常見的方法。

#include #include using namespace std;int main() { vector a; // 定義的時候不指定vector的 cout << a。size() << endl; // 這個時候size是0 for (int i = 0; i < 10; i++) { a。push_back(i); // 在vector a的末尾添加個元素i } cout << a。size() << endl; // 此時會發現a的size變成了10 vector b(15); // 定義的時候指定vector的,預設b元素都是0 cout << b。size() << endl; for (int i = 0; i < b。size(); i++) { b[i] = 15; } for (int i = 0; i < b。size(); i++) { cout << b[i] << “ ”; } cout << endl; vector c(20, 2); // 定義的時候指定vector的並把所有的元素賦個指定的值 for (int i = 0; i < c。size(); i++) { cout << c[i] << “ ”; } cout << endl; //auto此次相當於vector::iterator for (auto it = c。begin(); it != c。end(); it++) { // 使迭代器的式訪問vector cout << *it << “ ”; } return 0; }

容器

vector

set

map

這些遍歷的時候都是使迭代器訪問的,

c。begin()

是個指標,指向容器的第個元素,

c。end()

指向容器的最後個元素的後個位置,所以迭代器指標

it

for

迴圈判斷條件是

it != c。end()

2。3。2 STL之集合set的使用

set

是集合,個

set

的各元素是各不相同的,且

set

會按照元素進從到排序,以下是

set

的常法:

#include #include using namespace std;int main() { set s; // 定義個空集合s s。insert(1); // 向集合s插個1 cout << *(s。begin()) << endl; // 輸出集合s的第個元素 (前的星號表示要對指標取值) for (int i = 0; i < 6; i++) { s。insert(i); // 向集合s插i } for (auto it = s。begin(); it != s。end(); it++) { // 迭代器遍歷集合s的每個元素 cout << *it << “ ”; } cout << endl << (s。find(2) != s。end()) << endl; // 查詢集合s中的值,如果結果等於s。end()表示未找到 (因為s。end()表示s的最後個元素的下個元素所在的位置) cout << (s。find(10) != s。end()) << endl; // s。find(10) != s。end()表示能找到10這個元素 s。erase(1); // 刪除集合s中的1這個元素 cout << (s。find(1) != s。end()) << endl; // 這時候元素1就應該找不到啦~ return 0; }

2。3。3 STL之對映map的使用

map

是鍵值對,如個名對應個學號,就可以定義個字串

string

型別的名為“鍵”,學號

int

型別為“值”,如

map m

; 當然鍵、值也可以是其它變數型別。

map

會動將所有的鍵值對按照鍵從到排序,

map

使時的頭件

#include

以下是

map

中常的法:

#include #include #include using namespace std;int main() { map m; // 定義個空的map m,鍵是string型別的,值是int型別的 m[“hello”] = 2; // 將key為“hello”, value為2的鍵值對(key-value)存map中 cout << m[“hello”] << endl; // 訪問map中key為“hello”的value, 如果key不存在,則返回0 cout << m[“world”] << endl; m[“world”] = 3; // 將“world”鍵對應的值修改為3 m[“,”] = 1; // 設組鍵值對,鍵為“,” 值為1 // 迭代器遍歷,輸出map中所有的元素,鍵it->first獲取,值it->second獲取 for (auto it = m。begin(); it != m。end(); it++) { cout << it->first << “ ” << it->second << endl; } // 訪問map的第個元素,輸出它的鍵和值 cout << m。begin()->first << “ ” << m。begin()->second << endl; // 訪問map的最後個元素,輸出它的鍵和值 cout << m。rbegin()->first << “ ” << m。rbegin()->second << endl; // 輸出map的元素個數 cout << m。size() << endl; return 0; }

2。3。4 STL之棧的使用

stack

在頭件

#include

中,是資料結構的棧。以下是常法:

#include #include using namespace std;int main() { stack s; // 定義個空棧s for (int i = 0; i < 6; i++) { s。push(i); // 將元素i壓棧s中 } cout << s。top() << endl; // 訪問s的棧頂元素 cout << s。size() << endl; // 輸出s的元素個數 s。pop(); // 移除棧頂元素 return 0; }

2。3。5 STL之佇列queue的使用

佇列

queue

在頭件

#include

中,是資料結構的佇列。以下是常法:

#include #include using namespace std;int main() { queue q; // 定義個空佇列q for (int i = 0; i < 6; i++) { q。push(i); // 將i的值依次壓佇列q中 } cout << q。front() << “ ” << q。back() << endl; // 訪問佇列的隊元素和隊尾元素 cout << q。size() << endl; // 輸出佇列的元素個數 q。pop(); // 移除佇列的隊元素 return 0; }

2。3。6 STL之

unordered_map

unordered_set

的使用

unordered_map

在頭件

#include

中,

unordered_set

在頭件

#include

中。

unordered_map

map

(或者

unordered_set

set

)的區別是,

map

會按照鍵值對的鍵

key

進排序(

set

會按照集合中的元素進排序,從到順序),

unordered_map

(或者

unordered_set

)省去了這個排序的過程,如果偶爾刷題時候

map

或者

set

超時了,可以考慮

unordered_map

(或者

unordered_set

)縮短程式碼運時間、提程式碼效率。於法和

map

set

是樣的。

2。3。7

位運算

bitset

bitset

來處理進位制位常便。頭件是

#include

bitset

可能在PAT、藍橋OJ中不常,但是在LeetCode OJ中經常到。且知道

bitset

能夠簡化些操作,可能些複雜的問題能夠直接

bitset

就很輕易地解決。以下是些常法:

#include #include using namespace std;int main() { bitset<5> b(“11”); //5表示5個進位 // 初始化式: // bitset<5> b; 都為0 // bitset<5> b(u); u為unsigned int,如果u = 1,則被初始化為10000 // bitset<5> b(s); s為字串,如“1101” -> “10110” // bitset<5> b(s, pos, n); 從字串的s[pos]開始,n位度 for(int i = 0; i < 5; i++) cout << b[i]; cout << endl << b。any(); //b中是否存在1的進位制位 cout << endl << b。none(); //b中不存在1嗎? cout << endl << b。count(); //b中1的進位制位的個數 cout << endl << b。size(); //b中進位制位的個數 cout << endl << b。test(2); //測試下標為2處是否進位制位為1 b。set(4); //把b的下標為4處置1 b。reset(); //所有位歸零 b。reset(3); //b的下標3處歸零 b。flip(); //b的所有進位制位逐位取反 unsigned long a = b。to_ulong(); //b轉換為unsigned long型別 return 0;}

2。3。8 演算法庫之sort函式

sort

函式在頭件

#include

,主要是對個數組進排序(

int arr[]

陣列或者

vector

陣列都),

vector

是容器,要

v。begin()

v。end()

表示頭尾;

int arr[]

arr

表示陣列的地址,

arr+n

表示尾部。

#include #include #include using namespace std;bool cmp(int a, int b) { // cmp函式返回的值是bool型別 return a > b; // 從到排列}int main() { vector v(10); for (int i = 0; i < 10; i++) { cin >> v[i]; } sort(v。begin(), v。end());// 因為這沒有傳引數cmp,所以按照預設,v從到排列 int arr[10]; for (int i = 0; i < 10; i++) { cin >> arr[i]; } sort(arr, arr + 10, cmp); // arr從到排列,因為cmp函式排序規則設定了從到 return 0; }

注意:

sort

函式的

cmp

必須按照規定來寫,即必須只是 > 或者 < ,如:

return a > b

; 或者

return a < b

; 不能是 <= 或者

>

= ,(實際上等於號加了也是毫意義,

sort

是不穩定的排序),否則可能會出現段錯誤。

2。3。9 演算法庫之sort定義cmp函式

sort

預設是從到排列的,也可以指定第三個引數

cmp

函式,然後定義個

cmp

函式指定排序規則。

cmp

最好的還是在結構體中,尤其是很多排序的題。如個學結構體

stu

有學號和成績兩個變數,要求如果成績不同就按照成績從到排列,如果成績相同就按照學號從到排列,那麼就可以寫個

cmp

陣列實現這個看上去有點複雜的排序過程:

#include using namespace std;struct stu { // 定義個結構體stu,number表示學號,score表示分數 int number; int score; }bool cmp(stu a, stu b) { // cmp函式,返回值是bool,傳的引數型別應該是結構體stu型別 if (a。score != b。score) // 如果學分數不同,就按照分數從到排列 return a。score > b。score; else // 如果學分數相同,就按照學號從到排列 return a。number < b。number; } // 有時候這種簡單的if-else語句我喜歡直接個C語的三運算子表示~ bool cmp(stu a, stu b) { return a。score != b。score ? a。score > b。score : a。number < b。number; }

2。4 cctype標頭檔案裡的判斷函式

#include

本質上來源於C語標準函式庫中的頭件

#include

,其實並不屬於C++新特性的範疇,但是刷題時會經常碰到。

比如下面的程式碼就是判斷一個字元是否是字母

#include #include using namespace std;int main() { char c; cin >> c; if (isalpha(c)) { cout << “c is alpha”; } return 0;}

不僅僅能判斷字,還能判斷數字、寫字、寫字等,總的來說如下:

isalpha

字(包括寫、寫)

islower

(寫字)

isupper

(寫字)

isalnum

(字寫寫+數字)

isblank

(space和

\t

isspace

( space 、

\t

\r

\n

此外還有兩個常用函式,

tolower

toupper

,作是將某個字元轉為寫或者寫,這樣就不像原來那樣動判斷字元

c

是否是寫,如果是寫字元就

c = c + 32

; 的法將

c

轉為寫字元。

char c = ‘A’;char t = tolower(c); // 將c字元轉化為寫字元賦值給t,如果c本身就是寫字元也不變cout << t; // 此處t為‘a’

2。5 演算法刷題常用到的c++11特性

2。5。1 auto宣告

auto

是C++11的新特性,可以讓編譯器根據初始值型別直接推斷變數的型別。如這樣:

auto x = 100; // x是int變數auto y = 1。5; // y是double變數

在STL中使迭代器的時候,

auto

可以代替串的迭代器型別宣告

// 本來set的迭代器遍歷要這樣寫:for(set::iterator it = s。begin(); it != s。end(); it++) { cout << *it << “ ”; }// 現在可以直接替換成這樣的寫法:for(auto it = s。begin(); it != s。end(); it++) { cout << *it << “ ”; }

2。5。2 for in range 迴圈

除了像C語的

for

語句

for (i = 0; i < arr。size(); i++)

這樣,C++11標準還為C++添加了種新的for迴圈式,叫做基於範圍(range-based)的

for

迴圈,這在遍歷陣列中的每個元素時使會較簡便。如想要輸出陣列

arr

中的每個值,可以使如下的式輸出:

int arr[4] = {0, 1, 2, 3};for (int i : arr) cout << i << endl; // 輸出陣列中的每個元素的值,每個元素佔據

i

變數從陣列的第個元素開始,不斷執迴圈,

i

依次表示陣列中的每個元素。注意,使

int i

的式定義時,該語句只能來輸出陣列中元素的值,不能修改陣列中的元素,如果想要修改,必須使

int &i

這種定義引變數的式。如想給陣列中的每個元素都乘以2,可以使如下式:

int arr[4] = {0, 1, 2, 3};for (int &i : arr) // i為引變數 i = i * 2; // 將陣列中的每個元素都乘以2,arr[4]的內容變為了{0, 2, 4, 6}

這種基於範圍的

for

迴圈適於各種型別的陣列,將上述兩段程式碼中的

int

改成其他變數型別如

double

char

都是可以的。另外,這種

for

迴圈式不僅可以適於陣列,還適於各種STL容器,如

vector

set

等。加上上節所講的C++11很好的

auto

宣告,將

int

double

等變數型別替換成

auto

,起來就更便。

// v是個int型別的vector容器for (auto i : v) cout << i << “ ”;// 上的寫法等價於for (int i = 0; i < v。size(); i++) cout << v[i] << “ ”;

2。5。3

to_string

to_string

的頭件是

#include

to_string

最常的就是把個

int

型變數或者個數字轉化為

string

型別的變數,當然也可以轉

double

float

等型別的變數,這在很多字串處理的題中很有處,以下是示例程式碼

#include #include using namespace std;int main() { string s1 = to_string(123); // 將123這個數字轉成字串 cout << s1 << endl; string s2 = to_string(4。5); // 將4。5這個數字轉成字串 cout << s2 << endl; cout << s1 + s2 << endl; // 將s1和s2兩個字串拼接起來並輸出 printf(“%s\n”, (s1 + s2)。c_str()); // 如果想printf輸出string,得加個。c_str() return 0; }

2。5。4 stoi、stod

使

stoi

stod

可以將字串

string

轉化為對應的

int

型、

double

型變數,這在字串處理的很多問題中很有幫助。以下是示例程式碼和法輸的處理法:

#include #include using namespace std;int main() { string str = “123”; int a = stoi(str); cout << a; str = “123。44”; double b = stod(str); cout << b; return 0; }

不僅有

stoi

stod

兩種,相應的還有:

stof

(string to flfloat)

stold

(string to long double)

stol

(string to long)

stoll

(string to long long)

stoul

(string to unsigned long)

stoull

(string to unsigned long long)

2。6 常見錯誤型別

平常刷題提交評測遇到錯誤時常見型別如下。

3 演算法基礎之常見模板

3。1 基礎演算法

快速排序

void quick_sort(int q[], int l, int r){ if (l >= r) return; int i = l - 1, j = r + 1, x = q[l + r >> 1]; while (i < j) { do i ++ ; while (q[i] < x); do j —— ; while (q[j] > x); if (i < j) swap(q[i], q[j]); } quick_sort(q, l, j), quick_sort(q, j + 1, r);}

歸併排序

void merge_sort(int q[], int l, int r){ if (l >= r) return; int mid = l + r >> 1; merge_sort(q, l, mid); merge_sort(q, mid + 1, r); int k = 0, i = l, j = mid + 1; while (i <= mid && j <= r) if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ]; else tmp[k ++ ] = q[j ++ ]; while (i <= mid) tmp[k ++ ] = q[i ++ ]; while (j <= r) tmp[k ++ ] = q[j ++ ]; for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];}

整數二分

bool check(int x) {/* 。。。 */} // 檢查x是否滿足某種性質// 區間[l, r]被劃分成[l, mid]和[mid + 1, r]時使用:int bsearch_1(int l, int r){ while (l < r) { int mid = l + r >> 1; if (check(mid)) r = mid; // check()判斷mid是否滿足性質 else l = mid + 1; } return l;}// 區間[l, r]被劃分成[l, mid - 1]和[mid, r]時使用:int bsearch_2(int l, int r){ while (l < r) { int mid = l + r + 1 >> 1; if (check(mid)) l = mid; else r = mid - 1; } return l;}

浮點數二分

bool check(double x) {/* 。。。 */} // 檢查x是否滿足某種性質double bsearch_3(double l, double r){ const double eps = 1e-6; // eps 表示精度,取決於題目對精度的要求 while (r - l > eps) { double mid = (l + r) / 2; if (check(mid)) r = mid; else l = mid; } return l;}

高精度加法

// C = A + B, A >= 0, B >= 0vector add(vector &A, vector &B){ if (A。size() < B。size()) return add(B, A); vector C; int t = 0; for (int i = 0; i < A。size(); i ++ ) { t += A[i]; if (i < B。size()) t += B[i]; C。push_back(t % 10); t /= 10; } if (t) C。push_back(t); return C;}

高精度減法

// C = A - B, 滿足A >= B, A >= 0, B >= 0vector sub(vector &A, vector &B){ vector C; for (int i = 0, t = 0; i < A。size(); i ++ ) { t = A[i] - t; if (i < B。size()) t -= B[i]; C。push_back((t + 10) % 10); if (t < 0) t = 1; else t = 0; } while (C。size() > 1 && C。back() == 0) C。pop_back(); return C;}

高精度乘低精度

// C = A * b, A >= 0, b >= 0vector mul(vector &A, int b){ vector C; int t = 0; for (int i = 0; i < A。size() || t; i ++ ) { if (i < A。size()) t += A[i] * b; C。push_back(t % 10); t /= 10; } while (C。size() > 1 && C。back() == 0) C。pop_back(); return C;}

高精度除以低精度

// A / b = C 。。。 r, A >= 0, b > 0vector div(vector &A, int b, int &r){ vector C; r = 0; for (int i = A。size() - 1; i >= 0; i —— ) { r = r * 10 + A[i]; C。push_back(r / b); r %= b; } reverse(C。begin(), C。end()); while (C。size() > 1 && C。back() == 0) C。pop_back(); return C;}

一維字首和

S[i] = a[1] + a[2] + 。。。 a[i]a[l] + 。。。 + a[r] = S[r] - S[l - 1]

二維字首和

S[i, j] = 第i行j列格子左上部分所有元素的和以(x1, y1)為左上角,(x2, y2)為右下角的子矩陣的和為:S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

一維差分

給以(x1, y1)為左上角,(x2, y2)為右下角的子矩陣中的所有元素加上c:S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c

位運算

求n的第k位數字: n >> k & 1返回n的最後一位1:lowbit(n) = n & -n

雙指標演算法

for (int i = 0, j = 0; i < n; i ++ ){ while (j < i && check(i, j)) j ++ ; // 具體問題的邏輯}常見問題分類: (1) 對於一個序列,用兩個指標維護一段區間 (2) 對於兩個序列,維護某種次序,比如歸併排序中合併兩個有序序列的操作

離散化

vector alls; // 儲存所有待離散化的值sort(alls。begin(), alls。end()); // 將所有值排序alls。erase(unique(alls。begin(), alls。end()), alls。end()); // 去掉重複元素// 二分求出x對應的離散化的值int find(int x) // 找到第一個大於等於x的位置{ int l = 0, r = alls。size() - 1; while (l < r) { int mid = l + r >> 1; if (alls[mid] >= x) r = mid; else l = mid + 1; } return r + 1; // 對映到1, 2, 。。。n}

區間合併

// 將所有存在交集的區間合併void merge(vector &segs){ vector res; sort(segs。begin(), segs。end()); int st = -2e9, ed = -2e9; for (auto seg : segs) if (ed < seg。first) { if (st != -2e9) res。push_back({st, ed}); st = seg。first, ed = seg。second; } else ed = max(ed, seg。second); if (st != -2e9) res。push_back({st, ed}); segs = res;}

3。2 常見資料結構

單鏈表

// head儲存連結串列頭,e[]儲存節點的值,ne[]儲存節點的next指標,idx表示當前用到了哪個節點int head, e[N], ne[N], idx;// 初始化void init(){ head = -1; idx = 0;}// 在連結串列頭插入一個數avoid insert(int a){ e[idx] = a, ne[idx] = head, head = idx ++ ;}// 將頭結點刪除,需要保證頭結點存在void remove(){ head = ne[head];}

雙鏈表

// e[]表示節點的值,l[]表示節點的左指標,r[]表示節點的右指標,idx表示當前用到了哪個節點int e[N], l[N], r[N], idx;// 初始化void init(){ //0是左端點,1是右端點 r[0] = 1, l[1] = 0; idx = 2;}// 在節點a的右邊插入一個數xvoid insert(int a, int x){ e[idx] = x; l[idx] = a, r[idx] = r[a]; l[r[a]] = idx, r[a] = idx ++ ;}// 刪除節點avoid remove(int a){ l[r[a]] = l[a]; r[l[a]] = r[a];}

// tt表示棧頂int stk[N], tt = 0;// 向棧頂插入一個數stk[ ++ tt] = x;// 從棧頂彈出一個數tt —— ;// 棧頂的值stk[tt];// 判斷棧是否為空if (tt > 0){}

佇列

普通佇列

// hh 表示隊頭,tt表示隊尾int q[N], hh = 0, tt = -1;// 向隊尾插入一個數q[ ++ tt] = x;// 從隊頭彈出一個數hh ++ ;// 隊頭的值q[hh];// 判斷佇列是否為空if (hh <= tt){}

迴圈佇列

// hh 表示隊頭,tt表示隊尾的後一個位置int q[N], hh = 0, tt = 0;// 向隊尾插入一個數q[tt ++ ] = x;if (tt == N) tt = 0;// 從隊頭彈出一個數hh ++ ;if (hh == N) hh = 0;// 隊頭的值q[hh];// 判斷佇列是否為空if (hh != tt){}

單調棧

常見模型:找出每個數左邊離它最近的比它大/小的數int tt = 0;for (int i = 1; i <= n; i ++ ){ while (tt && check(stk[tt], i)) tt —— ; stk[ ++ tt] = i;}

單調佇列

常見模型:找出滑動視窗中的最大值/最小值int hh = 0, tt = -1;for (int i = 0; i < n; i ++ ){ while (hh <= tt && check_out(q[hh])) hh ++ ; // 判斷隊頭是否滑出視窗 while (hh <= tt && check(q[tt], i)) tt —— ; q[ ++ tt] = i;}

KMP

// s[]是長文字,p[]是模式串,n是s的長度,m是p的長度求模式串的Next陣列:for (int i = 2, j = 0; i <= m; i ++ ){ while (j && p[i] != p[j + 1]) j = ne[j]; if (p[i] == p[j + 1]) j ++ ; ne[i] = j;}// 匹配for (int i = 1, j = 0; i <= n; i ++ ){ while (j && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++ ; if (j == m) { j = ne[j]; // 匹配成功後的邏輯 }}

Trie樹

int son[N][26], cnt[N], idx;// 0號點既是根節點,又是空節點// son[][]儲存樹中每個節點的子節點// cnt[]儲存以每個節點結尾的單詞數量// 插入一個字串void insert(char *str){ int p = 0; for (int i = 0; str[i]; i ++ ) { int u = str[i] - ‘a’; if (!son[p][u]) son[p][u] = ++ idx; p = son[p][u]; } cnt[p] ++ ;}// 查詢字串出現的次數int query(char *str){ int p = 0; for (int i = 0; str[i]; i ++ ) { int u = str[i] - ‘a’; if (!son[p][u]) return 0; p = son[p][u]; } return cnt[p];}

並查集

(1)樸素並查集: int p[N]; //儲存每個點的祖宗節點 // 返回x的祖宗節點 int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } // 初始化,假定節點編號是1~n for (int i = 1; i <= n; i ++ ) p[i] = i; // 合併a和b所在的兩個集合: p[find(a)] = find(b);(2)維護size的並查集: int p[N], size[N]; //p[]儲存每個點的祖宗節點, size[]只有祖宗節點的有意義,表示祖宗節點所在集合中的點的數量 // 返回x的祖宗節點 int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } // 初始化,假定節點編號是1~n for (int i = 1; i <= n; i ++ ) { p[i] = i; size[i] = 1; } // 合併a和b所在的兩個集合: size[find(b)] += size[find(a)]; p[find(a)] = find(b);(3)維護到祖宗節點距離的並查集: int p[N], d[N]; //p[]儲存每個點的祖宗節點, d[x]儲存x到p[x]的距離 // 返回x的祖宗節點 int find(int x) { if (p[x] != x) { int u = find(p[x]); d[x] += d[p[x]]; p[x] = u; } return p[x]; } // 初始化,假定節點編號是1~n for (int i = 1; i <= n; i ++ ) { p[i] = i; d[i] = 0; } // 合併a和b所在的兩個集合: p[find(a)] = find(b); d[find(a)] = distance; // 根據具體問題,初始化find(a)的偏移量

// h[N]儲存堆中的值, h[1]是堆頂,x的左兒子是2x, 右兒子是2x + 1// ph[k]儲存第k個插入的點在堆中的位置// hp[k]儲存堆中下標是k的點是第幾個插入的int h[N], ph[N], hp[N], size;// 交換兩個點,及其對映關係void heap_swap(int a, int b){ swap(ph[hp[a]],ph[hp[b]]); swap(hp[a], hp[b]); swap(h[a], h[b]);}void down(int u){ int t = u; if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2; if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1; if (u != t) { heap_swap(u, t); down(t); }}void up(int u){ while (u / 2 && h[u] < h[u / 2]) { heap_swap(u, u / 2); u >>= 1; }}// O(n)建堆for (int i = n / 2; i; i —— ) down(i);

一般雜湊

(1) 拉鍊法 int h[N], e[N], ne[N], idx; // 向雜湊表中插入一個數 void insert(int x) { int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx ++ ; } // 在雜湊表中查詢某個數是否存在 bool find(int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1; i = ne[i]) if (e[i] == x) return true; return false; }(2) 開放定址法 int h[N]; // 如果x在雜湊表中,返回x的下標;如果x不在雜湊表中,返回x應該插入的位置 int find(int x) { int t = (x % N + N) % N; while (h[t] != null && h[t] != x) { t ++ ; if (t == N) t = 0; } return t; }

字串雜湊

核心思想:將字串看成P進位制數,P的經驗值是131或13331,取這兩個值的衝突機率低小技巧:取模的數用2^64,這樣直接用unsigned long long儲存,溢位的結果就是取模的結果typedef unsigned long long ULL;ULL h[N], p[N]; // h[k]儲存字串前k個字母的雜湊值, p[k]儲存 P^k mod 2^64// 初始化p[0] = 1;for (int i = 1; i <= n; i ++ ){ h[i] = h[i - 1] * P + str[i]; p[i] = p[i - 1] * P;}// 計算子串 str[l ~ r] 的雜湊值ULL get(int l, int r){ return h[r] - h[l - 1] * p[r - l + 1];}

STL簡介

vector, 變長陣列,倍增的思想 size() 返回元素個數 empty() 返回是否為空 clear() 清空 front()/back() push_back()/pop_back() begin()/end() [] 支援比較運算,按字典序pair first, 第一個元素 second, 第二個元素 支援比較運算,以first為第一關鍵字,以second為第二關鍵字(字典序)string,字串 size()/length() 返回字串長度 empty() clear() substr(起始下標,(子串長度)) 返回子串 c_str() 返回字串所在字元陣列的起始地址queue, 佇列 size() empty() push() 向隊尾插入一個元素 front() 返回隊頭元素 back() 返回隊尾元素 pop() 彈出隊頭元素priority_queue, 優先佇列,預設是大根堆 size() empty() push() 插入一個元素 top() 返回堆頂元素 pop() 彈出堆頂元素 定義成小根堆的方式:priority_queue, greater> q;stack, 棧 size() empty() push() 向棧頂插入一個元素 top() 返回棧頂元素 pop() 彈出棧頂元素deque, 雙端佇列 size() empty() clear() front()/back() push_back()/pop_back() push_front()/pop_front() begin()/end() []set, map, multiset, multimap, 基於平衡二叉樹(紅黑樹),動態維護有序序列 size() empty() clear() begin()/end() ++, —— 返回前驅和後繼,時間複雜度 O(logn) set/multiset insert() 插入一個數 find() 查詢一個數 count() 返回某一個數的個數 erase() (1) 輸入是一個數x,刪除所有x O(k + logn) (2) 輸入一個迭代器,刪除這個迭代器 lower_bound()/upper_bound() lower_bound(x) 返回大於等於x的最小的數的迭代器 upper_bound(x) 返回大於x的最小的數的迭代器 map/multimap insert() 插入的數是一個pair erase() 輸入的引數是pair或者迭代器 find() [] 注意multimap不支援此操作。 時間複雜度是 O(logn) lower_bound()/upper_bound()unordered_set, unordered_map, unordered_multiset, unordered_multimap, 雜湊表 和上面類似,增刪改查的時間複雜度是 O(1) 不支援 lower_bound()/upper_bound(), 迭代器的++,——bitset, 圧位 bitset<10000> s; ~, &, |, ^ >>, << ==, != [] count() 返回有多少個1 any() 判斷是否至少有一個1 none() 判斷是否全為0 set() 把所有位置成1 set(k, v) 將第k位變成v reset() 把所有位變成0 flip() 等價於~ flip(k) 把第k位取反

3。3 搜尋與圖論

樹與圖的儲存

樹是一種特殊的圖,與圖的儲存方式相同。

對於無向圖中的邊ab,儲存兩條有向邊

a->b, b->a

因此我們可以只考慮有向圖的儲存。

(1) 鄰接矩陣:

g[a][b]

儲存邊

a->b

(2) 鄰接表:

// 對於每個點k,開一個單鏈表,儲存k所有可以走到的點。h[k]儲存這個單鏈表的頭結點int h[N], e[N], ne[N], idx;// 新增一條邊a->bvoid add(int a, int b){ e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;}// 初始化idx = 0;memset(h, -1, sizeof h);

樹與圖的遍歷

時間複雜度

O(n+m)O(n+m)

n

表示點數,

m

表示邊數

(1) 深度優先遍歷

int dfs(int u){ st[u] = true; // st[u] 表示點u已經被遍歷過 for (int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) dfs(j); }}

(2) 寬度優先遍歷

queue q;st[1] = true; // 表示1號點已經被遍歷過q。push(1);while (q。size()){ int t = q。front(); q。pop(); for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) { st[j] = true; // 表示點j已經被遍歷過 q。push(j); } }}

拓撲排序

時間複雜度

O(n+m)O(n+m)

n

表示點數,

m

表示邊數

bool topsort(){ int hh = 0, tt = -1; // d[i] 儲存點i的入度 for (int i = 1; i <= n; i ++ ) if (!d[i]) q[ ++ tt] = i; while (hh <= tt) { int t = q[hh ++ ]; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (—— d[j] == 0) q[ ++ tt] = j; } } // 如果所有點都入隊了,說明存在拓撲序列;否則不存在拓撲序列。 return tt == n - 1;}

樸素dijkstra演算法

時間複雜度

O(n^2+m)

n

表示點數,

m

表示邊數

int g[N][N]; // 儲存每條邊int dist[N]; // 儲存1號點到每個點的最短距離bool st[N]; // 儲存每個點的最短路是否已經確定// 求1號點到n號點的最短路,如果不存在則返回-1int dijkstra(){ memset(dist, 0x3f, sizeof dist); dist[1] = 0; for (int i = 0; i < n - 1; i ++ ) { int t = -1; // 在還未確定最短路的點中,尋找距離最小的點 for (int j = 1; j <= n; j ++ ) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; // 用t更新其他點的距離 for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], dist[t] + g[t][j]); st[t] = true; } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n];}

堆最佳化版dijkstra

時間複雜度

O(m\log n)

n

表示點數,

m

表示邊數

typedef pair PII;int n; // 點的數量int h[N], w[N], e[N], ne[N], idx; // 鄰接表儲存所有邊int dist[N]; // 儲存所有點到1號點的距離bool st[N]; // 儲存每個點的最短距離是否已確定// 求1號點到n號點的最短距離,如果不存在,則返回-1int dijkstra(){ memset(dist, 0x3f, sizeof dist); dist[1] = 0; priority_queue, greater> heap; heap。push({0, 1}); // first儲存距離,second儲存節點編號 while (heap。size()) { auto t = heap。top(); heap。pop(); int ver = t。second, distance = t。first; if (st[ver]) continue; st[ver] = true; for (int i = h[ver]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > distance + w[i]) { dist[j] = distance + w[i]; heap。push({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n];}

Bellman-Ford演算法

時間複雜度

O(mn)

n

表示點數,

m

表示邊數

int n, m; // n表示點數,m表示邊數int dist[N]; // dist[x]儲存1到x的最短路距離struct Edge // 邊,a表示出點,b表示入點,w表示邊的權重{ int a, b, w;}edges[M];// 求1到n的最短路距離,如果無法從1走到n,則返回-1。int bellman_ford(){ memset(dist, 0x3f, sizeof dist); dist[1] = 0; // 如果第n次迭代仍然會鬆弛三角不等式,就說明存在一條長度是n+1的最短路徑,由抽屜原理,路徑中至少存在兩個相同的點,說明圖中存在負權迴路。 for (int i = 0; i < n; i ++ ) { for (int j = 0; j < m; j ++ ) { int a = edges[j]。a, b = edges[j]。b, w = edges[j]。w; if (dist[b] > dist[a] + w) dist[b] = dist[a] + w; } } if (dist[n] > 0x3f3f3f3f / 2) return -1; return dist[n];}

spfa 演算法(佇列最佳化的Bellman-Ford演算法)

時間複雜度 平均情況下

O(m)O(m)

,最壞情況下

O(nm)

n

表示點數,

m

表示邊數

int n; // 總點數int h[N], w[N], e[N], ne[N], idx; // 鄰接表儲存所有邊int dist[N]; // 儲存每個點到1號點的最短距離bool st[N]; // 儲存每個點是否在佇列中// 求1號點到n號點的最短路距離,如果從1號點無法走到n號點則返回-1int spfa(){ memset(dist, 0x3f, sizeof dist); dist[1] = 0; queue q; q。push(1); st[1] = true; while (q。size()) { auto t = q。front(); q。pop(); st[t] = false; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; if (!st[j]) // 如果佇列中已存在j,則不需要將j重複插入 { q。push(j); st[j] = true; } } } } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n];}

spfa判斷圖中是否存在負環

時間複雜度

O(mn)

n

表示點數,

m

表示邊數

int n; // 總點數int h[N], w[N], e[N], ne[N], idx; // 鄰接表儲存所有邊int dist[N], cnt[N]; // dist[x]儲存1號點到x的最短距離,cnt[x]儲存1到x的最短路中經過的點數bool st[N]; // 儲存每個點是否在佇列中// 如果存在負環,則返回true,否則返回false。bool spfa(){ // 不需要初始化dist陣列 // 原理:如果某條最短路徑上有n個點(除了自己),那麼加上自己之後一共有n+1個點,由抽屜原理一定有兩個點相同,所以存在環。 queue q; for (int i = 1; i <= n; i ++ ) { q。push(i); st[i] = true; } while (q。size()) { auto t = q。front(); q。pop(); st[t] = false; for (int i = h[t]; i != -1; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; cnt[j] = cnt[t] + 1; if (cnt[j] >= n) return true; // 如果從1號點到x的最短路中包含至少n個點(不包括自己),則說明存在環 if (!st[j]) { q。push(j); st[j] = true; } } } } return false;}

floyd演算法

時間複雜度是

O(n^3)

n

表示點數

初始化: for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) if (i == j) d[i][j] = 0; else d[i][j] = INF;// 演算法結束後,d[a][b]表示a到b的最短距離void floyd(){ for (int k = 1; k <= n; k ++ ) for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);}

樸素版prim演算法

時間複雜度是

O(n2+m)

n

表示點數,

m

表示邊數

int n; // n表示點數int g[N][N]; // 鄰接矩陣,儲存所有邊int dist[N]; // 儲存其他點到當前最小生成樹的距離bool st[N]; // 儲存每個點是否已經在生成樹中// 如果圖不連通,則返回INF(值是0x3f3f3f3f), 否則返回最小生成樹的樹邊權重之和int prim(){ memset(dist, 0x3f, sizeof dist); int res = 0; for (int i = 0; i < n; i ++ ) { int t = -1; for (int j = 1; j <= n; j ++ ) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; if (i && dist[t] == INF) return INF; if (i) res += dist[t]; st[t] = true; for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]); } return res;}

Kruskal演算法

時間複雜度是

O(m\log m)

n

表示點數,

m

表示邊數

int n, m; // n是點數,m是邊數int p[N]; // 並查集的父節點陣列struct Edge // 儲存邊{ int a, b, w; bool operator< (const Edge &W)const { return w < W。w; }}edges[M];int find(int x) // 並查集核心操作{ if (p[x] != x) p[x] = find(p[x]); return p[x];}int kruskal(){ sort(edges, edges + m); for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化並查集 int res = 0, cnt = 0; for (int i = 0; i < m; i ++ ) { int a = edges[i]。a, b = edges[i]。b, w = edges[i]。w; a = find(a), b = find(b); if (a != b) // 如果兩個連通塊不連通,則將這兩個連通塊合併 { p[a] = b; res += w; cnt ++ ; } } if (cnt < n - 1) return INF; return res;}

染色法判別二分圖

時間複雜度是

O(n+m)

n

表示點數,

m

表示邊數

int n; // n表示點數int h[N], e[M], ne[M], idx; // 鄰接表儲存圖int color[N]; // 表示每個點的顏色,-1表示未染色,0表示白色,1表示黑色// 引數:u表示當前節點,c表示當前點的顏色bool dfs(int u, int c){ color[u] = c; for (int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if (color[j] == -1) { if (!dfs(j, !c)) return false; } else if (color[j] == c) return false; } return true;}bool check(){ memset(color, -1, sizeof color); bool flag = true; for (int i = 1; i <= n; i ++ ) if (color[i] == -1) if (!dfs(i, 0)) { flag = false; break; } return flag;}

匈牙利演算法

時間複雜度是

O(nm)

n

表示點數,

m

表示邊數

int n1, n2; // n1表示第一個集合中的點數,n2表示第二個集合中的點數int h[N], e[M], ne[M], idx; // 鄰接表儲存所有邊,匈牙利演算法中只會用到從第一個集合指向第二個集合的邊,所以這裡只用存一個方向的邊int match[N]; // 儲存第二個集合中的每個點當前匹配的第一個集合中的點是哪個bool st[N]; // 表示第二個集合中的每個點是否已經被遍歷過bool find(int x){ for (int i = h[x]; i != -1; i = ne[i]) { int j = e[i]; if (!st[j]) { st[j] = true; if (match[j] == 0 || find(match[j])) { match[j] = x; return true; } } } return false;}// 求最大匹配數,依次列舉第一個集合中的每個點能否匹配第二個集合中的點int res = 0;for (int i = 1; i <= n1; i ++ ){ memset(st, false, sizeof st); if (find(i)) res ++ ;}

3。4 數學知識

試除法判定質數

bool is_prime(int x){ if (x < 2) return false; for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) return false; return true;}

試除法分解質因數

void divide(int x){ for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) { int s = 0; while (x % i == 0) x /= i, s ++ ; cout << i << ‘ ’ << s << endl; } if (x > 1) cout << x << ‘ ’ << 1 << endl; cout << endl;}

樸素篩法求素數

int primes[N], cnt; // primes[]儲存所有素數bool st[N]; // st[x]儲存x是否被篩掉void get_primes(int n){ for (int i = 2; i <= n; i ++ ) { if (st[i]) continue; primes[cnt ++ ] = i; for (int j = i + i; j <= n; j += i) st[j] = true; }}

線性篩法求素數

int primes[N], cnt; // primes[]儲存所有素數bool st[N]; // st[x]儲存x是否被篩掉void get_primes(int n){ for (int i = 2; i <= n; i ++ ) { if (!st[i]) primes[cnt ++ ] = i; for (int j = 0; primes[j] <= n / i; j ++ ) { st[primes[j] * i] = true; if (i % primes[j] == 0) break; } }}

試除法求所有約數

vector get_divisors(int x){ vector res; for (int i = 1; i <= x / i; i ++ ) if (x % i == 0) { res。push_back(i); if (i != x / i) res。push_back(x / i); } sort(res。begin(), res。end()); return res;}

約數個數和約數之和

如果 N = p1^c1 * p2^c2 * 。。。 *pk^ck約數個數: (c1 + 1) * (c2 + 1) * 。。。 * (ck + 1)約數之和: (p1^0 + p1^1 + 。。。 + p1^c1) * 。。。 * (pk^0 + pk^1 + 。。。 + pk^ck)

歐幾里得演算法

int gcd(int a, int b){ return b ? gcd(b, a % b) : a;}

求尤拉函式

int phi(int x){ int res = x; for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) { res = res / i * (i - 1); while (x % i == 0) x /= i; } if (x > 1) res = res / x * (x - 1); return res;}

篩法求尤拉函式

int primes[N], cnt; // primes[]儲存所有素數int euler[N]; // 儲存每個數的尤拉函式bool st[N]; // st[x]儲存x是否被篩掉void get_eulers(int n){ euler[1] = 1; for (int i = 2; i <= n; i ++ ) { if (!st[i]) { primes[cnt ++ ] = i; euler[i] = i - 1; } for (int j = 0; primes[j] <= n / i; j ++ ) { int t = primes[j] * i; st[t] = true; if (i % primes[j] == 0) { euler[t] = euler[i] * primes[j]; break; } euler[t] = euler[i] * (primes[j] - 1); } }}

快速冪

求 m^k mod p,時間複雜度 O(logk)。int qmi(int m, int k, int p){ int res = 1 % p, t = m; while (k) { if (k&1) res = res * t % p; t = t * t % p; k >>= 1; } return res;}

擴充套件歐幾里得演算法

// 求x, y,使得ax + by = gcd(a, b)int exgcd(int a, int b, int &x, int &y){ if (!b) { x = 1; y = 0; return a; } int d = exgcd(b, a % b, y, x); y -= (a/b) * x; return d;}

高斯消元

// a[N][N]是增廣矩陣int gauss(){ int c, r; for (c = 0, r = 0; c < n; c ++ ) { int t = r; for (int i = r; i < n; i ++ ) // 找到絕對值最大的行 if (fabs(a[i][c]) > fabs(a[t][c])) t = i; if (fabs(a[t][c]) < eps) continue; for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]); // 將絕對值最大的行換到最頂端 for (int i = n; i >= c; i —— ) a[r][i] /= a[r][c]; // 將當前行的首位變成1 for (int i = r + 1; i < n; i ++ ) // 用當前行將下面所有的列消成0 if (fabs(a[i][c]) > eps) for (int j = n; j >= c; j —— ) a[i][j] -= a[r][j] * a[i][c]; r ++ ; } if (r < n) { for (int i = r; i < n; i ++ ) if (fabs(a[i][n]) > eps) return 2; // 無解 return 1; // 有無窮多組解 } for (int i = n - 1; i >= 0; i —— ) for (int j = i + 1; j < n; j ++ ) a[i][n] -= a[i][j] * a[j][n]; return 0; // 有唯一解}

遞迴法求組合數

// c[a][b] 表示從a個蘋果中選b個的方案數for (int i = 0; i < N; i ++ ) for (int j = 0; j <= i; j ++ ) if (!j) c[i][j] = 1; else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

透過預處理逆元的方式求組合數

首先預處理出所有階乘取模的餘數fact[N],以及所有階乘取模的逆元infact[N]如果取模的數是質數,可以用費馬小定理求逆元int qmi(int a, int k, int p) // 快速冪模板{ int res = 1; while (k) { if (k & 1) res = (LL)res * a % p; a = (LL)a * a % p; k >>= 1; } return res;}// 預處理階乘的餘數和階乘逆元的餘數fact[0] = infact[0] = 1;for (int i = 1; i < N; i ++ ){ fact[i] = (LL)fact[i - 1] * i % mod; infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;}

Lucas定理

若p是質數,則對於任意整數 1 <= m <= n,有: C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)int qmi(int a, int k, int p) // 快速冪模板{ int res = 1 % p; while (k) { if (k & 1) res = (LL)res * a % p; a = (LL)a * a % p; k >>= 1; } return res;}int C(int a, int b, int p) // 透過定理求組合數C(a, b){ if (a < b) return 0; LL x = 1, y = 1; // x是分子,y是分母 for (int i = a, j = 1; j <= b; i ——, j ++ ) { x = (LL)x * i % p; y = (LL) y * j % p; } return x * (LL)qmi(y, p - 2, p) % p;}int lucas(LL a, LL b, int p){ if (a < p && b < p) return C(a, b, p); return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;}

分解質因數法求組合數

當我們需要求出組合數的真實值,而非對某個數的餘數時,分解質因數的方式比較好用: 1。 篩法求出範圍內的所有質數 2。 透過 C(a, b) = a! / b! / (a - b)! 這個公式求出每個質因子的次數。 n! 中p的次數是 n / p + n / p^2 + n / p^3 + 。。。 3。 用高精度乘法將所有質因子相乘int primes[N], cnt; // 儲存所有質數int sum[N]; // 儲存每個質數的次數bool st[N]; // 儲存每個數是否已被篩掉void get_primes(int n) // 線性篩法求素數{ for (int i = 2; i <= n; i ++ ) { if (!st[i]) primes[cnt ++ ] = i; for (int j = 0; primes[j] <= n / i; j ++ ) { st[primes[j] * i] = true; if (i % primes[j] == 0) break; } }}int get(int n, int p) // 求n!中的次數{ int res = 0; while (n) { res += n / p; n /= p; } return res;}vector mul(vector a, int b) // 高精度乘低精度模板{ vector c; int t = 0; for (int i = 0; i < a。size(); i ++ ) { t += a[i] * b; c。push_back(t % 10); t /= 10; } while (t) { c。push_back(t % 10); t /= 10; } return c;}get_primes(a); // 預處理範圍內的所有質數for (int i = 0; i < cnt; i ++ ) // 求每個質因數的次數{ int p = primes[i]; sum[i] = get(a, p) - get(b, p) - get(a - b, p);}vector res;res。push_back(1);for (int i = 0; i < cnt; i ++ ) // 用高精度乘法將所有質因子相乘 for (int j = 0; j < sum[i]; j ++ ) res = mul(res, primes[i]);

卡特蘭數

給定n個0和n個1,它們按照某種順序排成長度為2n的序列,滿足任意字首中0的個數都不少於1的個數的序列的數量為: Cat(n) = C(2n, n) / (n + 1)

NIM遊戲

給定N堆物品,第i堆物品有Ai個。兩名玩家輪流行動,每次可以任選一堆,取走任意多個物品,可把一堆取光,但不能不取。取走最後一件物品者獲勝。兩人都採取最優策略,問先手是否必勝。

我們把這種遊戲稱為NIM博弈。把遊戲過程中面臨的狀態稱為局面。整局遊戲第一個行動的稱為先手,第二個行動的稱為後手。若在某一局面下無論採取何種行動,都會輸掉遊戲,則稱該局面必敗。

所謂採取最優策略是指,若在某一局面下存在某種行動,使得行動後對面面臨必敗局面,則優先採取該行動。同時,這樣的局面被稱為必勝。我們討論的博弈問題一般都只考慮理想情況,即兩人均無失誤,都採取最優策略行動時遊戲的結果。

NIM博弈不存在平局,只有先手必勝和先手必敗兩種情況。

定理: NIM博弈先手必勝,當且僅當 A1 ^ A2 ^ … ^ An != 0

公平組合遊戲ICG

若一個遊戲滿足:

由兩名玩家交替行動;

在遊戲程序的任意時刻,可以執行的合法行動與輪到哪名玩家無關;

不能行動的玩家判負;

則稱該遊戲為一個公平組合遊戲。

NIM博弈屬於公平組合遊戲,但城建的棋類遊戲,比如圍棋,就不是公平組合遊戲。因為圍棋交戰雙方分別只能落黑子和白子,勝負判定也比較複雜,不滿足條件2和條件3。

有向圖遊戲

給定一個有向無環圖,圖中有一個唯一的起點,在起點上放有一枚棋子。兩名玩家交替地把這枚棋子沿有向邊進行移動,每次可以移動一步,無法移動者判負。該遊戲被稱為有向圖遊戲。

任何一個公平組合遊戲都可以轉化為有向圖遊戲。具體方法是,把每個局面看成圖中的一個節點,並且從每個局面向沿著合法行動能夠到達的下一個局面連有向邊。

Mex運算

S

表示一個非負整數集合。定義

mex(S)

為求出不屬於集合

S

的最小非負整數的運算,即:

mex(S) = min{x}

x

屬於自然數,且

x

不屬於

S

SG函式

在有向圖遊戲中,對於每個節點

x

,設從

x

出發共有

k

條有向邊,分別到達節點

y_1, y_2, …, y_k

,定義

SG(x)

x

的後繼節點

y_1, y_2, …, y_k

SG

函式值構成的集合再執行

mex(S)

運算的結果,即:

SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})

特別地,整個有向圖遊戲

G

SG

函式值被定義為有向圖遊戲起點

s

SG

函式值,即

SG(G) = SG(s)

有向圖遊戲的和

G_1, G_2, …, G_m

m

個有向圖遊戲。定義有向圖遊戲

G

,它的行動規則是任選某個有向圖遊戲

G_i

,並在

G_i

上行動一步。

G

被稱為有向圖遊戲

G_1, G_2, …, G_m

的和。

有向圖遊戲的和的

SG

函式值等於它包含的各個子游戲

SG

函式值的異或和,即:

SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)

定理

有向圖遊戲的某個局面必勝,當且僅當該局面對應節點的

SG

函式值大於0。

有向圖遊戲的某個局面必敗,當且僅當該局面對應節點的

SG

函式值等於0。

4 算題說明

在進行高階演算法題前,至少得掌握這幾種基本的演算法思想:

模擬、列舉、遞迴、分治、二分查詢

,要了解

動態規劃、貪心、回溯、深度優先搜尋、寬度優先搜尋

的大致流程。

基礎部分的書籍推薦《

演算法基礎與線上實踐

》、進階部分的書籍推薦《

演算法競賽進階指南

》。

第二部分來自我結合網上資料的總結,第三部分的演算法模板來自北大的某位大神,當我們逐漸刷題量變多時,可以整理出自己的模板,從而能夠更加自如地應對大部分題。