Loading

[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, 图来源

image-20220317234357517

  • 重载 < 运算符
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

可以看自己时间参加一次线上竞赛

posted @ 2022-03-18 15:34  —O0oO-  阅读(738)  评论(0编辑  收藏  举报