【算法競賽bug經驗談】編程經驗總結【C/C++】
0.總結
Get to the points first. The article comes from LawsonAbs!
- 記錄我自己編寫算法題時犯過的錯誤
1.踩過的坑
寫算法題時,悲催不是你的wrong answer
,而是在你寫完代碼之後,你卻發現你的思路存在紕漏。這裏總結了一些我寫題時的一些常犯錯誤,供大家編程時參考。
- 1.見到大批的WA,想想題意搞清楚了嗎?
- 2.寫
<
,>
號的時候,需要考慮會用到=
號嗎?
上面這種問題,經常出現在二分法,排序等情況下。 - 3.寫
if
條件的時候,要考慮後面的else
是什麼樣的情況?是二選一的嗎?if
內的代碼塊有必要二選一執行嗎? - 4.
if
中的條件是什麼樣的情況成立?&&
和||
符號正確嗎? - 5.數組下標越界了嗎?
如果出現Runtime Error
,通常就是出現了數組越界的情況,這時,就需要考慮是否在判斷數組邊界是否越界。 - 6.數組開的夠大嗎?是導致
Runtime Error
的原因嗎? - 7.遞歸有明確的返回條件嗎?遞歸哪裏可能會出現死循環?
- 8.
while
,for
循環是導致死循環的原因嗎? - 9.二分法時,如果涉及到比較精度的問題,可能會因爲精度而導致死循環。
- 10.可能在有些變量沒有初始化時,導致本地運行和評判機運行出現偏差。從而出現明明“對的程序”無法AC
- 11.二分法的邊界一定要搞清楚!十分重要
- 12.一些常數的取法是否有問題?比如說
π
是取成3,還是3.14,還是3.1415926?如果題意沒有說明,那麼這些就是容易出現問題的地方。 - 13.貪心算法能證明出來是正確的嗎?如果不能,請謹慎使用,否則在你不僅WA了答案,還浪費了時間。
給我這樣的感受的題有很多,比如揹包問題(讓你求出平均價值最大的放法,如果是直接按照 價值/體積 形成的權重來從大到小放,是會出現問題的。);再比如絡谷 P1514 引水入城 問題,這裏是我的關於此題的題解,我最開始的想法是: dfs+貪心(失敗)。直到WA之後才恍然大悟自己是多麼幼稚! - 14.
--y
與y--
是有很大區別的。
第一種寫法是先判斷下一個,第二個是先判斷當前的情況,再將y的值減一。即使是高手,有時候也會在這個簡單的問題上犯糊塗,一定要注意! - 15.如果數組沒有初始化,很有可能得到錯誤的答案。如果數組初始化不夠(意思就是隻初始化了一部分的數組),也會導致錯誤的答案。
比如,有如下語句:
const int maxN = 105;
const int maxV = 100005;
int f[maxV];
fill(f,f+maxN,0);
當計算 f[i]
的值的時候,就很容易出現問題。這是一個很隱蔽的錯誤,而且不易察覺,大多數時候,我們都以爲是自己寫的代碼或者是思想錯了,哪知道問題在於這個細節問題!因爲我們申請的是 maxV
大小的 int
型數組,結果只初始化了一部分(maxN大小),導致WA!我就曾因爲這個問題浪費了30min 來找bug!【蒟蒻實證!】
-
16.複製得到的代碼一定要對具體的條件做修改,否則很難發現這種隱蔽的問題。常見的複製代碼的情況有:
01.朝不同的方向【N,S,E,W】行駛,比如迷宮問題,棋盤問題等等。
02.switch...case
語句中的條件
03.if...else if...else
中語句的條件 -
17.代碼順序的顛倒可能會導致嚴重的錯誤。
【20200410】我在寫一道dijkstra
算法題時,計算最短路徑的個數。但是因爲搞反了兩條語句,就幾乎WA了大半的結果。編碼十分鐘,調試2小時!!血的教訓。寫代碼時一定要全神貫注,切莫開小差! -
18.不要拿到一道題就開始編代碼, 一定要先動腦,再動手!否則就是編碼10分鐘,調試10小時。
-
19.做題有大致的步驟:
(1)抽象題目。對應於學過的什麼知識點,主要考什麼?
(2)分析時間複雜度,該用什麼樣的算法解決這個問題?【這一步驟很關鍵,我經常把貪心的題搞出dp,然後怎麼也推導不出來dp的轉移方程,後來才知道應該是O(N)複雜度的貪心。-_-||】 -
20.昨天【20200620】在寫洛谷的一道題【SP4033 PHONELST - Phone List 】的時候,我因爲把輸出放到main外的void函數之中,導致在調用這個void函數中可能會多次輸出同一個答案,從而導致輸出不對。
因爲一份電話名單可能會出現多個重複前綴的情況,如果這種輸出放在build()函數裏,就會出現錯誤!這個bug 我找了1h+。 -
21.今天【20200621】在寫洛谷的一道題【P3879 [TJOI2010]閱讀理解】時,碰到一個bug,我覺得我寫trie樹應該是很熟練的了,但就是死活過不了…就是下面這種情況:
於是,我想啊想,難道是因爲我數組開的範圍不夠大? 代碼內容如下:
在蹦出這個念頭之後,我細想了一下,好像真有問題。而且對於這種二維數組,這種越界問題,就會導致**“修改數據”** 的情況。將endF[maxN]maxN]
修改爲endF[maxN][maxM]
就可以一下全部AC了。【因爲題目中說一篇短文的字數<103,而且每個單詞的長度小於20。所以在去掉部分空格之後,即使沒有任何公共的節點,都可以在20000內將所有節點全部容納!所以可以這麼定義一個二維數組。】
跟隨上面這個問題,我們來看一個很好玩兒的東西。
#include<iostream>
using namespace std;
const int maxN = 10;
int arr[maxN][maxN]; //
int tot = 0; //記住tot不要超過100就行
int main(){
for(int i = 0;i<10;i++)
for(int j = 0;j< 10;j++)
arr[i][j] = 1;//全部賦值爲1
//下面見證一下“奇蹟”
int cnt = 1;
for(int i = 0;i<5;i++){
/*主要j的取值 =>
01.因爲這裏j的取值越界了,但是整個二維數組的值
卻沒有越界,所以你會得到Wrong Answer的界面,但是得不到Runtime Error
的提醒。
02.同時,可以看到這裏的賦值手法有點兒“一維”狀態空間的意思。
03.在起初的位置上,往後推20個。比如arr[4][20]就是在arr[4][0]後推20個,
這樣得到的結果就是arr[4][0] = 81,…… arr[4][20] = arr[5][10] = 100;
*/
for(int j = 0;j<20;j++){
arr[i][j] = cnt ;
cnt ++;
}
}
for(int i = 0;i< 10;i++){
for(int j = 0;j< 10;j++){
printf("%4d",arr[i][j]);
}printf("\n");
}
}
執行結果如下:
---------------------------------智慧分割線-------------------------------
2.define
使用不當
#define N 100000000001
define 定義的值不能過大,否則會到值編譯錯誤,停止工作。
#define
的用法不僅僅侷限於對數據,同樣對於字符串,以及其他的都是適用的,比如以下的程序。
#include <stdio.h>
#define P printf("I Love programming\n");
int main()
{
P;
return 0;
}
如果覺得define
不好使,也可以使用const來定義靜態變量。
3. 未引用正確的方法庫
報錯:[Error] 'strcmp' was not declared in this scope
需要針對具體的報錯信息添加頭文件。比如在上面的這個報錯信息中,就需要添加#include<cstring>
【c++】或者是#include<string.h>
【c】。
4.整型的範圍【非常重要!】
算法題中經常會出現整數範圍的考察,這是一個非常基礎但重要的知識點,務必掌握。這裏我講解一些最常用的整型。
4.1 int
範圍大小
int
聲明的數據是帶正負號的整數,一般情況其佔內存大小是4B
,即32位,有一位是符號位,故最大正整數是2^31-1
,最大負數是2^31
【這與計算機的存儲方式有關,因爲是二進制補碼存儲】。在 #include<climits>
頭文件中可以看到int的最大值和最小值。代碼如下所示:
#include<iostream>
#include<climits>
using namespace std;
int main(){
cout << "INT_MAX="<< INT_MAX<<"\n";
cout << "INT_MIN="<< INT_MIN<<"\n";
}
4.2 long long
的範圍大小
同樣的道理,我們可以獲取到long long
類型的最大、小值。
4.3 其它
這個庫裏 (應該) 還有很多其它好東西,值得研究。
4.4 關於cpp
中的一個整數是什麼類型的討論
使用如下代碼測試:
int main(){
cout << sizeof(1)<<"\n";
cout << sizeof(1ll)<<"\n";
}
執行結果如下:
可以看到所佔內存大小分別是4B
和8B
。
我就是因爲這個問題沒搞清楚,在寫一道程序題時就出現了問題。本來可以用long long
存儲的數,我自己硬是寫了一個高精!
5.二叉樹相關常見錯誤
5.1 對指針理解不夠
如下代碼是我在爲每個節點設置樹高。但是其中有一處很明顯的錯誤。
void btHigh(node* root)
{
if (root == NULL){
root->height = 0;
return ;//一定不要少了這個邊界條件
}
btHigh(root->lchild);
btHigh(root->rchild);
//二叉樹的高度爲左子樹和右子樹中高的那個高度加1
int ret1 = root->lchild->height;
int ret2 = root->rchild->height;
root->height = ret1 > ret2 ? ret1 + 1 : ret2 + 1;
}
裏面存在的錯誤代碼是:
if (root == NULL){
root->height = 0;
return ;//一定不要少了這個邊界條件
}
root->height = 0
是典型的低級錯誤,怎麼能在root=NULL
的條件下給root->height
賦值呢? 也就是說,if
中的判斷是要有意義的,不是白判斷的。