CSP初赛知识点整理

· · 算法·理论

linux指令

命令 全称/解释 作用
ls List 列出目录内容
pwd Print Working Directory 显示当前所在目录的路径
cd Change Directory 切换当前目录
mkdir Make directory 创建新目录
rmdir Remove directory 删除空目录
rm Remove 删除文件或目录
cp Copy 复制文件或目录
mv Move 移动或重命名文件/目录
touch 创建一个新的空文件或更新文件时间戳
cat Catenate 查看文件全部内容
head 查看文件开头10行
tail 查看文件末尾10行

变量类型

类型名 等价类型 位宽 字节
signed char signed char 8 1
unsigned char unsigned char 8 1
short,short int,signed short,signed short int short int 16 2
unsigned short,unsigned short int unsigned short int 16 2
int,signed,signed int int 32 4
unsigned,unsigned int unsigned int 32 4
long,long int,signed long,signed long int long int 32 4
unsigned long,unsigned long int unsigned long int 32 4
long long,long long int,signed long long,signed long long int long long int 64 8
unsigned long long,unsigned long long int unsigned long long int 64 8

memset(a, 0x3f, sizeof a)的计算方式:

首先假设a为int型,由上表可知为32位。memset按照16进制赋值,这时候3f的含义其实是(1f)_{16} = (21)_{10}=(11111)_2

我们计算memset的时候按照上面的字节数进行计算,这时候a里面的每一个元素是0x1f1f1f1f,不要胡思乱想,按照16进制计算即可。

0x1f1f1f1f = 1 * 16^7 + F * 16^6 + 1 * 16^5 + F * 16^4 + 1 * 16^3 + F * 16^3 + 1 * 16 + F

位运算

&:相同为1,不同为0

|:含1为1,全0为0

^:相同为0,不同为1

位运算常见技巧(加粗为在阅读中可能看不懂的技巧):

功能 示例 位运算
去掉最后一位 101101\rightarrow 10110 x >> 1
在最后加一个 0 101101\rightarrow 1011010 x << 1
在最后加一个 1 101101\rightarrow 1011011 x << 1 or 1
把最后一位变成 1 101100\rightarrow 101101 x or 1
把最后一位变成 0 101101\rightarrow 101100 x or 1 xor 1
最后一位取反 101101\rightarrow 101100 x xor 1
把右数第 k 位变成 1 k=3101001\rightarrow101101 x or (1 << (k - 1))
把右数第 k 位变成 0 k=3101101\rightarrow 101001 x and ~(1 << (k - 1))
右数第 k 位取反 k=3101001\rightarrow 101101 x xor (1 << (k - 1))
取末三位 1101101\rightarrow 101 x and 7
取末 k k=51101101\rightarrow 01101 x and ((1 << k) - 1)
取右数第 k k=41101101\rightarrow 1 (x >> (k - 1)) and 1
把末 k 位变成 1 k=4101001\rightarrow 101111 x or ((1 << k) - 1)
k 位取反 k=4101001\rightarrow 100110 x xor ((1 << k) - 1)
把右边连续的 1 变成 0 100101111\rightarrow 100100000 x and (x + 1)
把右起第一个 0 变成 1 100101111\rightarrow 100111111 x or (x + 1)
把右边连续的 0 变成 1 11011000\rightarrow 11011111 x or (x - 1)
取右边连续的 1 100101111\rightarrow 1111 (x xor (x + 1)) >> 1
去掉右起第一个 1 的左边 100101000\rightarrow 1000 x and (x xor (x - 1))

数论相关

辗转相除法gcd(a,b) = gcd(b, a mod b)

费马小定理:若p为素数,a^p mod p = a mod p

lucas定理C(n, k) = C(n % p, k % p) * C(n / p, k / p)

排列组合常忘思路:隔板法

错排公式Dn = (n - 1) * (Dn-1 + Dn-2)

图论相关

欧拉路径:经过图中每条边恰好一次的路径。

欧拉回路:经过图中每条边恰好一次的回路。

欧拉图:存在欧拉回路的图。

半欧拉图:不存在欧拉回路但是存在欧拉路径的图。

二分图:可以刻画成这个样子的图。

割点/边:删掉之后图不连通的点/边。

点/边双(连通分量):随便删掉一个点/边仍然联通的子图。

树的直径:树上任意两节点之间最长的简单路径。

