算法
算法竞赛学习笔记
从输入输出、常用函数到图论与动态规划的一组持续更新的竞赛备赛记录。
注意点
- cin cout 换成 scanf printf
- 用空间换速度:三层循环写成两层。https://www.acwing.com/problem/content/1223/
- scanf 读字符容易包括空格回车,所以建议直接读字符串
- 超过10 0000用scanf
- 遇到sf用exit(0)二分查找错误
- 整数类型大于4位用long long
- while(x)只是判断不等于0
- strlen(sc[i]+1)若字符串从第一个位置开始
-
#define x first #define y second typedef pair<int,int> PII PII a[100]; a[1] = {1,2} makepair

函数
< algorithm >中的函数
sort(a+1,a+n+1); 起始位置到终点位置。常与pair进行联合使用,先排pair的first再排pair的second
< sstream >
- getline()
string line;
getline(cin, line); // 忽略掉第一行的回车
getline(cin,line); //从标准输入流(即键盘)读取一行字符串,并将其存储到变量 line 中。在读取这一行时,以换行符为行的结束标志。
stringstream ssin(line); //把line放进流里(stringstream是C++中定义的一个类,代表了一个输入输出流)
while (ssin >> a[n]) n ++ ; //用输入流提取运算符将流中内容存入a[n]
<string.h>
void *memset(void *str, int c, size_t n)
< queue >
升序队列,小根堆
priority_queue <int,vector<int>,greater<int>> q;
降序队列,大根堆
priority_queue <int,vector<int>,less<int>> q;
< map >
map<typename1,typename2>mp;
构造映射,键为typename1,值为typename2
会以键大小从小到大映射
可用于字符串统计
<unordered_map>
unordered_map<elemType_1, elemType_2>
unordered_map<int, int> map;
map.insert({1,3})
map.empty()
输入
while(scanf("%d%d%d",&n,&m,&k) != -1)
所有区间的i
for(int i = 1; i <= n; i++)
for(int j = i; j <= n; j++)
所有区间段
按区间大小从小到大枚举
for(int len = 2; len <= n; len++)
for(int i = 1; i + len + -1 <= n; i++)
{
int j = i + len - 1;
for(int k = i; k < j; k++)
}
取整
上取整
- int(ce /ceil 返回double 类型 < cmath>
- a mod b = (a + b - 1) / b = (a - 1) / b + 1
排序
快速排序

https://www.zhihu.com/search?type=content&q=%20%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F
这个和Y总的代码实现不同,但思维是相通的
- 当边界用i,边界点不能取q[l],例[0,1]会出现死循环
- 当边界用j,边界点不能取q[r],例[0,1]会出现死循环
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
归并排序

- 对比
双指针
为了维护操作某一段区间
找单调性
设置i,j双指针,j经过每个循环+1,当不满足条件时,i++,直至满足条件。
树状数组和线段数
快速求前缀和(区间查询) O(logn)
在某个位置上的数加上一个数(单点修改)O(logn)
- 线段数 完全包含 树状数组
- 能用树状数组就用树状数组
- 从1开始
树状数组

- tr[x] 表示区间和是**(x-lowbit(x),x]**, 记住就行
- add(x, k)让后面所有包含元素x区间和都增加k
- query(x) 累加x前面全部的元素,每个i包含了(i-lowbit(i),i]的数
//返回二进制的第一个非零的1
int lowbit(int x)
{
return x & -x;
}
void add(int x, int k)
{
for(int i = x; i <= n; i+=lowbit(i)) tr[i] += k;
}
void query(int x)
{
int res = 0;
for(int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
线段数

struct node{
int l,r; //左右区间
int sum; //总和
}tr[N*4];
void push_up(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); //递归右儿子
push_up(u); //做完儿子,更新当前节点信息
}
}
int query(int u, int l, int r) //从根节点开始往下找对应的区间
{
if(l <= tr[u].l && tr[u].r <= r) return tr[u].sum;//如果当前区间已经完全被查询包含,直接返回值
int mid = tr[u].l + tr[u].r >> 1;
//先判断和左边是否有交集
int sum = 0;
if(mid >= l) 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; //若当前已经是叶节点,总和加上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);
push_up(u);
}
}
for(int i=1;i<=n;i++)scanf("%d",&w[i]);
build(1,1,n);/*第一个参数是根节点的下标,根节点是一号点,然后初始区间是 1 到 n */
二分和前缀和
整数二分
- mid = (l + r + 1) / 2 ,若 mid = (l + r) / 2 有边界问题:若 l = r - 1,mid = l,边界还是 l 和 r 出现死循环。
- mid = (l + r + 1) / 2 取上界,mid = (l + r) / 2 取下界。

实数二分
- 不用考虑边界
前缀和
- 预处理:用数组纪录和 https://www.acwing.com/problem/content/797/
- 若空间不够,原数组可省略,只用前缀和数组


