代码教训总结

2020/1/5
给定序列 $a_{1}, \dots, a_{n}$,判断是否存在 $1 \le i < j \le n$ 满足 $a_i < a_j$。
我的写法:

vector<int> mx = a;
down (i, n - 2, 0) {
    mx[i] = max(mx[i], mx[i + 1]);
}
bool flag = false;
rng (i, 0, n - 1) {
    if (a[i] < mx[i + 1]) {
        flag = true;
        break;
    }
}

这么做很笨。注意到,存在 $1 \le i < j \le n$ 满足 $a_i < a_j$ 等价于存在 $1 \le i < n$ 满足 $a_i < a_{i + 1}$。


2019/12/22
大整数加一的写法。(从 tourist 的代码里学的)
设 $N$ 是一个很大的正整数,在十进制下表示为字符串 ss[0] 是最高位且 s[0] != 0s.back() 是最低位(个位)。
问题:给定字符串 s,求 $N+1$ 在十进制下的表示。
代码可以这样写

string add_one(string s) {
    for (int i = (int)s.size() - 1; i >= 0; --i) {
        if (s[i] == '9') {
            s[i] = '0';
        }
        else {
            s[i]++;
            break;
        }
    }
    if (s.front() != '0') return s;
    return '1' + s;
}

2019/11/30
尺取法的典型写法(来自 ABC146E)。

  ll ans = 0;
  for (int l = 0, r = 0; l <= n; ++l) {
    for (; r <= n && a[r].first == a[l].first && a[r].second - a[l].second < md; ++r);
    ans += r - l - 1;
  }

在外层循环内枚举左端点,在内层循环内将右端点往右移动。


2019/11/29
滑动窗口最小/大值(单调队列)的写法(来自 ABC146F)。
在新值入队这一步只维护队列的单调性(deque::pop_back())而在需要用滑动窗口的最小/大值时才检查当前队首元素对应的下标与当前考虑的下标之间的距离是否已超过滑动窗口的长度(deque::pop_front())。
若在新元素入队时除了维护队列单调性还维护单调队列首尾元素对应的下标距离不超过滑动窗口长度,但是在需要用滑动窗口的最小/大值时不检查队首元素对应的下标与当前考虑的下标之间的距离是否已超过滑动窗口的长度,就错了。
ABC146F 为例,正确的写法是

auto work = [n, m](string s, vi& dp) {
  deque<pii> que;
  que.eb(0, 0);
  up (i, 1, n) {
    if (s[i] == '1') {
      dp[i] = -1;
    } else {
      while (!que.empty() && que.front().second + m < i) {
        que.pop_front();
      }
      if (que.empty()) {
        dp[i] = -1;
      } else {
        dp[i] = que.front().first + 1;
        while (que.back().first >= dp[i]) {
          que.pop_back();
        }
        que.eb(dp[i], i);
      }
    }
  }
};

但是这样写就错了:

auto work = [n, m](string s, vi& dp) {
  deque<pii> que;
  que.eb(0, 0);
  up (i, 1, n) {
    if (s[i] == '1' || que.empty()) {
      dp[i] = -1;
    } else {
      dp[i] = que.front().first + 1;
      while (que.back().first >= dp[i]) {
        que.pop_back();
      }
      que.eb(dp[i], i);
      while (que.front().second + m <= i) {
        que.pop_front();
      }
    }
  }
};

2019/11/24
在写 Dijkstra 时常犯的一个错误:有些问题中,距离要用 long long 表示才行,但声明优先队列时却忘了这一点,声明成pq<pii, greater<>>,应该声明成pq<pli, greater<>>

  auto dijkstra = [&g](int s, vl& d) {
    pq<pii, greater<>> que; // 这里声明错了,应该是 pq<pli, greater<>> que;
    d[s] = 0;
    que.emplace(0, s); 
    while (!que.empty()) {
      auto p = que.top();
      que.pop();
      int u = p.second;
      if (d[u] != p.first) continue;
      FOR (e, g[u]) {
        if (chkmin(d[e.first], d[u] + e.second)) {
          que.emplace(d[e.first], e.first); // 这里可以编译器可以警告
        }
      }
    }
  };

上述代码里que.emplace(d[e.first], e.first);这条语句,d[e.first]类型是long longlong longint,编译器本可以发出警告的(-Wconversion),不过由于std::pair的构造函数位于系统头文件中,为了使编译器的输出信息更简洁,gcc 的警告选项默认忽略系统头文件。可以用-Wsystem-headers改变这一默认行为。不过加上-Wsystem-headers选项后编译器会输出许多其他警告,淹没了我们感兴趣的警告。对于这个问题,我还没找到好办法。


2019/11/19
关于二分查找写法的一个注意点。
考虑一个可以用二分答案来解决的求满足某个条件的最大的整数的问题。
一般来说初始时我们可以确定答案的范围是 $l, r$(包括两个端点),更确切地说 $l, r$ 之间的每个整数都可能是答案。另外还有一个判断函数 $p$,对于 $l, r$ 之间的一个整数 $x$,若 $p(x)$ 为真则最大值不小于 $x$,否则最大值小于 $x$。
下列实现是有问题的

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$。因此正确的写法(之一)是

++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会返回true

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}$ 对大素数取模。

如果需要计算的组合数的个数接在 $n^2$ 的级别(此时 $n$ 一般不超 5000),打表($\binom{n}{m} = \binom{n-1}{m} + \binom{n-1}{m-1}$)比求逆元($\binom{n}{m} = \frac{n!}{m! (n-m)!}$)快很多。

相比于加法,求逆元是很慢的。

当 $n$ 比较大(~$10^5$)必须要按 $\binom{n}{m} = \frac{n!}{m! (n-m)!}$ 计算时,应该将的阶乘的逆元预处理出来。

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];
    }
};

我曾把预处理阶乘的逆元写错:

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

posted @ 2018-01-01 14:41  Pat  阅读(374)  评论(0编辑  收藏  举报