树的重心:如果在树中选择某个节点并删除,这棵树将分为若干棵子树,统计子树节点数并记录最大值。取遍树上所有节点,使此最大值取到最小的节点被称为整个树的重心。说人话就是,以树的重心为根时,最大子树的大小取最小值。

树的中心:在树中,如果节点x作为根节点时,从x出发的最长链最短,那么称x为这棵树的中心。

计算带权路径长度:每个叶子节点的权值乘以它的路径长度(从根到该节点的边数),然后求和。

排序相关

不稳定:快速、希尔、选择、堆。

参考网上的记忆口诀:情绪不稳定,快些(希)选堆零食吃。

选择、冒泡、插入是比较朴素的排序方式。故时间复杂度均为n^2。

快速、堆、归并均为nlogn,但是快速排序可以被卡到n^2。

由于快速排序本质是归并的改良版本,所以空间复杂度均为n。

  1. 冒泡排序

    • 原理:重复遍历列表,比较相邻元素,如果顺序错误就交换。每一轮将最大的元素“冒泡”到最后。
  2. 选择排序

    • 原理:每次从未排序部分中选择最小(或最大)的元素,放到已排序部分的末尾。
  3. 插入排序

    • 原理:将每个待排序元素插入到前面已排好序部分的正确位置,就像打扑克牌理牌一样。
  4. 快速排序

    • 原理分治策略。
      1. 选基准:从数组中挑一个元素作为“基准”。
      2. 分区:将数组重新排列,所有比基准小的放左边,比基准大的放右边。
      3. 递归:递归地对左右两个子数组进行快速排序。
  5. 归并排序

    • 原理分治策略。
      1. :递归地将数组分成两半,直到每个子数组只有一个元素。
      2. :将两个已排序的子数组合并成一个新的有序数组。
  6. 堆排序

    • 原理
      1. 建堆:将待排序数组构建成一个最大堆(或最小堆)。
      2. 排序:反复将堆顶元素(最大/最小值)与堆末尾元素交换,并减小堆大小,重新调整堆。
  7. 计数排序

    • 原理:不是比较排序。假设输入数据都是小范围整数,统计每个元素出现的次数,然后直接算出每个元素的位置。
  8. 桶排序

    • 原理:将数据分到有限数量的里,每个桶再分别排序(通常使用其他排序算法)。
  9. 基数排序

    • 原理:按照位数(个位、十位、百位...)来进行排序,从最低位开始排。

排列组合

C_m^n= \binom{m}{n} = \frac{m!}{n! \cdot (m-n)!}

指针相关

指针的算术运算和数组下标访问是等价的。当你有一个指针 p 时,p[a] 实际上被编译器解释为 *(p + a)

常用函数

函数 作用 常用场景 注意
unique(begin, end) “去除”相邻重复 去重 必须先排序,并配合 erase
lower_bound(begin, end, val) 找第一个>=val的元素 有序序列的查找 返回迭代器
upper_bound(begin, end, val) 找第一个>val的元素 有序序列的查找 返回迭代器
next_permutation(begin, end) 下一个排列 生成全排列 会修改原序列
reverse(begin, end) 反转序列 字符串、数组反转
max_element(begin, end) 找最大值 找最大元素及其位置 返回迭代器
fill(begin, end, value) 填充值 初始化数组

易忘模板

LCA求直径:

#include <cstring>
#include <iostream>
#include <vector>

constexpr int MXN = 40005;
using namespace std;
vector<int> v[MXN];
vector<int> w[MXN];

int fa[MXN][31], cost[MXN][31], dep[MXN];
int n, m;
int a, b, c;

// dfs,用来为 lca 算法做准备。接受两个参数:dfs 起始节点和它的父亲节点。
void dfs(int root, int fno) {
  // 初始化:第 2^0 = 1 个祖先就是它的父亲节点,dep 也比父亲节点多 1。
  fa[root][0] = fno;
  dep[root] = dep[fa[root][0]] + 1;
  // 初始化:其他的祖先节点:第 2^i 的祖先节点是第 2^(i-1) 的祖先节点的第
  // 2^(i-1) 的祖先节点。
  for (int i = 1; i < 31; ++i) {
    fa[root][i] = fa[fa[root][i - 1]][i - 1];
    cost[root][i] = cost[fa[root][i - 1]][i - 1] + cost[root][i - 1];
  }
  // 遍历子节点来进行 dfs。
  int sz = v[root].size();
  for (int i = 0; i < sz; ++i) {
    if (v[root][i] == fno) continue;
    cost[v[root][i]][0] = w[root][i];
    dfs(v[root][i], root);
  }
}