一维差分
快速给某一个区间加上一个数,最终求每一个数的值。
核心操作:b[l] += x, b[r+1] -= x
首先给定一个原数组a:a[1], a[2], a[3],,,,,, a[n];
然后我们构造一个数组b : b[1] ,b[2] , b[3],,,,,, b[i];
使得 a[i] = b[1] + b[2 ]+ b[3] +,,,,,, + b[i]
也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。
二维差分

KMP
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
Trie
高效存储和查找字符串集合的数据结构
题目限制一般在26个字母

插入
void insert(char *str)
{
int p = 0; //类似指针,指向当前节点
for(int i = 0; str[i]; i++)
{
int u = str[i] - 'a'; //将字母转化为数字
if(!son[p][u]) son[p][u] = ++idx;
//该节点不存在,创建节点,其值为下一个节点位置
p = son[p][u]; //使“p指针”指向下一个节点位置
}
cnt[p]++; //结束时的标记,也是记录以此节点结束的字符串个数
}
查找
int query(char *str)
{
int p = 0;
for(int i = 0; str[i]; i++)
{
int u = str[i] - 'a';
if(!son[p][u]) return 0; //该节点不存在,即该字符串不存在
p = son[p][u];
}
return cnt[p]; //返回字符串出现的次数
}
并查集
将两个集合合并hi
询问两个元素是否在一个集合当中
-
原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。

-
变形:找到需要维护的变量
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]); //find 函数不仅有找祖宗的功能,还把这个查找路径上所有节点直接变成了祖宗节点的孩子,即路径压缩
return p[x];
}
堆

- 完全二叉树:除了最后一层节点,上面所有节点都是满的,最后节点从左到右排列。
- 小根堆:每个点小于等与左右儿子,所以根节点是最小值。
- 存储;down(X); up(x);

为什么从2/n开始down?
因为n是最大值,n/2是n的父节点,因为n是最大,所以n/2是最大的有子节点的父节点,所以从n/2往前遍历,就可以把整个数组遍历一遍
-
理解heap_swap中的次序
房间a中放有元素10,房间b中放油元素20。
10(犯人1)是第一个插入的,20(犯人二)是第二个插入的。
h[a] = 10, h[b] = 20 swap: h[a] = 20,h [b] = 10 hp[a] = 1 ,hp[b] = 2 swap:hp[a] = 2 ,hp[b] = 1 ph[1] = a ,ph[2] = b swap:ph[1] = b ,ph[2] = a
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
数学问题
- 尽力分析
- 打表找规律 用dfs
- a, b的最大不能组成的数:(a - 1) * (b - 1) - 1
动态规划
dp分析法:
状态表示
集合:不重不漏
属性:最大值,最小值,数量
状态计算:用集合表示,不重不漏
背包问题——最值、个数
- 没有固定模板,核心在于状态表示和转移。
- 0-1背包:物品只能选一次。一维二层枚举需逆序,因为f[ j ]需由f[ i-1 ][ j ]状态更新而来。
f[i][j] = max(f[i-1][j], f[i-1][j-v) + w) // 二维
f[j] = max(f[j],f[j-v] + w) //一维,二层循环需逆序,f[x][j-v)由上一层得到,j-v小于循环中的j,小的j的先被改变
- 完全背包:物品可以用无数次。

f[i][j] = max(f[i-1][j], f[i][j-v) + w) // 二维
f[j] = max(f[j],f[j-v] + w) //一维,二层循环不用逆序, f[x][j]由上一层得到,f[x][j-v]由第i层得到
- 多重背包:物品可以一次或多次。
法一:将多重背包拆解成01背包
法二:当数据量大,需通过二进制优化。
二进制优化:最少用几个数能表示小于等于k的所有数。可以将一个数拆成几个数,把一个问题拆成logn。
-
分组背包:每一组选一个,最后可看成0-1背包问题

背包问题参考 https://blog.csdn.net/raelum/article/details/128996521
数位统计——==分情况讨论==
递归和递推
- 递归将问题分为子问题
- 递推由子问题推
枚举
- 判断闰年:
int leap = year % 100 && year % 4 == 0 || year % 400 == 0;
距离公式
- 曼哈顿距离
$d_{M}(P_1, P_2) = |x_1 - x_2| + |y_1 - y_2|$
-
欧几里得距离
$$ d(x_1,x_2) = \sqrt{\sum_{i=1}^n{(x_{1,i}-x_{2,i})^2}} $$
单调队列
- 求窗口里面的最大最小值
- 求离他最近比他小的元素或最大的元素
狄杰斯塔拉
不断选择距离最近的点
快速幂
快速求a * (b^x)
while(x)
{
if(x & 1) a = a * b;
b = b ^ 2;
b >>= 1;
}
矩阵乘法
void mul(int res[][N], int a[][N], b[][N])
{
int temp[][N]={0};
for(int i=0; i<N; i++)
for(int j=0; j<N; j++)
for(int k=0; k<N; k++)
temp[i][j] = temp[i][j] + a[i][k] * b[k][j];
memcpy(res,temp,sizeof temp);
}