如題所述,專門記錄自己在各類考試、做題中出現的花式錯誤。
算法思想
-
分塊相關算法寫掛的:
-
[Ynoi2019模擬賽] Yuno loves sqrt technology II
寫莫隊發現自己沒有排序,T 飛了。大致可以描述為:
void Init() { //對 Q 數組進行排序 } int main() { Init(); ...... //讀入 Q return 0; }
處理方式:一定要將數據讀入完之后再預處理 。
-
題目見上。
寫分塊的時候對邊角塊處理不當......void UpdateBlock( int id, int l, int r, ... ); //塊內處理 void UpdateAll( int L, int R ); //全局處理 { if( 左邊有邊角塊 ) UpdateBlock( 塊編號, L, R, ... ); ...... }
事實上應該將左右邊界限制在塊的范圍內。
-
其一:分塊修改時,應該枚舉完整個 \([lef, rig]\) 的區間,而不是只枚舉 \([qL, qR]\) 內的數。這種情況主要出現在區間的元素被重排了的情況。
其二:無論是分塊還是線段樹,一定要注意將交集為空的情況判掉!
-
-
數據結構寫掛的:
-
使用了倆優先隊列的 " 可刪堆 " ,結果在
push
和erase
的時候都沒有清除多余元素,於是堆中冗余元素就超多,結果 TLE 了:typedef priority_queue<int, vector<int>, greater<int> > Heap; struct RHeap { Heap q, rub; void Push( const int v ) { q.push( v )/*這里需要 Maintain 一下*/; } void Erase( const int v ) { rub.push( v )/*這里需要 Maintain 一下*/; } int Size() { Maintain(); return q.size() - rub.size(); } int Top() { Maintain(); return q.empty() ? 0 : q.top(); } RHeap() { while( ! q.empty() ) q.pop(); while( ! rub.empty() ) rub.pop(); } void Maintain() { while( ! q.empty() && ! rub.empty() && q.top() == rub.top() ) q.pop(), rub.pop(); } }
上面的“問題”已經破案了,真正的原因是
數組開小了。但是謹慎起見,最好還是每次加入
maintain
一下。👍額外提醒一點,惰性刪除堆千萬不要刪除不存在的元素。
-
這道題用單調棧維護所有位置的最值的和的時候,細節很多。尤其注意的是,每個點管轄的區間實際上是從棧內的上一個元素開始的,也就是 \((stk_{i-1},i]\) ,而不能將自己作為自己那一段的終點。請參考以下這段代碼:
while( top1 && a[stk1[top1]] > a[i] ) mn -= 1ll * ( a[stk1[top1]] - a[stk1[top1 - 1]] ) * ( i - stk1[top1 - 1] - 1 ), top1 --; // 這里不能寫 i-stk[top1] ,不然就會少算一些位置 mn += 1ll * ( a[i] - a[stk1[top1]] ) * ( i - stk1[top1] - 1 ) + a[i], stk1[++ top1] = i;
-
線段樹的邊界:
寫了兩棵線段樹,大小不一致,並且查后繼時如果沒有后繼則默認為 \(siz+1\)(但實際上應該是 \(+\infty\))。這就導致了在較小的樹上查后繼的時候,得到了較小的范圍,而在較大的樹上查詢時會查漏一些點。
處理方法:注意大小關系,注意某些特殊值的取值(無窮大無窮小等),尤其是大小不一致時容易翻車。
笑麻了,這都 1202 年了,居然還可以寫錯線段樹。
具體來說,線段樹區間修改,因為遞歸邊界有鍋,導致運行起來與暴力無異:
void Update( const int x, const int l, const int r, const int segL, const int segR, const int delt ) { if( segL > segR ) return ; if( l == r /*segL <= l && r <= segR*/ ) { Add( x, delt ); return ; } // 這里寫錯了 int mid = ( l + r ) >> 1; Normalize( x ); if( segL <= mid ) Update( x << 1, l, mid, segL, segR, delt ); if( mid < segR ) Update( x << 1 | 1, mid + 1, r, segL, segR, delt ); Upt( x ); }
-
第一次犯錯:以為區間反轉對應的端點可以直接算出來,結果需要用
kth()
來找;注意相對順序會改變。第二次犯錯:
kth()
的時候沒有下傳標記。序列平衡樹很久沒碰過了 qwq ......
-
怎么又是平衡樹:寫非旋 Treap(或者其它任何結構)在結構變化的時候,一定要在結構確定下來后,更新維護的信息。
例如,非旋 Treap 就需要在子樹分裂完成和子樹合並完成后及時更新信息。
-
帶權並查集相關:
例如 「LG P5787」二分圖這道題,很多時候,帶權並查集維護的是一棵生成樹上,某個結點到根的信息。那么在連接 \((u,v)\) 這條邊的時候,從 \(u\) 所在的根 \(r_u\),到 \(v\) 所在的根 \(r_v\),實際上是先到 \(u\),再經過 \((u,v)\) 到 \(v\),最后到 \(r_v\),因此合並信息的時候,也應該按照這條路徑來合並。
-
可撤銷並查集相關:
其一,斷開從 \(u\) 到 \(v\) 的父子邊的時候,\(u\) 的父親應該還原成 \(u\) 而不是 0。
其二,可撤銷並查集不能寫路徑壓縮,不能寫路徑壓縮,不能寫路徑壓縮!!!
-
-
標記處理的問題:
-
注意:左偏樹刪除堆頂的時候,它的標記可能還沒有下傳。因此需要下傳標記之后再合並左右子樹。
有的數據結構也是同樣的。如果在刪除的時候,被刪除的節點對於其他節點的影響需要全部清除。
-
-
一道重鏈剖分的題目,寫錯了兩個地方:
-
其一,居然把重鏈剖分寫成了輕鏈剖分😢,就一個符號寫錯了;
- 其二,中途某個位置沒有開
long long
,一直沒有檢查到;
處理方法:第一個問題屬於寫錯了只會影響復雜度的類型,應該多測一下極限數據,什么樣子的都應該測一下;第二個問題則應該檢查每一個變量類型開得是否合理。
- 其二,中途某個位置沒有開
-
-
圖論算法寫掛的:
-
使用 Boruvka 算法的時候,一定要注意,找出的是點,對應的是連通塊,並查集維護的根與連通塊的編號沒有任何關系,千萬不要用混了!
-
網絡流的建圖:
調了一個半小時,終於發現問題所在:竟然是反向邊弄錯了,最初
cnt
賦值為 0 而非 1 !!!處理方法:單個變量不要忘記初始化!
-
關於 SPFA 判斷有無負環的注意事項:
- SPFA 只能判斷從起點出發能不能遇到負環。如果有一個負環不能從起點出發達到,這個負環就碰不到。
- 一些簡單的優化有極佳的效果,例如
dist[]
初值設置為 0 和 DFS 版 SPFA 在負環判斷中表現良好。
-
一定要注意,如果用 Dijkstra 跑費用流,那么每次最短路求完之后一定要修改勢為當前最短路的結果。
設在當前完成了最短路之后,結點 \(u\) 的最短路標號為 \(d_u\),那么在一次增廣后,原先圖上的邊 \((u,v,w)\) 仍然滿足 \(d_v\le d_u+w\)。而如果當前邊有流經過,反向邊 \((v,u,-w)\) 被加入圖中,那么 \((u,v,w)\) 必然在原圖最短路上,也即 \(d_v=d_u+w\),反過來也可以得到 \(d_u=d_v-w\),所以此時 \(w'=w+d_v-d_u=0\),仍然是非負的。
最初的時候我們會跑一次最短路求出勢,而這個勢只是對於原圖有效,增廣后就不能再使用它了。由於原圖上可能有一些特殊性質,因而我們可以用非 Bellman-Ford 類算法求出它,以獲得較好的復雜度。
-
二分圖匹配的匈牙利算法:
枚舉點搜索增廣路的時候,理論上如果點已經匹配,那么再去搜索就是無效的,甚至可能導致錯誤。
......然而我沒有判掉這種情況,甚至愉快地通過了 uoj 的模板題.....
二分圖最大權匹配的 KM 算法:
話說這個東西貌似也可以叫做匈牙利算法。如果某些點的
slack
還不夠小,那么記得修改slack
而不是讓它保持原樣。rep( i, 1, N ) if( ! visY[i] ) { if( slk[i] > delt ) slk[i] -= delt; // REMEMBER to reduce slack due to change of labX !!!!! else { ... } }
-
分治過程中,如果在點上有信息或限制,並且運算不滿足冪等性,那么應當保證分治重心的信息不會被重復統計,在“統計”和“存入”的時候需要注意是否應當計入分治重心。
-
判斷(半)歐拉圖、求歐拉(回)路一定要檢查圖是否連通!!!
-
在圖上進行記憶化搜索的時候,如果沒有搜索到需要的結果,那么也要聲明某個狀態已經被搜索過了,不能把狀態留在那里等着其它起點再去找一遍,浪費時間。
-
-
思考不全面的:
-
發呆想了半天才發現枚舉的那一天可以放在欽定的前 \(k\) 個里面。
處理方法:思路一定要全,分清問題的主從關系。
-
注意,Pollard Rho 的原理是,假如 \(n=p\times q,p<q\),那么我們隨機生成一組數 \(x_1,x_2,\dots,x_k\) 之后,如果 存在 \(i,j,x_i\equiv x_j\pmod p,x_i\not\equiv x_j\pmod n\),那么 \(\gcd(x_i-x_j,n)\) 一定會生成一個 \(n\) 的一個非平凡因子。
因此,朴素做法是枚舉環長,也即 \(j-i\),並取 gcd;而加上 Floyd 判環法之后則是枚舉 \(i\) 並檢查 \(2i,i\) 兩個元素。加上了倍增,其實也就是在倍增環長,因此需要每次和路徑首個元素做差,而非和前一個元素做差。
又把 Pollard Rho 寫錯一遍.jpg。
錯誤一:對於迭代方程 \(x_{i+1}=x_i^2+c\),其中初始值 \(x_0\) 和 \(c\) 都應該在范圍 \([1,n-1]\) 中生成,否則在 \(n\) 非常小的時候會翻大車。
比如,當 \(n=4\) 的時候,很容易直接陷入死循環。
錯誤二:如果樣本累積若干次后,變成了 0,就應該直接退出該次取樣,而不能取 \(\gcd\) 之后以為找到了非平凡因子。
-
注意,旋轉的時候是尋找 \((i,i+1)\) 這條邊的對踵點,因此假如為 \(j\),那么應該檢查 \(\operatorname{dist}(i,j)\) 和 \(\operatorname{dist}(i,j+1)\)。
當然將四個點對全部檢查一遍自然沒什么問題 -
一定要注意,如果用 Dijkstra 跑費用流,那么每次最短路求完之后一定要修改勢為當前最短路的結果。
設在當前完成了最短路之后,結點 \(u\) 的最短路標號為 \(d_u\),那么在一次增廣后,原先圖上的邊 \((u,v,w)\) 仍然滿足 \(d_v\le d_u+w\)。而如果當前邊有流經過,反向邊 \((v,u,-w)\) 被加入圖中,那么 \((u,v,w)\) 必然在原圖最短路上,也即 \(d_v=d_u+w\),反過來也可以得到 \(d_u=d_v-w\),所以此時 \(w'=w+d_v-d_u=0\),仍然是非負的。
最初的時候我們會跑一次最短路求出勢,而這個勢只是對於原圖有效,增廣后就不能再使用它了。由於原圖上可能有一些特殊性質,因而我們可以用非 Bellman-Ford 類算法求出它,以獲得較好的復雜度。
-
關於網格圖上高斯消元的問題。
設網格大小為 \(n\times m\),且 \(n,m\) 同階。這類問題一般有兩種解決方案:
-
帶狀矩陣高斯消元,本質上就是利用位置 hash 的性質偷了個懶,大大減少了所需掃描的范圍。復雜度為 \(O(nm\times \min\{n,m\}^2)\)。
但是這個方法在網格圖消元之外比較危險。網格圖上可以保證對角線上系數為 1,但是其它問題中就不一定了。如果出現了需要換行的情況就比較麻煩,不好好處理很容易直接破壞掉帶性質;一般來說,如果需要換,那么我們會選擇換列而非換行,這是因為如果某一列之下有超出帶的范圍的系數,我們可以在之后的消元中解決掉它。
-
確定主元,這個相對來說適用性更廣,效率更高,可以做到 \(O(n^3)\),但是寫起來較為復雜......
-
-
關於 BSGS 算法和擴展 BSGS 算法:
一句話:求離散對數的時候一定要注意 \(p=1\) 和 \(b=1\) 的特殊情況!!!
-
注意一點,當 \(t> \max \{\lfloor\log_2 s\rfloor\}\) 的時候,此時倍增長度不再由 \(\log_2 s\) 決定,而是由 \(\log_2 n\) 決定。注意邊界的細節。
-
-
其它算法各種亂七八糟鐵鍋亂燉導致寫掛的:
-
關於整體二分的寫法問題。
這里由於我們不能簡單地修改要求,所以在進入右區間的時候,我們必須保證左區間的邊已經被加入。
如果寫法是在離開區間時刪除新加入的邊,且在進入右區間之前將所有的左邊的邊加入,那么沒有問題。
但是如果寫法是在葉子的位置加入邊,並且之后不刪除,那么就必須保證葉子可以到達(現在外層相當於是遍歷線段樹的過程)。因此如果:
void Divide( const int qL, const int qR, const int vL, const int vR ){ if( vL > vR || qL > qR ) return ; ...}
那么就有問題(因為很有可能詢問區間為空就返回,沒有到葉子節點)。
-
多校賽某題。
\(n=10^{10}\) 的時候做杜教篩,預處理范圍只有 \(10^6\),導致杜教篩跑得非常慢......
處理方法:熟悉時間復雜度原理,杜教篩需要預篩到 \(n^{\frac{2}{3}}\) 才可以。
-
后綴數組模板。
注意中間的“桶”數組大小與字符串長度相同,而非與字符集大小相同!
const int MAXN = 1e6 + 5, MAXC = 300;int buc[MAXN]; // 注意不是 MAXC
-
關於不同顏色計數的樹上差分方式。
如果按照 DFN 排序之后,某個顏色之前和之后都有顏色出現了,那么應該選擇一個較低的 LCA 來刪除多於貢獻,而不應該在兩個 LCA 上都刪除一遍。
-
DP 的轉移:
比較下面的兩種轉移寫法:
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ ) #define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- ) int h[][]; //other codes omitted. per( p, N, 0 ) per( q, N + 1, 0 ) { int tmp = 0; for( int k = 0, up = 1 ; i * k <= p && j * k <= q ; k ++ ) Upt( tmp, Mul( h[p - i * k][q - j * k], Mul( ifac[k], up ) ) ), up = Mul( up, Add( g[i][j], k ) ); h[p][q] = tmp; }
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ ) #define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- ) int h[][]; //other codes omitted. for( int k = 1, up = 1 ; i * k <= N && j * k <= N + 1 ; k ++ ) { up = Mul( up, Add( g[i][j], k - 1 ) ); for( int p = N - i * k ; ~ p ; p -- ) for( int q = N + 1 - j * k ; ~ q ; q -- ) Upt( h[p + i * k][q + j * k], Mul( Mul( ifac[k], up ), h[p][q] ) ); }
如果要求用
g[i][j]
將所有k
都轉移一遍之后,h[][]
才能發生變動,那么第二種寫法就有問題。這是因為,轉移過程中較小的k
會對較大的k
造成影響,相當於是按照k
划分而不是按照g[i][j]
划分。總而言之,明確 DP 過程的階段,同時找出合適的寫法。
-
二分的結果:
如果在二分過程中計算方案,那么最終二分出來的答案是
l
而不是上一次代入的參數。因此,輸出方案之前需要重新構造一遍!!! -
關於高斯消元。
之一:高斯-約旦消元的精度似乎比高斯消元的精度要高。但是據說如果高斯消元不處理精度誤差那么就不會出現問題。
之二:跟行列式不同,需要將單行的主元系數化一。
-
關於 WQS 二分。
我們二分的是切凸包的斜率。如果目標位置被共線的邊卡住了,那么我們只能二分出正確的斜率,而多半不能找到正確的切點。但是我們只需要知道截距和斜率就可以算出答案,所以此時的答案不是切點的結果,而是目標位置在該斜率下的結果。
-
實現細節
-
編譯過程出錯的:
-
由於
g++
的版本與現行版本不一致,庫的包含關系就不完全相同。這個時候如果缺少頭文件也有可能會本地通過編譯,但是提交后可能會 CE。典型例子就是
Dev-C++
上cmath
還包含在algorithm
里面。如果僅包含algorithm
並且使用cmath
內部的函數,在新版的g++
下編譯就會報錯。 -
經典的“隱形”庫函數
y0(double), y1(double), yn(int,double); j0(double), j1(double), jn(int,double)
,這幾個小混蛋都在cmath
里面,存在於編譯器的include
文件夾里的math.h
里面。問題是,它們幾乎找不到蹤跡。如果去 C++ reference 里面查你根本找不到它們。
如果在 Windows 環境下使用 9.3.0 版本的
g++
編譯也不會檢查出問題,但是,但是,一旦包含cmath
並且自定義了重名的變量/函數,那么在Linux
環境下編譯的時候才會 CE。😓去查了一下,發現
j.()
表示的是第一類貝塞爾函數,y.()
表示的是第二類貝塞爾函數。 -
初始化疑雲:
這里集中了一些由於不當初始化而導致的編譯問題。
在 Compiler Explorer 上以
-std=c++17
編譯,所以匯編碼應該對應的是 Intel x86-64 的 CPU。-
經典錯誤一:使用大括號初始化數組:
如果是平凡地直接使用大括號括起來,全部清空,那么初始化不會出問題:
struct QwQ { // something }; int a[100000] = {}; QwQ b[100000] = {};
無論使用內置類型或自定義類型。
但是,如果在大括號里面初始化了某些位置的值,那么會導致大括號被展開成完整的初始化列表(補上空白)。一個直接的表現就是編譯出來的可執行文件非常大。當數組很大的時候效果尤其明顯:
int a[10000000] = { 1, 2 }; // with a large *.exe appearing
-
經典錯誤二:錯誤地使用結構體初始化:如果在結構體或類的初始化中對非內置類型數組進行初始化,無論如何賦初值,如下:
struct Rubbish { int rub[1000]; Rubbish(): rub{} {} // safe operation }; struct RubbishBin { Rubbish a[1000]; RubbishBin(): a{} {} // DANGEROUS operation }; RubbishBin rb;
只要對這樣的類型進行實例化,那么在匯編碼中,構造函數會被完全展開。在上面的這段代碼中,實際效果就是對於
a
中每個對象都調用Rubbish :: Rubbish()
。直接看一下匯編碼的樣子:外在的表現就是編譯時間變長,但是可執行文件大小正常,嗯。
實際上,由於全局變量會自動清空(匯編碼里面會有一個
.zero
的指令)所以寫成:struct Rubbish { int rub[1000]; Rubbish(): rub{} {} // safe operation }; struct RubbishBin { Rubbish a[1000]; RubbishBin()/*: a{}*/ {} // Fine }; RubbishBin rb;
也可以達到清空的效果,並且不會出問題。
-
-
-
清空的問題
-
同一變量多次重復使用,中間沒有清空,直接暴斃:
int lst = N + 1; for( int i = N ; ~ i ; i -- ) { nxt[i][1] = lst; if( S[i] == '1' ) lst = i; } //這里本應該清空 lst 的 for( int i = N ; ~ i ; i -- ) { if( S[i] == '1' || S[i + 1] == '0' ) nxt[i][0] = lst; if( S[i] == '0' ) lst = i; }
-
對於判定性問題,如果我們一旦搜索到符合要求的結果就退出,那么在多次搜索的情況下,一定要注意在退出之前有沒有將公共內存清空。無論是使用
return
結束語句還是使用throw
跳轉都需要注意這個問題。 -
注意分治過程中,公共數組的清空,特別是在改寫法的時候,不要忘了加上被修改結構的清空過程。
-
-
注意邊界
-
預處理應該按照值域為范圍來清,結果只清到了點數范圍。
-
模擬賽中出現的 typo 。
按理說應該是很常見的問題,我居然還在犯。遍歷 \(n\times m\) 的平面的時候,弄混了 \(n\) 和 \(m\) 的關系,於是寫錯了循環邊界,成功地掛分了:
for( int i = 1 ; i <= n ; i ++ ) //這里的 n 應該是 m ...
記清楚變量的含義,尤其是常見易混變量 \(n,m,x,y\) 之類的。
-
這次在預處理逆元的時候,恰好處理到了 \(2^{18}-1\) 的范圍,但是實際調用時使用到了 \(2^{18}\) 的逆元,然后就爆炸了......
處理方法:一定要注意,使用任何函數/循環/數組內存時,是否會越界或超出有效范圍;此外,在對復雜度影響不大時也可以適當使用快速冪/exgcd 求逆元;
-
「Gym102979L」Lights On The Road
第一次寫 K 短路,結果被坑到了兩個地方:
-
建圖都建錯了,頭尾兩個虛點不能連在一起,而 \(n=1\) 的時候我會連起來。這個寫的時候確實沒想到;
-
當 \(n=1\) 的時候,最短路樹上沒有非樹邊,這個時候堆為空。將空節點插進去做 K 短路就會 RE......
這個問題更大一些,寫的時候想到了這一點,但是沒有想到堆會成為空,沒有判。
處理方法:注意邊界情況,不要被細節卡了。
-
-
某道計數題。
數據范圍明確給出:
這就說明,輸入數據可以有 \(n<k\)。但是我的程序,如果不特判 \(n<k\) 就會干出奇奇怪怪的事情,導致各種 WA。
-
答案的范圍是 \([0,2^{31})\),所以計算 \(L,R\) 的時候,如果中途不取模可能會爆
int
。處理方式:涉及到的變量如果非常之大,每一步都取模是必要的;對拍的時候也應該造完全極限的數據。
-
最終求出來的次小生成樹的答案可以達到 \(10^{14}\),但是最大值只開到了 \(10^9\)。
處理方法:注意,不同的數據類型應該單獨設計最大值,最大值盡量不要多個數據類型通用。
-
最開始,給每個數組開一個
vector
,里面存的是真實值,所以范圍是 \([0,10^8]\),用int
完全裝得下。后來,發現不應該裝真實值,應該裝前綴和,所以就改了內容,沒改類型,范圍變成了 \([0,10^{14}]\),
int
就會爆炸。這樣的問題應該在最初編寫期就解決,這說明思考還不完全就着急寫代碼了。
-
保證下標或指針指向的是有效的空間。
這里主要關注如何控制下標或指針不會越界,而不考慮空間開小了的情況。
例如 「模板」最小表示法,如果給定的串是
aaa...aa
的形式,則需要判斷長度指針 \(k\) 是否到達了 \(|S|\),否則會訪問到有效的空間之外。又注:對於下標進行運算的時候也要注意是否會越界。例如,進行減法的時候需要確認是否會越過下界,進行加法的時候需要確認是否會超出上界。
-
經典錯誤:如果用
vector
實現多項式,那么在寫加法的時候,很容易忘記邊界,將任意位置的計算都寫成F[i]+G[i]
。但實際上,如果i
取遍所有有效位,那么下標很有可能越界,導致 UB,返回一些亂七八糟的值。 -
陽間邊界問題:左移超出邊界是 Undefined Bahaviour,比如不能算
1llu << 64
,不然返回什么東西誰也說不清楚。
-
-
實現有誤、不精細導致超時的:
-
[HDU6334]Problem C. Problems on a Tree
畫蛇添足,本身不需要用
map
的地方偏偏使用了,導致程序及其慢,map
占用了將近 \(\frac 1 3\) 的時間。補充:可以使用
clock()
檢查運行時間。 -
[CF446C]DZY Loves Fibonacci Numbers
分塊寫法,清理標記的時候,沒有判斷有沒有標記:void Normalize( int id ) { //這里缺少了是否存在標記的判斷 for( int k = lef[id] ; k <= rig[id] ; k ++ ) //...... }
最開始以為分塊的標記和線段樹類似,現在才意識到,分塊下放標記的時間是 \(O(T)\) 的,所以不判掉就會特別慢......
-
求歐拉路一定要用當前弧優化。
-
對於編碼方式比較復雜的問題,一定要注意各種編碼是否正確轉換了。
比如,二分圖匹配的時候,經常會犯忘了特殊處理右部點標號的問題。
-
注意,鄰接表建圖后,遍歷邊的順序是與輸入順序相反的。
-
只要可以離散化,都建議寫離散化之后再寫線段樹。權值線段樹實在是太慢了......
當年冰火戰士也有這個詭異的坑點。
-
保證復雜度的剪枝寫太弱了,導致復雜度是錯的。
需要注意寫下來的代碼(比如剪枝)是否和所想的相同。本質上還是在問思路是否清晰。
在洛谷上居然還可以卡過去,實在是誤人子弟。
-
-
數據雜糅,不進行區分:
-
題目本身並不難,但是要注意,由於我們將值存儲在
Trie
的末尾,所以被刪除了的節點本質上就是變成了空節點。因此,空節點存儲的值應該等於被刪除了的值。例如,如果空節點值為0
,那么被刪除的節點的值也應該是0
。否則,
Trie
的結構就可能導致將空節點判斷為有值的節點的錯誤出現(例如,插入一個較長串,並查詢它的前綴)。處理方式:同類標記盡量統一。
-
線段樹(以及任何靜態結構)上,如果需要刪除結點,那么在維護最值的時候我們一般會將要刪除的結點賦成一個極值。
但是一定要注意,這里的極值應當區分初始化的極值,尤其是在需要同時取出極值所在的位置的時候。這里就需要明確:初始化所賦的極值是可以取的,但是刪除所賦的極值就是為了避免取到的。
-
-
類型寫錯的:
-
整除分塊的時候,使用中間變量記錄取整除的值,但這個中間變量沒有開
long long
:for( long long l = 1, r ; l <= n ; l = r + 1 ) { int val = n / l; // 就是這里!!!應該開 long long!!! }
-
-
各種奇怪的問題:
-
最大生成樹,運算符直接重載為了小於。
-
【UR #2】跳蚤公路
循環變量用上了不常用的名字,結果之后就寫錯名字:for( 對 i 循環 ) for( int u = 1 ; i <= n ; u ++ ) //......
處理方式:盡量規避奇怪的循環變量名稱,同時寫的時候也要帶腦子。
-
關於取模安全的問題
其實就是計算過程中,尤其是取模,很容易寫着寫着就忘記取模了。
簡單的加減乘除還比較容易記住,但是進行像自加、自減、賦字面量這樣的運算的時候很容易忘記取模。
比較安全的做法是:
- 封裝幾個函數替代常用的運算,然后在函數內部取模。運算時強制使用它們;
- 封裝模域類,然后只用這個類運算。這個對於多模數的情況比較友好;
常見的問題有:
-
注意特殊的模數,尤其是題目輸入模數的時候,注意模數是否可能為 1。
例如,有的題目取模的模數是單獨輸入的,特別注意模數可否為 1,特別注意賦的初始值是否落在正確的范圍內,不確定可以取一下模;
沒有把握就在輸出取模,
雖然這看起來也只是權益之計。
-
DP 的邊界、清理:
-
聯測某題。
在轉移的時候,本應在所有數據計算完之后再對不合法數據進行清理,結果邊清理邊計算,導致較小的不合法數據影響了之后的結果。
處理方法:注意操作的順序,不止是在 DP 的轉移中,寫的時候就應該注意到順序的問題。
-
-
浮點數的精度問題:
-
「Gym102798E」So Many Possibilities...
實數概率 DP,因為 \(\epsilon\) 設得太大,導致用它判空狀態的時候將有效狀態也判成空,漏了不少結果,答案偏小......
用實數算 DP 的時候,沒有必要用上 \(\epsilon\)。需要用來卡常的時候,算好可能的系數量級,然后反推 \(\epsilon\) 的大小;寧可設得小一點,也不要漏掉有效狀態。不要再出現這樣不知所謂還被卡了半天的錯誤!
-
模擬賽題目。
需要對浮點數進行 \(10^6\) 組運算,每組運算
+,-,*,/
都齊了。結果就
不出意外地爆掉了double
的精度,只有開long double
才能通過。注意
double
在大數小數混合運算時的精度問題!!! -
奇妙的題目。構造割的時候,如果源點的出邊全部滿流了,那么一種割就是 \(\newcommand\set[1]{\{#1\}}[s,V\setminus \set{s}]\),然而這顯然不是我們想要的。
為了避免這種情況,我們必須調整答案,使得源點的出邊略有剩余容量;具體操作起來就是讓答案變小一點,而后從 \(s\) 開始遍歷尋找一組割。
-
-
STL 的奇妙特性:
眾所周知,為了
使操作更麻煩簡化操作,STL 的set
和map
等關聯容器內部基本上都提供了reverse_iterator
這種迭代器。從字面意思就可以知道,這種迭代器是逆向訪問容器的。比如
set
默認使用 < 比較,如果用reverse_iterator
來訪問它則會從大到小遍歷set
的元素。相應地,容器也會有rbegin()
和rend()
這樣的函數,一看就懂了。問題是,
reverse_iterator
的運算也是和iterator
呈鏡像對稱。比如++ reverse_iterator
,那么從set
原本的順序來看,就相當於向變小的方向移動,反過來也類似。如果和
iterator
混用就很容易弄錯,比如今天上午我就調了 0.5 h。此外,一般來說容器的
erase()
都只會接受iterator
。既不可以往里面丟reverse_iterator
,也並不存在rerase()
這樣的函數。
對於
vector
這樣使用random access iterator
的容器,一般來說使用迭代器訪問的效率低於下標訪問。另外,如果用
for( x : y )
這樣的循環,那么內部是使用迭代器實現的,因此需要注意大數據下的運行效率。 -
讀題出錯的:
-
有點奇怪。題目里面說
There is no pipe which connects nodes number 1 and N
,但是它的真正含義是不存在從 1 流向 \(n\) 的管道,但是可以存在從 \(n\) 流向 1 的管道。這個時候就會出現環流,因此需要建立新的源點與匯點,避免“源”和“匯”出現環。思考的時候要細致一點,每個方面都要想到;同時對於題目理解要清晰到位,結合好題目前提與背景。
-
-
任何時候,使用 DFS 傳回
bool
信息表示某個過程是否已經結束時,應該在任何一次遞歸調用結束之后,查詢返回值並判斷是否應該結束過程。否則會導致各種亂七八糟的問題。 -
注意任何數學運算是否安全。
例如 「POI2011 R1」避雷針 Lightning Conductor 這一題,需要注意的細節是,實值函數 \(a_j+\sqrt{|i-j|}\) 才具有單調性。雖然實際求答案的時候,\(a_j+\sqrt{|i-j|}\) 和 \(a_j+\lceil\sqrt{|i-j|}\rceil\) 是一樣的,但是前者是實值函數,后者是整值函數。而在整值函數上單調性已經被破壞了,因此只要涉及到與決策有關的比較,都必須用實值而非整值。
-
多組數據,注意輸出換行!!!
不是開玩笑,尤其是在用
cout
輸出的時候。由於不常用,就很容易忘記輸出endl
。
-
-
調整代碼時出的問題:
-
對某個數組進行平移(或者其它下標變換)時,並沒能做到對於每個用到數組的位置都去修改下標,最終導致錯誤訪問。
這個問題解決起來比較麻煩,最好的方法還是一開始就確定好如何定下標,避免以后再去修改。
-
比賽與策略
-
2020.08.24 的模擬賽
-
多組數據,小范圍搜索,大范圍騙分的時候,沒有注意小范圍搜索的用時,導致 TLE 。
處理方式:不要貪心,同時嚴格把控小范圍的時間空間開銷,自己要預先測試!
-
從母串 \(S\) 里面提取子串 \(T\) ,然后本應該在 \(T\) 上進行的操作,全部搞在了 \(S\) 上面,導致 WA 。
-
中途修改寫法,把外部的寫成的某一個步驟封裝或者改寫成函數。其中函數的某個參數是全局變量,但是內部相關參數沒有改名字,導致 WA 。
處理方式:善用替換功能。
-
-
2020.08.27 的模擬賽
-
寫 DP,雖然時間復雜度不對頭,但是
我很自信,於是就把空間開到了極限,希望能卡過。然后它就 MLE 了,嗚嗚嗚~
處理方式:比賽最后檢查的時候一定要算一遍空間,不要太貪心。
補充:善用
MinGW
內部提供的size
,可以作為靜態空間的參考; -
最后 45 分鍾 rush 一個正解。由於人很慌,而且是數據結構題目,所以小數據就拼了個暴力上去,想着是有保底的分數。
測出來我就發現,我正解寫對了,但是暴力居然寫錯了?!
-
以為 T3 不太難,於是硬剛它。沒有想到它是很惡心的結論題目,於是我就花費很多時間,換來了 10pts 的好分數。
處理方式:開場時每道題先粗略地思考一下,評估難度;規划好時間,避免吊死在一棵樹上,一定不可干這種傻事!。
補充:如果覺得題目難度比較大,那么應該果斷先寫出正確的暴力爭取基礎分。
-
-
2020.09.19 的模擬賽:
-
不讀題的:
某題有多解,要求輸出 " 字典序最小的一組 " 。
由於方案構造起來並不復雜,所以......直接沒有看到這個要求(甚至過了大樣例),暴斃。
-
-
2021.03.27 的 NOI Online:
想了簡單的 \(O(nk^2)\) 之后,覺得這個算法:啊, " 反正 ' 最多 ' 只能過 \(k\le 100\) 的部分分,就照着空間開吧 " 。
於是給 \(k\) 開了 100 的 int 的空間,於是就爆炸了!
處理方法:請對着題目數據范圍,在保證不會失分,不會 MLE 的情況下開空間。
-
UNR #5 T3
暴力 40pts,最后應該需要檢查每一個狀態,但是我直接把最后一個狀態當做答案......
明明寫個對拍就能找出的錯,居然沒有發現,也沒有想過去寫對拍,反而測了中樣例就不管了......
即使當場只准備寫騙分,也一定要將工作做齊備,對拍、人肉調錯不能少!
-
某道計數題。
數據范圍明確給出:
這就說明,輸入數據可以有 \(n<k\)。但是我的程序,如果不特判 \(n<k\) 就會干出奇奇怪怪的事情,導致各種 WA。
明確提示:
-
一定要注意數據范圍,注意特判邊界情況;
-
讀題不要想當然,如果有時間可以考慮更全面的情況,把程序修改得盡量健壯;
-
造數據自測和對拍的時候一定要注意達到任何可能的邊界,極大極小都要碰到;
-
檢查流程不能遺漏,萬不可白丟分。
-
-
2021.10.11 模擬賽:
有一道題目改到一半棄療了,結果提交的源程序包含了錯誤的代碼,甚至會直接 CE,導致💥。
處理方法:檢查時間要留充分,需要檢查文件、空間、編譯和所有樣例!!!
-
2021.10.16 模擬賽:
策略出大問題,后面兩道題目選一道死磕,以為自己寫好了正解,結果發現讀錯題了,最后暴力都沒寫過。
總結:一者,如果模擬賽的后面兩道題明顯不簡單,則一定要積極寫部分分,千萬不要死磕正解;二者,寫較難的題目的時候一定要從暴力先寫起,一來可以驗證正確性,二來可以保底;三者,給每道題安排好時間,避免死磕在某一題目上。
-
2021.10.17 模擬賽:
以為自己 T3 過了,其實是假算法,
還過了大樣例,掛麻了。總結:對於有一定思維過程的題目,只要不是太難寫,一定要寫好純暴力對拍!
-
2021.10.18 模擬賽:
第一題比較簡單,以為自己可以寫對,並且覺得對拍效果不大,結果就沒有寫對拍,然后就 WA 爆了。
總結:前面兩道題目一定不能丟分!!!基礎題目一定要保證穩當!!!一定要留出 30min+ 的時間檢查題目,尤其是前兩道題!!!寫對拍!!!
-
2021.11.3 模擬賽:
最后一道題目的部分分沒有深入思考。明明有一個很簡單的部分分居然都沒有去想過。
如果有時間,一定要在准備騙分的題目上多思考幾個部分分。不要因為某道題准備騙分而留出較少的時間來思考和實現。
-
2021.11.7 模擬賽:
比賽開始粗略地掃了一眼題目,結果把最后一題的題目意思讀錯了。等到去做題的時候頗為匆忙,也沒有來得及再仔細確認題目,導致悲慘的讀錯題幾乎爆炸。
讀題一定要仔細,一開始可以只把握大意,但是在做題之前一定要認真閱讀題面。
-
2021.11.14 模擬賽:
很多人掛掉了 T1,包括我。
簡單地轉化了問題之后,就開始把一道類似但實際上並不等同的題目的思路往上面套,並且用錯誤思路同時完成了提交代碼和對拍程序,最后兩個錯誤程序拍得相當歡快——然后爆炸了。
世界上沒有兩個相同的問題,進行任何類型的遷移都要比較兩個情景是否是一致的,是否有邊界區分......總之,無論什么時候,“套做法”都得慎之又慎;
此外,盡量避免對拍用的“正確”程序和測試程序有較大重合,包括思路和實現——除非測試內容與重合內容關系不大,否則不要冒險。
-
2021.11.16 模擬賽:
今天的比賽時間分配比較失衡。
T1 明明很簡單,結果自己想的時候就只會很復雜的做法,在這上面花了 1 h 40 min。
T2 其實也不難,但是想題的時候掉到死胡同里去了,居然也不會倒出來重新想一想;一條道走到黑,這是個壞習慣。
T3, T4 總共只花了 1 h,導致需要相當思考的 T4 連暴力也很難寫出來。
完成前兩題就花了 2 h 30 min,效率低而且收益也低,寫代碼難以集中,錯碼率挺高的。
之后模擬賽不多,一定要有緊迫感,抓緊時間。如果覺得一時半會兒想不出來就先寫個保證正確的暴力,保證每道題目的保底暴力分數。
-
2021.11.17 模擬賽:
主要是 T2 一開始思路錯了,導致白花了 40 min 而一分也沒有。
注意,一定要深入思考之后,確定沒有問題再開始寫代碼,不要空耗時間尋求心理安慰;如果把這些浪費的時間投入到之后的暴力中,事實證明收益也很不錯。