C++ Basic
C++基础/基本注意点
类型范围
int -2147483648~2147483647
long -2147483648~2147483647
long long -9223372036854775808~9223372036854775807 (别用cin,用scanf(“%lld”)) (2^64)
有时候long long 过不了可以用long double
常用定义
正无穷 0x3f3f3f3f
正无穷的初始化
使用<cstring>
头文件:memset(v,0x3f,sizeof v) //将v的值初始化为无穷大0x3f
数据输入输出
-
数据规模大的使用 scanf 读取 printf 输出
-
输出固定n位数(不足补0)
printf("%05d",x) //输出五位整数,不足补0
-
关于字符串的处理(带空格)
getline(cin,str); //注意前面如果按了回车一定要getchar()一下!!或者cin.ignore()
连续读取带所需数字的字符串如
12 is the root of 35
,可以使用sscanf
sscanf(str.c_str(),"%d is the root of %d",&a,&b);
而
sprintf
可以把数字或者其他写入字符数组中char res[5]; sprintf(res,"%04d",num);
-
小数位保留
直接 printf(“%.xlf”,xxx);就能保留小数点后几位 round(x);作用是对小数点后一位进行四舍五入,例如 1.5->2.0 ; 1.56->2.0
转义
%
的转义是%%
位运算
-
除以2的位运算:
>>1
,例如:(a+b)/2
写成a+b>>1
,a/=2
写成a>>=1
;同理,乘以2就是<<1
(注意:只能对整数操作) -
不等于-1,例如
i!=-1
的快速写法~i
-
对2取模,简写成x&1 例如:
if(x%2==1)
改写成if(x&1==1)
浮点数的比较方法
由于计算机中浮点数的存储问题,不能直接==
,需要判断他们相减的值是不是小于某个很小的数,加很多括号是为了防止宏定义出错
const double eps = 1e-8;
#define Equ(a, b) ((fabs((a) - (b))) < (eps))
#define More(a, b) (((a) - (b)) > (eps))
#define Less(a, b) (((a) - (b)) < (-eps))
#define LessEqu(a, b) (((a) - (b)) < (eps))
if(Equ(a, b)) {
cout << "true";
}
圆周率 Pi
const double Pi = acos(-1.0);
数组复制
#include<cstring>
memcpy(a,b,sizeof b); //把b数组复制到a数组中
结构体
结构体可以{aa,bb,cc}
这样写,例如:
struct Person {
string name;
int age,w;
}
vector<Person> ages[N];
int main(){
char name[10];
int age,w;
scanf("%s%d%d",name,&age,&w);
ages[age].push_back({name,age,w}); //将结构体插入ages容器数组中,可以使用类似pair的写法
}
//结构体中也可以定义方法
struct Student(){
Student(){} //构造函数
Student(string _id):id(_id){ //带参数的构造函数
for(int i=1;i<=k;i++) grade[i] = -2;
total = cnt = 0;
}
void calc(){ //自定义计算方法
for(int i=1;i<=k;i++){
total += max(0,grade[i]);
if(grade[i]==p_score[i]) cnt++;
}
}
bool has_submit(){
for(....)
....
return false;
}
bool operator< (const Student& t) const {//运算符重载
.... return ....
}
}
STL 操作
注意事项
xxx
的最后一个元素的迭代指针是xxx.end()-1
- 如果要重复用记得
clear()
一下…
向量 vector
- vector找最值和对应位置,使用中的
min_element()和max_element()
,返回最值的迭代器
auto pos = min_element(v.begin(),v.end())和max_element(v.begin(),v.end())
对应位置是pos-v.begin();值是*pos
-
建立一个逆序vector
vector<int> B(A.rbegin(),A.rend())
-
遍历vector
vector<Person> ages[N];
for(auto &age:ages) sort(age.begin(),age.end());
-
找一个元素
v.find(key) 返回下标指针
-
去重
unique()
需要和``v.erase()`配合才是真正的去重v.erase(unique(v.begin(),v.end()),v.end());
-
初始化二维vector固定大小
//例如在固定大小的地图中 int row = grid.size(); int col = gird[0].size(); vector<vector<int>> dp(row,vector<int>(col,0));
-
push_back()和emplace_back()
emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。
push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
映射 map
- 查找这个map中有没有这个key:
m.count(key)
unordered_map是没自动排序的map,此时通过max_element()找到的最大值,他的key不一定是最小的,若是map则两个相同的值中,找到的key是最小的。
- 找到Map中最大的value对应的key
使用max_element()
配合cmp()
函数中使用pair组成键值对
bool cmp(const pair<type1,type2> left,const pair<type1,type2> right){
return left.second<right.second;
}
- 对map的排序
对map排序实际上还是要转换在vector中进行,重载比较函数
集合 set
添加
s.insert(key)
删除
s.erase(key或者iterator)
这里参数可以是值也可以是位置指针
注意,在multiset中erase一个数的话会把set里的数全删除,删一个数使用s.erase(s.find(x))
优先队列 priority_queue
定义
priority_queue<type,container,function>
使用优先队列实现堆
- 小根堆:
priority_queue<PII,vector<PII>,greater<PII>> heap
PII是pair经过typedef - 大根堆:
priority_queue<PII,vector<PII>,less<PII>> heap
字符串 string
-
删除特定字符
使用
find()
和erase()
-
大小写转换
遍历字符串,对每个字符使用头文件中的
tolower()
,toupper()
-
字符串转数字、数字转字符串
数字 --> 字符串 :
to_string()
字符串 --> 数字:
1. 整数 stoi()2. 浮点,使用 atof(str.c_str()) //这里要传char* 也可以 stof() 和 stod()
-
找到对应子串位置
str.find("substr...");//如果没找到返回-1,找到返回其下标
-
在特定位置插入字串
s.insert(pos,size,char);
例如在字符串开头插入n个0
s.insert(0,n,'0')
-
sscanf()
,sprintf()
从字符串中读入数据和把数据写入字符串中 -
string
和char[]、char*
的转换关系:-
string -> const char*使用
c_str()
-
char*和char[]可以直接转string,如:
string s; char* p="hello"; s=p;
-
-
带空格的字符串按空格分割,使用
stringstream
#include<sstream>vector<string> res; //存放分割后字符串string words="ACD BCD CCD"; string word; stringstream input(words); while(input>>word){ res.push_back(word); cout<<word<<endl; }
关于
isalnum(char c)
判断该字符是不是字符数字或者字母,是返非0,否则为0,还有isupper()
,islower()
等
队列queue、栈stack
他们不支持clear()
操作
-
queue::emplace()
将元素插入队列末尾
-
queue::push()
插入队尾
-
queue::pop()
队头出队
关于permutation
-
is_permutation(v1.begin(),v1.end(),v2.begin())
判断v2是不是v1的序列,是1否0 -
next_permutation
和prev_permutation
用法string s = "12345"; do{ cout<<s<<endl; }while(next_permutation(s.begin(),s.end())); string s = "12345"; do{ cout<<s<<endl; }while(prev_permutation(s.begin(),s.end()));
排序
注意要稳定排序有个stable_sort()
1. 默认函数greater<type>()
和less<type>()
sort的排序默认从小到大递增排列,也可以增加第三个参数传入一个比较函数,C++提供两个默认的比较函数
greater<>()
从大到小,递减排列
less<>()
从小到大,递增排列(默认)
注意:在创建map<>
,set<>
等STL类型时,通常可以传入比较函数改变其默认排序
快排模板
void quick_sort(int q[],int l,int r){
if(l>=r) return;
int x = q[l],i=l-1,j=r+1;
while(i<j){
}
}
2. 使用运算符重载
运算符重载可以放到结构体中
//例如在Person结构体中利用运算符重载排序,要求按资产w降序,否则年龄age升序,其次姓名name升序
struct Person{
string name;
int age,w;//年龄和资产
bool operator< (const Person &t) const//重载小于号
{
if(w!=t.w) return w > t.w; //先按资产排序,资产不同则资产大的排在前面
if(age!=t.age) return age < t.age; //否则年龄不同的话,年龄小的排在前面
return name < t.name; //最后姓名字典序小的排在前面
} //注意后面使用 < 的时候,若 a<b 说明a的资产更大,年龄更小,姓名字典序更小
}
3. 使用lamda表达式
sort(a,a+n,[](int x,int y){
if(cnt[x]!=cnt[y]) return cnt[x]>cnt[y];
return x<y;
});
//也可以匿名函数
auto cmp = [](int x,int y){if(cnt[x]!=cnt[y]) return cnt[x]>cnt[y];return x<y;};
4. 手写cmp()函数
bool cmp(const int a,const int b){ //若e[a]<e[b] 则a排在b的前面
if(e[a]!=e[b]) return e[a]<e[b];
return e[a]>e[b];
}
排序算法操作
K路归并
标准:利用堆找每一路最大/小
其次:使用遍历找
开个vector数组,然后把数组中的vector都排好序,利用遍历找最小
//遍历找最小模板
int t = -1;
for(int i=a,a<=b;i++){
if(t==-1||ages[t][idx[t]]<ages[i][idx[i]])
t=i;
}
//for结束后,t就是最小值的下标
while (true) {
d<<=1;
bool eq = match();
for (int i = 0; i < n; i += d) {
sort(a + i, a + min(n,i + d)); //这里间隔d个排序为什么用min,因为最后面不够d个只能到n了
}
if (eq) break;
}
关于排序题的排名输出(相同分数要求排名一样)
int rank = 1;
for(int i=0;i<list.size();i++){
if(i&&grade[i]!=grad[i-1]) rank=i+1; //如果成绩不等rank就是i+1;
//输出信息...
}
一些题型套路
- 推理题都是用枚举来做
- 链表题都丢到数组里做
基本算法技巧
双指针
核心思想
单调性,将双重循环优for(i:n){for(j:n){}}化到O(n)
基本模板
for(int i=0,j=0;i<n;i++){ while(j<n&&check(i,j)) j++; //然后是具体逻辑}
离散化
用于要使用值做下标的情况下,值域非常大,但是个数不多的时候,把序列映射到从0开始的连续自然数
- 若序列中有重复元素则需要去重
- 需要能快速算出序列中的值离散化后的值(二分)【起始也可以排序后用一个unordered_map记录位置】
//1. 把所有要用到的值加入一个数组中
//2. 对数组进行排序去重
all.erase(unique(all.begin(),all.end()),all.end());
//3. 遍历一次记录对应位置关系
unordered_map<int,int> pos;
for(int i=1;i<=all.size();i++) pos[all[i-1]]=i; //如果要用到前缀和就从1开始映射
//4. 之后每次要用到原本的值,就套一个pos[x]
基础算法模板
二分
左闭右闭,找是否存在满足条件的元素,没找到返回-1
int BS(int l,int r,int key){ while(left<=right){ int mid = l+r>>1; //防止溢出可以mid=left+(right-left)/2 if(A[mid]==key) return mid; else if(A[mid]>key) r = mid-1; else l=mid+1; } return -1;}
找第一个满足条件的元素位置【该条件一定是从左到右先不满足,然后满足】
int BS(int l,int r){ while(l<r){ int mid = l+r>>1; if(check(mid)) r=mid; else l=mid+1; } return l;}
找第一个不满足性质的元素位置【从左到右满足,然后不满足】
int BS(int l,int r){ while(i<r){ int mid = l+r+1>>1; if(check(mid)) l=mid; else r=mid-1; } return l;}
DFS、回溯枝剪
深搜关键在于顺序
关于n皇后的技巧性(数组设置)
int row[N],col[N],dg[N],udg[N];
//这里col[i]表示第i行是否有皇后,dg和udg有坐标变换u-i+n和u+i//即dg[x+y]表示该斜是否有皇后,udg[x-y+n]表示另一斜是否有皇后
//dfs 原始思维 O(2^(n^2))
void dfs(int x,int y,int s) //x表示行,y表示列,s是皇后个数
{
if(y==n) y=0,x++; //列越界,跳至下一行
if(x==n) { //到达最后一行
if(s==n){
for(int i=0;i<n;i++) puts(g[i]);
puts("");
}
return;
}
dfs(x,y+1,s); //不放皇后
if(!row[x]&&!col[y]&&!dg[x+y]&&!udg[x-y+n]){
g[x][y] = 'Q'; //记录
row[x] = col[y] = dg[x+y] = udg[x-y+n] = true;
dfs(x,y+1,s+1);
row[x] = col[y] = dg[x+y] = udg[x-y+n] = false; //回溯状态恢复
g[x][y] = '.'; }
}
//dfs 全排列思想 O(n!)
void dfs(int u) //第u行
{
if(u == n){ //到达最后一行
for(int i=0;i<n;i++) puts(g[i]);
puts("");
return;
}
for(int i=0;i<n;i++) //遍历i列和对应斜线
if(!col[i]&&!dg[u+i]&&!udg[n-u+i]){
g[u][i] = 'Q';
col[i] = dg[u+i] = udg[n-u+i] = true;
dfs(u+1);
col[i] = dg[u+i] = udg[n-u+i] = false; //回溯状态恢复
g[u][i] = '.';
}
}
高精度加法(大数加法)
//C = A + B, A>=0,B>=0 这里就是把A和B的数字相加放到C中
//注意:这里 A,B,C 都是倒序的 比如[1,5] + [1,6] = [2,1,1] <-- 51 + 61 = 112
vector<int> add(vector<int> &A,vector<int> &B){
if(A.size()<B.size()) return add(B,A); //保证A是位数多的
vector<int> C;
int t = 0; //保存每一位相加结果
for(int i=0;i<A.size();i++){
t += A[i];
if(i<B.size()) t+=B[i];
C.push_back(t%10);
t /= 10;
}
if(t) C.push_back(t); //如果t还有进位,再push进去
return C;
}
大数乘除 。。。上Python吧
数据结构相关算法
链表
存储
//数组模拟链表int head,e[N],ne[N],idx; //head头节点下标,e是节点值,ne是节点的下一个节点下标,idx存储用到了哪个地址
链表题做法
先存下链表,然后遍历下链表存在数组中,在数组中进行操作后再把数组变链表
并查集
基本操作
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
//路径压缩find
//初始化操作
for(int i=1;i<=n;i++) p[i]=i;
//合并操作
pa = find(pa), pb = find(pb);
if(pa!=pb) p[pa] = pb;
//并查集中找代表节点,需要通过find()来找
-
统计每个集合中人数
int cnt[N];for(int i=0;i<N;i++) cnt[find(i)]++;//若要再统计集合个数int k=0;while(cnt[k]) k++;
-
统计集合个数(连通块个数)
可以扫描一遍p数组,如果p[i]==i,则集合数量++;
-
维护根结点为点权最大的
//把点权最大的作为根结点for(int i=1;i<idx;i++){ int pi = find(i); if(v[i]>v[pi]){ p[i]=i; p[pi]=i; }}
图
存储
//邻接矩阵int g[N][N];
//邻接表1. 使用vectorvector<int> g[N];g[ver].push_back(b);
//加边2. 使用数组模拟int h[N],e[N],ne[N],w[N],idx;
//结点地址,下一个边,下一个边的结点地址,x到y边的权重,idxmemset(h,-1,h);
//将邻接表表示节点地址的h初始化为-1
void add(int x,int y,int z){
//邻接表加边模板
e[idx]=y, w[idx]=z, ne[idx]=h[x],h[x]=idx++; //y插在对头。}
//使用邻接表时的遍历边
for(int i=h[ver];i!=-1;i=ne[i]){ ...}
遍历
DFS
int dfs(int u){
st[u] = true;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(!st[j]) dfs(j);
}
}
BFS
queue<int> q;
st[root] = true;
q.push(root);
while(q.size()){
int t = q.front();
q.pop();
for(int i=h[t];~i;i=ne[i]){
int j=e[i];
if(!st[j]){
st[j]=true;
q.push(j);
}
}
}
一层一层遍历的BFS
//例如计算前L层中节点个数(若根结点为0层的话)
bool st[N];
int bfs(int s){
memset(st,0,sizeof st);
queue<int> q;
q.push(s);
st[s] = true;
int res = 0; //结点个数
for(int step=0;step<=L;step++){ //遍历L层
int sz = q.size(); //目前层数中结点个数
res += sz;
for(int i=0;i<sz;i++){ //遍历当前层数中所有点
int t = q.front();
q.pop();
for(int j=h[t];~j;j=ne[j]){
int k = e[k];
if(!st[k]){
st[k] = true;
q.push(k);
}
}
}
}
return res; //包括根节点}//抽出模板来就是:
for(int step=0;step<Layer;step++){
int sz=q.size(); ...
for(int i=0;i<sz;i++){
int t = q.front();
q.pop();
for(int j=h[t];~j;j=ne[j]){
int k = e[k];
int(!st[k]){
st[k]=true;
q.push(k);
}
}
}
}
//或者加一层layer把,然后和常规一样了int layer[N];
判断连通
1. 使用DFSbool vis[N]; //表示结点是否被访问
void dfs(int u){
vis[u] = true;
for(int i=1;i<=n;i++){
if(!vis[i]&&g[u][i]) dfs(i);
}
}
2. 使用BFS
int q[N],vis[N];
void bfs(int u){
int hh=0,tt=0;
q[0] = u;
vis[u] = true;
while(hh<=tt){
int k = q[hh++];
vis[k] = true;
for(int i=1;i<=n;i++)
if(!vis[i]&&g[k][i]) q[++tt] = i;
}
}
for(int i=1;i<=n;i++){
if(!vis[i]) return false; //说明不连通
}
3. 使用并查集
int p[N];
int find(int x){
if(x!=p[x]) p[x] = find(p[x]);
return p[x];
}
for(int i=1;i<=n;i++) p[i] = i;
int cnt = n;
应用 – 最短路
Dijkstra(只能处理边权为正数的问题)
//朴素Dijjkstra O(n^2)int dist[N]; //每个点到起点的距离int d[N][N]; //邻接矩阵,记得也要初始化正无穷bool st[N]; //存储每个点的最短距离是否已经确定void dijkstra(){ memset(dist,0x3f,sizeof dist); //dist初始化 dist[S] = 0; //起点S置0 for(int i=0;i<n;i++){ //暴力寻找距离最近的点 int t = -1; for(int j=1;j<=n;j++){ if(!st[j]&&t==-1||(dist[j]<dist[t])) t = j; } st[t] = true; //找到最小后进行标记 for(int j=1;j<=n;j++){ //更新各个节点距离,这里是遍历边的数量m! dist[j] = min(dist[j],dist[t]+d[t][j]); //如果还有其他值需要进行判定更新可以在这里写 } }}//堆优化 O(mlogn)//一般用在稀疏图,用邻接表存,用邻接表时重边就无所谓了typedef piar<int,int> PII; //节点距离和结点编号组成pairpriority_queue<PII,vector<PII>,greater<PII>> heap; //优先队列模拟小根堆int h[N],e[N],ne[N],w[N],idx; //邻接表实现图int dist[N],pre[N];bool st[N]; //判断该点是否已经存在集合中memset(h,-1,sizeof h);void dijkstra(int S,int D){ heap.clear(); memset(dist,0x3f,sizeof dist); memset(st,false,sizeof st); dist[S] = 0; heap.push({0,S}); while(heap.size()){ auto t = heap.top(); heap.pop(); int ver = t.second, distance = t.first; if(st[ver]) continue; //该结点已经在集合中了,直接看下一个 st[ver] = true; //把该结点加入集合 for(int i=h[ver];~i;i=ne[i]){ int j = e[i]; if(dist[j] > distance + w[i]){ dist[j] = distance + w[i]; heap.push({dist[j],j}); pre[j] = ver; }else if(dist[j]==distance+w[i] && ...){ //第二判优条件 ... } } } if(dist[D]=0x3f3f3f3f) return -1; else return dist[D];}//遍历pre生成路径vector<int> path;for(int i=D;i!=S;i=pre[i]) path.push_back(i);cout<<S;for(int i=path.size()-1;i>=0;i--) cout<"->"<<path[i];cout<<endl;
Dijkstra+DFS
//有些条件无法再dijkstra中满足,需要通过dfs遍历所有路径判断//先在dijkstra中存下最短路typedef pair<int,int> PII;priority_queue<PII,vector<PII>,greater<PII>> heap;int h[N],e[N],ne[N],w[N],idx;int dist[N];bool st[N];int c[N]; //假设这个是第二判优值vector<set<int>> pre;void dijkstra(int S){ memset(dist,0x3f,sizeof dist); memset(st,0,sizeof st); dist[S] = 0; heap.push({0,S}); while(heap.size()){ auto t=heap.top(); heap.pop(); int ver=t.second,d = t.first; if(st[ver]) continue; st[ver]=true; for(int i=h[ver];~i;i=ne[i]){ int j = e[i]; if(dist[j]>d+w[i]){ dist[j] = d+w[i]; pre[j].clear(); pre[j].insert(ver); heap.push({dist[j],j}); }else if(dist[j]==d+w[i]){ pre[j].insert(ver); heap.push({dist[j],j}); } } }}//遍历所有最短路,生成最优路//要求://1. 一个全局最优值 optValue//2. 记录最优路径的path数组//3. 临时记录dfs路径的tmpvoid dfs(int v){ //当前结点 if(v==st) //如果到了起点(叶子节点) { tmp.push_back(v); int value=0; for(int i=path.size()-1;i>0;i--)//这里遍历n-1条边,计算值是否是最优值 ... if(value优于optValue) path = tmp; tmp.pop_back(); return; } tmp.push_back(v); for(int i=h[v];~i;i=ne[i]){ int j=e[i]; dfs(j); } tmp.pop_back();}
SPFA(可以处理负边权)
//用到bfs和队列,挺像dij#include<queue>bool st[N]; //标记该点是否在队列int spfa(int S){ queue<int> q; memset(dist,0x3f,sizeof dist); memset(st,false,sizeof st); dist[S]=0; q.push(s); st[S] = true; while(!q.empty()){ int t = q.front(); q.pop(); st[t] = false; for(int i=h[t];~i;i=ne[i]){ int j = e[i]; if(dist[j]>dist[t]+w[i]){ dist[j]=dist[t]+w[i]; pre[j]=t; if(!st[j]) { q.push(j); st[j] = true; } } } } return dist[D]; //返回目的地距离}if(spfa()==0x3f3f3f3f) //不可到达else ...
SPFA判断负环
int dist[N],cnt[N]; //cnt是最短路中经过的点数bool spfa(){ for(int i=1;i<=n;i++){ q.push(); st[i]=true; } while(q.size()){ int t=q.front(); q.pop; st[t]=false; for(int i=h[t];~i;i=ne[i]){ int j=e[i]; if(dist[j]>dist[t]+w[i]){ dist[j]=dist[t]+w[i]; cnt[j]=cnt[t]+1; if(cnt>=n) return true; if(!st[j]){ q.push(j); st[j]=true; } } } } return false;}
Floyd(多源最短路)
int dis[N][N];for(int k=0;i<n;k++) //遍历所有边找k for(int i=0;i<n;i++) for(int j=0;j<n;j++) if(dis[i][k]<INF&&dis[k][j]<INF&&dis[i][k]+dis[k][j]<dis[i][j]) dis[i][j] = dis[i][k]+dis[k][j];
拓扑排序
//使用BFSint h[N],e[N],ne[N],idx; //记得h一开始memset成-1int q[N],d[N]; //d表示该点的入度bool toposort(){ int hh=0,tt=-1; //这里因为没有把对头q[0]初始化,所以tt=-1 for(int i=1;i<=n;i++) //遍历1~n个结点 if(!d[i]) q[++tt]=i; //把入度为0的点加入队列 while(hh<=tt){ int t = q[hh++]; for(int i=h[t];~i;i=ne[i]){ int j=e[i]; if(--d[j]==0) q[++tt] = j; //这里要==0,因为有可能会出现负数 } } return tt==n-1; //如果无环则队列中有n个结点,否则比n少}//输出拓扑序for(int i=0;i<n;i++) cout<<q[i]<<' ';
判断是否为拓扑排序
由于拓扑排序在前面的点的下标一定小于后面的点,遍历每条边,看起点下标是否小于终点下标
超级源点和超级汇点
有时候有多个出发点,比如多个入度为0的,这时候需要一个虚拟起点连接所有真正的起点
DAG最长路(关键路径)
有三种方法做:
- 将边权取相反数,通过SPFA做(先通过拓扑排序判断一下有无环)
- 使用e[N],l[N],ve[N],vl[V],w[N]的关键路径求解做法
- 使用DP,DP[i][j]表示i到j的最长路
树
通过两个遍历建树
中序+前/后序
int l[N],r[N],pos[N]; //l,r是左右孩子,pos是节点在中序遍历中对应位置int in[N],pre[N]; //或者是postorder,反正一定要有一个中序遍历int build(int il,int ir,int pl,int pr){ int root = pre[pl]; //如果是后序遍历就是post[pr] int k = pos[root]; if(il<k) l[root] = build(il,k-1,pl+1,pl+k-il); //后序是build(il,k-1,pl,k-il+pl-1) if(ir>k) r[root] = build(k+1,ir,pl+k-il+1,pr); //build(k+1,ir,k-il+pl,pr-1) return root;}
中序+层序
思路:先求出先序,然后通过先序和中序建树
int layer[N],in[N],pre[N];int l[N],r[N],pos[N];int idx;void makepre(int il,int ir,int ll,int lr){ int i=0,j=0; for(i=ll;i<=lr;i++){ bool find=false; for(j=il;j<=ir;j++) if(l[i]==in[j]){ pre[idx++]=in[j]; //构造先序 find=true; break; } if(find) break; } if(j>il) makepre(il,j-1,ll,lr); //ll和lr是一直不变的 if(j<ir) makepre(j+1,ir,ll,lr);}int build(int il,int ir,int pl,int pr){ //这个就和上面的一样了}
层序遍历
//借助队列void bfs(int root){ //单独遍历一层的办法 q[0]=root; int hh=0,tt=0; while(hh<=tt){ int head=hh,tail=tt; //这里表示该层的头和尾 while(hh<=tail){ //对这一层处理,加入队列 int k = q[hh++]; if(l[k]) q[++tt] = l[k]; if(r[k]) q[++tt] = r[k]; } }}
完全二叉树
存储
使用一维数组存取层序遍历,一个点x左儿子2x,右节点2x+1,父节点x/2下取整
给定值,建立完全二叉搜索树
排序得到中序遍历,然后按中序遍历下树,边遍历边填数
判断是否为完全二叉树
//通过递归把节点值放入数组中,放完最大下标为n则是完全二叉树【在二叉树中叫做先序遍历】int maxk;void dfs(int u,int k){ if(maxk<k) maxk = k; if(l[u]!=-1) dfs(l[u],k*2); if(r[u]!=-1) dfs(r[u],k*2+1);}//使用bfs判断int q[N];bool bfs(int u){ int hh=0,tt=0; q[0] = u; pos[u] = 1; //存放节点对应位置 while(hh<=tt){ int k = q[hh++]; if(pos[u]>n) return false; if(l[k]) q[++tt] = l[k], pos[l[k]] = pos[k]*2; if(r[k]) q[++tt] = r[k], pos[r[k]] = pos[k]*2+1; } return true;}
二叉排序树 BST
插入
void insert(int &u,int w){ if(!u) u=++idx,v[u] = w; //如果节点不存在,分配一个地方存 else if(w<v[u]) insert(l[u],w); else insert(r[u],w);}
删除
int findMin(int root){ while(r.count(root)) root = r[root]; return root;}int findMax(int root){ while(l.count(root)) root = l[root]; return root;}int del(int u){ }//如果是叶节点,直接删除//否则删除其中序遍历下的前驱/后继
平衡二叉树 AVL
int l[N],r[N],v[N],h[N],idx; //左孩子,右孩子,节点值,节点高度,当前用的节点void update(int u){ //调整节点高度 h[u] = max(h[l[u],h[r[u]])+1; }int get_balance(int u){ return h[l[u]]-h[r[u]];} //左子树高度减右子树高度int R(int &u){ //右旋 int p = l[u]; l[u] = r[p],r[p] = u; update(u),update(p); u = p;}int L(int &u){ //左旋,和右旋是对称的 int p = r[u]; r[u] = l[p],l[p] = u; update(u),update(p); u = p;}void insert(int &u,int w){ if(!u) u =++idx,v[u] = w; else if (w<v[u]){ insert(l[u],w); if(get_balance(u)==2){ if(get_balance(l[u])==1) R(u); else L(l[u]),R(u); } } else{ insert(r[u],w); if(get_balance(u)==-2){ if(get_balance(r[u]==-1)) L(u); else R(r[u]),L(u); } } update(u);}
记忆化搜索(树形dp)
int f[N],p[N]; //比如:f存储到根节点的距离,p[u]是表示u节点的父节点 都初始化为-1int dfs(int u){ if(f[u]!=-1) return f[u]; //f[u]已经存在了,不再搜索 if(p[u]==-1) //是根节点 return f[u] = 0; return f[u] = dfs(p[u]) + 1; }
最低公共祖先 (LCA)
爬山法
思路:在下面的节点一直移动到和另一个同一层开始,一起向上爬,直到相等 时间复杂度:O(h)
需要优化常数:把int范围内的数映射到0~N-1 如把最小的数映射到0,第二小的数映射到1…(离散化) 这种没说数据范围的就需要离散化,或者你的n开无敌大。。int范围内
int h[N],p[N]; //求LCA中要用到两个信息:节点深度和节点父节点int pre[N],in[N]; //存离散化后的值int seq[N]; //存原本的序列unordered_map<int,int> pos; //存储离散化后的位置//离散化,要取原本的值就是seq[i]for(int i=0;i<n;i++){ pos[seq[i]] = i;}//二叉树重建int build(int il,int ir,int pl,int pr,int depth) //由于这里只要父节点和高度的信息,只用h和p,参数传入深度{ h[root]=depth; ...}//爬山法int x=a,y=b;while(a!=b){ if(h[a]<h[b]) b=p[b]; //b在下面,b上去 else a=p[a];}if(a!=x&&b!=y){ }else if(a==x&&b!=y){ }else if(a!=x&&b==y){ }else{ //a==x&&b==y }
Huffman – 参照贪心
堆
概念
大根堆:父节点的值大于等于子节点
小根堆:父节点的值小于等于子节点
判断是不是堆(是什么堆)
bool lt = false, gt = false;for (int i = 1; i <= n; i ++ ) for (int j = 0; j < 2; j ++ ) //遍历左右孩子 if (i * 2 + j <= n) { //如果左右孩子合法 if (h[i]<h[i*2+j]) lt=true; else gt=true; }if (lt && gt) puts("Not Heap");else if (lt) puts("Min Heap");else puts("Max Heap");
建立
//动态插入//大根堆,插入一个上调一个for(int i=1;i<=n;i++){ cin>>h[i]; up(i);}//标准堆的初始化for(int i=n/2;i>=0;i--){ //从最后一个非叶子节点开始,down down(i);}
基本操作
//向下调整int h[N];void down(int u){ int t = u;//要和最小的孩子交换 if(t*2<size && h[u*2]<h[t]) t=u*2; if(t*2+1<size && h[u*2+1]<h[t]) t=u*2+1; if(u!=t){ //如果u需要被交换 swap(h[u],h[t]); down(t); }}//大根堆向上调整void up(int u){ while(u/2&&h[u/2]<h[u]){ //如果父节点存在且父节点小于该节点,交换,递归调整父节点 swap(h[u/2],h[u]); u/=2; }}
插入
//插入到最后一个位置,然后upheap[++size] = x;up(size);
删除
// 1. 删除最小值//把最后一个元素覆盖到堆顶,删除后然后downheap[1] = heap[size--];down(1);// 2. 删除任意元素,修改也是一个道理heap[k] = heap[size--];if(k变小了){ up(k);}else { down(k);}//或者直接down(k);up(k); //只会执行里面的一个
实际题目中的应用
几种实现方式:
-
手写堆
-
优先队列 — 其实如果有些题只是要用到堆这个数据结构用来排序优化找最值可以用set替代(例如dijkstra的堆优化)【优先队列和set一个原理】(但是不支持修改任意元素,会产生冗余)
-
优先队列不支持修改内部元素和遍历,还是用set吧。。。
-
使用stl的make_heap(),可以用vector模拟heap方便访问内部元素
#include<algorithm> vector<int> heap; //建立 auto cmp = [](const int x,const int y){return x>y;}; //匿名函数,类型只能是auto make_heap(heap.begin(),heap.end(),cmp);//这里cmp可以用greater和less,greater是小根堆,less是大根堆; 默认大根堆 //当然对于特定题目条件最好还是自己写cmp,比如有时候是<=,有时候是<??? //插入 heap.push_back(data); //先用push_back插入,然后push_heap一下 push_heap(heap.begin(),heap.end(),cmp); //删除 pop_heap(heap.begin(),heap.end(),cmp); //把堆顶丢到最后面,所以pop_heap之后再pop_back()一下即可 heap.pop_back(); //排序,排序后就不是一个堆了 sort_heap(heap.begin(),heap.end(),cmp); //使用前要保证符合堆特性 is_heap(); //判断是否能是一个二叉堆 is_heap_until() //返回第一个破坏二叉堆结构元素的迭代器
完整的模拟堆
#include<string>#include<iostream>#include<algorithm>#define N 100010using namespace std;//这里由于要删除和修改第k个插入的数,开ph和hp两个数组int h[N],hsize,ph[N],hp[N]; //ph[k]表示第k个插入的数的下标,hp[k]堆里面的点是第几个插入的void heap_swap(int a,int b){ //交换堆中两个元素 swap(ph[hp[a]],ph[hp[b]]); //交换第k个插入的下标 swap(hp[a],hp[b]); //交换两结点时第几个交换的 swap(h[a],h[b]); //交换堆中两个元素}//小根堆void down(int u){ int t=u; //找到左右孩子中最小的那一个,然后和父亲交换,递归down孩子 if(u*2<=hsize&&h[u*2]<h[t]) t=u*2; if(u*2+1<=hsize&&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/2]>h[u]){ //如果父亲比孩子大,交换,再看新父亲是否需要up heap_swap(u/2,u); u/=2; }}int main(){ int n,m=0; scanf("%d",&n); getchar(); while(n--){ string cmd; cin>>cmd; if(cmd=="I"){ //插入 int x; cin>>x; hsize++; m++; //表示第几个插入的 ph[m]=hsize; hp[hsize]=m; h[hsize]=x; up(hsize); }else if(cmd=="D"){ //删除第k个 int k; cin>>k; k = ph[k]; heap_swap(k,hsize); hsize--; down(k),up(k); }else if(cmd=="C"){ //修改第k个 int k,x; cin>>k>>x; k=ph[k]; h[k]=x; down(k),up(k); }else if(cmd=="PM"){ //输出最小 printf("%d\n",h[1]); }else{ //删除最小 heap_swap(1,hsize); //由于存在下标映射,所有交换都改heap_swap hsize--; down(1); } } return 0;}
对顶堆
上面用一个小根堆维护,下面用大根堆,下面元素个数比上面多一个或一样多;则中位数一定在下面的堆顶
小根堆堆顶作为分界线
如果两堆个数相差>=1,把多的堆顶弹出加入另一堆顶
(可以使用两个set模拟)
插入
把x和下面堆的堆顶比较,大于插入上面的堆,x<=下面的堆顶,插入下面。
**调整:**如果上面元素多,把上面的最小值pop插入到下面的堆;如果下面多则把下面的最大值插入到上面
删除
和下面堆的最大值比较,小于就在下面删除,否则在上面删除
应用
- 求中位数
具体实现
multiset<int> up,down; //上面存大的,小根堆,下面存小的,大根堆,下面元素个数永远比上面打一个或相同,中位数再下面的堆顶void adjust(){ while(up.size()>down.size()){ //pop上面最小的,插入下面 down.insert(*up.begin()); up.erase(up.begin()); } while(down.size()>up.size()+1){ //pop下面最大的,插入上面 auto it = down.end(); it--; up.insert(*it); down.erase(it); }}cin>>x;//插入if(up.empty()||x<*up.begin()) down.insert(x); //插入下面的情况:1.上面为空;2.小于上面最小值else up.insert(x);adjust();//删除auto it = down.end();it--;if(x<*it) down.erase(down.find(x)); //小于下面最大的,在下面else up.erase(up.find(x)); //否则在上面adjust();//中位数就是下面的最大值auto it = down.end();it--;cout<<*it;
前缀和
前缀和用于快速求数组中一段区间内的和 【下标一定要从1开始】
如我们要求Dl+Dl+1+…Dr**(原数组中一段区间内的和)**
则S0=0,S1=D1,S2=D1+D2,Si表示前i个数组元素的和,然后用Sr-(Sl-1)
//求Sis[0]=0;for(int i=1;i<=n;i++){ s[i]=s[i-1]+a[i];}//求区间l到r内和cout<<s[r]-s[l-1];
二维前缀和
//初始化S[i][j]s[0][0]=0;for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) s[i][j] = s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];//求一段子区域和(x1,y1)->(x2,y2) 就类似概率论离散求分布函数一样cout<<s[x2][y2]-s[x2][y1-1]-s[x1-1,y2]+s[x1-1][y1-1];
对于环的处理,把它展开 如1->2->3->4->5, 5->1
转成1 2 3 4 5 1 2 3 4 5
那么,1到3就可以走1->2->3
也可以走3->4->5->1
差分
差分就是前缀和的逆,类似微分和积分
O(n)时间内从差分数组B求一次前缀和到原数组A,用途,高效让A内一段区间内的值+C
int a[N],b[N]; //a原数组,b差分数组void insert(int l,int r,int c){ b[l]+=c,b[r+1]-=c;}for(int i=1;i<=n;i++) insert(i,i,a[i]); //初始化差分数组//若要累加insert(l,r,c);for(int i=1;i<=n;i++) b[i]+=b[i-1]; //求一下前缀和for(int i=1;i<=n;i++) printf("%d ",b[i]); //打印序列
贪心
区间贪心
1. 选择不相交区间(做法和区间选点一样)
描述
n个开区间(ai,bi),选择尽量多个区间,之间没有公共点(例如在时间不冲突情况下选尽量多课)
做法
- 按照右端点从小到大排序,从第一个区间开始选
- 把所有和上一个区间相交的区间排除在外
//例如
struct req {
string start;
string end;
bool operator<(const req &t)const {
return end < t.end; //结束时间早的排在前面
}
} list[N];
for (int i = 0; i < n; i++)
cin >> list[i].start >> list[i].end;
sort(list, list + n); //排序
int res = 0;
string prev;
for (int i = 0; i < n; i++) {
if (list[i].start >= prev) { //下一个开始时间晚于上一个结束时间则选择,否则不选
prev = list[i].end;
res++;
}
}
2. 区间选点(和上面的一毛一样)
描述
n个闭区间[ai,bi],选择尽量少的点,使每个区间内都至少一个点
做法
- 按区间右端点从小到大排(右端点相同时按左端点从大到小)
- 从前往后依次枚举每个区间,若当前区间已经包含点,跳过;否则,选当前区间右端点
//遍历代码如下int ed=-2e9; //将ed初始化为负无穷for(int i=0;i<n;i++){ if(arr[i].l>ed){ //该区间的左端点>上一个右端点 cnt++; ed=arr[i].r; }}
3. 区间分组
描述:给定 N 个闭区间 [ai,bi],将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
做法:
- 将所有区间按左端点从小到大排序
- 从前往后处理每个区间,判断能否将其放到某个现有的组(即:该区间左端点是否大于组内右端点最大值,大于即有交集,不能放)
- 如果不存在这样的组,开新组
- 存在,放进去并更新 Max_r
//实现遍历的时候使用一个小根堆维护组内区间右端点的最小值priority_queue<int,vector<int>,greater<int>> heap;for(int i=0;i<n;i++){ auto s = list[i]; //当前区间 if(heap.empty()||heap.top()>=s.left) heap.push(s.right); //当前左端点小于右端点的最小值,新开一个组 else { int t=heap.top(); heap.pop(); heap.push(s.right); } cout<<heap.size()<<endl; //组的个数}
4. 区间覆盖
描述:N个闭区间[ai,bi]以及一个线段区间[s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。(一个目标区间,n个备选区间,从备选的中选最少的覆盖目标区间)
做法:
- 将所有区间按左端点从小到大排序
- 从前往后枚举每个区间,在所有能覆盖开始位置start的区间中,选右端点最大的区间。然后将start更新成右端点的最大值
bool find = false; //找到方案bool变量for(int i=0;i<n;i++){ int j=i,r=-2e9; //r是能覆盖开始位置start的的区间中右端点最大值 while(j<n&&list[j].left<=start){ //遍历所有左端点在目标区间内右端点的最大值是多少 r=max(r,list[j].right); j++; } if(r<st){ res = -1; //无解 break; } res++; if(r>=ed){ find = true; break; } st=r; i=j-1;}if(!find) ...
H指数
排序后看最大的i个数是不是满足都大于i,即排序后看倒数第i个数是否大于i
置换群
转换成图论
i在p[i](有序序列)的位置上,就让i到p[i]连一条边
此时连成的图的每个点出度入度都是1,它必然是一堆环
如果要交换排序,目标就是把n个环变成n个自环
而操作: 交换两个数 <=> {
一个环变成两个环(环内交换):{
- 和0【可以操作的点】的下一个点交换,下一个点会变自环 - 和非next点交换会变两个环,到时候还是要合并回来(redundancy)
}
两个环变一个环(环外交换)
}
//例题:swap(0,i)操作进行排序
#include...
using namespace std;
const int N = 100010;
int n,p[N];
int main(){
cin>>n;
for(int i=0;i<n;i++){
int id;
cin>>id;
p[id] = i; //记录位置
}
int res = 0;
for(int i=1;i<n;i++) //扫描所有不是自环的点
{
while(p[0]!=0) swap(p[0],p[p[0]]),res++; //把0在的环变自环,单0不在原本位置时,和0的下一个数交换
while(i<n&&p[i]==i) i++;
if(i<n) swap(p[0],p[i]),res++; //此时i必然在另一个环里面
}
printf("%d\n",res);
return 0;
}
哈夫曼树
可以使用优先队列模拟小根堆
#include<iostream>#include<queue>#include<vector>using namespace std;int n,x;int main(){ cin>>n; priority_queue<int,vector<int>,greater<int>> heap; for(int i=0;i<n;i++) cin>>x,heap.push(x); int res=0; while(heap.size()>1){ int a = heap.top();heap.pop(); int b = heap.top();heap.pop(); res+=a+b; heap.push(a+b); } cout<<res<<endl; return 0;}
排序不等式(打水问题)
描述:给n个人打水时间,安排打水顺序使得等待时间最小
做法:
按时间排序,然后总时间按公式算(注意可能爆出int,最好用long long)
sort(t,t+n);for(itn i=0;i<n;i++) res+=t[i]*(n-i-1); //也可以for(int i=1;i<=n;i++) res+=t[i-1]*(n-i)printf("%lld\n",res);
绝对值不等式
描述:在一条数轴上有N个点表示商家an,an中选一个点x到其他商家距离之和最小
即:max{|a1-x|+|a2-x|+|a3-x|+…}
做法:
对所有an排序,n是奇数,x选中位数;n是偶数,中间两个数都可以,总和就是每个点到中位数的点的距离之和
sort(a,a+n);int res=0;for(int i=0;i<n;i++) res+=abs(a[i]-a[n/2]);
推公式
比如耍杂技的牛,排序后算就可以了
动态规划 dp
概念
具有最优子结构,核心是找到状态转移方程和边界条件
滚动数组
若更新状态只依赖于与之前的状态,可以使用滚动数组,降低空间复杂度
关于时间
还是看看算法笔记吧。。。
- 全部转换为统一单位
- 使用string
- scanf(%d:%d:%d),转换成总秒数之类的…
- 输出持续时间可以通过总秒数转换而来,例如
printf("%02d:%02d:%02d",sum/3600,sum%3600/60,sum%60);
数学
最大公约数(约数:因数)gcd
//辗转相除法(欧几里得算法)int gcd(int a,int b){ return b?gcd(b,a%b):a; }
最小公倍数 lcm
在gcd的基础上,把ab/d,防止溢出就是a/d*b
int lcm(int a,int b){ int d = gcd(a,b); return a/d*b;}
拓展欧几里得
求x、y,使得ax+by=gcd(a,b)
int exgcd(int a,int b,int &x,int &y){ if(!b){ x=1,y=0; return a; } int d = exgcd(b,a&b,y,x); y -= (a/b)*x; return d;}
分数
- down必须为非负数,分数为负则另up为负
- 分数为0则令up为0,down为1
- 分子和分母需要时刻保持最简
typedef long long int LL; //防止溢出LL gcd(LL a,LL b){ return b?gcd(b,a%b):a;}struct Fraction{ LL up,down; void reduction(){ if(down<0){ up=-up; down=-down; } if(up==0) down=1; else{ int d = gcd(abs(up),abs(down)); //这里记得加绝对值 up/=d; down/=d; } } Fraction operator+(const Fraction &FR){ Fraction r; r.down = down*FR.down; r.up = up*FR.down+FR.up*down; r.reduction(); return r; } Fraction operator-(const Fraction &FR){ Fraction r; r.down = down*FR.down; r.up = up*FR.down-FR.up*down; r.reduction(); return r; } Fraction operator*(const Fraction &FR){ Fraction r; r.down = down*FR.down; r.up = up*FR.up; r.reduction(); return r; } Fraction operator/(const Fraction &FR){ Fraction r; r.down = down*FR.up; r.up = up*FR.down; r.reduction(); return r; }};//输出if(r.down==1) printf("%lld\n",r.up); //integerelse if(abs(r.up)>abs(r.down)) printf("%d %d/%d",r.up/r.down,abs(r.up)%r.down,r.down);//fakeelse printf("%d/%d",r.up,r.down); //real
质数
判断质数(试除法)
约数定理:一个数的所有约数(因数)是成对出现:如果d是n的约数,则n/d也是n的约数,故只要枚举d<=n/d的约数 --> d<=根号n(试除法)
bool isPrime(int n){ if(n==1) return false; for(int i=2;i*i<=n;i++) //这里优化写法是写成 for(int i=2;i<=n/i;i++) 防止i*i溢出 if(n%i==0) return false; return true;}
素数筛
线性筛
bool st[N]; //标记一个数是不是素数int primes[N]; //存储素数int cnt;void get_primes(int x){ for(int i=2;i<=x;i++){ if(!st[i]) primes[cnt++]=i;//如果是素数,加入素数表中 //筛选 for(int j=0;primes[j]<=x/i;j++){ //从所有素数中筛选 st[primes[j]*i] = true; if(i%primes[j]==0) break;//primes[j]一定是i的最小质因子 } }}
分解质因子
从小到大枚举所有约数,如果可以被除,就除干净
//分解质因子 x是要分解的数,k是质因子相乘个数void divide(int x){ for(int i=2;i<=x/i;i++){ if(x%i==0){ int s=0; while(x%i==0) x/=i,s++; cout<<i<<' '<<s<<endl;//底数和指数 } } if(x>1) cout<<x<<' '<<1<<endl; //唯一一个大于根号x的质因数 cout<<endl;}
计算n!的质因子个数
int cal(int n,int p) //计算n!有多少个质因子p{ if(n<p) return 0; return n/p+cal(n/p,p);}
约数
求所有约数(试除法)
vector<int> get_divisor(int x){ vector<int> res; for(int i=1;i<=x/i;i++) //这里和素数不一样从1开始 if(x%i==0){ res.push_back(i); if(i!=x/i) res.push_back(x/i); } sort(res.begin(),res.end()); return res;}
欧拉函数
求欧拉函数
/phi(N) = 1~N中与N互质的数的个数
int phi(int x){ int res=x; for(int i=2;i<=x/i;i++){ if(x%i==0){ res=res/i*(i-1); //res/=质数*(质数-1) while(x%i==0) x/=i; } } if(x>1) res=res/x*(x-1); return res;}
线性筛欧拉函数
int primes[N],cnt; //存储所有素数int euler[N]; //存储每个数的欧拉函数bool st[N]; //标记x是否被筛掉void get_eulers(int x){ euler[1]=1; for(int i=2;i<=x;i++){ if(!st[i]){ primes[cnt++]=i; euler[i]=i-1; } for(int j=0;primes[j]<x/i;j++){ int t = primes[j]*i; st[t]=true; if(i%primes[j]==0){ euler[t]=euler[i]*primes[j]; break; } euler[t]=euler[i]*(primes[j]-1); } }}
快速幂
LL qmi(int a,int b,int p){ LL res=1%p; while(b){ if(b&1) res=res*a%p; a=a*(LL)a%p; b>>=1; } return res;}
组合数
计算Cmn(m在下面) [需要根据数据范围选方法]
//线性,可能乘法溢出typedef long long LLLL C(LL n,LL m){ LL ans = 1; for(LL i=1;i<=m;i++) ans=ans*(n-m+i)/i; return ans;}//递归LL C(LL n,LL m){ if(m==0||n==m) return 1; return C(n-1,m)+C(n-1,m-1);}//递推LL res[N][N];LL C(LL n,LL m){ if(m==0||m==n) return 1; if(res[n][m]!=0) return res[n][m]; return res[n][m]=C(n-1,m)+C[n-1][m-1];}//递推打表(m,n<=2000)int c[N][N];void init(){ for(int i=0;i<N;i++) for(int j=0;j<=i;j++) if(!j) c[i][j]=1; else c[i][j]=(c[i-1][j]+c[i-1][j-1]);}
关于逆元
计算Cmn%p
//逆元、费马小定理(m,m<=100000)int fact[N],infact[N]; //阶乘%p,逆元%pfact[0]=infact[0]=1;int qmi(int a,int k,it p); //快速幂{ int res = 1; while(k){ if(k&1) res = (LL)res*a%p; k>>=1; } return res;}for(int i=1;i<N;i++){ fact[i]=(LL)fact[i-1]*i%mod; infact[i]=(LL)infact[i-1]*qmi(i,p-2,p)%mod;}Cab%mod(LL)fact[a]*infact[b]%mod*infact[a-b]%mod;//Lucas 要求p是素数int Lucas(int n,int m){ if(m==0) return 1; return C(n%p,m%p)*Lucas(n/p,m/p)%p;}
卡特兰数
int Cat(n){ return C(2*n,n)/(n+1);}
TIPS
- 要找到小的可以使用swap,stl也可以直接swap