前言
離NOIP還有一個星期,匆忙的把寒假整理的算法補充完善,看着當時的整理覺得那時還年少。第二頁貼了幾張從貼吧里找來的圖片,看着就很熱血的。旁邊的同學都勸我不要再放PASCAL啊什么的了,畢竟我們的下一級直接學C++。即便我本人對C++也是贊賞有加,不過PASCAL作為夢的開始終究不能忘記。不像機房中其余的OIERS,我以后並不想學計算機類的專業。當年來學這個競賽就是為了興趣,感受計算機之美的。經過時遷,計划趕不上變化,現在尚處於迷茫之中,也很難說當時做的決定是對是錯。然而我一直堅信迷茫的時候選擇難走的路會看見更好的風景。
這篇文章簡單的說了一下NOIP考試中會常用的算法,可能難度掌握的不是太好,有一部分內容不是NOIP考查范圍,然而隨着難度的增加,看一些更高級的算法也沒有壞處。還有一些非常非常基礎的比如鏈表啊什么的就直接沒有寫上(別問我為什么整理了那么多的排序算法)。
最后祝大家在NOIP中取得理想的成績!
搜索
DFS
框架
procedure dfs(x); var begin if 達到目標狀態 then 輸出結果並退出過程; if 滿足剪枝條件 then exit; for i:=1 to 搜索寬度 do begin 備份現場;(注意如果現場使用了全局變量,則需要使用局部變量備份) dfs(參數+增量); 恢復現場; end;
優化
(1) 最優化剪枝:求最優值時,當前的狀態無論如何不可能比最優值更優,則退出,可與展望結合剪枝
(2) 可行性剪枝:提前判斷該狀態是否能得到可行解,如不能則退出
(3) 記憶化搜索:對於已經搜索過的狀態直接退出
(4) 改變搜索順序:對於看起來希望更大的決策先進行搜索
(5) 優化搜索策略
(6) 預處理找到大體搜索翻譯
(7) 改寫成IDA*算法
(8) 卡時(注意現在聯賽中禁止使用meml掐時)
BFS
框架
初始化;把初始布局存入 設首指針head=0; 尾指針tail:=1; repeat inc(head),取出隊列首記錄為當前被擴展結點; for i:=1 to 規則數 do {r是規則編號} begin if 新空格位置合法 then begin if 新布局與隊列中原有記錄不重復 tail增1,並把新布局存入隊尾; if 達到目標 then 輸出並退出; end; end; until head>=tail; {隊列空}
優化
判重的優化:hash,二叉排序樹
雙向廣搜或啟發式搜索
改寫成A*算法
二分優化
排序
冒泡排序
var a:array[1..100] of longint;t,n,i,j:longint; procedure sort; begin for i:=1 to n-1 do{與每個數都進行比較} for j:=1 to n-i do if a[j]>a[j+1] then begin t:=a[j]; a[j]:=a[j+1]; a[j+1]:=t; end; end;
選擇排序
var a:array[1..100] of longint;t,n,i,j:longint; procedure sort; begin for i:=1 to n-1 do for j:=1+i to n do{大數沉小數浮} if a[j]>a[i] then begin t:=a[j]; a[j]:=a[i]; a[i]:=t; end; end;
插入排序
var a:array[0..100] of longint;n,i,j,t:longint; procedure sort; begin for i:=2 to n do for j:=1 to (i-1) do begin if (a[i]<a[j]) then begin t:=a[j]; a[j]:=a[i]; a[i]:=t; end; end; end;
桶排序
var a,b:array[0..100] of longint;r,i,j,t,k,n:longint; procedure sort; begin for i:=0 to 100 do b[i]:=0;{為B數組清零,小桶內容清零} for i:=1 to n do b[a[i]]:=b[a[i]]+1; {桶的序號就是那個要排序的東西;出現一次,桶里得旗數加一} for i:=0 to 100 do{掃描所有的桶} begin if b[i]<>0 then{桶里有旗} for j:=1 to b[i] do write(i,' ');{桶的序號就是那個數} end; end;
快速排序
var a:array[1..100] of longint; n,i,h,g:longint; procedure kp(l,r:longint);{變量不能與全局變量相同,否則會被抹去} var b,m,i,j,t:longint; begin i:=l; j:=r; m:=a[(l+r) div 2];{基准數最好從中間取} repeat while a[j]>m do dec(j); while a[i]<m do inc(i);{兩側的哨兵移動} if i<=j then {哨兵未碰面}{“=”利用repeat循環的性質,使repeat循環得以結束} begin t:=a[j]; a[j]:=a[i a[i]:=t;{交換兩個哨兵的值} inc(j); dec(j);{哨兵繼續運動} end; until i>j; if j>l then kp(l,j); if i<r then kp(i,r);{都是循環不結束后進行的動作} end; begin read(n); for i:=1 to n do read(a[i]); kp(1,n); {“一”位置與“N”位置} for i:=1 to n-1 do write(a[i],' '); write(a[n]);{防止多輸出空格使程序結果出錯} end.
堆排序
var a:array[1..100] of longint; n,i,b:longint; procedure jianshu(i:longint); begin while ((a[i]>a[i*2])or(a[i]>a[i*2+1]))and(i<=n div 2) do {當父親數大於子女數時並且他有孩子時進行} begin if a[i*2]<=a[i*2+1]{左兒子小於右兒子} then begin b:=a[i*2]; a[i*2]:=a[i];a[i]:=b;{左右兒子的值互換} jianshu(i*2);{繼續為左兒子建樹} end else begin b:=a[i*2+1];a[i*2+1]:=a[i];a[i]:=b; jianshu(i*2+1);{上同,不過是為右兒子建樹} end; end; end; procedure tiao; begin while n<>0 do begin write(a[1]); a[1]:=a[n]; n:=n-1; for i:=(n div 2) downto 1 do jianshu(i); end; end; begin read(n); for i:=1 to n do read(a[i]); for i:=(n div 2) downto 1 do jianshu(i); tiao; end.
數學定理
中國剩余定理
若有一些兩兩互質的整數m1, m2,… mn,則對任意的整數:a1,a2,...an,以下聯立同余方程組對模數m1, m2,… mn 有公解:
康托展開
a[i]為當前未出現的元素中是排在第幾個(從0開始)
把一個整數X展開成如下形式:
X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[2]*1!+a[1]*0!
其中a[i]為當前未出現的元素中是排在第幾個(從0開始),並且0<=a[i]<i(1<=i<=n)
錯排通項
考慮一個有n個元素的排列,若一個排列中所有的元素都不在自己原來的位置上,那么這樣的排列就稱為原排列的一個錯排。
f[1]=0;f[2]=1;
f[n] =(n-1)(f[n-2) + f[n-1])
f[n]:=n![1-1/1!+1/2!-1/3!……+(-1)^n*1/n!]
f[n] = (n!/e+0.5),其中e是自然對數的底,[x]為x的整數部分。
費馬大定理
- 費馬大定理,又被稱為“費馬最后的定理”,由法國數學家費馬提出。它斷言當整數n >2時,關於x, y, z的方程 xn + yn = zn 沒有正整數解。
- 被提出后,經歷多人猜想辯證,歷經三百多年的歷史,最終在1995年被英國數學家安德魯·懷爾斯證明。
費馬小定理
假如a是一個整數,p是一個素數,那么 ap ≡a (mod p)。
如果a不是p的倍數,這個定理也可以寫成ap-1 ≡1 (mod p)。----這個更加常用
逆元
由費馬小定理:假如p是質數,且gcd(a,p)=1,那么ap-1≡1(mod p)
逆元:如果ab≡1(mod p),那么在模p意義下,a、b互為逆元。
所以,假如p是質數,且gcd(a,p)=1,那么a的逆元是ap-2
逆元的作用:在模意義下進行除法。乘a的逆元等同於除以a。
歐拉函數
在數論中,對正整數n,歐拉函數是小於或等於n的正整數中與n互質的數的數目。此函數以其首名研究者歐拉命名,它又稱為φ函數、歐拉商數等。
若m,a為正整數,且m,a互素,(gcd(a,m) = 1),則aφ(m)≡1,其中為φ(m)歐拉函數,mod m為同余關系。
歐拉定理實際上是費馬小定理的推廣。
Stirling數
第一類s(p,k)的一個的組合學解釋是:將p個物體排成k個非空循環排列的方法數。
s(p,k)的遞推公式: s(p,k)=(p-1)*s(p-1,k)+s(p-1,k-1) ,1<=k<=p-1
邊界條件:s(p,0)=0 ,p>=1 s(p,p)=1 ,p>=0
遞推關系的說明:
考慮第p個物品,p可以單獨構成一個非空循環排列,這樣前p-1種物品構成k-1個非空循環排列,方法數為s(p-1,k-1);
也可以前p-1種物品構成k個非空循環排列,而第p個物品插入第i個物品的左邊,這有(p-1)*s(p-1,k)種方法。
第二類S(p,k)的一個組合學解釋是:將p個物體划分成k個非空的不可辨別的(可以理解為盒子沒有編號)集合的方法數。
k!S(p,k)是把p個人分進k間有差別(如:被標有房號)的房間(無空房)的方法數。
S(p,k)的遞推公式是:S(p,k)=k*S(p-1,k)+S(p-1,k-1) ,1<= k<=p-1
邊界條件:S(p,p)=1 ,p>=0 S(p,0)=0 ,p>=1
遞推關系的說明:
考慮第p個物品,p可以單獨構成一個非空集合,此時前p-1個物品構成k-1個非空的不可辨別的集合,方法數為S(p-1,k-1);
也可以前p-1種物品構成k個非空的不可辨別的集合,第p個物品放入任意一個中,這樣有k*S(p-1,k)種方法。
PS:第一類斯特林數和第二類斯特林數有相同的初始條件,但遞推關系不同。
Stirling's approximation
快速求階乘,不推薦用於競賽。
數論
GCD&LCM
//GCD function gcd(a,b:longint):longint; begin if b=0 then gcd:=a else gcd:=gcd (b,a mod b); end ; //LCM function lcm(a,b:longint):longint; begin if a<b then swap(a,b); lcm:=a; while lcm mod b>0 do inc(lcm,a); end;
素數
//單個判斷 function prime (n: longint): boolean; var i longint; begin for i:=2 to trunc(sqrt(n)) do if n mod i=0 then exit(false) exit(true); end;
//篩法打表 procedure main; var i,j:longint; begin fillchar(f,sizeof(f),true); f[0]:=false;f[1]:=false; for i:=2 to trunc(sqrt(maxn)) do if f[i] then begin j:=2*i; while j<= maxn do begin f[j]:=false; inc(j,i); end; end; end;
快速冪
{a^b mod n} function f(a,b,n:int64):int64; var t,y:int64; begin t:=1; y:=a; while b<>0 do begin if(b and 1)=1 then t:=t*y mod n; y:=y*y mod n; {這里用了一個很強大的技巧,y*y即求出了a^(2^(i-1))不知道這是什么的看原理} b:=b shr 1;{去掉已經處理過的一位} end; exit(t); end;
模運算法則
(A+B) mod C = (A mod C + B mod C) mod C
(A-B) mod C = (A mod C - B mod C) mod C
(A * B) mod C = (A mod C) * (B mod C) mod C
(A / B) mod C = ???
結合律
((a+b) mod p + c)mod p = (a + (b+c) mod p) mod p
((a*b) mod p * c)mod p = (a * (b*c) mod p) mod p
分配律
((a +b)mod p × c) mod p = ((a × c) mod p + (b × c) mod p) mod p
組合和全排列
排列A(n,m)(m是上標)=n!/(n-m)!=nx(n-1)x...xm
組合C(n,m)=n!/m!(n-m)!=[nx(n-1)x...xm]/m!
階乘
{a^b mod n} function f(a,b,n:int64):int64; var t,y:int64; begin t:=1; y:=a; while b<>0 do begin if(b and 1)=1 then t:=t*y mod n; y:=y*y mod n; {這里用了一個很強大的技巧,y*y即求出了a^(2^(i-1))不知道這是什么的看原理} b:=b shr 1;{去掉已經處理過的一位} end; exit(t); end;
約瑟夫環問題
遞推公式,設有n個人(0,...,n-1),數m,則第i輪出局的人為f(i)=(f(i-1)+m-1)%(n-i+1),f(0)=0;
例:有M個猴子圍成一圈,每個有一個編號,編號從1到M。打算從中選出一個大王。經過協商,決定選大王的規則如下:從第一個開始,每隔N個,數到的猴子出圈,最后剩下來的就是大王。要求:從鍵盤輸入M,N,編程計算哪一個編號的猴子成為大王。
var a:array[1..1000] of longint; m,n,i,j,sum:longint; weizhi:longint=0; begin readln(m,n); for i:=1 to m do a[i]:=i; sum:=m; repeat weizhi:=weizhi+1; if weizhi>m then weizhi:=weizhi-m; if a[weizhi]<>0 then begin j:=j+1; if j=n then begin a[weizhi]:=0; j:=0; sum:=sum-1; end; end; until sum=1; for i:=1 to m do if a[i]<>0 then write(a[i]); end.
Catalan數
h(n)= h(0)*h(n-1)+h(1)*h(n-2) + ... + h(n-1)h(0) (n>=2)
h[0]:=1; h[1]:=1; for i:=2 to n do begin j:=i-1; k:=0; while k<>i do begin h[i]:=h[i]+h[k]*h[j]; dec(j); inc(k); end; end;
- 矩陣連乘: P=a1×a2×a3×……×an,依據乘法結合律,不改變其順序,只用括號表示成對的乘積,試問有幾種括號化的方案?(h(n-1)種)
- 一個棧(無窮大)的進棧序列為1,2,3,…,n,有多少個不同的出棧序列。
- 對於一個二進制的01串,共n+m位,滿足n個1,m個0,且0<=n-m<=1,該串還滿足從左向右1的個數永遠大於0的個數。問共有多少種排列方式。
- 在一個凸多邊形中,通過若干條互不相交的對角線,把這個多邊形划分成了若干個三角形。求不同划分的方案數。
- 給定N個節點,能構成多少種不同的二叉樹?
擴展歐幾里德算法
若存在一組解x0,y0,滿足b*x0+(a mod b)*y0=d則取x=y0,y=x0-(a div b)*y0,有
ax+by=d這樣,我們可以用類似輾轉相除的迭代法求解。
function extended-gcd(a, b:longint; var x, y:integer); var x1, y1 :integer; begin if b=0 then begin extended-gcd:=a; x:=1; y:=0 end else begin extended-gcd:=extended-gcd(b, a mod b, x1, y1); x:=y1; y:=x1-(a div b)*y1; end; end;
對自然數因子的計算
怎樣求一個數有多少個因數?
對於一個已知的自然數,要求出它有多少個因數,可用下列方法:
首先將這個已知數分解質因數,將此數化成幾個質數冪的連乘形式,然后把這些質數的指數分別加一,再相乘,求出來的積就是我們要的結果。
例如:求360有多少個因數。
因為360分解質因數可表示為:360=2^3×3^2×5,2、3、5的指數分別是3、2、1,這樣360的因數個數可這樣計算出:(3+1)(2+1)(1+1)=24個。
怎樣求有n個因數的最小自然數?
同樣擁有n個(n為確定的數)因數的自然數可以有多個不同的數,如何求出這些數中的最小數?
這是與上一個問題相反的要求,是上一題的逆運算。
比如求有24個因數的最小數是多少?
根據上一問題解決過程的啟示,可以這樣做,先將24分解因式,把24表示成幾個數連乘積的形式,再把這幾個數各減去1,作為質數2、3、5、7......的指數,求出這些帶指數的數連乘積,試算出最小數即可。具體做法是:
因為:24=4×6, 24=3×8, 24=4×3×2,
現在分別以這三種表示法試求出目標數x:
(1)、24=4×6,4-1=3,6-1=5
X=2^5×3^3=864
(2)、24=3×8,3-1=2,8-1=7
X=2^7×3^2=1152
(3)24=4×3×2,4-1=3, 3-1=2, 2-1=1
X=2^3×3^2×5=360
綜合(1)、(2)、(3)可知360是有24個因數的最小數。
矩陣乘法
矩陣乘法有下列三要素:
(1)可乘原則:前列數 = 后行數
(2)乘積階數:前行數 × 后列數
(3)Cij乘積元素:= 前矩陣的第i行元素與后矩陣的第j列對應元素的乘積之和
矩陣乘法求斐波那契數列的原理:
f(n) 是第n項的值。
f(1)= 1; f(2) =1;
f(n)= f(n-1) + (n-2)
分兩步推導:
位運算
Pascal和C中的位運算符號
下面的a和b都是整數類型,則:
C語言 | Pascal語言
——-+————-
a & b | a and b
a | b | a or b
a ^ b | a xor b
~a | not a
a << b | a shl b
a >> b | a shr b
注意C中的邏輯運算和位運算符號是不同的。520|1314=1834,但520||1314=1,因為邏輯運算時520和1314都相當於True。同樣的,!a和~a也是有區別的。
注意:在數學中TRUE對應一;FALSE對應0
常用位運算
=== 1. and運算 ===
and運算通常用於二進制取位操作,例如一個數 and 1的結果就是取二進制的最末位。這可以用來判斷一個整數的奇偶,二進制的最末位為0表示該數為偶數,最末位為1表示該數為奇數.
X and –X返回的值是X第一個1出現的位數
=== 2. or運算 ===
or運算通常用於二進制特定位上的無條件賦值,例如一個數or 1的結果就是把二進制最末位強行變成1。如果需要把二進制元元元元最末位變成0,對這個數or 1之后再減一就可以了,其實際意義就是把這個數強行變成最接近的偶數。
=== 3. xor運算 ===
xor運算通常用於對二進制的特定一位進行取反操作,因為異或可以這樣定義:0和1異或0都不變,異或1則取反。
xor運算的逆運算是它本身,也就是說兩次異或同一個數最后結果不變,即(a xor b) xor b = a。
=== 4. not運算 ===
not運算的定義是把內存中的0和1全部取反。使用not運算時要格外小心,你需要注意整數類型有沒有符號。如果not的對象是無符號整數(不能表示負數),那么得到的值就是它與該類型上界的差,因為無符號類型的數是用$0000到$FFFF依次表示的。
=== 5. shl運算 ===
a shl b就表示把a轉為二進制后左移b位(在后面添b個0)。例如100的二進制為1100100,而110010000轉成十進制是400,那么100 shl 2 = 400。可以看出,a shl b的值實際上就是a乘以2的b次方,因為在二進制數后添一個0就相當於該數乘以2。
=== 6. shr運算 ===
和shl相似,a shr b表示二進制右移b位(去掉末b位),相當於a除以2的b次方(取整)。我們也經常用shr 1來代替div 2,比如二分查找、堆的插入操作等等。想辦法用shr代替除法運算可以使程序效率大大提高。最大公約數的二進制算法用除以2操作來代替慢得出奇的mod運算,效率可以提高60%。
常用位運算詳細示例
功能 | 示例 | 位運算
———————-+—————————+——————–
去掉最后一位 | (101101->10110) | x shr 1
在最后加一個0 | (101101->1011010) | x shl 1
在最后加一個1 | (101101->1011011) | x shl 1+1
把最后一位變成1 | (101100->101101) | x or 1
把最后一位變成0 | (101101->101100) | x or 1-1
最后一位取反 | (101101->101100) | x xor 1
把右數第k位變成1 | (101001->101101,k=3) | x or (1 shl (k-1))
把右數第k位變成0 | (101101->101001,k=3) | x and not (1 shl (k-1))
右數第k位取反 | (101001->101101,k=3) | x xor (1 shl (k-1))
取末三位 | (1101101->101) | x and 7
取末k位 | (1101101->1101,k=5) | x and (1 shl k-1)
取右數第k位 | (1101101->1,k=4) | x shr (k-1) and 1
把末k位變成1 | (101001->101111,k=4) | x or (1 shl k-1)
末k位取反 | (101001->100110,k=4) | x xor (1 shl k-1)
把右邊連續的1變成0 | (100101111->100100000) | x and (x+1)
把右起第一個0變成1 | (100101111->100111111) | x or (x+1)
把右邊連續的0變成1 | (11011000->11011111) | x or (x-1)
取右邊連續的1 | (100101111->1111) | (x xor (x+1)) shr 1
去掉右起第一個1的左邊 | (100101000->1000) | x and (x xor (x-1))
補碼
計算機用$0000到$7FFF依次表示0到32767的數,剩下的$8000到$FFFF依次表示-32768到-1的數。32位有符號整數的儲存方式也是類似的。稍加注意你會發現,二進制的第一位是用來表示正負號的,0表示正,1表示負。這里有一個問題:0本來既不是正數,也不是負數,但它占用了$0000的位置,因此有符號的整數類型范圍中正數個數比負數少一個。對一個有符號的數進行not運算后,最高位的變化將導致正負顛倒,並且數的絕對值會差1。也就是說,not a實際上等於-a-1。這種整數儲存方式叫做“補碼”。
動態規划
PS:鑒於網上有大量的專門講述DP的文章,這里不多做累贅的說明。總而言之,DP作為NOIP必考算法,熟練的掌握是極其必要的。
特征
1、最優子結構
如果問題的一個最優解中包含了子問題的最優解,則該問題具有最優子結構。也稱最優化原理。
最優子結構也可以理解為“整體最優則局部最優”。反之不一定成立。
2、重疊子問題
在解決整個問題時,要先解決其子問題,要解決這些子問題,又要先解決他們的子子問題 ……。而這些子子問題又不是相互獨立的,有很多是重復的,這些重復的子子問題稱為重疊子問題。
動態規划算法正是利用了這種子問題的重疊性質,對每一個子問題只解一次,而后將其解保存在一個表中,以后再遇到這些相同問題時直接查表就可以,而不需要再重復計算,每次查表的時間為常數。
3.無后效性原則
已經求得的狀態,不受未求狀態的影響。
步驟
1、找出最優解的性質,並刻畫其結構特征;
2、遞歸地定義最優值(寫出動態規划方程);
3、以自底向上的方式計算出最優值;記憶化搜索(樹型)、遞推
4、根據計算最優值時得到的信息,構造一個最優解。
關鍵
狀態轉移方程的構造是動態規划過程中最重要的一步,也是最難的一步.對於大多數的動態規划,尋找狀態轉移方程有一條十分高效的通道,就是尋找變化中的不變量(已經求得的值).定量處理的過程也就是決策實施的過程.
格式
動態規划的一般倒推格式為:
f[Un]=初始值; for k←n-1 downto 1 do //枚舉階段 for U取遍所有狀態 do //枚舉狀態 for X取遍所有決策 do //枚舉決策 f[Uk]=opt{f[Uk+1]+L[Uk,Xk]}; L[Uk,Xk]:狀態Uk通過策略Xk到達狀態Uk+1的費用 輸出:f[U1]:目標
動態規划的一般順推格式為:
f[U1]=初始值; for k←2 to n do //枚舉每一個階段 for U取遍所有狀態 do for X取遍所有決策 do f[Uk]=opt{f[Uk-1]+L[Uk-1,Xk-1]}; L[Uk-1,Xk-1]: 狀態Uk-1通過策略Xk-1到達狀態Uk 的費用 輸出:f[Un]:目標
推薦參考資料
劉永輝:《通過金礦模型介紹動態規划》
dd_engi:《背包九講》
ncepu:《動態規划_從新手到專家》
董的博客:《背包問題應用》
知乎問題:什么是動態規划?動態規划的意義是什么?
推薦習題
PS:以CODEVS為主要OJ,POJ上的DP題目早有人整理
背包型動態規划:1014,1068
序列型動態規划:1044,1576,3027
區間型動態規划:1048,1154,1166
棋盤性動態規划:1010,1169,1219,1220,
划分型動態規划:1017,1039,1040
樹型動態規划:1163,1380
圖論算法
回路問題
概念補充:奇點就是從這個點出發的線有奇數條的點。
Euler回路
定義:若圖G中存在這樣一條路徑,使得它恰通過G中每條邊一次,則稱該路徑為歐拉路徑。若該路徑是一個圈,則稱為歐拉(Euler)回路。
充要條件:圖連通且無奇點
Hamilton回路
定義:經過圖的每個頂點且僅一次的算法
充要條件:圖連通且奇點數為0個或2個
一筆畫問題
如何判斷一個圖形是否可以一筆不重地畫出
■⒈凡是由偶點組成的連通圖,一定可以一筆畫成。畫時可以把任一偶點為起點,最后一定能以這個點為終點畫完此圖。
■⒉凡是只有兩個奇點的連通圖(其余都為偶點),一定可以一筆畫成。畫時必須把一個奇點為起點,另一個奇點終點。
■⒊其他情況的圖都不能一筆畫出。(奇點數除以二便可算出此圖需幾筆畫成。)
Einstein學起了畫畫,
此人比較懶~~,他希望用最少的筆畫畫出一張畫。。。
給定一個無向圖,包含 n 個頂點(編號1~n),m 條邊,求最少用多少筆可以畫出圖
中所有的邊
解法:注意此題可能圖不是聯通的,所以需要用並查集預處理后完成
var n,m,i,u,v,sum1,sum2,mid:longint; b,f:array[0..1010] of longint; procedure intt; begin assign(input,'draw.in'); assign(output,'draw.out'); reset(input); rewrite(output); end; procedure outt; begin close(input); close(output); end; procedure sort(l,r: longint); var i,j,x,y: longint; begin i:=l; j:=r; x:=f[(l+r) div 2]; repeat while f[i]<x do inc(i); while x<f[j] do dec(j); if not(i>j) then begin y:=f[i]; f[i]:=f[j]; f[j]:=y; y:=b[i]; b[i]:=b[j]; b[j]:=y; inc(i); j:=j-1; end; until i>j; if l<j then sort(l,j); if i<r then sort(i,r); end; function root(x:longint):Longint; begin if f[x]=x then exit(x) else root:=root(f[x]); f[x]:=root; exit(root); end; begin intt; readln(n,m); for i:=1 to n do f[i]:=i; for i:=1 to m do begin read(u,v); if root(u)<>root(v) then f[root(u)]:=root(v); inc(b[u]); inc(b[v]); end; for i:=1 to n do mid:=root(i); sort(1,n); v:=n; while v>1 do begin while f[v-1]<>f[v] do dec(v); inc(sum2); mid:=f[v]; while f[v]=mid do begin if b[v] mod 2=1 then inc(sum1); dec(v); end; if sum1>0 then sum1:=sum1-2; sum2:=sum2+sum1 div 2; sum1:=0; end; writeln((sum1 div 2)+sum2); outt; end.
圖的遍歷
DFS
遍歷算法(遞歸過程):
1)從某一初始出發點i開始訪問: 輸出該點編號;並對該點作被訪問標志(以免被重復訪問)。
2)再從i的其中一個未被訪問的鄰接點j作為初始點出發繼續深搜。(按序號從小到大的順序訪問)
3)當i的所有鄰接點都被訪問完,則退回到i的父結點的另一個鄰接點k再繼續深搜。
直到全部結點訪問完畢。
var a:array[1..100,1..100]of longint;p:array[1..100] of boolean;m,n,j:longint; procedure init; var i,x,y:longint; begin readln(n);readln(m); for i:=1 to m do begin read(x,y);a[x,y]:=1;a[y,x]:=1; end; end; procedure dfs(k:longint); var i:longint; begin write(k,' ');p[k]:=true; for i:=1 to m do if ((p[i]=false)and(a[k,i]=1)) then dfs(i);{k(i)是具體的點} end; begin fillchar(p,sizeof(p),false); init; dfs(1); end.
BFS
按層次遍歷:
從圖中某結點i出發,在訪問了i之后依次訪問i的各個未曾訪問的鄰接點(按序號由小到大的順序),
然后分別從這些鄰接點出發按廣度優先搜索的順序遍歷圖,直至圖中所有可被訪問的結點都被訪問到。
var a:array[1..100] of longint; closed:longint=1; open:longint=0; t:array [1..100,1..100] of longint; p:array [1..100]of boolean; j,n,m,g:longint; procedure add(x:longint); begin inc(closed); a[closed]:=x; end; function del:longint; begin inc(open); del:=a[open]; end; procedure init; var i,x,y:longint; begin readln(n); readln(m); for i:=1 to m do begin read(x,y); t[x,y]:=1; t[y,x]:=1; end; end; procedure bfs(j:longint); var i,k:longint; begin a[1]:=(j); write(j,' '); p[j]:=true; while open<closed do begin inc(open); k:=a[open]; for i:=1 to n do if ((p[i]=false)and(t[k,i]=1)) then begin write(i,' '); p[i]:=true; inc(closed); a[closed]:=i; end; end; end; begin for g:=1 to m do begin p[g]:=false; a[g]:=0; end; init; bfs(1); end.
最短路徑
dijkstra算法
- 算法思想
設給定源點為Vs,S為已求得最短路徑的終點集,開始時令S={Vs} 。當求得第一條最短路徑(Vs ,Vi)后,S為{Vs,Vi} 。根據以下結論可求下一條最短路徑。
設下一條最短路徑終點為Vj ,則Vj只有:
◆ 源點到終點有直接的弧<Vs,Vj>;
◆ 從Vs 出發到Vj 的這條最短路徑所經過的所有中間頂點必定在S中。即只有這條最短路徑的最后一條弧才是從S內某個頂點連接到S外的頂點Vj 。
若定義一個數組dist[n],其每個dist[i]分量保存從Vs 出發中間只經過集合S中的頂點而到達Vi的所有路徑中長度最小的路徑長度值,則下一條最短路徑的終點Vj必定是不在S中且值最小的頂點,即:
dist[i]=Min{ dist[k]| Vk∈V-S }
利用上述公式就可以依次找出下一條最短路徑。
- 示例程序
- 算法思想
var a:array[1..100,1..100]of integer;//鄰接矩陣 flag:array[1..100] of boolean;//已經找到最短路徑的節點標志 path:array[1..100]of integer; w,x,n,i,j,min,minn,k:integer; begin readln(n,k); for i:=1 to n do//讀取鄰接矩陣,無路徑寫-1 begin for j:=1 to n do begin read(a[i,j]); If a[i,j]=-1 then a[I,j]:=maxint; End; readln; end; fillchar(flag,sizeof(flag),false);//標明所有節點都未找到最短路徑 flag[k]:=true; //源節點除外 fillword(path,sizeof(path) div 2,k); path[k]:=0; minn:=k;//標記最小的點 for x:=2 to n do begin min:=32767;//標記要找下一個最短路徑點的距離 for i:=1 to n do//找下一點點 if (a[k,i]<min) and (flag[i]=false) then begin min:=a[k,i]; minn:=i; end; flag[minn]:=true;//標記下一個點的找到 for j:=1 to n do //更新最短路徑 if (j<>minn) and (a[k,minn]+a[minn,j]<a[k,j]) and (flag[j]=false) then begin a[k,j]:=a[k,minn]+a[minn,j]; path[j]:=minn; end; end; for i:=1 to n do write(a[k,i],' ');//輸出源點到各個點的距離 writeln; for i:=1 to n do write(path[i],' ');//輸出源點到各個點的距離 end.
floyd算法
在原路徑里增加一個新結點,如果產生的新路徑比原路徑更小,則用新路徑值代替原路徑的值。這樣依次產生n個矩陣(n為網絡結點數)
用公式表示就是,對於K=1,2,3…n,第k個矩陣
運算過程中K從1開始,而 i,j 則分別從1到n取遍所有值,然后k加1,直到k等於n時停止。
- 示例程序
- 算法思想
var st,en,f:integer; k,n,i,j,x:integer; a:array[1..10,1..10] of integer;//鄰接矩陣 path:array[1..10,1..10] of integer;//path[a,b]為A點到B點的最短路徑要走的第一個點 begin readln(n);//讀取節點數 for i:=1 to n do//讀取鄰接矩陣 begin for j:=1 to n do begin read(k); if k<>0 then a[i,j]:=k else a[i,j]:=maxint; path[i,j]:=j; end; readln; end; for x:=1 to n do//I,J點之間加入X點,判斷是否更短,並更新。 for i:=1 to n do for j:=1 to n do if a[i,j]>a[i,x]+a[x,j] then begin a[i,j]:=a[i,x]+a[x,j]; path[i,j]:=path[i,x]; end; readln(st,en);//讀取開始結束點 writeln(a[st,en]);//寫出開始結束長度 f:=st; while f<> en do//寫出路徑 begin write(f); write('-->'); f:=path[f,en]; end; writeln(en); end.
spfa算法
2.1.SPFA算法需要什么
SPFA需要用到一個先進先出的隊列Q。
SPFA需要對圖中的所有頂點做一個標示,標示其是否在隊列Q中。
2.2.SPFA是怎樣執行的
2.2.1 SPFA的初始化
SPFA的初始化和Dijkstra類似。
先把所有頂點的路徑估計值初始化為代價最大值。比如:maxint。
所有頂點都標記為不在隊列中。
起始頂點放入隊列Q中。
起始頂點標記在隊列中。
起始頂點的最短路徑估計值置為最小值,比如0。
然后下面是一個循環
2.2.2 SPFA循環
循環結束的條件是隊列Q為空。第一次進入循環的時候,只有起始頂點一個元素。
每次循環,彈出隊列頭部的一個頂點。
對這個頂點的所有出邊進行松弛。如果松弛成功,就是出邊終點上對應的那個頂點的路徑代價值被改變了,且這個被松弛的頂點不在隊列Q中,就把這個被松弛的頂點入隊Q。注意,這里頂點入隊的條件有2:1.松弛成功。2.且不在隊列Q中。
當隊列Q沒有了元素。算法結束
- 示例程序
- 算法思想
const maxp=10000;{最大結點數} maxv=100000;{最大邊數} var p,c,s,t:longint;{p,結點數;c,邊數;s:起點;t:終點} a,b:array[1..maxp,0..maxp]of longint; {a[x,y]存x,y之間邊的權;b[x,c]存與x相連的第c個邊的另一個結點y} d:array[1..maxp]of integer;{隊列} v:array[1..maxp]of boolean;{是否入隊的標記} // rn:array[1..maxp]of integer;{各個點的入隊次數} dist:array[1..maxp]of longint;{到起點的最短路} head,tail:longint;{隊首/隊尾指針} procedure init; var i,x,y,z:longint; begin read(p,c); for i:=1 to c do begin readln(x,y,z); {x,y:一條邊的兩個結點;z:這條邊的權值} inc(b[x,0]);b[x,b[x,0]]:=y;a[x,y]:=z; {b[x,0]:以x為一個結點的邊的條數,b[x,a]:x點連接的第A個節點} inc(b[y,0]);b[y,b[y,0]]:=x;a[y,x]:=z;{無向圖的操作,取舍} end; readln(s,t); end; procedure spfa(s:longint); var i,j,now,sum:longint; begin fillchar(d,sizeof(d),0); fillchar(v,sizeof(v),false); for j:=1 to p do dist[j]:=maxlongint; dist[s]:=0; v[s]:=true; d[1]:=s; // rn[s]:=1; {隊列的初始狀態,s為起點} head:=1;tail:=1; while head<=tail do{隊列不空,注意:此處head>tial未空} begin now:=d[head]; for i:=1 to b[now,0]do{now指向的所有節點} if dist[b[now,i]]>dist[now]+a[now,b[now,i]]then {如果最短路徑大於now節點的最短路徑和now節點到該節點的路徑之和} begin dist[b[now,i]]:=dist[now]+a[now,b[now,i]];{修改最短路} if not v[b[now,i]]then{擴展結點入隊——如果此節點尚未入隊,則入隊,且表示} begin inc(tail); d[tail]:=b[now,i]; v[b[now,i]]:=true; // inc(rn[b[now,i]]); // if rn[b[now,i]]>c then begin writeln('fu huan');halt; end; end; end; v[now]:=false;{釋放結點,一定要釋放掉,因為這節點有可能下次用來松弛其它節點} inc(head); end; end; procedure print; begin writeln(dist[t]); end; begin init; spfa(s); print; end.
最小生成樹
prim算法
假設N=(V,E)是連通網,TE是N上最小生成樹中邊的集合。
1、算法實現:算法從U={u0}(u0∈V),TE={}開始,重復執行下述操作:
在所有u∈U,v∈V-U的邊(u,v)中找一條代價最小的邊(u0 ,v0),將其並入集合TE,同時將v0並入U集合。
當U=V則結束,此時TE中必有n-1條邊,則T=(V,{TE})為N的最小生成樹。時間復雜度為0(n2)
2、算法思想:普里姆算法構造最小生成樹的過程是從一個頂點U={u0}作初態,不斷貪心尋找與U中頂點相鄰且代價最小的邊的另一個頂點,擴充到U集合直至U=V為止。
- 優化方法
我們很容易就可以發現prim算法的關鍵:每次如何從生成樹T到T外的所有邊中,找出一條最小邊。例如,在第k次前,生成樹T中已有k個頂點和(k-1)條邊,此時,T到T外得所有邊數為k*(n-k),當然,包括沒有邊的兩頂點,我們記權值為“無窮大”的邊在內,從如此多的邊中查找最短邊,時間復雜度為O(k(n-k)),顯然無法滿足我們的期望。
我們來看O(n-k)的方法:假定在進行第k次前已經保留着從T中到T外的每一個頂點(共n-k個)的各一條最短邊,在進行第k次時,首先從這(n-k)條最短邊中,找出一條最最短邊(它就是從T到T外的最短邊),假設為(vi,vj),此步需要進行(n-k)次比較;然后把邊(vi,vj)和頂點vj並入T中的邊集TE和頂點集U中,此時,T外只有n-(k+1)個頂點,對於其中的每個頂點vt,若(vj,vt)邊上的權值小於原來保存的從T中到vt的最短邊的權值,則用(v,vt)修改之,否則,保持原最小邊不變。這樣就把第k次后T中到T外的每一個頂點vt的各一條最短邊都保留下來了,為第(k+1)次最好了准備。這樣Prim的總時間復雜度為O(n^2)。
- 示例程序(優化后)
- 算法思想
const max=1000; var map:array[1..MXN,1..MXN] of longint; cost:array[1..MXN] of longint; visit:array[1..MXN] of boolean; i,j,n,m,x,y,v:longint; function prim():longint; var i,j,min,mini,ans:longint; begin ans:=0; for i:=1 to n do begin visit[i]:=false;cost[i]:=maxlongint;end; //visit[i]是i點是否被訪問的標志,cost[i]是到i點的最小權邊。 for i:=2 to n do if map[1,i]<>0 then cost[i]:=map[1,i];visit[1]:=true; for i:=1 to n-1 do begin min:=maxlongint; for j:=1 to n do if not visit[j] and (cost[j]<min) then begin min:=cost[j];mini:=j;end; visit[mini]:=true;inc(ans,min); for j:=1 to n do if not visit[j] and (map[mini,j]>0) and (map[mini,j]<cost[j]) then cost[j]:=map[mini,j]; //更新圈內圈外存儲的最短距離 end; exit(ans); end; begin readln(n,m); for i:=1 to m do begin readln(x,y,v); if (map[x,y]=0) or (map[x,y]>v) then begin map[x,y]:=v;map[y,x]:=v; end; end; writeln(prim()); end.
Kruskal算法
先構造一個只含 n 個頂點、而邊集為空的子圖,把子圖中各個頂點看成各棵樹上的根結點,之后,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不同的樹,則將其加入子圖,即把兩棵樹合成一棵樹,反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。依次類推,直到森林中只有一棵樹,也即子圖中含有 n-1 條邊為止。
時間復雜度為為O(e^2), 使用並查集優化后復雜度為 O(eloge),與網中的邊數有關,適用於求邊稀疏的網的最小生成樹。
設圖G的度為,G=(V,E),設該圖的最小生成樹為T=(V,TE),設置邊的集合TE的初始狀態為空集。將圖G中的邊按權值從小到大排好序,然后從小的依次開始選取,若選取得邊使生成樹T不形成回路,則把它並入TE中,保留作為T的一條邊;若選取得邊使生成樹形成回路,則舍棄;如此繼續進行,直到使TE中包含n-1條邊為止。
實現克魯斯卡爾(Kruskal)算法時,要解決以下兩個問題:
1.選擇代價最小的邊(堆排序,或簡單選擇排序);
2.判定邊所關聯的兩個頂點是否在同一個連通分量中(集合)
- 算法的關鍵與優化:
kruskal算法實現過程中的關鍵和難點在於:如何判斷欲加入的一條邊是否與生成樹中已經保留的邊形成回路?我們可以將頂點划分到不同的集合中,每個集合的頂點表示一個無回路的連通分量。初始時,我們把n個頂點划分到n個集合中,每個集合只有一個頂點,表明頂點之間互不相通。當選取一條邊時,若它的兩個頂點分屬兩個不同的集合,則表明此邊連通了兩個不同的連通分量,因每個連通分量無回路,所以連同后得到的連通分量仍無回路。因此這條邊應該保留,且合並成一個連通分量,若選取得邊的兩個頂點屬於同一個連通分量,則應舍棄,因為一個無回路的連通分量內加入一條新邊必然產生回路。
- 示例程序
const MXN=1000; type rqmap=record s,t,v:longint; end; var map:array[1..MXN*MXN] of rqmap; father:array[1..MXN] of longint; n,m,i,ingraph,ans:longint; procedure qsort(b,e:longint);//排序 var i,j,x:longint; t:rqmap; begin i:=b;j:=e;x:=map[(i+j)>>1].v; while (i<=j) do begin while (map[i].v<x) do inc(i); while (map[j].v>x) do dec(j); if (i<=j) then begin t:=map[i];map[i]:=map[j];map[j]:=t;inc(i);dec(j);end; end; if i<e then qsort(i,e); if j>b then qsort(b,j); end; function find(x:longint):longint; begin if (father[x]=x) then exit(x); father[x]:=find(father[x]);//路徑壓縮 exit(father[x]); end; procedure union(a,b:longint); //並查集 begin father[find(a)]:=find(father[b]); end; begin readln(n,m); for i:=1 to n do father[i]:=i; for i:=1 to m do readln(map[i].s,map[i].t,map[i].v); qsort(1,m);ans:=0;ingraph:=1;i:=0; while (ingraph<n) do begin inc(i); if find(map[i].s)<>find(map[i].t) then begin inc(ingraph);inc(ans,map[i].v);union(map[i].s,map[i].t); end; end; writeln(ans); end.
topology排序
整體思想:掃描縱列,如果縱列上沒有任何后繼節點,則找到,把這一列的橫排都填為零
var a:array[1..100,1..100] of longint; s:array[1..100] of longint; ans:array[1..100] of longint; i,j,n,kg,x,w:longint; begin readln(n); fillchar(a,sizeof(a),0); fillchar(s,sizeof(s),0); for i:=1 to n do for j:=1 to n do begin read(a[i,j]); end; i:=1; x:=1; for j:=1 to n do begin kg:=0; for w:=1 to n do if a[w,j]=1 then inc(kg); if kg=0 then begin ans[x]:=j; inc(x); for w:=1 to n do a[j,w]:=0; end; end; for i:=1 to n do write(ans[i],' '); end.
關鍵路徑
利用拓撲排序
const maxx=50; var a:array[1..maxx,1..maxx]of longint;//鄰接矩陣 p:array[1..maxx] of longint;//拓撲路徑 b:array[1..maxx] of longint;//b[i]表示第i個點到起點的距離 c:array[1..maxx] of longint;//c[i]表示在關鍵路徑時i點的前驅 pd:array[1..maxx] of boolean; n,m,i,j,k:longint; procedure change; var i,j:longint; begin fillchar(a,sizeof(p),0); fillchar(b,sizeof(b),0); fillchar(c,sizeof(c),0); fillchar(p,sizeof(p),0); fillchar(pd,sizeof(pd),false); end; procedure tp(k:longint); var i,j:longint; begin if k=n+1 then exit; for i:=1 to n do begin if (b[i]=0) and (pd[i]) then begin p[k]:=i;//拓撲序列 pd[i]:=false; for j:=1 to n do if a[i,j]<>0 then dec(b[j]);//b[j] 用來存入度 tp(k+1); end; end; end; procedure main; var i,j:longint; begin b[p[1]]:=0; c[p[1]]:=0; for i:=2 to n do for j:=1 to n do if b[p[j]]+a[p[j],p[i]]>b[p[i]] then begin b[p[i]]:=b[p[i]]+a[p[j],p[i]]; c[p[i]]:=p[j]; end; end; procedure output; var i:longint; begin for i:=1 to n-1 do write(c[i],'-->'); writeln(c[n]); writeln(b[n]); end; begin change; readln(n); for i:=1 to n do for j:=1 to n do read(a[i,j]); tp(1); main; output; end.
按照概念
(1)關鍵路徑:AOE網中,從事件i到j的路徑中,加權長度最大者稱為i到j的關鍵路徑(Critical Path),記為cp(i,j)。特別地,始點0到終點n的關鍵路徑cp(0,n)是整個AOE的關鍵路徑。
(2)事件最早/晚發生時間
etv:事件vi的最早發生時間為,從始點到vi的最長(加權)路徑長度,即cp(0,i)
ltv:事件vi的最晚發生時間為:在不拖延整個工期的條件下,vi的可能的最晚發生時間。即ltv(i) = etv(n) - cp(i, n)
要從右往左推
(3)活動最早/晚開始時間
活動ak=<vi, vj>的最早開始時間ete(k):等於事件vi的最早發生時間,即ete(k) = etv(i) = cp(0, i)
活動ak=<vi, vj>的最晚開始時間lte(k)定義為:在不拖延整個工期的條件下,該活動的允許的最遲開始時間,
即lte(k) = ltv(j)–len(i, j),這里,ltv(j)是事件j的允許的最晚發生時間,len(i, j)是ak的權。
活動ak的最大可利用時間:定義為lte(k)-ete(k)
4.關鍵路徑:是活動的集合
若活動ak的最大可利用時間等於0(即(lte(k)=ete(k)),則稱ak 為關鍵活動,否則為非關鍵活動。
顯然,關鍵活動的延期,會使整個工程延期。但非關鍵活動不然,只要它的延期量不超過它的最大可利用時間,就不會影響整個工期。
const maxx=100; var a:array[1..maxx,1..maxx] of longint; et,eet:array[1..maxx] of longint; //eet表示活動最早可以開始的時間,et表示活動最遲應該開始的時間 n,i,j,k,x,y,w:longint; min,max:longint; begin fillchar(a,sizeof(a),char(-1)); fillchar(et,sizeof(et),0); fillchar(eet,sizeof(eet),0); readln(n); readln(x,y,w); while x<>0 do begin a[x,y]:=w; readln(x,y,w); end; eet[1]:=0; for i:=2 to n do begin max:=0; for j:=1 to n do if a[j,i]<>-1 then if a[j,i]+eet[j]>max then max:=a[j,i]+eet[j]; eet[i]:=max; end; et[n]:=eet[n]; for i:=n-1 downto 1 do//最遲開始時間需要倒着推 begin min:=maxint; for j:=1 to n do if et[j]-a[i,j]<min then//事件vi的最晚發生時間為:在不拖延整個工期的條件下,vi的可能的最晚發生時間。即ltv(i) = etv(n) - cp(i, n) min:=et[j]-a[i,j]; if a[i,j]<>-1 then et[i]:=min; end; for i:=1 to n-1 do if et[i]=eet[i] then //如果最早開始時間和最晚開始時間相同即認為是關鍵事件 write(i,'-->'); writeln(n); writeln(max); end. //默認為節點按順序輸入,即序號前面的結點會影響序號后面結點的活動
次短路
刪邊:每次刪最短路上的一條邊,用Dijkstra+堆求單源最短路徑,則每次求最短路徑時間復雜度為O(N*log(N+M) + M),所以總的時間復雜度為O(N*M*log(N+M) + M^2)。
次小生成樹
{O(n^3)}
先求MST 並記錄選擇的邊
依次刪掉這些邊(n-1次)做MST求出新的生成樹的值
則這些值中最小的即為the Second MST
{O(n^2)}
在求MST的同時,對於新加入的節點,max[i][j]記錄已加入的節點i到新加入節點j的路徑上邊權最大的值。
求完后mst記錄最小生成樹的權值和,枚舉每一條不在樹中的邊u~v,必然與MST形成一個圈,max[u][v]中存的就是圈中除了新加入的邊外最大的邊了。那么加入u~v后,生成樹的權值min=mst+g[u][v]-max[u][v];
最小的min就是次小生成樹
匈牙利算法
var g:array[1..maxn,1..maxm]of boolean; //圖,g[i,j]:節點i到節點j是否有邊 y:array[1..maxm]of boolean; //訪問標記,y[i]:節點i是否已被訪問過 link:array[1..maxm]of longint; //與節點i匹配的點,link[i]=0 表示i沒有被匹配 function find(v:longint):boolean; //從v出發找匹配 var i:longint; begin for i:=1 to m do //枚舉與v相連的點i if g[v,i] and (not y[i]) then //如果i與v相連且還未被訪問過 begin y[i]:=true; //標記i已被訪問過 if (link[i]=0)or find(link[i]) then //如果i無匹配或原來的匹配點link[i]可以另找一個匹配 begin link[i]:=v; //讓v-i與i匹配 find:=true; //匹配成功並返回 exit; end; end; find:=false; //匹配不成功 end; begin //read the graph into array g[][] for i:=1 to n do begin fillchar(y,sizeof(y),0); if find(i) then inc(ans); end; //now then ans stores the max match end.
博弈
取對稱狀態
Alice and Bob decide to play a funny game. At the beginning of the game they pick n(1 <= n <= 106) coins in a circle, as Figure 1 shows. A move consists in removing one or two adjacent coins, leaving all other coins untouched. At least one coin must be removed. Players alternate moves with Alice starting. The player that removes the last coin wins. (The last player to move wins. If you can't move, you lose.)
如果n<=2那么先手的會獲得勝利,當n>=3時,先手的走了一步以后,后手的可以把這個一個大圖分成兩個完全相同的小圖,每步都是如此,則在n步以后,先手的總會無棋可取,后手的獲得勝利。
var t,n,i:longint; begin while true do begin readln(n); if n=0 then halt; if n<=2 then writeln('Alice') else writeln('Bob'); end; end.
取NIM值
Here is a simple game. In this game, there are several piles of matches and two players. The two player play in turn. In each turn, one can choose a pile and take away arbitrary number of matches from the pile (Of course the number of matches, which is taken away, cannot be zero and cannot be larger than the number of matches in the chosen pile). If after a player’s turn, there is no match left, the player is the winner. Suppose that the two players are all very clear. Your job is to tell whether the player who plays first can win the game or not.
隨機博弈指的是這樣的一個博弈游戲,目前有任意堆石子,每堆石子個數也是任意的,雙方輪流從中取出石子,規則如下:1)每一步應取走至少一枚石子;每一步只能從某一堆中取走部分或全部石子;2)如果誰取到最后一枚石子就勝。也就是尼姆博弈(Nimm Game)。必敗局面:也叫奇異局勢。無論做出何出操作,最終結果都是輸的局面。必敗局面經過2次操作后,可以達到另一個必敗局面。必勝局面:經過1次操作后可以達到必敗局面。即當前局面不是必敗局面就是必勝局面,而必勝局面可以一步轉變成必敗局面。
最終狀態:
(1)最后剩下一堆石子;(必勝局面)
(2)剩下兩堆,每堆一個;(必敗局面)
(3)當石子剩下兩堆,其中一堆只剩下1顆,另一堆剩下多於n顆石子時,當前取的人只需將多於1顆的那一堆取出n-1顆,則局面變為剛才提到的必敗局面。(必勝局面)
判斷當前局勢是否為必勝(必敗)局勢:
1)把所有堆的石子數目用二進制數表示出來,當全部這些數按位異或結果為0時當前局面為必敗局面,否則為必勝局面;
2)在必勝局面下,因為所有數按位異或的結果
是大於零的,那么通過一次取,將這個(大於其它所有數按位異或的結果的)數下降到其它所有數按位異或的結果,這時局面就變為必敗局面了。
定理:一組自然數中必然存在一個數,它大於等於其它所有數按位異或的結果。
證明:原命題等價於,設a1^a2^... ^an=p,p≠0時,必存在k,使得ak^p<ak< span="">(當p=0時,對於任意的k,有ak^p=ak)。
設p的最高位是第q位,則至少存在一個k,使得ak的第q位也是1,而ak^p的第q位為0,所以ak^p<ak
補綴一點,(a^b)^b=a^(b^b)=a^0=a,所以ak^p相當於“其它所有數按位異或的結果”。
var x,y,n:int64;i:longint; procedure main; begin y:=0; read(n); for i:=1 to n do begin read(x); y:=y xor x; end; if y=0 then writeln('No') else writeln('Yes'); end; begin while not seekeof do main; end.
分治
二分法找最值
var n,m,a1,b1,a2,b2,i:longint; q:array[1..100]of longint; procedure fenzhi(a,b:longint;var max,min:longint); var d,max1,max2,min1,min2:longint; begin if a=b then begin max:=q[a]; min:=q[a]; end else if b-a=1 then begin if a>b then begin max:=q[a]; min:=q[b]; end else begin max:=q[b]; min:=q[a]; end; end else begin d:=(a+b) div 2; fenzhi(a,d,max1,min1); fenzhi(d+1,b,max2,min2); if max1>max2 then max:=max1 else max:=max2; if min1<min2 then min:=min1 else min:=min2; end; end; begin read(n); for i:=1 to n do read(q[i]); m:=n div 2; fenzhi(1,m,a1,b1); fenzhi(m+1,n,a2,b2); if b1<b2 then write(b1,' ') else write(b2,' '); if a1>a2 then write(a1,' ') else write(a2,' '); end.
回溯
N皇后
procedure try(i:longint); var j:longint; begin if i=n+1 then begin print;exit;end; for j:=1 to n do if a and b[j+i] and c[j-i] then begin x:=j; a[j]:=false; b[j+i]:=false; c[j-i]:=false; try(i+1); a[j]:=true; b[i+j]:=true; c[j-i]:=true; end; end;
Hanoi Tower
{h(n)=2*h(n-1)+1 h(1)=1
初始銅片分布在3個柱上,給定目標柱goal;h[1..3,0..n]存放三個柱的狀態,now與nowp存最大的不到位的銅片的柱號和編號,h[I,0]存第I個柱上的個數。
procedure hanoi(n,a,b,c:byte); {將第n塊銅片從a柱通過b柱移到c柱上} begin if n=0 then exit; hanoi(n-1,a,c,b); {將上面的n-1塊從a柱通過c柱移到b柱上} write(n,’moved from’,a,’to’,c); hanoi(n-1,b,a,c);{ 將b上的n-1塊從b柱通過a柱移到c柱上} end;
高精度計算
高精度加法
var s1,s2:string; a,b:array[1..100] of longint; la,le,len,i:longint; begin readln(s1); readln(s2); la:=length(s1); for i:= 1 to la do a[i]:=ord(s1[la+1-i])-48; le:=length(s2); for i:=1 to le do b[i]:=ord(s2[le+1-i])-48; len:=la; if le>len then len:=le; for i:=1 to len do begin a[i+1]:=a[i+1]+(a[i]+b[i]) div 10; a[i]:=(a[i]+b[i]) mod 10; end; if a[len+1]>0 then len:=len+1;{判斷最高位} for i:=len downto 1 do write(a[i]); end.
高精度減法
const n=1000; type arr=array[0..n]of longint; var a,b:arr;s:string; function f:boolean; var i:longint=1; begin f:=true; while ((i<n)and(a[i]=b[i])) do inc(i); if a[i]<=b[i] then f:=false; end; procedure init(var p:arr); var m:char; k,i:longint; begin k:=1; read(m); while m in['0'..'9'] do begin p[k]:=ord(m)-48;k:=k+1;read(m);{使得能夠不斷讀數} end; for i:=1 to k do begin p[n+i-k]:=p[i];p[i]:=0;{把最小位放在前面,便於順次詳見} end; end; procedure work; var t,g:longint;i:longint; begin if f then write(''); if not f then begin s:='-'; {判斷減數和被減數大小,如果被減數大於減數就填負號后交換位置} for i:=1 to n do begin t:=a[i];a[i]:=b[i];b[i]:=t; end; end; g:=0;{進位標志} for i:=n downto 1 do begin if a[i]>=b[i]+g then begin a[i]:=a[i]-b[i]-g;g:=0; end{不需要借位時} else begin a[i]:=10+a[i]-b[i]-g;g:=1; end; end; end; procedure output; var i:longint=1; begin while((a[i]=0)and(i<n))do inc(i); {當在范圍限度內且為零時就不斷不斷向后移以尋找做完減法后的最高位} while i<n do begin write(a[i]);inc(i); end; end; begin fillchar(a,sizeof(a),0);fillchar(b,sizeof(b),0); init(a); readln; init(b);readln; work; write(s);output; end.
高精度乘法
const max=200; type arr=array[1..max]of longint; arrr=array[1..2*max] of longint; var a,b:arr;c:arrr;k,s1,s2,n:longint; procedure init(var p:arr); var s:string; i:longint; begin read(s);n:=length(s); if s='0' then begin write('0');halt; end; for i:=n downto 1 do p[n-i+1]:=ord(s[i])-48;{把小位數放在前面} end; procedure work; var i,j,x,l:longint; begin for i:=1 to s1 do for j:=1 to s2 do begin l:=a[i]*b[j]; c[i+j-1]:=c[i+j-1]+l; {解決錯位相加問題,a、b數組的值正好放在c數組【i+j-1】的位置上,就可以用這個位置原來的數加上新乘出的數解決問題,在運算過程中,a、b數組里面的值不能改變,所以必須要新開一個新數組} end; for i:=1 to k do begin c[i+j]:=c[i+j]+c[i+j-1] div 10; {這一步必須在前面否則下面一步就改變了c數組里面的值} c[i+j-1]:=c[i+j-1] mod 10; end; while c[k]=0 do dec(k);{尋找最高位} x:=c[k]; while x>0 do begin c[k]:=x mod 10; x:=x div 10; inc(k); end;{解決最高位問題} end; procedure output; var i:longint; begin for i:=k-1 downto 1 do write(c[i]); end; begin fillchar(a,sizeof(a),0); fillchar(b,sizeof(b),0);fillchar(c,sizeof(c),0); init(a);readln;s1:=n; init(b);readln; s2:=n; k:=s1+s2; {乘法得出結果后的最高位數為兩個位數的因數之和} work;output; end.
高精度除法
type arr=array[0..10000]of longint; var a,b,c,d:arr;a1,b1,n,j,I,len:longint;s,s1,s2:string; procedure init; begin readln(s);len:=length(s); a1:=pos(' ',s);s1:=copy(s,1,a1-1);s2:=copy(s,a1+1,len-a1);a1:=length(s1); for i:=1 to a1 do a[i]:=ord(s1[a1-i+1])-48; b[0]:=length(s2); for i:=1 to b[0] do b[i]:=ord(s2[b[0]-i+1])-48; end; function f:boolean; var i:longint; begin if d[0]>b[0] then exit(true); if d[0]<b[0] then exit(false); for i:=d[0] downto 1 do begin if d[i]>b[i] then exit(true) else if d[i]<b[i] then exit(false); end; exit(true); end; procedure change(x:longint); var i:longint; begin inc(d[0]); for i:=d[0] downto 2 do d[i]:=d[i-1]; d[1]:=x; end; function work:boolean; var i:longint; begin if f=false then exit(false); for i:=1 to d[0] do begin d[i+1]:=d[i+1]-1+(d[i]+10-b[i]) div 10; d[i]:=(d[i]+10-b[i])mod 10; end; while (d[d[0]]=0) and(d[0]>1) do dec(d[0]); exit(true); end; procedure output; var i:longint; begin while (c[a1]=0) and(a1>1) do dec(a1); for i:=a1 downto 1 do write(c[i]); writeln; end; begin fillchar(a,sizeof(a),0); fillchar(b,sizeof(b),0); fillchar(c,sizeof(c),0); fillchar(d,sizeof(d),0); init; if a1<b[0] then begin write('0...'); for j:=a1 downto 1 do write(a[j]); halt; end; for j:=a1 downto 1 do begin change(a[j]); while work do inc(c[j]); end; output; end.
高精度階乘
{高精度乘單精度} const max=2000; var a:array[1..max] of longint;n,i,j,l,q:longint; begin readln(n); a[1]:=1;l:=1; {l表示前面的數依次的階乘} for i:=2 to n do begin for j:=1 to l do a[j]:=a[j]*i;{做乘法} for j:=1 to l do begin a[j+1]:=a[j+1]+a[j] div 10; a[j]:=a[j] mod 10;{處理進位問題} while a[l+1]<>0 do begin l:=l+1; a[l+1]:=a[l+1]+a[l] div 10; a[l]:=a[l] mod 10; end; {處理最高位及數位問題} end; end; for i:=l downto 1 do write(a[i]); end.
高級數據結構
二叉樹
//二叉樹的先序遍歷 type note=record father,lch,rch:longint;end; var a:array[1..100] of note;n,j,t,m:longint;s:longint; procedure duru; var i:longint;f,l,r:longint; begin read(n); for i:=1 to n do begin readln(f,l,r); a[f].lch:=l;{f的左孩子為l} a[f].rch:=r;{f的右孩子為r} if l<>0 then a[l].father:=f;{如果有左孩子那么左孩子的父親是f} if r<>0 then a[r].father:=f;{如果有右孩子那么右孩子的父親是f} end; end;{輸入的過程,第一個是父親,后兩個為左右孩子,0表示空} function root:longint; var i:longint; begin for i:=1 to n do{i表示結點} if a[i].father=0 then begin root:=i;exit;{如果這個結點的父親為零,則這個結點就是根} end; end;{找根的程序} procedure xiangen(t:longint); begin if t<>0 then begin write(t,' ');{先根} xiangen(a[t].lch);{再對左子樹先根遍歷,左孩子當根} xiangen(a[t].rch);{再對右子樹先根遍歷,后孩子當根} end; end;{進行先序遍歷並輸出的程序} begin duru;m:=root; writeln(m);xiangen(m); end. //二叉樹的中序遍歷 procedure zhonggen(t:longint); begin if t<>0 then begin write(t,' '); zhonggen(a[t].lch); zhonggen(a[t].rch); end; end; //二叉樹的后序遍歷 procedure hougen(t:longint); begin if t<>0 then begin hougen(a[t].lch); hougen(a[t].rch); write(t,' ') end; end; //已知二叉樹的前序遍歷與中序遍歷求后序遍歷 var s1,s2:string; procedure work(s1,s2:string); var b,a:longint;{b,a的值在不斷改變,必須放在局部變量里} begin if s1<>'' then begin b:=length(s1); a:=pos(s1[1],s2); work(copy(s1,2,a-1),copy(s2,1,a-1)); work(copy(s1,a+1,b-a),copy(s2,a+1,b-a)); write(s1[1]); end; end; begin readln(s1);readln(s2); work(s1,s2); end.
並查集
{已按秩優化} a:array[1..max] of longint;//a[i]表示i的前驅 function find(i:longint):longint;//非遞歸算法找i的根 var j,k,t:longint; begin j:=i; while a[j]<>0 do j:=a[j]; // 順着i向上找根 find:=j; k:=i; //從i開始對子樹結點進行路徑壓縮 while a[k]<>0 do begin t:=k;k:=a[k];a[t]:=j;end; end; function root(i:longint):longint;//遞歸算法找i的根 var j,k,t:longint; begin if a[i]=0 then exit(i); //若i為根,返回本身結點序號 root:=root(a[i]); //否則繼續向上找 a[i]:=root; end; procedure union(x,y:longint);//合並 begin if root[x]=root[y] then else f[y]:=x; end;
樹狀數組
{求2^k的代碼:} function lowbit(t:longint):longint; begin lowbit:=t and(t xor (t-1)); end; {當想要查詢一個SUM(n)(求A[1]..A[n]的和),可以依據如下算法即可: step1:令sum = 0,轉第二步; step2:假如n <= 0,算法結束,返回sum值,否則sum = sum + Cn,轉第三步; step3:令n = n–lowbit(n),轉第二步。} function getsum(k:integer):integer; var t:integer; begin t:=0; while k>0 do begin t:=t+c[k]; k:=k-lowbit(k); end; getsum:=t; end; {修改算法如下(給某個結點i加上x): step1: 當i > n時,算法結束,否則轉第二步; step2: Ci = Ci + x, i = i + lowbit(i)轉第一步。 i = i +lowbit(i)這個過程實際上也只是一個把末尾1補為0的過程。} procedure add(i:longint;x:longint); begin while i<=n do begin c[i]:=C[i]+x; i:=i+lowbit(i); end; end;
線段樹
type xtree=record a,b,left,right,cover:longint; end; //a,b是區間左右端點,當a=b表示一個葉子節點;left,right表示左右兒子,cover表示是否被覆蓋 var tree:array[1..1000] of xtree; c,d,number,tot:longint; //tot為一共有多少個結點 procedure make(a,b:longint); var now:longint;//now必須為局部變量,dfs中枚舉變量一樣的道理 begin inc(tot); now:=tot; tree[now].a:=a; tree[now].b:=b; tree[now].cover:=0; if a+1<b then begin tree[now].left:=tot+1; make(a,(a+b)div 2); tree[now].right:=tot+1; //若now為全局變量,建左子樹會修改now的值,導致此處建樹不正確 make((a+b) div 2,b); end; end; //建樹過程 procedure insert(num:longint); begin if (c<=tree[num].a)and(tree[num].b<=d) then inc(tree[num].cover) //若當前區間被[c,d]覆蓋,則cover+1 else begin if c<(tree[num].a+tree[num].b)div 2 then insert(tree[num].left); if d>(tree[num].a+tree[num].b)div 2 then insert(tree[num].right); end; //否則將區間[c,d]插到左右子樹中 end; //插入線段[c,d](c,d為全局變量)到線段樹第num個節點區間 procedure delete(num:longint); begin if (c<=tree[num].a)and(tree[num].b<=d) then dec(tree[num].cover) //若當前區間被[c,d]覆蓋,則cover-1 else begin if c<(tree[num].a+tree[num].b)div 2 then delete(tree[num].left); if d>(tree[num].a+tree[num].b)div 2 then delete(tree[num].right); end; //否則將區間[c,d]在左右子樹中刪除 end; //在線段樹第num個節點區間中刪除線段[c,d] procedure count(num:longint); begin if tree[num].cover>0 then number:=number+(tree[num].b-tree[num].a) //若當前區間被完全覆蓋,則累加到number全局變量中 else begin if tree[num].left>0 then count(tree[num].left); if tree[num].right>0 then count(tree[num].right); end; //否則,若為部分覆蓋,則累加左右子樹的測度 end; //計算整個線段樹的測度(被覆蓋區間的總長度) begin readln(c,d); make(c,d); readln(c,d); insert(1);//插入線段[c,d]; readln(c,d); delete(1);//刪除線段[c,d] count(1);//計算線段樹的測度 writeln(number); end.
二叉搜索樹
{treap示范代碼: 標准的代碼縮進風格,優美的算法實現。 經典標程,完全掌握后水平會提高很多 不改變bst的性質(在bst所有子樹中均滿足:左子樹的所有節點<=根<=右子樹的所有節點) 通過旋轉操作,使根的hr最小(即所有的hr構成堆的關系)} var //l[i],r[i],v[i]:i號結點的左兒子、右兒子,關鍵值 //hr[i]:i號節點的優先值(treap所有子樹中,根的hr必須是最小的) //s[i]:i號節點為根的子樹節點總數 l,r,hr,s,v:array[0..2000000]of longint; n,root,m:longint; procedure init;//初始化 begin readln(n); m:=0; //randomize; //考試要求程序每次運行結果必須一致,慎用。確實要用:randomize 100; fillchar(s,sizeof(s),0); fillchar(l,sizeof(l),0); fillchar(r,sizeof(r),0); root:=0; end; //旋轉是平衡二叉樹的精髓,它不會改變bst的性質(左子樹<=根<=右子樹) //左旋使樹的左子樹深度+1,右子樹深度-1 //右旋使樹的右子樹深度+1,左子樹深度-1 procedure l_rotate(var x:longint);inline;//左旋以x為根的子樹(注意var參數及意義) var y:longint; begin y:=r[x]; //保存x的右兒子到y中 r[x]:=l[y]; //將y的左兒子作為x的右兒子 l[y]:=x; //x作為y的左兒子 s[y]:=s[x]; //維護旋轉后的子樹大小 s[x]:=s[l[x]]+s[r[x]]+1; x:=y; //y為根 end; procedure r_rotate(var x:longint);inline;//右旋以x為根的子樹 var y:longint; begin y:=l[x]; l[x]:=r[y]; r[y]:=x; s[y]:=s[x]; s[x]:=s[l[x]]+s[r[x]]+1; x:=y; end; //插入(遞歸,if key<=root,則插入到左子樹,否則到右子樹,直到盡頭再新建節點) procedure insert(var k,key:longint);inline; begin if k=0 then//已到盡頭,新建節點並寫入key及隨機值hr begin inc(m); v[m]:=key; s[m]:=1; hr[m]:=random(maxlongint); k:=m;//修改k,使父節點指向當前節點(修改前從父節點指向0) exit; end; inc(s[k]); if key<=v[k] then//若key<=根則插入到左子樹,否則到右子樹 begin insert(l[k],key);//若l[k]=0,到下層則新建節點並修改l[k]=m if hr[l[k]]>hr[k] then //旋轉 r_rotate(k); exit; end; if key>v[k] then begin insert(r[k],key); if hr[r[k]]>hr[k] then l_rotate(k); exit; end; end; {刪除:在k號節點為根的子樹中刪除key 基本方法:由於是靜態結構,為了提高效率,並沒真正刪除 若找到則刪除,若沒找到,則刪除查找盡頭的節點 主程序中需判斷返回值,若不等於key,重新插入key即可 找到后的處理: 若為葉節點,直接刪除,否則,將要刪除的節點左子樹的最右節點(思考:為什么?)代替它 } function delete(var k:longint;key:longint):longint;inline; begin dec(s[k]);//維護節點總數 //如果找到,或已到盡頭 if (key=v[k])or(l[k]=0)and(key<=v[k])or(r[k]=0)and(key>v[k]) then begin delete:=v[k];//返回要刪除的節點(不一定=key) if (l[k]=0)or(r[k]=0) then //若左右子樹只有一個,則讓兒子代替根即可 begin k:=l[k]+r[k];//用兒子替換當前要刪除的節點 exit; end; v[k]:=delete(l[k],key+1);//k左右子樹都有,則用左子樹的最右節點替換k exit; end; if key<=v[k] then//若k<=v[k],則在左子樹中刪,否則,在右子樹中刪 exit(delete(l[k],key)); if key>v[k] then exit(delete(r[k],key)); end; function find(var k,key:longint):boolean;inline;//查找 begin if k=0 then//遞歸邊界 exit(false); if key>v[k] then find:=find(r[k],key) else find:=(v[k]=key)or find(l[k],key); end; //key的排名(key排在第幾,按從小到大的順序) function rank(var t,key:longint):longint;inline; begin if t=0 then exit(1); if key<=v[t] then rank:=rank(l[t],key) else rank:=s[l[t]]+1+rank(r[t],key); end; function select(var t:longint;k:longint):longint;inline;//選擇排在第k位的數 begin if k=s[l[t]]+1 then//若找到第k位的節點,則返回節點key值 exit(v[t]); if k<=s[l[t]] then//遞歸 select:=select(l[t],k) else select:=select(r[t],k-1-s[l[t]]); end; function pred(var t,key:longint):longint;inline;//找key的前趨 begin if t=0 then exit(key); if key<=v[t] then//key<=根,原問題等價於在左子樹中找key pred:=pred(l[t],key) else begin //key>根,原問題等價於在右子樹中找key,但右兒子返回時,要判斷你是不是key的前趨 pred:=pred(r[t],key);//右子樹的返回值 if pred=key then //如果右子樹的返回值=key說明在右子樹中沒找到key的前趨 pred:=v[t]; //右子樹沒有key的前趨,那你就是了。 end; end; function succ(var t,key:longint):longint;inline;//找key的后繼 begin if t=0 then exit(key); if v[t]<=key then succ:=succ(r[t],key) else begin succ:=succ(l[t],key); if succ=key then succ:=v[t]; end; end; procedure order;inline;//操作解釋和執行 var a,b:longint; begin readln(a,b); case a of 1:insert(root,b); 2:delete(root,b); 3:writeln(find(root,b)); 4:writeln(rank(root,b)); 5:writeln(select(root,b)); 6:writeln(pred(root,b)); 7:writeln(succ(root,b)); end; end; procedure main;inline;//主模塊 var i:longint; begin init; for i:=1 to n do order; end; begin//主程序 main; end.
進制轉換
1.將m進制數n轉化成一個十進制數
{輸入共一行表示m進制的n化成十進制的數。} var s:string;sum,n,m,i,r,t,c:longint; begin readln(s); val(copy(s,pos(' ',s)+1,length(s)),m,c); delete(s,pos(' ',s),length(s)); {使整數m中放進制,字符串s中放需要的數} t:=1;n:=length(s);sum:=0; for i:=n downto 1 do begin case s[i] of '0'..'9':r:=ord(s[i])-48; 'A'..'Z':r:=ord(s[i])-55; end; sum:=sum+r*t; t:=t*m; end; write(sum); end.
2.將十進制數n轉換成m進制數
{輸入共一行表示n的m進制。} var a:char; i,n,m:integer; c:array[10..16]of char; //存儲10~16對應的7個字母 procedure nm(n:integer); //M為全局變量,所以這里只要N就可以了 var r:integer; begin if n=0 then exit; //直到商等於0 r:=n mod m; //求余數 nm(n div m); //反向取余數 if r>=10 then write(c[r]) else write(r); //大於10的數特殊處理 end; begin a:='A'; for i:=10 to 16 do begin c[i]:=a; a:=succ('A') end; //后繼函數,即前一個或后一個ASCII碼代表的字符. 如:pred('b')='a',succ('b')='c'. readln(n,m); //輸入 nm(n); //引用過程,過程中包含輸出。 end。
后記
本文未涉及的內容包括但不限於:
哈希(HASH)
強連通分量
離散化
尺取法
網絡流
計算幾何(包括凸包)
2-SAT
LCA
倍增法
KMP算法
后綴數組
Grundy數
trie樹
AC自動機
FFT快速傅里葉變換
斯坦納樹
單純形法
矩陣
三分搜索
α-β剪枝
離散對數
微積分
樹鏈剖分
負進制轉換
群論
…
不過沒有關系,這些基本都屬於NOIP涉及不到的范圍,學無止境,把握好當下的才是關鍵!
參考文獻
百度百科
《數學》第三冊 蘇州大學出版社
《動態規划學案》 山東省實驗中學 王乃廣
《PASCAL省選模板》island
《NOIP2012最終征途》亟隱 Slack
《自然數的因數》桃花島主
《二分圖的最大匹配、完美匹配和匈牙利算法》pi9nc
推薦文章&書籍&博客&網站
劉汝佳《算法競賽入門經典1&2》
啊哈磊《啊哈!算法》
Thomas H.Cormen等 《算法導論》
編程之美小組《編程之美》
秋葉拓哉等《挑戰程序設計競賽》
Charles Petzold 《CODE》
顧森《思考的樂趣》
《NOI導刊》(一本雜志,已經停辦,不過可以找來以前的看看)
《騙分導論》
《人品導論》
免費程序員圖書下載(http://it-ebooks.info/)
快速查詢數列(http://oeis.org/)
使用技巧:PDF自由轉換(http://smallpdf.com/cn)
PS:其實在競賽之路上幫助最大的還是大牛神犇們的博客,從中可以學到很多的東西。我也養成了寫博客的習慣,做完題寫寫博客加加注釋,整理思路的同時還能助人為樂。寫博客推薦去博客園或是CSDN社區,這是專門面向程序猿的博客,支持自己設置板式,代碼高亮。因為有群體限制,所以交流起來也更方便。
PS:有關NOIP技巧整理和測評網站OJ整理的文章請移步本人技術博
聯系我
技術博:川漢唐
QQ:1031304332
二零一五年七月 初稿
二零一五年十月 定稿
圖片君和鏈接君好像領便當了,可能看起來有些不方便,大家想看完整版還是去百度文庫下一份吧:http://wenku.baidu.com/view/31b31db43169a4517623a38e
本文原創,轉載請說明出處,歡迎用於學習方面的交流,用於商業用途前請先聯系作者。http://www.cnblogs.com/yangqingli/