「WTF」鐵鍋亂燉


如題所述,專門記錄自己在各類考試、做題中出現的花式錯誤。

算法思想

  • 分塊相關算法寫掛的:

    • [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, ... );
          ......
      }
      

      事實上應該將左右邊界限制在塊的范圍內

    • 「LG P3863」序列

      其一:分塊修改時,應該枚舉完整個 \([lef, rig]\) 的區間,而不是只枚舉 \([qL, qR]\) 內的數。這種情況主要出現在區間的元素被重排了的情況。

      其二:無論是分塊還是線段樹,一定要注意將交集為空的情況判掉!

  • 數據結構寫掛的:

    • [ARC097F]Monochrome Cat

      使用了倆優先隊列的 " 可刪堆 " ,結果在 pusherase 的時候都沒有清除多余元素,於是堆中冗余元素就超多,結果 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 一下。👍

      額外提醒一點,惰性刪除堆千萬不要刪除不存在的元素

    • [CF817D]Imbalanced Array

      這道題用單調棧維護所有位置的最值的和的時候,細節很多。尤其注意的是,每個點管轄的區間實際上是從棧內的上一個元素開始的,也就是 \((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 );
      }
      
    • [NOI2021] 密碼箱

      第一次犯錯:以為區間反轉對應的端點可以直接算出來,結果需要用 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

      其二,可撤銷並查集不能寫路徑壓縮,不能寫路徑壓縮,不能寫路徑壓縮!!!

  • 標記處理的問題:

    • 「JLOI2015」城池攻占

      注意:左偏樹刪除堆頂的時候,它的標記可能還沒有下傳。因此需要下傳標記之后再合並左右子樹

      有的數據結構也是同樣的。如果在刪除的時候,被刪除的節點對於其他節點的影響需要全部清除

  • 一道重鏈剖分的題目,寫錯了兩個地方:

    • 其一,居然把重鏈剖分寫成了輕鏈剖分😢,就一個符號寫錯了;

      • 其二,中途某個位置沒有開 long long,一直沒有檢查到

      處理方法:第一個問題屬於寫錯了只會影響復雜度的類型,應該多測一下極限數據,什么樣子的都應該測一下;第二個問題則應該檢查每一個變量類型開得是否合理。

  • 圖論算法寫掛的:

    • [AT4569]Connecting Cities

      使用 Boruvka 算法的時候,一定要注意,找出的是點,對應的是連通塊,並查集維護的根與連通塊的編號沒有任何關系,千萬不要用混了!

    • 網絡流的建圖:

      「UOJ77」A+B Problem

      調了一個半小時,終於發現問題所在:竟然是反向邊弄錯了,最初 cnt 賦值為 0 而非 1 !!!

      處理方法:單個變量不要忘記初始化!

    • 關於 SPFA 判斷有無負環的注意事項:

      • SPFA 只能判斷從起點出發能不能遇到負環。如果有一個負環不能從起點出發達到,這個負環就碰不到。
      • 一些簡單的優化有極佳的效果,例如 dist[] 初值設置為 0 和 DFS 版 SPFA 在負環判斷中表現良好。
    • 「ABC214H」Collecting

      一定要注意,如果用 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 { ... }
      }
      
    • 「SPOJ」Free Tour II

      分治過程中,如果在點上有信息或限制,並且運算不滿足冪等性,那么應當保證分治重心的信息不會被重復統計,在“統計”和“存入”的時候需要注意是否應當計入分治重心。

    • 判斷(半)歐拉圖、求歐拉(回)路一定要檢查圖是否連通!!!

    • 在圖上進行記憶化搜索的時候,如果沒有搜索到需要的結果,那么也要聲明某個狀態已經被搜索過了,不能把狀態留在那里等着其它起點再去找一遍,浪費時間。

  • 思考不全面的:

    • [AGC034C] Tests

      發呆想了半天才發現枚舉的那一天可以放在欽定的前 \(k\) 個里面。

      處理方法:思路一定要全,分清問題的主從關系

    • Pollard Rho 模板 LGP4718

      注意,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\) 之后以為找到了非平凡因子。

    • 旋轉卡殼模板 LGP1452

      注意,旋轉的時候是尋找 \((i,i+1)\) 這條邊的對踵點,因此假如為 \(j\),那么應該檢查 \(\operatorname{dist}(i,j)\)\(\operatorname{dist}(i,j+1)\)

      當然將四個點對全部檢查一遍自然沒什么問題

    • 「ABC214H」Collecting

      一定要注意,如果用 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\) 的特殊情況!!!

    • 「IOI2021」地牢游戲

      注意一點,當 \(t> \max \{\lfloor\log_2 s\rfloor\}\) 的時候,此時倍增長度不再由 \(\log_2 s\) 決定,而是由 \(\log_2 n\) 決定。注意邊界的細節。

  • 其它算法各種亂七八糟鐵鍋亂燉導致寫掛的:

    • [AGC002D] Stamp Rally

      關於整體二分的寫法問題。

      這里由於我們不能簡單地修改要求,所以在進入右區間的時候,我們必須保證左區間的邊已經被加入。

      如果寫法是在離開區間時刪除新加入的邊,且在進入右區間之前將所有的左邊的邊加入,那么沒有問題。

      但是如果寫法是在葉子的位置加入邊,並且之后不刪除,那么就必須保證葉子可以到達(現在外層相當於是遍歷線段樹的過程)。因此如果:

      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()。直接看一下匯編碼的樣子:

        asm.png

        外在的表現就是編譯時間變長,但是可執行文件大小正常,嗯。

        實際上,由於全局變量會自動清空(匯編碼里面會有一個 .zero 的指令)所以寫成:

        struct Rubbish {
            int rub[1000];
            
            Rubbish(): rub{} {}
            // safe operation
        };
        
        struct RubbishBin {
            Rubbish a[1000];
            
            RubbishBin()/*: a{}*/ {}
            // Fine
        };
        
        RubbishBin rb;
        

        也可以達到清空的效果,並且不會出問題。

  • 清空的問題

    • [CF1383E]Strange Operation

      同一變量多次重復使用,中間沒有清空,直接暴斃:

      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 跳轉都需要注意這個問題。

    • 「UR #21」 挑戰最大團

      注意分治過程中,公共數組的清空,特別是在改寫法的時候,不要忘了加上被修改結構的清空過程

  • 注意邊界

    • 預處理應該按照值域為范圍來清,結果只清到了點數范圍。

    • 模擬賽中出現的 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......

        這個問題更大一些,寫的時候想到了這一點,但是沒有想到堆會成為空,沒有判。

      處理方法:注意邊界情況,不要被細節卡了

    • 某道計數題。

      數據范圍明確給出:

      mistake.png

      這就說明,輸入數據可以有 \(n<k\)。但是我的程序,如果不特判 \(n<k\) 就會干出奇奇怪怪的事情,導致各種 WA。

    • 「SPOJ」MAXOR

      答案的范圍是 \([0,2^{31})\),所以計算 \(L,R\) 的時候,如果中途不取模可能會爆 int

      處理方式:涉及到的變量如果非常之大,每一步都取模是必要的;對拍的時候也應該造完全極限的數據

    • [BJWC2010]次小生成樹

      最終求出來的次小生成樹的答案可以達到 \(10^{14}\),但是最大值只開到了 \(10^9\)

      處理方法:注意,不同的數據類型應該單獨設計最大值,最大值盡量不要多個數據類型通用。

    • 「CF1442D」Sum

      最開始,給每個數組開一個 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)\),所以不判掉就會特別慢......

    • 求歐拉路一定要用當前弧優化

    • 對於編碼方式比較復雜的問題,一定要注意各種編碼是否正確轉換了

      比如,二分圖匹配的時候,經常會犯忘了特殊處理右部點標號的問題。

    • 注意,鄰接表建圖后,遍歷邊的順序是與輸入順序相反的

    • 「Gym102268」Best Subsequence

      只要可以離散化,都建議寫離散化之后再寫線段樹。權值線段樹實在是太慢了......

      當年冰火戰士也有這個詭異的坑點。

    • 「CEOI2006」 ANTENNA

      保證復雜度的剪枝寫太弱了,導致復雜度是錯的。

      需要注意寫下來的代碼(比如剪枝)是否和所想的相同。本質上還是在問思路是否清晰

      在洛谷上居然還可以卡過去,實在是誤人子弟。

  • 數據雜糅,不進行區分:

    • 「CF916D」Jamie and To-do List

      題目本身並不難,但是要注意,由於我們將值存儲在 Trie 的末尾,所以被刪除了的節點本質上就是變成了空節點。因此,空節點存儲的值應該等於被刪除了的值。例如,如果空節點值為 0,那么被刪除的節點的值也應該是 0

      否則,Trie 的結構就可能導致將空節點判斷為有值的節點的錯誤出現(例如,插入一個較長串,並查詢它的前綴)。

      處理方式:同類標記盡量統一

    • 線段樹(以及任何靜態結構)上,如果需要刪除結點,那么在維護最值的時候我們一般會將要刪除的結點賦成一個極值。

      但是一定要注意,這里的極值應當區分初始化的極值,尤其是在需要同時取出極值所在的位置的時候。這里就需要明確:初始化所賦的極值是可以取的,但是刪除所賦的極值就是為了避免取到的

  • 類型寫錯的

    • 「LOJ 6207」米緹

      整除分塊的時候,使用中間變量記錄取整除的值,但這個中間變量沒有開 long long

      for( long long l = 1, r ; l <= n ; l = r + 1 ) {
          int val = n / l; // 就是這里!!!應該開 long long!!!
      }
      
  • 各種奇怪的問題

    • [CF1408E]Avoid Rainbow Cycles

      最大生成樹,運算符直接重載為了小於。

    • 【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 在大數小數混合運算時的精度問題!!!

      • 「POJ3155」Hard Life

        奇妙的題目。構造割的時候,如果源點的出邊全部滿流了,那么一種割就是 \(\newcommand\set[1]{\{#1\}}[s,V\setminus \set{s}]\),然而這顯然不是我們想要的。

        為了避免這種情況,我們必須調整答案,使得源點的出邊略有剩余容量;具體操作起來就是讓答案變小一點,而后從 \(s\) 開始遍歷尋找一組割。

    • STL 的奇妙特性:

      眾所周知,為了使操作更麻煩簡化操作,STL 的 setmap 等關聯容器內部基本上都提供了 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 ) 這樣的循環,那么內部是使用迭代器實現的,因此需要注意大數據下的運行效率

    • 讀題出錯的:

      • 「SGU176」Flow Construction

        有點奇怪。題目里面說 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,最后應該需要檢查每一個狀態,但是我直接把最后一個狀態當做答案......

    明明寫個對拍就能找出的錯,居然沒有發現,也沒有想過去寫對拍,反而測了中樣例就不管了......

    即使當場只准備寫騙分,也一定要將工作做齊備,對拍、人肉調錯不能少!

  • 某道計數題。

    數據范圍明確給出:

    mistake.png

    這就說明,輸入數據可以有 \(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 而一分也沒有。

    注意,一定要深入思考之后,確定沒有問題再開始寫代碼,不要空耗時間尋求心理安慰;如果把這些浪費的時間投入到之后的暴力中,事實證明收益也很不錯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM