代码教训总结

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 时,老是采用引用的形式。

相关文章
相关标签/搜索