[ACM入门] 从 C 到 C with STL
[ACM入门] 从 C 到 C with STL
简单分享一下自己 ACM 的一些小经验。
注意, 这里讲的 C++ 基本上都是指带有 STL 的面向过程的编程, 只是 C++ 中的一部分, C++ 内容还是很多的, 但是适用于竞赛的一些 STL 和库函数的使用并不麻烦, 上手成本不是很高, 可以很容易的上手使用。
相关网站
刷题网站
工具知识类
相关工具
- cf-tool [codeforces 比赛助手] [https://github.com/xalanq/cf-tool]
- sublimeText [轻量编辑器] + FastOlympicCoding [测样例插件]
- vscode [建议整理并保存代码]
基础知识
ACM赛制规则
5小时, 4小时的时候封榜单,榜单冻结。
过题数量优先, 其次是时间。
时间等于 = 每道题通过时间(单位/分钟)之和 + 20 * 失败提交次数
AC 之后这道题不再计算罚时(应该)
例如我在 10分钟过了 A 题, 50分钟的时候尝试 B 题 WA 了或者 TLE了, 反正没过, 60分钟的时候改好了交 B 题过了, 此时我的罚时就是 10 + 60 + 20 = 90
罚时非常不划算 , 20分钟的时间显然足够认真检查程序使其一发通过。(一些很***钻的bug除外)
cpp A + B
c 风格的 a + b
#include <stdio.h>
int main(){
int a, b;
scanf("%d%d",&a,&b);
printf("%d\n", a + b);
}
过渡到 cpp
#include <bits/stdc++.h>
using namespace std; //
int main(){
ios::sync_with_stdio(0);cin.tie(0);
int a, b;
cin >> a >> b;
cout << a << b << '\n';
}
#define FIO ios::sync_with_stdio(0);cin.tie(0);
#define endl '\n'
- 不要用
endl
时间复杂度
对于 1s 时限, 个人经验是 1e8 左右没有问题
1e5 : O(n), O(nlogn), \(O(n\sqrt n)\)
1e6 : O(n), O(nlogn), 注意输入不要用裸 cin
空间复杂度
一般不会特意卡, 除非有些题目不让你用特定的数据结构
int a[N];
对于 N = 1e6 , 可以算一下大小, 1e6 * 4 byte = 4 Mb, 对于一个 1e6 的 int 数组, 占用的空间是 4M。
常见数据范围
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int main(){
cout << INT_MAX << '\n'; //2147483647
cout << INT_MIN << '\n'; //-2147483648
cout << LLONG_MAX << '\n'; //9223372036854775807
cout << LLONG_MIN << '\n'; //-9223372036854775808
cout << UINT_MAX << '\n'; //4294967295
cout << ULLONG_MAX << '\n'; //18446744073709551615
}
记不住也可以大概算一下, \(log_{10}2\) 大概 0.3 多一些, int 一般 32 位, 31 * 0.3 = 9 多一点, 所以 1e9 的数据不会爆 int, 同样 1e18 的数据用 long long 也没有问题
填充 inf 常用数据 0x3f3f3f3f
, 因为 INT_MAX 是 0x7fffffff
, 用 0x3f3f3f3f
是因为 inf + inf < INT_MAX, 仍然是一个极大值,且大于 1e9
const int INF = 1e9;
const int N = 1e5 + 10;
int dis[N];
int main(){
memset(dis, 0x3f, sizeof dis); //所有字节设置成 0x3f, memset以字节作为单位
}
//-----
int vis[N];
void solve(int n){
memset(vis, 0, sizeof vis); //可能会有 TLE 的风险
memset(vis, 0, sizeof(int) * (n + 5)); //注意不要越界
}
int main(){
int T;
cin >> T:
while(T--){
solve();
}
}
常用容器
vector<_Tp>
动态可变长数组
vector<int> L = {1,2,3,4,5};
L.begin() | L.end() | ||||
---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 |
- 初始化
int n = 100;
vector<int> L(n, 100);
vector<int> G = {1,2,3};
vector<int> L;
L.resize(n);
- 遍历
vector<int> L;
for(int i = 0;i < L.size();i++){
cout << L[i] << '\n';
}
//!!! L.size() : uint64_t ⚠️
for(vector<int>::iterator it = L.begin();i != L.end();i++){
cout << *i << '\n';
}
for(auto i = L.begin();i != L.end();i++){
cout << *i << '\n';
}
for(int& v : L){ //注意 &
cout << v << '\n';
}
vector<string> L;
for(string v : L){ // ⚠️
cout << v << '\n';
}
for(string& v : L){
cout << v << '\n';
}
- 访问,修改
vector<int> L = {1,2,3};
{
int a = L.at(0); // 1
int b = L[0]; // 1
}
{
int a = L.front(); // 1, L[0]
int b = L.back(); // 3, L[L.size() - 1]
}
L.push_back(4); // L : {1,2,3,4}
L.pop_back(); // L : {1,2,3}
L.push_back(4); // L : {1, 2, 3, 4}
L.emplace_back(5); // L : {1,2,3,4,5}
L.insert(L.end(), 6); //L : {1,2,3,4,5,6}
L.insert(L.begin(), 7); //L : {7,1,2,3,4,5,6}
L.erase(L.begin()); // L : {1,2,3,4,5,6}
L.erase(L.begin(), prev(L.end())); // L : {6}
L.clear(); // L : {}
L.empty(); // true
vector<int> a = {1,2,3,4,5};
vector<int> b = {1,2,3,4,5};
//可以按字典序比较
if (a == b){
// true
}
- 输入
int n;
vector<int> L;
for(int i = 0;i < n;i++){
int x; cin >> x;
L.push_back(x);
}
//-------
int n;
vector<int> L(n);
for(int& x : L){
cin >> x;
}
什么时候用会方便一些?
- 当数组大小内容不确定的时候, 如给一个 N * M 的二位矩阵, \(N,M \le 1e6\) 且 \(N*M \le 1e6\)
const int N = 1e6 + 10;
int dat[N][N]; //🙅 很显然开不下
int n, m;
cin >> n >> m;
vector<vector<int>> dat(n, vector<int>(m, 0)); //这样就得到了一个 n * m 的矩阵
又比如说给节点数为 \(N\) 的树, \(N \le 1e5\)
const int N = 1e5 + 10;
vector<int> G[N];
int main(){
int n;
cin >> n;
for(int i = 0;i < n - 1;i++){
int a, b;cin >> a >> b;
G[a].push_back(b);
G[b].push_back(a);
}
}
- 当我们需要
insert
,erase
的能力的时候, 例如一些模拟题, 就可以免去自己手写操作的麻烦。 - (不靠谱) 需要维护数据量比较小 (1e5) 的有序序列的时候, 可以尝试使用
int main(){
vector<int> L;
int n;cin >> n;
for(int i = 0;i < n;i++){
int val; cin >> vall;
L.insert(lower_bound(L.begin(),L.end(),val), val);
//do some thing
}
}
大部分的时候如果只是存数据的时候, 使用数组就可以了, 简单方便。
string
string s;
cin >> s;
s = "hello world";
s[0]; // 'h'
s.at(0); // 'h'
s.front(); // 'h'
s.back(); // 'd'
cout << s << '\n';
printf("%s\n", s.c_str()); //const char*
s.data(); //char*
s = "abc";
s.size(); //3
s.length(); //3
s.empty(); //false
- 操作
s = "hello";
s.clear(); // ""
s.insert(s.begin(), 'c'); // "chello";
s.insert(/*pos=*/ 1, /*n=*/ 2, /*c=*/ 'p'); // "cpphello";
s.erase(3); // "cpp"
s.erase(0, 2); // "p"
s.erasse(s.begin(), s.end()); // ""
s = "hello";
s.push_back('x'); // "hellox"
s.pop_back(); // "hello"
s += ' '; // "hello "
s += "world"; // "hello world"
s = s + "world"; //⚠️
s = "hello world";
s.replace(0, 5, "HELLO"); // "HELLO world";
s = "cppjava";
s.substr(3); // "java";
s.substr(0, 3); // "cpp";
//find, 类似 strstr
s.find("java"); // 3 : size_t (uint64_t)
s.find("xxx"); // string::npos
string a = "hello";
string b = "hello";
if (a == b){
//true
}
- 数值转换
string s = "123";
int a = stoi(s);
long long b = stoll(s);
double d = stod(s);
int x = 12334;
string x_str = to_string(x);
- 遍历
string s;
for(int i = 0;i < s.size();i++){
char c = s[i];
}
for(char c : s){
}
map<Key, T>
存储 kv 键值对, 要求 key 能够排序(可比较), 按 key 值有序。 存储映射关系
插入和查询操作都是 \(log(n)\) 复杂度, 底部是平衡二叉树实现(红黑树)
- 基本存取
map<string, int> mp; // name -> age
for(int i = 0;i < n;i++){
string name;int age;
cin >> name >> age;
mp[name] = age;
}
mp["Alice"] = 14;
mp["Bob"] = 8;
cout << mp["Alice"] << '\n'; // 14
- 修改, 删除
mp["Alice"] = 14;
mp["Alice"] = 15;
cout << mp.at("Alice") << '\n'; // 15;
mp.erase("Alice");
cout << mp.count("Alice") << '\n'; // 0
cout << mp["Alice"] << '\n'; // 0 ⚠️, 没有 key 的话访问会创建 value
cout << mp.count("Alice") << '\n'; //1
- 查找
mp["Alice"] = 15;
if(mp.find("Alice") != mp.end()){
}
if(mp.count("Alice")){
}
//mp.lower_bound(Key x)
//mp.upper_bound(Key x)
- 遍历
map<string,int> mp;
for(map<string,int>::iterator it = mp.begin(); it != mp.end();it++){
string key = it->first;
int value = it->second;
}
for(auto it = mp.begin(); it != mp.end();it++){
string key = it->first;
int value = it->second;
}
for(auto& it : mp){
string key = it.first;
int value = it.second;
}
for(auto& [k, v] : mp){ // 🌟, 注意 '&' 避免额外拷贝
string key = k;
int value = v;
}
pair<T1, T2>
可以把两种类型的数据绑定在一块, 形成一个 pair 对
#define PII pair<int,int>
typedef pair<int,int> PII;
using PII = pair<int,int>;
#define fi first
#define se second
对于两个同类型的 pair, 可以做比较, 第一个值作为第一关键字, 第二个值作为第二关键字
最简单的例子可以用 pair<int,int>
来存二维坐标的整点
- 初始化
typedef pair<int,int> Point;
Point p(1,2);
Point u = {3, 4};
Point v = make_pair(5, 6);
cout << u.first << ',' << u.second << '\n'; // 3,4
auto& [x,y] = u;
cout << x << ',' << y << '\n';
//----- 读 n 个点
vector<pair<int,int>> L(n);
for(auto& [x,y] : L){
cin >> x >> y;
}
for(auto& p : L){
cin >> p.first >> p.second;
}
assert(n > 1);
int dis = abs(L[0].fi - L[n-1].fi) + abs(L[0].se - L[n-1].se);
priority_queue<_Tp>
优先队列, 抽象出来就是一个容器, 可以往里面丢东西, 得到里面的极值, 或者把极值丢出去。
这些操作都是 \(log(n)\) 的复杂度, 默认每次取最大的出来
priority_queue<int> Q;
Q.push(1); //{1}
Q.push(4); //{1,4}
Q.push(5); //{1,4,5}
cout << Q.top() << '\n'; // 5
Q.pop();
cout << Q.top() << '\n'; // 4
Q.pop();
cout << Q.top() << '\n'; // 1
Q.pop();
动态维护极值, 一般用在一些算法或者贪心中
queue<_Tp>
queue<int> Q;
Q.push(1); // {1}
Q.push(2); // {1, 2}
Q.front(); // 1
Q.back(); // 2
Q.pop(); // {2}
Q.empty(); // false
Q.clear(); //🙅
while (!Q.empty()){
Q.pop();
}
基本能代替数组模拟的队列, 可以放心使用, 在一些性能热点可以考虑换成数组版
int q[N];
int st, ed;
void init(){
st = ed = 0;
}
void push(int x){
q[ed++];
}
void pop(){
st++;
}
void size(){
return ed - st;
}
数组版有一个好处就是可以很容易遍历元素 for(int i = st;i < ed;i++)
stack<_Tp>
真没啥用
stack<int> stk;
stk.push(1); //[ 1,
stk.push(2); //[ 1, 2
int tp = stk.top(); // 2
stk.pop() ; // [ 1
不如数组(在刷题方面)
int stk[N];
int tp;
void push(int x){
stk[tp++] = x;
}
set<_Tp>
有序集合
set<int> g = {1,2,3};
set<int> s;
s.insert(5); // {5};
s.insert(1); // {1, 5};
s.insert(7); // {1, 5, 7};
for(int v : s){
...
}
set<string> nameSet;
s.insert("Bob");
s.insert("Bob");
s.insert("Alice");
for(string& name : nameSet){
...
}
if(nameSet.find("Bob") != nameSet.end()){
cout << "Bob in set" << '\n';
}
if(nameSet.count("Bob")){
cout << "Bob in set" << '\n';
}
s.empty();
s.size();
s.clear();
常用函数
sort
vector<int> L = {5,3,2,4,1};
sort(L.begin(),L.end());
int A[5] = {4,2,3,5,1};
sort(A, A + 5);
复杂度 \(nlog(n)\) , 不是单纯的快排
lower_bound
找到第一个大于等于给定值的元素位置(指针或迭代器), 要求有序。
vector<int> L = {1,3,5};
int pos1 = lower_bound(L.begin(), L.end(), 3) - L.begin(); // 1
int pos2 = lower_bound(L.begin(), L.end(), -9999) - L.begin(); // 0
int pos3 = lower_bound(L.begin(), L.end(), 9999) - L.begin(); // 3
int A[3] = {1,3,5};
int pos1 = lower_bound(A, A + 3, 3) - A; // 1
...
对于 set
, map
需要通过对象去调用 , 否则使用 std::lower_bound
会变成 \(O(n)\)
set<int> s = {1,2,3};
set<int>::iterator it = s.lower_bound(2); //*it == 2
set<int>::iterator it = s.lower_bound(4); //it == s.end()
lower_bound(s.begin(), s.end(), 3); //🙅
map<int, int> vis = {
{5, 1},
{6, 1}
};
map<int,int>::iterator it = vis.lower_bound(6); //*it == {6,1}
upper_bound
找到第一个严格大于给定值的元素位置
vector<int> L = {1,3,5};
int pos1 = L.upper_bound(L.begin(), L.end(), 3); // 2
max/min
int a = 5,b = 6;
cout << max(a, b) << '\n'; // 6
int c = 7;
cout << max({a,b,c}) << '\n'; // 7
int d = 8;
cout << max({a,b,c,d}) << '\n';// 8
int a = 100;
long long b = 200;
cout << max(a, b) << '\n'; // Error
//这种情况最好是手动进行类型转换
cout << max(1ll * a, b) << '\n';
#define max(a,b) a > b ? a : b
//严格来讲这样写很不好, 但在比赛中我的经验是这样用也不会有啥问题, 不推荐
max_element/min_element
int A[5] = {5,3,4,2,1};
int *m = max_element(A, A + 5);
cout << "max value : " << *m << '\n';
cout << "max value index : " << m - A << '\n';
杂项
其他容器
unordered_map
,unordered_set
,multiset
,multimap
自定义比较
必须满足这三个要求, 否则会导致难以察觉的 bug, 图来源
- 重载
<
运算符
struct student{
string name;
int age;
//参数作为另一个比较对象, 逻辑是 *this < b
bool operator < (const student& b)const{
if(name != b.name){
return name < b.name;
}
// return age <= b.age; //🙅, 显然这里违背了第一条 comp(a,a) == false
return age < b.age; //正确的
}
};
vector<student> L;
sort(L.begin(), L.end());//直接就可以排序
- 自定义比较函数
struct student{
string name;
int age;
};
bool cmp(const student& a, const student& b){
if(a.name != b.name) return a.name < b.name;
return a.age < b.age;
}
vector<student> L;
sort(L.begin(), L.end(), cmp); //作为第三个参数
//lambda表达式
sort(L.begin(), L.end(), [](const student& a, const student& b){
if(a.name != b.name) return a.name < b.name;
return a.age < b.age;
});
和判题机交互
if(cond){
while(1);
}
//如果提交 TLE 了说明有数据使得条件成立
不是特别推荐, 迫不得已的时候再用。 自爆一次罚时毕竟
按行输入
int n;
cin >> n; //🙅
for(int i = 0;i < n;i++){
getline(cin, s); //s 不包含'\n'
cout << s << '\n';
}
/*
这样的写法结果并不理想, cin>>n之后会留一个空格没有被吃掉,
可以使用 getchar(); 或者 cin.ignore(); 吃掉空格
*/
int n;
char s[100];
scanf("%d", &n); getchar(); //吃空格
//或者 scanf("%d ", &n); or scanf("%d\n", &n);
for(int i = 0;i < n;i++){
fgets(s, 100, stdin); //包括 '\n';
printf("%s", s);
}
一些函数
iota
, 递增赋值
int fa[N];
for(int i = 0;i < n;i++) fa[i] = i;
iota(fa, fa + n, 0);
- random_shuffle, 随机打乱数据, 一般用来瞎搞
int A[5] = {1,2,3,4,5};
random_shuffle(A, A + 5);
vector<int> L = {1,2,3,4,5};
random_shuffle(L.begin(), L.end());
- next_permutation,可以求全排列, 一般用作暴力枚举
int A[3] = {1,2,3};
do{
for(int i = 0;i < 3;i++){
cout << A[i] << " \n"[i == 2];
}
}while(next_permutation(A, A + 3));
/*
stdout:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
*/
- reverse, 反转数据
int A[3] = {1,2,3};
reverse(A, A + 3); //A: {3,2,1}
rand()
&srand()
//rand() 生成一个随机数, between 0 and RAND_MAX
//srand() 埋随机数种子, 同一个种子生产的随机数序列相同
int main(){
cout << RAND_MAX << '\n'; //与环境有关,我本机是 2147483647, 也就是 INT_MAX
cout << rand() << '\n'; //为保证debug, 本地运行很多次结果都会是一样的
srand(time(NULL)); //这样每次都不一样
srand(666); //乱搞的时候也可以试自己的幸运数字
}
- nth_element, O(n) 找 k 小
int A[5] = {5,3,2,4,1};
nth_element(A, A + 1, A + 5);//这样下标为 1 的位置上的数就是排序后的那个数, 也就是说 A[1] 就是第二小的值, 前面的值都比他小, 后面的值都比他大, 但不保证有序
- __gcd
using ll = long long;
ll x = 1124211212, y = 12431513413412;
ll g = __gcd(x, y);
int a = 60, b = 80;
int gg = __gcd(a, b);
cout << __gcd(0, 10) << '\n'; //10
- __builtin_popcount
//数二进制1的个数
cout << __builtin_popcount(5) << '\n'; //101 : 2
推荐阅读
习题
-
周六 22:30-24:00 leetcode, 双周赛
-
周日 10:30-12:00 leetcode, 周赛
-
周日 19:35-21:50 Codeforces Round #778 (Div. 1 + Div. 2, based on Technocup 2022 Final Round)
-
周日 20:00-21:40 ABC 244
可以看自己时间参加一次线上竞赛