代碼教訓總結

2019/11/19
關於二分查找寫法的一個注意點。
考慮一個能夠用二分答案來解決的求知足某個條件的最大的整數的問題。
通常來講初始時咱們能夠肯定答案的範圍是 $l, r$(包括兩個端點),更確切地說 $l, r$ 之間的每一個整數均可能是答案。另外還有一個判斷函數 $p$,對於 $l, r$ 之間的一個整數 $x$,若 $p(x)$ 爲真則最大值不小於 $x$,不然最大值小於 $x$。
下列實現是有問題的html

while (l < r) {
  int mid = l + (r - l) / 2;
  if (p(mid)) {
    l = mid;
  }
  else {
    r = mid - 1;
  }
}

注意,當r == l + 1時,l + (r-l)/2 == l,而p(l)老是返回true,因而就死循環了,通常來講二分答案的代碼裏出現l = mid;這樣的語句每每是 bug。實際上當咱們肯定答案的範圍是 $l, r$ 時,應當當即把左端點變成 $l +1$,正由於咱們確定答案不小於 $l$,所以 $l$ 這個點就沒必要再 check 了。換言之,既然已經知道p(l)的返回值必定是true,此後天然再也不須要調用p(l)了。咱們應當保證每一輪循環以前 $l, r$ 中沒有沒必要 check 的點,也就是說沒有已經確知 $p(x)$ 的值的 $x$。所以正確的寫法(之一)是c++

++l;
while (l <= r) {
  if (p(mid)) {
    l = mid + 1;
  } else {
    r = mid - 1;
  } 
}

2019/11/17
在寫一道 DP 題時犯了這樣的錯誤數組

const int N = 1005;
long long dp[N][N][2];

int main() {
const int N = 1005;
long long dp[N][N][2];

int main() {
  int n, m;
  cin >> n >> m;
  // ...
  memset(dp, 0x3f, sizeof dp);
  // compute dp
  vector<int> a(n + 1);
  for (int i = 0; i <= n; ++i) {
    a[i] = dp[0][i][0]; // error: convert long long to int
  }
  // ...
  return 0;
}

數組a應當聲明爲vector<long long>,我花了很長時間也沒找出這個錯誤。
其實這樣的問題可讓編譯器幫忙找出來。GCC 提供了不少 warning 選項,其中有個-Wconversion就能夠發現這類錯誤,再加上-Werror,讓警告變成錯誤,這樣就不會忽略了。

對於 C++,雖然-Wconversion不會警告無符號和有符號整數之間的轉換,可是加了這個選項以後 CLion 會在代碼中警告無符號和有符號整數之間的轉換,好多地方會出現紅色波浪下劃線。也許 CLion 並不知道-Wconversion對 C++ 作了特殊處理。爲了讓代碼看起來清爽一些,能夠把-Wno-sign-conversion也加上,明確告知 CLion。函數


2019/11/16
① 函數內聲明的數組必定要初始化!
② 判斷一個數是不是素數不能忘記邊界狀況 1。
這樣寫是錯的,若x <= 1會返回truecode

auto is_prime = [](long long x) {
  for (long long i = 2; i * i <= x; ++i) {
    if (x % i == 0) {
      return false;
    }
  }
  return true;
};

2019/10/29
組合數 $\binom{n}{m}$ 對大素數取模。htm

若是須要計算的組合數的個數接在 $n^2$ 的級別(此時 $n$ 通常不超 5000),打表($\binom{n}{m} = \binom{n-1}{m} + \binom{n-1}{m-1}$)比求逆元($\binom{n}{m} = \frac{n!}{m! (n-m)!}$)快不少。blog

相比於加法,求逆元是很慢的。遊戲

當 $n$ 比較大(~$10^5$)必需要按 $\binom{n}{m} = \frac{n!}{m! (n-m)!}$ 計算時,應該將的階乘的逆元預處理出來。ci

template <typename T>
struct Binom {
    vector<T> fact, inv_fact;
 
    explicit Binom(int n) : fact(n + 1), inv_fact(n + 1) {
        fact[0] = 1;
        up (i, 1, n) fact[i] = fact[i - 1] * i;
        inv_fact[n] = 1 / fact[n];
        down (i, n, 1) {
            inv_fact[i - 1] = inv_fact[i] * i;
        }
    }
 
    T get_binom(int x, int y) const {
        assert(x <= SZ(fact) - 1);
        assert(x >= 0 && y >= 0);
        if (x < y) return 0;
        return fact[x] * inv_fact[y] * inv_fact[x - y];
    }
};

我曾把預處理階乘的逆元寫錯:get

inv_fact[n] = 1 / fact[n];
        down (i, n - 1, 1) {
            inv_fact[i] = inv_fact[i + 1] * i;
        }

有兩處錯誤,① inv_fact[i] = inv_fact[i + 1] * (i + 1); ② 漏掉了 inv_fact[0]
我有個疑問

down (i, n - 1, 0) {
    inv_fact[i] = inv_fact[i + 1] * (i + 1);
}

down (i, n, 1) {
    inv_fact[i - 1] = inv_fact[i] * i;
}

哪種寫法更快?答案是同樣快。


2019/10/4
關於線段樹的 lazy tag 下傳操做。
若是有多個 lazy tag,通常來講這些 tag 應當分別下傳。

舉例以下。線段樹節點

struct Node {
    int tag_sum_of_id;
    int tag_cnt;
    int min_booking; // 區間內每一個座位被預約的次數的最小值
};

其中有兩個 lazy tag,tag_cnttag_sum_of_id

push_down 操做的錯誤寫法

void push_down(int index) {
    if (seg[index].tag_cnt != 0) {
        seg[LSON(index)].tag_cnt += seg[index].tag_cnt;
        seg[LSON(index)].tag_sum_of_id += seg[index].tag_sum_of_id;
        seg[LSON(index)].min_booking += seg[index].tag_cnt;

        seg[RSON(index)].tag_cnt += seg[index].tag_cnt;
        seg[RSON(index)].tag_sum_of_id += seg[index].tag_sum_of_id;
        seg[RSON(index)].min_booking += seg[index].tag_cnt;

        seg[index].tag_cnt = 0;
        seg[index].tag_sum_of_id = 0;
    }
}

存在某個節點的 tag_cnt 等於零而 tag_sum_of_id 不等於零的情形。我這麼寫是犯了想固然的錯誤。

push_down 的正確寫法

void push_down(int index) {
    if (seg[index].tag_cnt != 0) {
        seg[LSON(index)].tag_cnt += seg[index].tag_cnt;
        seg[LSON(index)].min_booking += seg[index].tag_cnt;
        seg[RSON(index)].tag_cnt += seg[index].tag_cnt;
        seg[RSON(index)].min_booking += seg[index].tag_cnt;
        seg[index].tag_cnt = 0;
    }

    if (seg[index].tag_sum_of_id != 0) {
        seg[LSON(index)].tag_sum_of_id += seg[index].tag_sum_of_id;
        seg[RSON(index)].tag_sum_of_id += seg[index].tag_sum_of_id;
        seg[index].tag_sum_of_id = 0;
    }
}

2019/10/2
當心對待每個賦值表達式。

在寫一個 BFS 時,我寫下了這樣的代碼

vector<string> maze(R);
        For (row, maze) {
            scan(row);
        }
        vv<int> dis(R, vi(C, 500));
        queue<pii> que;
        // ....
        while (!que.empty()) {
            auto p = que.front();
            que.pop();
            int d = maze[p.first][p.second]; // maze should be dis
            rng (i, 0, 4) {
                int x = p.first + dx[i], y = p.second + dy[i];
                if (x >= 0 && x < R && y >= 0 && y < C && dis[x][y] > d + 1) {
                    dis[x][y] = d + 1;
                    que.emplace(x, y);
                }
            }
        }

其中 int d = maze[p.first][p.second]; 寫錯了。maze 表明地圖,應當是 dis


2019/9/27
std::next_permutation 枚舉全排列時,要保證初始排列是字典序最小的排列。

今天作網易遊戲的校招筆試。有個問題須要枚舉 "ASDFGH" 這六個字符的全排列。
一開始我是這樣寫的

string s = "ASDFGH";
do {
  // do something with s
} while (next_permutation(s.begin(), s.end()));

結果只得了 60% 的分數。

上述代碼錯誤之處在於它並未枚舉全部排列。初始排列 "ASDFGH" 並非字典序最小的排列,這麼寫只是枚舉了字典序大於等於 "ASDFGH" 的全部排列。

正確寫法

string s = "ASDFGH";
sort(s.begin(), s.end());
// 或者 string s = "ADFGHS";
do {
  // do something with s
} while (next_permutation(s.begin(), s.end()));

2019/9/23
調用 std::unique 或者 std::unique_copy(since C++17)以前,必定要確保數組是有序的(每每須要先調用 std::sort)。


2018/5/19
用整數表示集合時,判斷第 i 個元素是否屬於集合 S,能夠用 if (S & 1 << i)if (S >> i & 1) 。判斷第 i 個元素是否不屬於集合 S 時,我通常會用 if ( (S & 1 << i) == 0 ),可是這樣寫有一個壞處:經常會忘記 bitwise and(&)算符的優先級低於 == 算符。有兩個更好的寫法能夠避免這個問題,第一個是 if ( !(S & 1 << i) ),由於我知道 ! 算符的優先級是很高的;第二個是 if ( ~S & 1 << i ) 這個寫法不須要括號,能夠說是很優雅了 XD,可是因爲要多算 ~S, 是否會比較慢?(玄學) 。

Remark:&^|&&|| 都比 == 優先級低。


2018/4/21
WA 了首先要檢查輸入數據的類型是否跟數據範圍匹配。


2018/4/18
在某個問題中,須要實現如下過程。
維護一個二元組(std::pair<int,int>)的集合。
pair 的第二維表示 unique 且 const 的 ID,第一維只減不增,而且始終大於等於 $0$ 。
每次從集合中取出第一維爲 $0$ 的 pair(並將其從集合中刪除,保證這樣的 pair 存在),而後將集合中的某些 pair 的第一維減 $1$ 。

個人寫法

  • std::set<std::pair<int,int>> 支持取出第一維是 $0$ 的 pair
  • 用一個數組存儲每一個 ID 對應的 std::pair<int,int>::first 的當前值。
  • 若 set 中第一個元素的第一維與數組中的值不相等,則更新

這種作法是有問題的,由於並不能確任當前 set 中的第一個元素的第一維的實際值就是 $0$ 。
正確的作法是即時更新 set 中的元素(先 erase 舊的,再 insert 新的),並且上述數組也是沒必要要的


2018/1/1
對 $m$ 取模的運算,必定保證最後結果在 $0$ 到 $m-1$ 之間。

DP:要考慮是否有某些狀態雖然是個合法狀態,可是始終沒被計算到。

2018/1/2
變量命名:若用 #include <bits/stdc++.h> 引入頭文件,prevnext 這兩個名字都會引發變量名衝突,能夠用 prvnxt 代替。

2018/1/3
不要濫用 for 循環,用 while 循環合適時應當用 while

2018/4/2
「把簡單的問題搞複雜」、「很短的代碼就能解決,個人代碼卻很長」;這類錯誤是不能容忍的。

2018/4/3
用 range-for 遍歷容器 a 時,若要修改容器中的元素則應該用 for(auto &x: a) 。不修改時,也能夠這麼用,因此用 range-for 時,老是採用引用的形式。

相關文章
相關標籤/搜索