// lca。用倍增算法算取 x 和 y 的 lca 节点。
int lca(int x, int y) {
  // 令 y 比 x 深。
  if (dep[x] > dep[y]) swap(x, y);
  // 令 y 和 x 在一个深度。
  int tmp = dep[y] - dep[x], ans = 0;
  for (int j = 0; tmp; ++j, tmp >>= 1)
    if (tmp & 1) ans += cost[y][j], y = fa[y][j];
  // 如果这个时候 y = x,那么 x,y 就都是它们自己的祖先。
  if (y == x) return ans;
  // 不然的话,找到第一个不是它们祖先的两个点。
  for (int j = 30; j >= 0 && y != x; --j) {
    if (fa[x][j] != fa[y][j]) {
      ans += cost[x][j] + cost[y][j];
      x = fa[x][j];
      y = fa[y][j];
    }
  }
  // 返回结果。
  ans += cost[x][0] + cost[y][0];
  return ans;
}

void Solve() {
  cin.tie(nullptr)->sync_with_stdio(false);
  // 初始化表示祖先的数组 fa,代价 cost 和深度 dep。
  memset(fa, 0, sizeof(fa));
  memset(cost, 0, sizeof(cost));
  memset(dep, 0, sizeof(dep));
  // 读入树:节点数一共有 n 个,查询 m 次,每一次查找两个节点的 lca 点。
  cin >> n >> m;
  // 初始化树边和边权
  for (int i = 1; i <= n; ++i) {
    v[i].clear();
    w[i].clear();
  }
  for (int i = 1; i < n; ++i) {
    cin >> a >> b >> c;
    v[a].push_back(b);
    v[b].push_back(a);
    w[a].push_back(c);
    w[b].push_back(c);
  }
  // 为了计算 lca 而使用 dfs。
  dfs(1, 0);
  for (int i = 0; i < m; ++i) {
    cin >> a >> b;
    cout << lca(a, b) << '\n';
  }
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  int T;
  cin >> T;
  while (T--) Solve();
  return 0;
}

Floyd算法:

for (k = 1; k <= n; k++) {
  for (x = 1; x <= n; x++) {
    for (y = 1; y <= n; y++) {
      f[k][x][y] = min(f[k - 1][x][y], f[k - 1][x][k] + f[k - 1][k][y]);
    }
  }
}

SPFA

struct edge {
  int v, w;
};

vector<edge> e[MAXN];
int dis[MAXN], cnt[MAXN], vis[MAXN];
queue<int> q;

bool spfa(int n, int s) {
  memset(dis, 0x3f, (n + 1) * sizeof(int));
  dis[s] = 0, vis[s] = 1;
  q.push(s);
  while (!q.empty()) {
    int u = q.front();
    q.pop(), vis[u] = 0;
    for (auto ed : e[u]) {
      int v = ed.v, w = ed.w;
      if (dis[v] > dis[u] + w) {
        dis[v] = dis[u] + w;
        cnt[v] = cnt[u] + 1;  // 记录最短路经过的边数
        if (cnt[v] >= n) return false;
        // 在不经过负环的情况下,最短路至多经过 n - 1 条边
        // 因此如果经过了多于 n 条边,一定说明经过了负环
        if (!vis[v]) q.push(v), vis[v] = 1;
      }
    }
  }
  return true;
}

堆优化Dijstra

struct edge {
  int v, w;
};

struct node {
  int dis, u;

  bool operator>(const node& a) const { return dis > a.dis; }
};

vector<edge> e[MAXN];
int dis[MAXN], vis[MAXN];
priority_queue<node, vector<node>, greater<node>> q;

void dijkstra(int n, int s) {
  memset(dis, 0x3f, (n + 1) * sizeof(int));
  memset(vis, 0, (n + 1) * sizeof(int));
  dis[s] = 0;
  q.push({0, s});
  while (!q.empty()) {
    int u = q.top().u;
    q.pop();
    if (vis[u]) continue;
    vis[u] = 1;
    for (auto ed : e[u]) {
      int v = ed.v, w = ed.w;
      if (dis[v] > dis[u] + w) {
        dis[v] = dis[u] + w;
        q.push({dis[v], v});
      }
    }
  }
}