知识点
STL
vector, 变长数组,倍增的思想
size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end()
[]
支持比较运算,按字典序
pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
string,字符串
size()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址
queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素
priority_queue, 优先队列,默认是大根堆
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素
deque, 双端队列
size()
empty()
clear()
front()/back()
push_back()/pop_back()
push_front()/pop_front()
begin()/end()
[]
哈希表
#include<unordered_map>
unordered_map<int, int> map;
unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
hmap[4] = 14;
hmap[4] = 15;
cout << hmap[4]; //结果为15
hmap.insert({ 5,15 });
iter = hmap.find(2);
int count = hmap.count(key);
单调栈
//常见模型:找出每个数左边离它最近的比它大/小的数
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int stk[N], tt;
int main()
{
cin >> n;
for(int i = 0; i < n; i++)
{
int x;
cin >> x;
while(tt && stk[tt] >= x) tt--;
if(tt) cout << stk[tt] << " ";
else cout << -1 << " ";
stk[++tt] = x;
}
return 0;
}
单调队列
链接: link
#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
using namespace std;
const int N = 1000010;
int a[N];
int main()
{
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i ++ ) cin >> a[i];//读入数据
deque<int> q;
for(int i = 1; i <= n; i++)
{
while(q.size() && q.back() > a[i]) //新进入窗口的值小于队尾元素,则队尾出队列
q.pop_back();
q.push_back(a[i]);//将新进入的元素入队
if(i - k >= 1 && q.front() == a[i - k])//若队头是否滑出了窗口,队头出队
q.pop_front();
if(i >= k)//当窗口形成,输出队头对应的值
cout << q.front() <<" ";
}
q.clear();
cout << endl;
//最大值亦然
for(int i = 1; i <= n; i++)
{
while(q.size() && q.back() < a[i]) q.pop_back();
q.push_back(a[i]);
if(i - k >= 1 && a[i - k] == q.front()) q.pop_front();
if(i >= k) cout << q.front() << " ";
}
}
输出随机数
#include<stdlib.h>
#include<time.h>
//以上两个头文件必须加
srand(time(NULL));
//输出随机数前执行此语句
printf("%d",rand()%X);
//输出一个0~X-1的随机整数。
if(是样例) printf(样例);
else printf("-1");
单起点单终点最短路
Dijkstra-朴素O(n^2)
初始化距离数组, dist[1] = 0, dist[i] = inf;
for n次循环 每次循环确定一个min加入S集合中,n次之后就得出所有的最短距离
将不在S中dist_min的点->t
t->S加入最短路集合
用t更新到其他点的距离
Bellman_fordO(nm)
注意连锁想象需要备份, struct Edge{inta,b,c} Edge[M];
初始化dist, 松弛dist[x.b] = min(dist[x.b], backup[x.a]+x.w);
松弛k次,每次访问m条边
Floyd O(n^3)
初始化d
k, i, j 去更新d
dijkstra
链接: link
//伪代码
int dist[n],state[n];
dist[1] = 0, state[1] = 1;
for(i:1 ~ n)
{
t <- 没有确定最短路径的节点中距离源点最近的点;
state[t] = 1;
更新 dist;
}
#include<iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int h[N], e[M], ne[M], w[M], idx;//邻接表存储图
int state[N];//state 记录是否找到了源点到该节点的最短距离
int dist[N];//dist 数组保存源点到其余各个节点的距离
int n, m;//图的节点个数和边数
void add(int a, int b, int c)//插入边
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void Dijkstra()
{
memset(dist, 0x3f, sizeof(dist));//dist 数组的各个元素为无穷大
dist[1] = 0;//源点到源点的距离为置为 0
for (int i = 0; i < n; i++)
{
int t = -1;
for (int j = 1; j <= n; j++)//遍历 dist 数组,找到没有确定最短路径的节点中距离源点最近的点t
{
if (!state[j] && (t == -1 || dist[j] < dist[t]))
t = j;
}
state[t] = 1;//state[i] 置为 1。
for (int j = h[t]; j != -1; j = ne[j])//遍历 t 所有可以到达的节点 i
{
int i = e[j];
dist[i] = min(dist[i], dist[t] + w[j]);//更新 dist[j]
}
}
}
int main()
{
memset(h, -1, sizeof(h));//邻接表初始化
cin >> n >> m;
while (m--)//读入 m 条边
{
int a, b, w;
cin >> a >> b >> w;
add(a, b, w);
}
Dijkstra();
if (dist[n] != 0x3f3f3f3f)//如果dist[n]被更新了,则存在路径
cout << dist[n];
else
cout << "-1";
}
bellmen_ford
#include<iostream>
#include<cstring>
using namespace std;
const int N = 510, M = 10010;
struct Edge {
int a;
int b;
int w;
} e[M];//把每个边保存下来即可
int dist[N];
int back[N];//备份数组防止串联
int n, m, k;//k代表最短路径最多包涵k条边
int bellman_ford() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i++) {//k次循环
memcpy(back, dist, sizeof dist);
for (int j = 0; j < m; j++) {//遍历所有边
int a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], back[a] + w);
//使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
}
}
return dist[n];
}
int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i++) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
e[i] = {a, b, w};
}
int res = bellman_ford();
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else cout << res;
return 0;
}
多源最短路
floyd()
#include <iostream>
using namespace std;
const int N = 210, M = 2e+10, INF = 1e9;
int n, m, k, x, y, z;
int d[N][N];
void floyd() {
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main() {
cin >> n >> m >> k;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(i == j) d[i][j] = 0;
else d[i][j] = INF;
while(m--) {
cin >> x >> y >> z;
d[x][y] = min(d[x][y], z);
//注意保存最小的边
}
floyd();
while(k--) {
cin >> x >> y;
if(d[x][y] > INF/2) puts("impossible");
//由于有负权边存在所以约大过INF/2也很合理
else cout << d[x][y] << endl;
}
return 0;
}
最小生成树
链接: link
Kruskal
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int p[N];//保存并查集
struct E{
int a;
int b;
int w;
bool operator < (const E& rhs){//通过边长进行排序
return this->w < rhs.w;
}
}edg[N * 2];
int res = 0;
int n, m;
int cnt = 0;
int find(int a){//并查集找祖宗
if(p[a] != a) p[a] = find(p[a]);
return p[a];
}
void klskr(){
for(int i = 1; i <= m; i++)//依次尝试加入每条边
{
int pa = find(edg[i].a);// a 点所在的集合
int pb = find(edg[i].b);// b 点所在的集合
if(pa != pb){//如果 a b 不在一个集合中
res += edg[i].w;//a b 之间这条边要
p[pa] = pb;// 合并a b
cnt ++; // 保留的边数量+1
}
}
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) p[i] = i;//初始化并查集
for(int i = 1; i <= m; i++){//读入每条边
int a, b , c;
cin >> a >> b >>c;
edg[i] = {a, b, c};
}
sort(edg + 1, edg + m + 1);//按边长排序
klskr();
//如果保留的边小于点数-1,则不能连通
if(cnt < n - 1) {
cout<< "impossible";
return 0;
}
cout << res;
return 0;
}
树状数组和线段树
树状数组 (logn)
- 给某个位置上的数加上一个数 ----- 单点修改
- 快速的求动态的前缀和 ------ 区间查询
延申 ----- 区间修改和单点查询 + 差分
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int a[N], tr[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x, int v)
{
for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}
int query(int x)
{
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for (int i = 1; i <= n; i ++ ) add(i, a[i]);
while (m -- )
{
int k, x, y;
scanf("%d%d%d", &k, &x, &y);
if (k == 0) printf("%d\n", query(y) - query(x - 1));
else add(x, y);
}
return 0;
}
线段树
- 单点修改
- 区间查询
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int w[N];
struct Node
{
int l, r;
int sum;
}tr[N * 4];
void pushup(int u)
{
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void build(int u, int l, int r)
{
if (l == r) tr[u] = {l, r, w[r]};
else
{
tr[u] = {l, r};
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
int query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
int mid = tr[u].l + tr[u].r >> 1;
int sum = 0;
if (l <= mid) sum = query(u << 1, l, r);
if (r > mid) sum += query(u << 1 | 1, l, r);
return sum;
}
void modify(int u, int x, int v)
{
if (tr[u].l == tr[u].r) tr[u].sum += v;
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) modify(u << 1, x, v);
else modify(u << 1 | 1, x, v);
pushup(u);
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
build(1, 1, n);
int k, a, b;
while (m -- )
{
scanf("%d%d%d", &k, &a, &b);
if (k == 0) printf("%d\n", query(1, a, b));
else modify(1, a, b);
}
return 0;
}
字符串哈希
#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5+5,P = 131;//131 13331
ULL h[N],p[N];
// h[i]前i个字符的hash值
// 字符串变成一个p进制数字,体现了字符+顺序,需要确保不同的字符串对应不同的数字
// P = 131 或 13331 Q=2^64,在99%的情况下不会出现冲突
// 使用场景: 两个字符串的子串是否相同
ULL query(int l,int r){
return h[r] - h[l-1]*p[r-l+1];
}
int main(){
int n,m;
cin>>n>>m;
string x;
cin>>x;
//字符串从1开始编号,h[1]为前一个字符的哈希值
p[0] = 1;
h[0] = 0;
for(int i=0;i<n;i++){
p[i+1] = p[i]*P;
h[i+1] = h[i]*P +x[i]; //前缀和求整个字符串的哈希值
}
while(m--){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(query(l1,r1) == query(l2,r2)) printf("Yes\n");
else printf("No\n");
}
return 0;
}
1. 求二进制中1的个数
链接: 二进制中1的个数
int get_count(int x)//返回x的二进制有多少个1
int get_count(int x)
{
int res = 0;
while (x)
{
res ++ ;
x -= x & -x;
}
return res;
}
2. 建树,树的DFS, BFS
链接: 排列数字
链接: 走迷宫
链接: 树的重心
链接: 图中点的层次
记得初始化头节点
const int N = 1e5 + 10, M = N * 2;
int h[N], e[M], ne[M], idx;
void add(int a, int b) //如果是无向图,加两条边
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int dfs(int u)
{
state[u] = true;
for(int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(!state[j])
dfs(j);
}
}
queue<int> q;
st[1] = true; //表示1号点已经被遍历过了
q.push(1);
while(q.size())
{
int t = q.front();
q.pop();
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(!st[j])
{
st[j] = true;
q.push(j);
}
}
}
3. 快速幂 O(logk)
链接: 快速幂
用来快速求出ak mod p的结果
数据范围: 1 <= a, p, k <= 109
//两个十的九次方数相乘会爆int
typedef long long LL;
int qmi(int a, int k, int p)
{
int res = 1;
while(k)
{
if(k & 1) res = (LL)res * a % p; //要先转型再计算
k >>= 1;
a = (LL)a * a % p;
}
return res;
}
4. 分解质因数 O(sqrt(n))
链接: 分解质因数
void divide(int x)
{
for(int i = 2; i * i <= x; i++) //x > 2 * 10^10的范围太大的话,i要定义成LL(9 * 10^19)
if(x % i == 0)
{
int s = 0;
while(x % i == 0) x /= i, s++;
cout << i << " " << s << endl;
}
//大于根号x的数只能有一个,此时x也是质因子
if(x > 1) cout << x << " " << 1 << endl;
cout << endl;
}
5. 欧拉函数
链接: 欧拉函数
int phi(int x)
{
int res = x;
for(int i = 2; i * i <= n; i++)
if(x % i == 0)
{
while(x % i == 0) x /= i;
res = res / i * (i - 1);
}
if(x > 1) res = res / x * ( x - 1 );
return res;
}
6. 最大公约数 O(n(log(n))
链接: 最大公约数
//辗转相除法
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
//辗转相减法
lcm最小公倍数
最小公倍数 = 两数乘积 / 最大公约数
int gcd(int a, int b){
do{
int c = a % b;
a = b;
b = c;
}while(b);
return a;
}
int lcm(int a, int b){
return a*b / gcd(a, b);
}
唯一分解定理
约数定理
约数个数定理
约数和定理
7. 二分 O(nlog(n))
链接: 数的范围
二分的作用是找到一个数是答案。
二分将区间分为两个部分,一边成立,一边不成立。
二分右半边的最左侧答案
int bsearch_1(int l, int r)
{
while(l < r)
{
int mid = l + r >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
二分左半边的最右侧答案
int bsearch_2(int l, int r)
{
while(l < r)
{
int mid = l + r + 1 >> 1;
if(check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
8. 差分 O(nlog(n))
链接: 差分矩阵
连续子区间相同变化。
for(int i = 1; i <= n; i++)
{
cin >> a[i];
b[i] = a[i] - a[i - 1];
}
for(int i = 1; i <= m; i++)
{
int l, r, c;
cin >> l >> r >> c;
b[l] += c;
b[r + 1] -= c;
}
for(int i = 1; i <= n; i++)
{
b[i] += b[i - 1];
}
9. 前缀和 O(nlog(n))
链接: 子矩阵的前缀和
求一段子区间上的和
for(int i = 1; i <= n; i++)
{
cin >> a[i];
a[i] += a[i - 1];
}
while(m--)
{
int l, r;
cin >> l >> r;
cout << a[r] - a[l - 1] << endl;
}
10. 双指针 O(log(n))
链接: 最长连续不重复子序列
双指针的本质是:i,j具有单调关系,从而i指针遍历的时候,j指针不需要回退。
//通用模板
for(int i = 0, j = 0; i < n; i++)
{
while(j < i && check(i, j)) j++;
... //每道题的具体逻辑
}
11. 日期问题
链接: 日期差值
//基础模板
//每个月有几天
int months[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
//判断闰年
int is_leap(int year)
{
if(year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
return 1;
return 0;
}
//求每月多少天
int get_days(int y, int m)
{
if(m == 2) return 28 + is_leap(y);
else return months[m];
}
//计算从0000/00/00到现在有多少天
//计算差值就是两个数相减
int cal(int y, int m, int d)
{
int res = 0;
for(int i = 1; i < y; i++)
res += 365 + is_leap(i);
for(int i = 1; i < m; i++)
res += get_days(y, i);
return res + d;
}
//读入
int y, m, d;
scanf("%04d%02d%02d", &y, &m, &d); //yyyymmdd
//读入多组数据
int y1, m1, d1, y2, m2, d2;
while (~scanf("%04d%02d%02d\n%04d%02d%02d",
&y1, &m1, &d1, &y2, &m2, &d2))
printf("%d\n", abs(cal(y1, m1, d1) - cal(y2, m2, d2)) + 1);
12. 并查集 O(1)
链接: 合并集合
在O(1)的时间复杂度里合并两个集合,和查询一个点是否在这个集合里。
int p[N]; //存储每个点的祖宗节点
//返回x的祖宗节点
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
//初始化,假定节点编号1~n
for(int i = 1; i <= n; i++) p[i] = i;
//合并a和b所在的两个集合:
p[find(a)] = find(b);
13. 状态压缩DP
数据范围 >= 30