算法题目易错点
无符号数与有符号数比较
有时我们可能会遇到一些数据结构的大小的判定,比如判断两个堆(假设为heap1,heap2)中元素个数差值是否大于1
heap1.size() - heap2.size() > 1
heap1.size() > heap2.size() + 1
看似等价的两个比较的不等式,产生的效果未必相同
由于数据结构的大小采用的往往是无符号整型数,如果heap1.size() - heap2.size()的结果为负数,比如\(-1\),在无符号数中实际为\(4294967295\),二进制表示为\(11111111111111111111111111111111\),可以看出无符号数会将有符号数补码的符号位当作正常的数位,所以对应的数值就会很大,显然与我们的判断目的相违背
并且还需要注意,无符号数与有符号数比较时,要将有符号数转化为无符号数
负数移位
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int c = -2;
c >> 1;
cout << c << endl;
c >>= 1;
cout << c << endl;
c >>= 1;
cout << c << endl;
return 0;
}
结果
从c++的代码执行效果来看,负数移位是在补码的基础上进行的,移动之后,填充符号位,正数填0,负数填1
-1的补码为\(11111111111111111111111111111111\),右移一位补码仍为\(11111111111111111111111111111111\),表示的仍然为-1
运算符优先级
加减法的优先级是高于移位运算符的
循环爆int
for (int i = 0; i <= n; ++ i)
考虑在n=2147483647时,当i=2147483647那次循环之后再加一后会变为-2147483648,这样该循环就会变为死循环,所以当循环变量是有可能遍历到某类型的最大值时,一定要考虑循环是否会超出该类型的表示范围。
该错误出现在质数二次筛法代码实现中
减法取模
当求(a - b) % mod
时,可能出现a-b为负数的情况,为了取得正值,正确的写法为(a - b + mod) % mod
时间复杂度与数据相关
我们应该控制时间复杂度与读入的数据无关,时间复杂度只应与我们的代码逻辑有关
例如轻拍牛头这道题目
虽然我的做法逻辑上不存在问题,但是由于代码的时间复杂度是随着数据的变化而变化,所以导致在一些极端情况下就会超时
错误写法
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int a[N], cnt[N];
int main()
{
int n;
cin >> n;
int maxx = 0;
for (int i = 0; i < n; ++ i)
{
cin >> a[i];
maxx = max(maxx, a[i]);
}
/**
* 这样的问题是
* 我们假设一种极端情况:n==1e5,序包含有1e5-1个1,1个1e6
* 在这种情况下,时间复杂度为(1e5-1)*1e6,这样就会超时
* 看似减少了运算的次数,但实际反倒更加耗时了
*
* 正确方法是读入数据时顺便统计每个数的个数
* 在之后遍历所有数a的每一个倍数b,倍数b的约数个数加上a的数量
* 虽然不管读入的数据有多大,都会遍历所有的数,但是这样做的时间复杂度不会随着数据的变化而变化,即使处于上面那种极端情况也不会出现问题
*/
for (int i = 0; i < n; ++ i)
for (int j = a[i]; j <= maxx; j += a[i])
++ cnt[j];
for (int i = 0; i < n; ++ i) cout << (cnt[a[i]] - 1) << endl;
return 0;
}
正确代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int a[N], cnt[N], s[N]; // a:原序列; cnt[i]:i在原序列中的个数 s[i]:i的约数个数
int main()
{
int n;//, maxx = 0;
cin >> n;
for (int i = 0; i < n; ++ i)
{
cin >> a[i];
++ cnt[a[i]];
// maxx = max(maxx, a[i]);
}
for (int i = 1; i < N; ++ i)
for (int j = i; j < N; j += i)
s[j] += cnt[i];
// 因为遍历到N-1还是遍历到maxx,时间复杂度是没有变化的,时间确实会少一些,但是时间复杂度本质并不会变化,换句话说这里的优化并没有优化掉算法瓶颈
// for (int i = 1; i <= maxx; ++ i)
// for (int j = i; j <= maxx; j += i)
// s[j] += cnt[i];
for (int i = 0; i < n; ++ i) cout << (s[a[i]] - 1) << endl; // 需要排除自己
return 0;
}
计算组合数和排列数时注意判断数据是否非法
记得判断if (a < b) return 0;
以下代码采用预处理阶乘和逆元的方法
void init()
{
fact[0] = infact[0] = 1;
for (int i = 1; i < N; ++ i) // 因为在计算A(a+c-i,k-i)时候涉及到(a + c - i)的阶乘,a和c最大都是1000,所以阶乘需要算到2000
{
fact[i] = (LL)fact[i - 1] * i % mod; // fact[i-1]*i不会爆int,不加LL也行
infact[i] = qpow(fact[i], mod - 2, mod); // mod是质数且fact[i]是%mod下的数,所以两者一定互质
}
}
inline int C(int a, int b) // 组合数
{
if (a < b) return 0;
int res = (LL)fact[a] * infact[b] % mod * infact[a - b] % mod;
return res;
}
inline int A(int a, int b) // 排列数
{
if (a < b) return 0;
int res = (LL)fact[a] * infact[a - b] % mod;
return res;
}
负权边导致无穷远数值发生改变
当图中存在负权边时,无穷远的数值可能会发生改变。例如在最短路算法中,无穷远0x3f3f3ff
加上负权边是可以更新无穷远0x3f3f3ff
的,所以判断是否为无穷远时一般方法为if (dis > INF / 2)
。需要注意这里的INF / 2
并非在所有题目中都适合,一般情况下是可行的,具体如何分析见bellman-ford算法
sort的多关键字排序
在sort单关键字排序时由于仅需要考虑一个因素,不存在优先级的问题,所以只要语法不出错就没问题
但在使用sort进行多关键字排序时,由于关键字之间存在优先级的高低,所以不同关键字之间的逻辑关系很重要,无论是符号重载还是手写cmp,大小关系的考虑一定要全面
在联络员一题中,需要根据关键字\(f\)和\(w\)对数组进行排序,规则为:首先根据\(f\)从小到大排,再根据\(w\)从小到大排(这错误犯好蠢啊)
出错的写法是
bool operator< (const Edge &t) const
{
return (f < t.f || w < t.w); // 这样写是错误的,在f>t.f时,不管w是什么结果,都应返回false,但是这样在f>t.f时,w还会起作用
}
正确的写法是
bool operator< (const Edge &t) const
{
if (f < t.f) return true;
else if (f == t.f)
{
if (w < t.w) return true;
return false;
}
return false;
}
计算\(2^n\)
1 << n
,由于1
是int类型,所以当n>=31
时,就会爆int
所以记得1LL << n
memset初始化数量问题
在构造有向无环图一题中
由于是多组测试数据且两个数组的数据量均在2e5+10
,所以两次memset(h, -1, sizeof h);
和memset(id, 0, sizeof id);
就导致最后一个数据点TLE了,把sizeof xxx
改为(n + 1) * 4
,即根据读入数据的量进行memset
邻接表勿忘初始化
使用邻接表存储树或图时,链表头指针数组不要忘记进行初始化,memset(h, -1, sizeof h);
加法乘法注意数据范围
题目中涉及到加法和乘法的地方务必要计算一下可能的最大值,如果不便计算,直接上 long long
,防止爆int
求最值
以求最大值为例,除非能够确定所有数都是\(>=0\)的,变量初始值可以设为0,否则还是要初始化为无穷小
最值赋初值
int
类型赋最大值可以是0x3f3f3f3f
但注意long long
类型赋最大值可以是1e18
(\(LONG_{MAX} = 9223372036854775807\))
memset和初始化单值顺序
觉得不会错的地方很多时候反倒会出问题,下面两句的顺序很重要,稍一马虎就容易写反
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
连通图or
非连通图
在图论题目中,若题干未明确指出是一个连通图,那么我们应当将其考虑为非连通图来处理更多的情况
成绩排序
这其实算个生活常识,所以题目中一般不会说明这一点
A同学 100分
B同学 100分
C同学 99分
按照成绩从高到低进行排序,排名应当为 1 1 3
,而非1 2 3
或 1 1 2
代码实现时,可以在正常遍历各成绩时,另外使用一个指针inx
,在需要保持次序时(例如1 1
的第2个1),inx
保持不变,需要改变时直接令inx=正常遍历的指针
即可
一道例题
memset
要慎用
用在循环中的memset
一定要算好复杂度,稍不注意就可能导致TLE
如果memset
的目的是将标记数组复原,那么可以考虑按照之前修改标记数组的反方式再修改一次从而达到复原的目的
因为memset
导致超时的一道题目:染色
mod后运算注意正负
例如:
MOD = 10
f[0] = 5 % MOD = 5
f[1] = 10 % MOD = 0
假设原f
数组是递增的,但是取模之后的结果就不一定仍是递增的了,此时f[0] - f[1] < 0
如果题目需要非负数解,则需要(f[0] - f[1] + MOD) % MOD
这种问题常见于预处理过程中取模,最后使用预处理后结果的情况
进制转换时对于0的处理
如果代码采取while(num)
的形式,那么num
初始为0的情况就无法处理,需要提前特判一下
memcpy
对string
的拷贝是浅拷贝
浅拷贝带来的效果是修改一方的值,另一方的值也会改变
因此对于下面这段程序,结果是c c
,而非预期的c b
string a[1] = {"a"};
string b[1] = {"b"};
memcpy(a, b, sizeof b);
a[0] = "c";
cout << a[0] << ' ' << b[0] << endl;
去掉小数部分和四舍五入
%.0lf
的最终效果是四舍五入
想要直接去掉小数部分输出可以强制类型转换为int
答案爆int
如果最终答案为求和,一定要注意会不会爆int 当然,运算中求和爆
int`是更容易被忽视的地方