freopen("input.txt","r",stdin); freopen("output.txt","w",stdout);
一、动态规划
1.背包DP
n件价值vi重量wi将容量为m的背包放入物品中
dp[i]表示不同重量下的最大值
for(int i=0;i<n;i ){ for(int j=m;j>=w[i];j--){ dp[j]=max(dp[j],dp[j-w[i]] v[i]); } } cout<<dp[m]<<endl;
dp[i]表示不同价值下的最小重量
memset(dp,INF,sizeof(dp)); dp[0]=0; for(int i=0;i<n;i ){ for(int j=M;j>=v[i];j--){ dp[j]=min(dp[j],dp[j-v[i]] w[i]); } } long long i,ans; for(i=0;i<M;i ) if(dp[i]<=t) ans=i; cout<<ans<<endl;
for(int i=0;i<n;i ){ for(int j=w[i];j<=m;j ){ dp[j]=max(dp[j-w[i]] v[i],dp[j]); } }
二进制优化
时间复杂:O(nmlog(si))
for(int i=0;i<n;i ){ int tv,tw,tm; cin>>tv>>tw>>tm; int c=1; while(tm>c){ w[cnt] = c*tw; v[cnt ] = c*tv; tm -= c; c<<=1; } w[cnt] = tm*tw; ////补充不足指数的差值 v[cnt ] = tm*tv; } for(int i=0; i<cnt; i ){ for(int j=t; j>=w[i]; j--){ dp[j]=max(dp[j],dp[j-w[i]] v[i]); } }
优化单调队列
时间复杂度:O(nm)
for(int i=1;i<=m;i ){ cin>>w>>v>>num; for(int d=0;d<w;d ){ ///模拟余数 head=tail=1; //初值 for(int j=0;j<=(n-d)/w;j ){ //(n-d)/w指目前有多少余数 int o=f[j*w d]-v*j;///进入队列的值 while(q[tail-1]<=o&&head<tail)tail--;///弹出队尾 q[tail]=o;//进队 p[tail ]=j;//下标 while(j-p[head]>num&&head<tail)head ;///弹出队头 f[j*w d]=max(f[j*w d],q[head] v*j);//找最大值 } } }
void solve(){ //将 n 二分,前半部分的所有可能值都存储在中 pair 中(数位01设置选择) int n1 = n / 2; int num1 = 1 << n1; for(int i = 0; i < num1; i ) { long long sw = 0, sv = 0; for(int j = 0; j < n1; j ) { if((i >> j) & 1) { sw = w[j]; sv = v[j]; } } pi[i] = make_pair(sw, sv); } //单调排序,筛除 w[i]>=w[j]但v[i]<=v[j]的 pair sort(pi, pi num1); int m = 1; for(int i = 1; i < num1; i ) { if(pi[m-1].second < pi[i].second) { pi[m ] = pi[i]; } } //处理另一半 n ,寻找满足 w 时 最大的 v int n2 = n - n1; int num2 = 1 << n2; long long ans = 0; for(int i = 0; i < num2; i ) { long long sw = 0, sv = 0; for(int j = 0; j < n2; j ) { if((i >> j) & 1) { sw = w[n1 j]; sv = v[n1 j]; } } if(sw <= W) { long long tv = (lower_bound(pi, pi m, make_pair(W - sw, INF)) - 1)->second; ans = max(ans, sv tv); } } cout<<ans<<endl; }
区间DP
for (int i = 0; i <= a.size(); i ) dp[i][0] = i; for (int j = 0; j <= b.size(); j ) dp[0][j] = j; for(int i=1;i<=a.size();i ){ for(int j=1;j<=b.size();j ){ if (a[i-1]==b[j-1]) { dp[i][j] = dp[i - 1][j - 1]; // 不需要编辑 } else { int insert1 = dp[i][j - 1] 1; // A 插入 B 第j个字符 int insert2 = dp[i - 1][j] 1; // B 插入 A 第一个字符 int replace = dp[i - 1][j - 1] 1; // A 替换成 B 第j个字符 dp[i][j] = min(replace, min(insert1, insert2)); } } } cout<<dp[a.size()][b.size()]<<endl;
for(int len=1;len<n;len ){ for(int l=1;l len<=n;l ){ int r = l len; dp[l][r]=INF; for(int k=l;k<r;k ){ dp[l][r]=min(dp[l][r],dp[l][k] dp[k 1][r] s[r]-s[l-1]); } } } cout<<dp[1][n]<<endl;
2.数位DP
获取x,单独存储在a[i]中
int solve(int x){ int pos=0; while(x){ a[pos ]=x; x/=10; } return dfs(pos-1,-1,0,true); }
深度优先搜索(DFS)
int dfs(int pos,int pre,int sta,bool limit){ if(pos==-1) return 1; if(!limit && dp[pos][sta]!=-1) return dp[pos][sta]; int up=limit ? a[pos] : 9; int tmp=0; for(int i=0;i<=up;i ) { if(pre==6 && i==2)continue; if(i==4) continue;///保证枚举的合法性 tmp =dfs(pos-1,i,i==6,limit && i==a[pos]); } if(!limit) dp[pos][sta]=tmp; return tmp; }
3.ST表
二维数组 st[i][j]
表示从序列第 i 数字开始,包括自己的后数 2^(j - 1)次数,最大值(或最小值)
即:序列间隔 [i, i 2^(j - 1)] 最大值(或最小值)
int n,log[N],st[N][40],a[N]; void init(){ log[0]=-1; for(int i=1;i<=n;i++){ log[i]=((i&(i-1))==0)?log[i-1]+1:log[i-1]; st[i][0]=a[i]; } for(int j=1;j<=log[n];j++) for(int i=1;i+(1<<j)-1<=n;i++) st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]); }
查询:
int query(int l,int r){ int k=log[r-l+1]; return max(st[l][k],st[r-(1<<k)+1][k]); }
二、搜索
1.回溯算法
1) 回溯的实现
递归
举例:针对N叉树的递归回溯方法
void backtrack(int t){ if(t>n) output(x); //叶子节点,输出结果,x是可行解 else for(int i=0;i<k;i++){ //当前节点的所有子节点 x[t]=value[i]; //每个子节点的值赋值给x //满足约束条件和限界条件 if(constraint(t)&&bound(t)) backtrack(t+1); } }
递推
举例:针对N叉树的递归回溯方法
void backtrack(){ int t=1; while(t){ if(exitsubnode(t)){ //若当前节点的存在子节点 for(int i=0;i<k;i++){ //遍历当前节点的所有子节点 x[t]=value[i]; //满足边界条件和限界条件 if(constraint(t)&&bound(t)){ //如果在节点t处得到了一个解 if(solution(t)) output(x);//输出可行解 else t++; //继续向下搜索 } } } else t--; //若不存在子节点,则返回上一层 } }
2) 子集树和排列树
子集树
所给的问题是从n个元素的集合S中找出满足某种性质的子集,相应的解空间成为子集树。 如0-1背包问题,从所给重量、价值不同的物品中挑选几个物品放入背包,使得在满足背包不超重的情况下,背包内物品价值最大。它的解空间就是一个典型的子集树。
void backtrack(int t){ if(t>n) output(x); else for(int i=0;i<k;i++){ x[t]=value[i]; if(constraint(t)&&bound(t)) backtrack(t+1); } }
排列树
所给的问题是确定n个元素满足某种性质的排列,相应的解空间就是排列树。 如旅行售货员问题,一个售货员把几个城市旅行一遍,要求走的路程最小。它的解就是几个城市的排列,解空间就是排列树。
void backtrack(int t){ if(t>n) output(x); else for(int i=t;i<n;i++){ swap(x[t],x[i]); if(constraint(t)&&bound(t)) backtrack(t+1); swap(x[t],x[i]); } }
2.BFS(广度优先搜索)
/**
-
广度优先搜索
-
@param Vs 起点
-
@param Vd 终点
*/
bool BFS(Node& Vs, Node& Vd){ queue<node> Q; Node Vn, Vw; int i; //初始状态将起点放进队列Q Q.push(Vs); hash(Vw) = true;//设置节点已经访问过了! while (!Q.empty()){ Vn = Q.front(); Q.pop(); while(Vw = Vn通过某规则能够到达的节点){ if (Vw == Vd){//找到终点了! //把路径记录,这里没给出解法 return true;//返回 } if (isValid(Vw) && !visit[Vw]){ //Vw是一个合法的节点并且为白色节点 Q.push(Vw);//加入队列Q hash(Vw) = true;//设置节点颜色 } } } return false;//无解 }
3.DFS
三、数据结构
vector<int> v; sort(v.begin(),v.end(),cmp); //排序 reverse(v.begin(),v.end()); //翻转 rotate(v.begin(),v.begin()+midnum,v.end()) //旋转 swap_ranges(v.begin(),v.begin()+midnum,v.end()) //区域交换 v.erase(unique(v.begin(),v.end()),v.end()) //去重
在STL类的使用中,有时会需要遍历,若包含的数据类型为自定义结构体,则需重载运算符
自定义结构体操作符:
struct node{ int x,y; bool operator < (const node& o) const{ if(x==o.x) return y<o.y; return x<o.x; } };
泛型:
template<typename T> struct node{ T x,y; node(T x=0,y=0):x(x),y(y){} } node<T> operator + (const node<T>& A,const node<T>& B){ return node<T>(A.x+B.x,A.y+B.y); }
-
正向迭代器:容器类名::iterator 迭代器名;
-
常量正向迭代器:容器类名::const_iterator 迭代器名;
-
反向迭代器:容器类名::reverse_iterator 迭代器名;
-
常量反向迭代器:容器类名::const_reverse_iterator 迭代器名;
1.Vector
//头文件 #include<vector> //创建vector对象 vector<int> vec; //尾部插入对象 vec.push_back(a); //使用下标访问结点 cout<<vec[0]; //在第i个元素前插入某元素 vec.insert(vec.begin(),a); //在头部插入 vec.insert(vec.begin()+i,a); //删除第i+1个元素 vec.erase(vec.begin()+i); //删除[i,j-1]的全部元素(下标从i开始,到j结束,不包括j) vec.erase(vec.begin()+i,vec.end()+j); //返回集合的大小 vec.size(); //清空集合 vec.clear();
2.Map
#include<map> map<char,int> s; s.size(); //访问 s['a'] = 1; s.at('b')=2; for(auto& x : s) cout<<x.first()<<':'<<x.second(); //插入 使用下标访问不存在的元素,将会在map容器中添加一个新的元素; 使用下标访问存在的元素,将会覆盖map容器中的该元素 //删除 if (s.find(key) != mymap.end()) s.erase (key); s.erase(0); //删除key为0的元素 s.erase(s.begin()); //删除迭代器指向的位置元素 // 查询map是否为空 bool empty(); // 查询map中键值对的数量 size_t size(); // 查询map所能包含的最大键值对数量,和系统和应用库有关。 // 此外,这并不意味着用户一定可以存这么多,很可能还没达到就已经开辟内存失败了 size_t max_size(); // 查询关键字为key的元素的个数,在map里结果非0即1 size_t count( const Key& key ) const; // //map插入pair类型 s.in
3.Set
multiset可遗留元素,即同一元素
set<int> s; s.insert(1); //插入元素 s.begin() //返回set容器第一个元素的迭代器 s.end() //返回一个指向当前set末尾元素的下一位置的迭代器 s.rbegin() //返回的值和end()相同 s.rend() //返回的值和begin()相同 s.clear() //删除set容器中的所有的元素 s.empty() //判断set容器是否为空 s.size() //返回当前set容器中的元素个数 s.max_size() //返回set容器可能包含的元素最大个数 //排序后返回某值 for(int i = 1 ; i <= 5; ++i) { s.insert(i); } pair<set<int>::const_iterator,set<int>::const_iterator> pr; pr = s.equal_range(3); cout<<"第一个大于等于 3 的数是 :"<<*pr.first<<endl; //输出3 cout<<"第一个大于 3的数是 : "<<*pr.second<<endl; //输出4 //删除 erase(iterator) ,删除定位器iterator指向的值 erase(first,second),删除定位器first和second之间的值 erase(key),删除键值key_value的值 //查找 find(key) ,返回给定值值得定位器,如果没找到则返回end()。 count(key) f lower_bound(key) ,返回第一个大于等于key_value的定位器 upper_bound(key),返回最后一个大于等于key_value的定位器 /* s.insert(key)有返回值:pair<set<int>::iterator,bool> if (s.insert(key).second){ set<int>::iterator p = s.insert(key).first; //则*p==key; } //若存在返回false,不存在返回true */ //查找小于等于key的数 set<int>::iterator it; it=--s.upper_bound(key);
4.Stack
1)stack类
//头文件 #include<stack> //栈的声明 stack<char> s; //入栈 s.push('a'); //若非空,则出栈 while(!s.empty()){ s.pop(); } //返回栈中元素个数 s.size(); //返回栈顶元素 s.top(); //栈的清空 while(!s.empty()) s.pop();
2)卡特兰数
一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列?
设h(n)为卡特兰数的第n项,令h(0)=1,h(1)=1,卡特兰数满足递推式 :
$$ h(n)=h(n-1)*(4*n-2)/(n+1) $$
$$ h(n+1)=h(n) * (4*n + 2) / (n + 2) $$
递推关系的解为:
$$ h(n)=C(2n,n)/(n+1) (n=0,1,2,...) $$
递推关系的另类解为:
$$ h(n)=C(2n,n) - C(2n,n-1) (n=0,1,2,...) $$
3)单调栈
1、新建一个空栈,将 A[1] 压入栈中,此时 A[1] 位于栈顶。
2、A[i] 与栈顶元素比较。如果A[i] 大,那么将其入栈。如果两者相等,跳过,继续下一个元素。
如果A[i]小于当前栈顶元素,说明已经找到第一个位于栈顶右边的比它小的值(此时这个较小的元素还未入栈),在它的左边(在栈内就是它脚下的元素)即为第一个左边比它小的值。此时需要这样做:
以栈顶元素为最小高度计算最大矩形面积,并更新现在的最大面积,宽度为左边界到右边界。
弹出栈顶元素。
重复第2步。
3.扫描完后,一般情况下会剩下一个单调递增的堆栈,那么一个一个出栈计算面积就可以了。左边依旧还是栈顶下面那个值,右边这个时候就不存在了,最后一个栈状态的栈顶即为右边界。
例:直方图最大矩形
int n,h[MAXN]; long long mxs(){ stack<int> s; int i=0; long long ans=0,tmp; while(i<n){ if(s.empty()||h[i]>=h[s.top()]){ s.push(i++); } else{ int tp=s.top(); s.pop(); tmp=h[tp]*(s.empty() ? i : i - s.top() - 1); ans=max(ans,tmp); } } while(!s.empty()){ int tp = s.top(); s.pop(); tmp=h[tp]*(s.empty() ? i : i - s.top() - 1); ans=max(ans, tmp); } return ans; }
5.Queue
1)queue类
//头文件 #include<queue> //声明队列 queue<int> q; //入队,将x元素放入队列末端 q,push(x); //出队,弹出队列的第一个元素 q.pop(); //返回队列首尾元素 q.front(); q.back(); //返回队列中元素数量 q.size(); //queue里面的元素是pair类型 queue< pair<int,i> > q;
2)优先队列 :priority_queue类
priority_queue模版类有三个模版参数:元素类型,容器类型,比较算子。
后两个可省略,默认容器为vector,默认算子为less,即非递增排列(出队时序列尾的元素出队)。
//升序队列 priority_queue < int,vector<int>,greater<int> > q; //降序队列 priority_queue < int,vector<int>,less<int> >q;
基础方法:
top 访问队头元素 empty 队列是否为空 size 返回队列内元素个数 push 插入元素到队尾 (并排序) emplace 原地构造一个元素并插入队列 pop 弹出队头元素 swap 交换内容
如: priority_queue<pair<int,int>>q; priority_queue< int,vector<int>,operator<int> >q;
自定义类算子写法:
T类中以n的大小单调递增排列
bool operator < (const T&t1,const T&t2){ return t1.n<t2.n; }
3)单调队列
举例:滑动窗口
给定一个数列,从左至右输出每个长度为m的数列段内的最小数和最大数。
struct node { long long int h,p; }v[1010000]; //h表示值,p表示位置 可以理解为下标
获取最小值维护单调递增队列
void getmin(){ int i,head=1,trail=0; // 默认起始位置为1 因为插入是v[++trail]故初始化为0 for(i=0;i<m-1;i++){ while(trail>=head&&h[i]<=v[trail].h) trail--; v[++trail].h=h[i],v[trail].p=i+1; } for(;i<n;i++){ while(t>=head&&h[i]<=v[trail].h) trail--; v[++trail].h=h[i],v[end].p=i+1; while(v[head].p<=i-m+1) head++; mn[i-m+1]=v[head].h; } }
获取最大值维护单调递减队列
void getmax(){ int i,head=1,trail=0; for(i=0;i<m-1;i++){ while(trail>=head&&h[i]>=v[trail].h) trail--; v[++trail].h=h[i],v[trail].p=i+1; } for(;i<n;i++){ while(trail>=head&&h[i]>=v[trail].h) trail--; v[++trail].h=h[i],v[trail].p=i+1; while(v[head].p<=i-m+1) head++; mx[i-m+1]=v[head].h; } }
4)deque
deque<int> t; t.size();//返回容器中元素的个数 t.empty();//判断容器是否为空 t.resize(num); deque.resize(num, elem); t.push_back(elem);//在容器尾部添加一个数据 t.push_front(elem);//在容器头部插入一个数据 t.pop_back();//删除容器最后一个数据 t.pop_front();//删除容器第一个数据 t.at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。 t.front();//返回第一个数据 t.back();//返回最后一个数据 t.insert(pos,elem);//在pos位置插入一个elem元素,返回新数据的位置。 t.insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。 t.clear();//移除容器的所有数据 t.erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。 t.erase(pos);//删除pos位置的数据,返回下一个数据的位置。
6.List
#include <list> list<int>lst1; //创建空list list<int> lst2(5); //创建含有5个元素的list list<int>lst3(3,2); //创建含有3个元素的list list<int>lst4(lst2); //使用lst2初始化lst4 list<int>lst5(lst2.begin(),lst2.end()); //同lst4 l1.assign(n,val); 给list赋值 l1.assign(l2.begin(),l2.end()); l1.back() 返回最后一个元素 l1.begin() 返回指向第一个元素的迭代器 l1.clear() 删除所有元素 l1.empty() 如果list是空的则返回true l1.end() 返回末尾的迭代器 l1.erase() 删除一个元素 l1.front() 返回第一个元素 l1.insert() 插入一个元素到list中 l1.insert(l1.begin(),100); 在l1的开始位置插入100。 l1.insert(l1.begin(),2,200); 在l1的开始位置插入2个100。 l1.insert(l1.begin(),l2.begin(),l2.end()); l1.max_size() 返回list能容纳的最大元素数量 l1.merge(l2,greater<int>()) 合并两个list,l2将清空 l1.pop_back() 删除最后一个元素 l1.pop_front() 删除第一个元素 l1.push_back(val) 在list的末尾添加一个元素 l1.push_front(val) 在list的头部添加一个元素 l1.rbegin() 返回指向第一个元素的逆向迭代器 l1.remove() 从list删除元素 l1.erase(it) 根据迭代器删除指定位置 l1.remove_if() 按指定条件删除元素 l1.resize(n) 改变list的大小,超出n的元素将被删除 l1.reverse() 把list的元素倒转 l1.size() 返回list中的元素个数 l1.sort() 给list排序 l1.splice() 合并两个list l1.swap(l2) 交换两个list swap(l1,l2) l1.unique() 删除list中重复的元素 list<int>::iterator i=l1.begin(); advance(i,n) 将i向后挪n位 元素被插入到 pos 所指向的元素之前 l1.splice(const_iterator pos,list& other) l1.splice(const_iterator pos,list& other,const_iterator it) 将other的第it元素转移到*this的第pos位置,操作后容器 other 变为空 l1.splice(const_iterator pos,list& other,const_iterator first ,const_iterator second)
7.树
树的重心:
定义:或。
1)二叉树
结构体写法
class TreeNode{ private: int val; TreeNode *left; TreeNode *right; public: TreeNode(int x) : val(x), left(NULL), right(NULL) {} }
vector<int> orderTraversal(TreeNode* root) { vector<int> res; order(root,res); return res; }
二叉树的先序遍历
void preorder(TreeNode* root, vector<int>& res) { if(!root) return ; res.push_back(root->val); preorder(root->left,res); preorder(root->right,res); }
二叉树的中序遍历
void inorder(TreeNode* root, vector<int>& res) { if(!root) return ; inorder(root->left,res); res.push_back(root->val); inorder(root->right,res); }
二叉树的后序遍历
void proorder(TreeNode* root, vector<int>& res) { if(!root) return ; proorder(root->left,res); porder(root->right,res); res.push_back(root->val); }
最大深度
int maxDepth(TreeNode* root) { if(!root) return 0; else if(!root->left) return maxDepth(root->right)+1; else if(!root->right) return maxDepth(root->left)+1; else return max(maxDepth(root->left),maxDepth(root->right))+1; }
最小深度
int maxDepth(TreeNode* root) { if(!root) return 0; else if(!root->left) return maxDepth(root->right)+1; else if(!root->right) return maxDepth(root->left)+1; else return min(maxDepth(root->left),maxDepth(root->right))+1; }
数组写法
int n,t[100]; int deep[100]; int v[10000000],d,w[100];
for(int i=0;i<n-1;i++){ cin>>x>>y; if(v[t[x]*2]){ t[y]=t[x]*2+1; v[t[y]]=1; } else{ t[y]=t[x]*2; v[t[y]]=1; } deep[y]=deep[x]+1; d=max(d,deep[y]); w[deep[y]]++; }
2)线段树
调用:build(1,n,1)
#define lson l,mid,i<<1 #define rson mid+1,r,i<<1|1 int sum[N<<2]; #define pushup(i) sum[i]=sum[i<<1]+sum[i<<1|1] void build(int l,int r,int i){ if(l==r){ cin>>sum[i]; re } int mid=(l+r)>>1; build(lson); build(rson); pushup(i); }
void pushdown(int o,int k) { sum[o*2]=(sum[o*2]*mul[o]+add[o]*(k-(k>>1)))%p; sum[o*2+1]=(sum[o*2+1]*mul[o]+add[o]*(k>>1))%p; //更新的顺序:先乘法标记的更新,再加上累加标记的更新。并且累加标记的更新还要注意累加的次数,一个是(k-(k>>1)),另一个是(k>>1)。 mul[o*2]=mul[o*2]*mul[o]%p mul[o*2+1]=mul[o*2+1]*mul[o]%p; //乘法标记也要更新的。千万别忘记 add[o*2]=(add[o*2]*mul[o]+add[o])%p; add[o*2+1]=(add[o*2+1]*mul[o]+add[o])%p; //加法标记更新的时候要先考虑乘法标记的更新。 mul[o]=1; add[o]=0; //标记清空。 }
void add(int k, int v, int l, int r, int i){//i是线段树数组中的下标,k,l,r是元素数组中的下标 if (l == r){ sum[i] += v; return; } int mid = (l + r) >> 1; if (k <= mid) add(k, v, lson); else add(k, v, rson); pushup(i); }
区间自加
void jia(int l,int r,int o)//三个参数,l和r代表当前在查找的区间的两个端点,o代表当前查找的区间的根结点 { if(x<=l&&r<=y)//如果区间完全被覆盖 { add[o]=(add[o]+v)%p;//对lazy标记直接累加v,并且模p(一步一模可以避免最后long long溢出的问题) sum[o]=(sum[o]+v*(r-l+1))%p;//对于该点的累加和也要相应加上每一个单点的改变量 return;//lazy标记的初衷,直接退出 } pushdown(o,r-l+1);//传递lazy标记 int mid=(l+r)>>1;//获取区间中点 if(x<=mid) jia(le);//如果x在中点左边,就说明有必要搜索左子树 if(y>mid) jia(re);//如果y在中点右边,就说明有必要搜索右子树 sum[o]=(sum[o*2]+sum[o*2+1])%p; //是时候更新根结点的累加和了。
区间乘法
void cheng(int l,int r,int o) { if(x<=l&&r<=y) //如果区间完全包含 { add[o]=(add[o]*v)%p; mul[o]=(mul[o]*v)%p; sum[o]=(sum[o]*v)%p; //累加和、乘法标记、加法标记全部更新。 return;//更新完就直接返回,不用多说话。 } pushdown(o,r-l+1);//传递标记 int mid=(l+r)>>1; if(x<=mid) cheng(le); if(y>mid) cheng(re); sum[o]=(sum[o*2]+sum[o*2+1])%p; //这一段同上文加法。 }
int query(int x, int y, int l, int r, int i){ if (x <= l && r <= y){ return sum[i]; } int mid = (l + r) >> 1; int ans = 0; if (x <= mid) ans += query(x, y, lson); if (y > mid) ans += query(x, y, rson); return ans; }
3)树状数组
#define lowbit(x) -x&x
单点修改+区间查询
void update(int x,int y){ for(int i=x;i<=n;i+=lowbit(i)) a[i] += y; }
long long getsum(int x){ long long sum = 0; for(int i=x;i;i-=lowbit(i)) sum += a[i]; return sum; }
//getsum(r)-getsum(l-1)
区间修改
void add(int p, int x){ while(p <= n) sum[p] += x, p += p & -p; } void range_add(int l, int r, int x){ add(l, x), add(r + 1, -x); }
区间修改+区间查询
void add(int u,int x){ for(int i=u;i<=n;i+=lowbit(i)){ b[i]+=x; c[i]+=(ll)u*x; } }
ll query(int x){ ll res=0; for(int i=x;i;i-=lowbit(i)) res+=(x+1)*b[i]-c[i]; return res; }
8.堆
快速建立的前提条件是已知所有数据
for(int i=1;i<=n;i++) scanf("%d",&h[i]); cnt=n; for(int i=n/2;i;i--) down(i);
实现插入、查询最值、弹出最值
#define MAXN 1e6+50 int h[MAXN]; int sz=0; void up(int p){ while(p>>1&&h[p]<h[p>>1]){ swap(h[p],h[p>>1]); p>>=1; } } void down(int p){ int t=p; if(p<<1 <= sz && h[p<<1] < h[t]) t=p<<1; if((p<<1|1) <= sz && h[p<<1|1] < h[t]) t=p<<1|1; if(t!=p){ swap(h[p],h[t]); down(t); } } void insert(int x){ h[++sz] = x; up(sz); } int top(){ return h[1]; } void pop(){ h[1] = h[sz--]; down(1); }
四、字符串基础
1.基础函数
输入:
while(getline(cin,s),s!="0")
字符串常见函数
//字符串的翻转 /strrev(s); //cstring reverse(s.begin(),s.end()); //algorithm,无返回值 string(s.rbegin(),s.rend()); //删除子串 s.erase(pos); s.erase(pos,end); s.insert(pos,str); //插入子串 s.find(str,pos); //查找子串 //找不到返回string::npos s.substr(pos,lenth); //截取子串 s.replace(pos,lenth,str); //替换z
类型的转换
//字符串转换为char* s.c_str() //字符串转整形 int atoi(const char *nptr); int stoi(string str); //字符串转浮点数 double stof(string str); //整形、浮点数转字符串 string str = to_string(v)
字符串的比较
#include <iostream> using namespace std; int main(){ string str1="hello"; cout<<str1.compare("helloo")<<endl;//返回-1; cout<<str1.compare("hello")<<endl;//返回0 ; cout<<str1.compare("hell")<<endl;//返回1; } //char数组的比较 #include <iostream> #include <cstring> using namespace std; int main(){ char* str1="hello"; char* str2="hell"; char *str3="helloo"; char *str4="hello"; //原型extern int strcmp(const char *s1,const char *s2); cout<<strcmp(str1,str2)<<endl;//返回1; cout<<strcmp(str1,str3)<<endl;//返回-1; cout<<strcmp(str1,str4)<<endl;//返回0. }
字符串截取
str.substr(i,j); //从i一直截取j个字符 str.substr(i); //从i一直截取到末尾 //char数组的截取 //从str2开始,截取num个字符到str1中 strncpy(str1,str2,num);
2.hash算法
不取模
#define BASE 37 #define PRIME 233317 long long gethash(char *s){ long long n=strlen(s); long long hash=0; for(long long i=0;i<n;i++){ hash=(hash*BASE+s[i])+PRIME; } return hash; }
取模
#define BASE 37 #define PRIME 233317 long long gethash(char *s,int mod){ long long n=strlen(s); long long hash=0; for(long long i=0;i<n;i++){ hash=(hash*BASE+s[i])%mod+PRIME; } return hash%mod; }
3.KMP算法
确定next数组
char p[MAXN],s[MAXN]; int nxt[MAXN],slen,plen; void getnext(){ slen=strlen(s),plen=strlen(p); nxt[0] = -1; int i = 0, j = -1; while (i < plen){ if (j == -1 || p[i] == p[j]){ nxt[++i] = ++j; } else{ j=nxt[j]; } } }
主函数
getnext(); int i=0,j=0,ans=0; while(i<slen){ if(j==-1||s[i]==p[j]){ i++; j++; } else{ j=nxt[j]; } if(j==plen){ j=0; ans++; //cout<<i-plen<<endl; //i--; //j=nxt[plen-1]; } }
4.Manacher算法
求回文子串
#include<bits/stdc++.h> using namespace std; const int mx = 10000; char ss[mx + 5], s[(mx << 1) + 5]; /// ss为源串,s为处理后的字符串 int len[(mx << 1) + 5]; void debug() { int i; for (i = 1; s[i]; ++i) printf("%c ", s[i]); puts(""); for (i = 1; s[i]; ++i) printf("%d ", len[i]); puts(""); } void init(){ memset(s, 0, sizeof(s)); s[0] = '#'; for (i = 0; ss[i]; ++i) s[(i << 1) + 1] = '#', s[(i << 1) + 2] = ss[i]; s[(i << 1) + 1] = '#'; memset(len, 0, sizeof(len)); } int main() { int right, mid, i, maxlen; while (gets(ss)) { init(); maxlen = right = mid = 0; for (i = 1; s[i]; ++i) { len[i] = (i < right ? min(len[(mid << 1) - i], right - i) : 1); /* 取min的原因:记点i关于mid的对称点为i', 若以i'为中心的回文串范围超过了以mid为中心的回文串的范围 (此时有i + len[(mid << 1) - i] >= right,注意len是包括中心的半长度) 则len[i]应取right - i(总不能超过边界吧) */ while (s[i + len[i]] == s[i - len[i]]) ++len[i]; maxlen = max(maxlen, len[i]); if (right < i + len[i]) mid = i, right = i + len[i]; } printf("%d\n", maxlen - 1); debug(); } return 0; }
// 判断源串中的某一子串 ss[l...r] 是否为回文串
inline bool Query(int l, int r) { return len[l + r + 2] - 1 >= r - l + 1; }
5.LCS(最长公共子序列)
string s,t; int ls,lt,dp[MAXN]; void init(){ int x; ls=s.size(),lt=t.size(); memset(dp, 0, sizeof(dp)); for(int i=0;i<ls;i++){ x=0; for(int j=0;j<lt;j++){ int tmp=dp[j]; if(s[i]==t[j]) dp[j]=x+1; else if(dp[j-1]>dp[j]) dp[j]=dp[j-1]; x=tmp; } } }
主函数
init(); cout<<dp[lt-1]<<endl;
6.LIS(最长上升子序列)
时间复杂度:O(n^2)
int a[MAXN],dp[MAXN],n,ans; void lis(){ ans=0; for(int i = 0; i<n; i++){ dp[i] = 1; for(int j = 0; j < i; j++){ if(a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1); } ans = max(dp[i], ans); } }
时间复杂度:O(nlogn)
#define INF 0x3f3f3f3f int arr[N],g[N],n; int lis(){ // memset(g,INF,sizeof(g)); fill(g, g+n, INF); int ans=-1; for(int i=0; i<n; i++){ int j = lower_bound(g, g+n, arr[i]) - g; ans=max(ans,j+1); g[j] = arr[i]; } return ans; }
7.后缀数组
sa[l] = 排序第 l 的后缀的开始位置
rank[i] = 后缀 suf(i) 的排名
故:rank[sa[l]] = l; sa[rank[i]] = i;
int ht[MAXN]; //维护rk相邻的两个后缀的lcp(最长公共前缀) int sa[MAXN]; //记录排名为i的后缀初始位置 int rk[MAXN],rk2[MAXN]; //rk指下标为i的后缀的排名 bool cmp(int i,int j){ if(rk[i]!=rk[j]) return rk[i]<rk[j]; //比前一半 int ri=(i+k<=n ? rk[i+k] : -1); int rj=(j+k<=n ? rk[j+k] : -1); return ri<rj; //比后一半 } void getsa(int n,char *str){ //str应从1开始 for(int i=1;i<=n;i++) sa[i]=i,rk[i]=str[i]; //开始长度为1的rk for(int k=1;k<=n;k++){ //k是当前比的后缀长度 sort(sa+1,sa+1+n,cmp); rk2[sa[1]]=1; for(int i=2;i<=n;i++) rk2[sa[i]]=rk2[sa[i-1]]; for(inr i=1;i<=n;i++) rk[i]=rk2[i]; //更新rk } } void getht(int n,char *str){ for(int i=1;i<=n;i++) rk[sa[i]]=i; int h=0; //先算出每个后缀的rank int h=0; //前一次算的两个后缀的lcp ht[1] = 0; for(int i=1;i<=n;i++){ int j=sa[rk[i]-1]; //i后缀的前一名 if(h>0) h--; //除去开头的位置,至少h-1的位置相同 for(;j+h<=n&&i+h<=n;h++) if(str[j+h]!=str[i+h]) break; ht[rk[i]] } }
$$ LCP(i, j) = min(height[k] ,min(i, j) < k <= max(i, j)) $$
8.求前后字典序排列
给出某一序列,求按字典序排列的前一序列
bool cmp(int x,int y){ return x>y; } string getpre(string str){ int n=str.length(); bool judge=false; //用于判断序列是否存在 ,也可返回 string s=str; int i,j; for(i=n-2;i>=0;i--){ if(s[i]>s[i+1]){ judge=true; break; } } for(j=n-1;j>i;j--) if(s[j]<s[i]) break; char tmp=s[i]; s[i]=s[j],s[j]=tmp; sort(s.begin()+i+1,s.end(),cmp); return s; }
求按字典序排列的后一序列
string getpro(string str){ int n=str.length(); bool judge=false; //用于判断序列是否存在 ,也可返回 string s=str; int i,j; for(i=n-2;i>=0;i--){ if(s[i]<s[i+1]){ judge=true; break; } } for(j=n-1;j>i;j--) if(s[j]>s[i]) break; char tmp=s[i]; s[i]=s[j],s[j]=tmp; sort(s.begin()+i+1,s.end()); return s; }
五、图论
建图
1)链式前向星
struct edge{ int to, w, next; }e[maxn]; int head[MAXN],cnt; void init(){ memset(head,-1,sizeof(head)); cnt=0; } void addedge(int u, int v, int w){ e[cnt].to = v; e[cnt].w = w; e[cnt].next = head[u]; head[u] = cnt++;//更新以u为起点上一条边的编号 }
遍历函数
for(int i = 1; i <= n; i++){//n个起点 cout << i << endl; for(int j = head[i]; j != -1; j = edge[j].next){//遍历以i为起点的边 cout << edge[j].to << " " << edge[j].w << endl; } cout << endl; }
1.最短路
1)
多源最短路
for(int k=1;k<=n;k++){ for(int x=1;x<=n;x++){ for(int y=1;y<=n;y++){ f[x][y]=min(f[x][y],f[x][k]+f[k][y]); } } }
Wallshall算法
for(int k=0;k<n;k++){ for(int i=0;i<n;i++){ if(m[j][i]==1&&j!=i) for(int k=0;k<n;k++){ if(m[i][k]) m[j][k]=1; } } }
或
void wallshell() { for (int t = 0;t < n;t++) { for (int i = 0;i < n;i++) { for (int j = 0;j < n;j++) { m[i][j] = m[i][j] || (m[i][t] && m[t][j]); } } } }
2)
单源最短路,O(V*E)
#define INF 0x3f3f3f3f int vis[N],dis[N]; int m[N][N],n,t;//n个点,t条边; void init(){ memset(m,Inf,sizeof(m)); for(int i=0;i<n;i++){ m[i][i]=0; } } void dijkstra(int u){ for(int i=0;i<n;i++){ dis[i]=m[u][i]; vis[i]=0; } vis[u]=1; for(int j=0;j<n-1;j++){ int mn=INF,tmp; for(int i=0;i<n;i++){ if(!vis[i]&&dis[i]<mn){ mn=dis[i]; tmp=i; } } vis[tmp]=1; for(int i=0;i<n;i++){ if(m[tmp][i]+dis[tmp]<dis[i]){ dis[i]=m[tmp][i]+dis[tmp]; } } } }
堆优化,O(logV*E)
3)SPFA算法
可用于带有负权边的计算,若死循环则途中存在闭环。O(nm)
注:此算法采用链式前向星建图
int mark[MAXN]; //记录每个点如队列的次数 bool SPFA(int s){ for(int i = 0;i < n;i ++){ mark[i] = 0;dis[i] = INF;vis[i] = 0; } queue<int> q; q.push(s); //我们只需要判断负环,随便找一个起点就好 dis[s] = 0; vis[s] = 1; //入队列 mark[s] ++; while(!q.empty()){ int u = q.front(); q.pop(); vis[u] = 0; //出队列 for(int i = head[u];i != -1;i = e[i].next){ int v = e[i].to; if(dis[v] > dis[u] + e[i].w){ dis[v] = dis[u] + e[i].w; if(!vis[v]){ //不在队列中的时候出队 q.push(v);mark[v] ++;vis[v] = 1; } //某点进入队列的次数超过N次则存在负环 if(mark[v] >= n) return false; } } } return true; }
3.DFS序
void dfs(int x,int pre,int d){//L,R表示一个子树的范围 L[x]=++tot; dep[x]=d; for(int i=0;i<e[x].size();i++){ int y=e[x][i]; if(y==pre)continue; dfs(y,x,d+1); } R[x]=tot; }
4.拓扑排序
int in[N]; vector<int> out[MAXN]; int topology(){ queue<int> q; //vector<int> ans; for(int i=0;i<n;i++){ if(in[i]==0) q.push(i); } while(!q.empty()){ int f=q.front(); q.pop(); //ans.push_back(f); for(int i=0;i<out[f].size();i++){ int x=out[f][i]; in[x]--; if(in[x]==0) q.push(x); } } }
//不方便使用vector时
bool topotopology(){ for(i=0;i<n;i++){ for(j=0;j<n;j++){ if(in[j]==0){ ans=j; vis[cnt++]=j; in[j]--; break; } } for(j=0;j<n;j++) if(out[ans][j]) in[j]--; } if(cnt==n) return true; else return false; }
5.并查集
int root[N]; int n,m,q; void init(){ for(int i=1;i<=n;i++){ root[i]=i; } } int find(int x){ return x==root[x] ? x : root[x]=find(root[x]); } void merge(int x,int y){ int fx=find(x); int fy=find(y); if(fx!=fy){ root[fy]=fx; } }
6.欧拉路与欧拉回路
1.无向图:连通图中,若奇数数度节点个数为0或2,则存在欧拉路。
2.有向图:连通图中,若出度比入度大1的节点和入度比出度大1的节点都为1或0,则存在欧拉路。
1.无向图:连通图中,若每个顶点的度数都是偶数,则存在欧拉回路。
2.有向图:连通图中,若每个顶点的入度都等于出度,则存在欧拉回路。
Fleury算法
void dfs( int u ) { sta.push(u); for( register int i = head[u]; i; i = line[i].nxt ) { if( vis[i] ) continue; vis[i] = vis[ i ^ 1 ] = true; dfs( line[i].to ); break; } } void fleury( int st ) { sta.push(st); while(!sta.empty() ) { int flag = 0, u = sta.top(); sta.pop(); for( register int i = head[u]; i; i = line[i].nxt) { if( vis[i] ) continue; flag = 1; break; } if( !flag ) printf( "%d\n", u ); else dfs(u); } }
7.二分图
最大匹配的匹配边的数目
选取最少的点,使任意一条边至少有一个端点被选择,满足该集合的点的数目
选取最多的点,使任意所选两点均不相连,满足该集合的点的数目
选取最少条路径,使得每个顶点属于至少是其中某条边的端点,满足该集合的边的数目。
二分图中,最大匹配数 = 最小点覆盖数 最大独立数+最小点覆盖数=顶点数 对于不存在孤立点的图,最小边覆盖数+最大匹配数= 顶点数
匈牙利算法
求最大匹配数
int girl[MAXN]; bool line[MAXN][MAXN],used[MAXN]; int n,m;
主函数:
bool find(int x){ int ij; for (j=1;j<=m;j++){ //扫描每个妹子 if (line[x][j]==true && used[j]==false){ //如果有暧昧并且还没有标记过(这里标记的意思是这次查找曾试图改变过该妹子的归属问题,但是没有成功,所以就不用瞎费工夫了) used[j]=true; if (girl[j]==0 || find(girl[j])) { //名花无主或者能腾出个位置来,这里使用递归 girl[j]=x; return true; } } } return false; }
主程序:
for (i=1;i<=n;i++){ memset(used,false,sizeof(used)); if find(i) all+=1; }
KM算法
求二分图最大权完美匹配
#include <iostream> #include <cstring> #include <cstdio> using namespace std; #define MAXN 305; #define INF 0x3f3f3f3f int love[MAXN][MAXN]; // 记录每个妹子和每个男生的好感度 int ex_girl[MAXN]; // 每个妹子的期望值 int ex_boy[MAXN]; // 每个男生的期望值 bool vis_girl[MAXN]; // 记录每一轮匹配匹配过的女生 bool vis_boy[MAXN]; // 记录每一轮匹配匹配过的男生 int match[MAXN]; // 记录每个男生匹配到的妹子 如果没有则为-1 int slack[MAXN]; // 记录每个汉子如果能被妹子倾心最少还需要多少期望值 int N; bool dfs(int girl) { vis_girl[girl] = true; for (int boy = 0; boy < N; ++boy) { if (vis_boy[boy]) continue; // 每一轮匹配 每个男生只尝试一次 int gap = ex_girl[girl] + ex_boy[boy] - love[girl][boy]; if (gap == 0) { // 如果符合要求 vis_boy[boy] = true; if (match[boy] == -1 || dfs( match[boy] )) { // 找到一个没有匹配的男生 或者该男生的妹子可以找到其他人 match[boy] = girl; return true; } } else { slack[boy] = min(slack[boy], gap); // slack 可以理解为该男生要得到女生的倾心 还需多少期望值 取最小值 备胎的样子【捂脸 } } return false; } int KM() { memset(match, -1, sizeof match); // 初始每个男生都没有匹配的女生 memset(ex_boy, 0, sizeof ex_boy); // 初始每个男生的期望值为0 // 每个女生的初始期望值是与她相连的男生最大的好感度 for (int i = 0; i < N; ++i) { ex_girl[i] = love[i][0]; for (int j = 1; j < N; ++j) { ex_girl[i] = max(ex_girl[i], love[i][j]); } } // 尝试为每一个女生解决归宿问题 for (int i = 0; i < N; ++i) { fill(slack, slack + N, INF); // 因为要取最小值 初始化为无穷大 while (1) { // 为每个女生解决归宿问题的方法是 :如果找不到就降低期望值,直到找到为止 // 记录每轮匹配中男生女生是否被尝试匹配过 memset(vis_girl, false, sizeof vis_girl); memset(vis_boy, false, sizeof vis_boy); if (dfs(i)) break; // 找到归宿 退出 // 如果不能找到 就降低期望值 // 最小可降低的期望值 int d = INF; for (int j = 0; j < N; ++j) if (!vis_boy[j]) d = min(d, slack[j]); for (int j = 0; j < N; ++j) { // 所有访问过的女生降低期望值 if (vis_girl[j]) ex_girl[j] -= d; // 所有访问过的男生增加期望值 if (vis_boy[j]) ex_boy[j] += d; // 没有访问过的boy 因为girl们的期望值降低,距离得到女生倾心又进了一步! else slack[j] -= d; } } } // 匹配完成 求出所有配对的好感度的和 int res = 0; for (int i = 0; i < N; ++i) res += love[ match[i] ][i]; return res; } int main() { while (~scanf("%d", &N)) { for (int i = 0; i < N; ++i) for (int j = 0; j < N; ++j) scanf("%d", &love[i][j]); printf("%d\n", KM()); } return 0; }
8.Tarjan算法
求强连通分量
int dfn[MAXN],low[MAXN],scc[MAXN],stk[MAXN]; //stk是栈的模拟 int index,sccnum,top; void tarjan(int root){ if(dfn[root]) return ; dfn[root]=low[root]=++index; stk[++top]=root; for(int i=head[root];i!=-1;i=e[i].next){ int v=e[i].to; if(!dfn[v]){ //如果结点v未访问过 tarjan(v); low[root]=min(low[root],low[v]); } else if(!scc[v]){ //如果还在栈内 low[root]=min(low[root],dfn[v]); } } if(low[root]==dfn[root]){ //后代不能找到更浅的点 sccnum++; while(true){ int x=stk[top--]; scc[x]=sccnum; if(x==root) break; } } }
int dfn[MAXN],low[MAXN],scc[MAXN],stk[MAXN]; //stk是栈的模拟 int index,sccnum,top; int num[MAXN]; //统计每个强连通分量有几个结点 void tarjan(int root){ if(dfn[root]) return ; dfn[root]=low[root]=++index; stk[++top]=root; for(int i=head[root];i!=-1;i=e[i].next){ int v=e[i].to; if(!dfn[v]){ //如果结点v未访问过 tarjan(v); low[root]=min(low[root],low[v]); } else if(!scc[v]){ //如果还在栈内 low[root]=min(low[root],dfn[v]); } } if(low[root]==dfn[root]){ //后代不能找到更浅的点 sccnum++; while(true){ int x=stk[top--]; scc[x]=sccnum; num[sccnum]++; if(x==root) break; } } }
9.LCA(最近公共祖先)
初始化
void init(){ cnt = 1; for(int i=0;i<N;i++){ root[i]=i; vis[i]=0; } }
需要并查集,主函数逆向建树
int root[N],vis[N],to[N],cnt; int find(int x){ if(root[x]==x) return x; else return root[x]=find(root[x]); } int dfs(){ int a=1,b=1; root[1]=find(1); int i; for(i=1;i!=root[1];i=to[i]){ vis[i]=a; a++; } vis[root[1]]=a; for(i=2;1;i=to[i]){ if(vis[i]){ a=vis[i]; break; } b++; } if(a==b) return 0; else if(a<b) return 1; else return 2; //return i; }
10.最小生成树
点数n,边数m Prim算法O(n^2) Kruscal算法O(mlogm) 稠密图推荐使用Prim算法 稀疏图推荐使用Kruskal算法
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
-
图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
-
在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
-
重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息,:
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
-
把图中的所有边按代价从小到大排序;
-
把图中的n个顶点看成独立的n棵树组成的森林;
-
按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
-
重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
六、数论
a 被 b 整除,a是大的那个数(一般来讲),记为:b|a
1.快速幂
思想:
求pow(a,b)时,将b化为二进制考虑,从而大幅减少时间复杂度。快速幂的时间复杂的为O(log2n)一般的幂乘运算为O(n)。
不取模:
//不取模,a的b次幂 long long dpow(long long a,long long b){ long long ans=1; while(b){ if(b&1) ans=ans*a; a=a*a; b>>=1; } return ans; }
求后n位:(取模)
取模运算可求得运算结果后n位:
//取模 long long dpow(long long a,long long b,long long mod){ long long ans=1; while(b){ if(b&1) ans=ans*a%mod; a=a*a%mod; b>>=1; } return ans%mod; }
求前n位:
求对数运算可求得运算结果前n位:
double t=b*log10(1.0*a);//求以10位底数的指数 double r=t-(int)t;//求指数的小数部分 int result=(int)(pow(10,n+r));//求前n位
2.快速乘
inline long long qmul(long long a, long long b, long long mod){ long long res = 0; while( b > 0 ){ if( b&1 ) res = (res + a) % mod; a = ( a + a ) % mod; b >>= 1; } return res; }
使用long double优化:
inline long long qmul(long long x, long long y, long long mod){ return ( x * y - (long long) ( (long double) x / mod*y )*mod + mod ) % mod; }
3.素数
#define MAXN 10000005 bool isprime[MAXN]; void prime(){ memset(isprime,true,sizeof(isprime));//初始化都是素数 isprime[1]=isprime[0]=false; for (long long i=2; i<=MAXN; i++) { if (isprime[i]) { //如果i是素数,让i的所有倍数都不是素数 for(long long int j = i*i; j <= MAXN; j += i){ isprime[j] = false; } } } }
long long eular[MAXN]; void initeular(){ for(int i=2;i<MAXN;i++){ if(!euler[i]) for(int j=i;j<MAXN;j+=i){ if(!euler[j]) euler[j]=j; euler[j]=euler[j]/i*(i-1); } } }
$$ φ(n)=n*(1-1/p_1)*(1-1/p_2)*……*(1-1/p_n) $$
long long eular(long long n){ long long ans = n; for(int i=2; i*i <= n; ++i){ if(n%i == 0){ ans = ans/i*(i-1); while(n%i == 0) n/=i; } } if(n > 1) ans = ans/n*(n-1); return ans; }
需调用快速幂和快速乘
bool Miller_Rabin(ll num){ if(num == 2) return true; //2为质数 if(!(num&1)||num<2) return false;//筛掉偶数和小于2的数 ll s = 0,t = num-1; //流程中的s和t,2的s次方*t = num-1 while(!(t&1)){ //当t为偶数的时候,可以继续分解 s++; t>>=1; } for (int i = 1; i <= 10; i++) { //进行十次测试即可得到比较准确的判断 ll a = rand()%(num-1)+1; //流程中的随机整数a,在1到num-1之间 ll x = dpow(a,t,num); //x为二次探测的解 for(int j = 1;j <= s;j++){ //x平方s次可以得到a的num-1次方 ll test = qmul(x,x,num); //test为x平方后对num取模 if(test == 1 && x != 1 && x != num-1) return false; //如果平方取模结果为1,但是作为解的x不是1或者num-1,说明num不是质数,返回 x = test; } if(x != 1) return false; //费马小定理作最后检测,a的num-1次方对num取模不等于1,一定不是质数 } return true; //腥风血雨后仍坚持到最后,基本就是真正的质数了 }
4.分数规划
给定一系列整数a1,a2,……,an以及b1,b2,……,bn
求解一组xi(1 <= i <= n,xi = 0或1)使得下式最大化
$$ ∑[1…n](ai*xi)/∑[1…n](bi*xi) $$
解法:
在值域[L,R]内二分mid,判断是否存在一组解xi使得
$$ ∑[1…n](ai*xi)/∑[1…n](bi*xi)>=mid $$
若是则说明二分值 mid 比能求得的最大值小,令L = mid;
否则说明二分值 mid 大于能求得的最大值,令R = mid;
直到二分值达到精度要求。
判断方法:
$$ 若ai-mid*bi<0则令xi=0,否则xi=1 $$
int a[maxn],b[maxn]; double tt[maxn]; d ans; int check(double x){ double sum=0; for(int i=1;i<=n;++i) tt[i]=a[i]-x*b[i];//每组ai-mid*bi sort(tt+1,tt+1+n); for(int i=n;i>=k+1;--i) sum+=tt[i];//检查前n-k组的和 return sum>=0; }
主函数:
double L=0,R=1,mid; while(R-L>1e-4)//注意精度要求 { mid=(L+R)/2; if(check(mid))L=mid; else R=mid; }
5.GCD(最大公约数)
int gcd(int a,int b){ return b? gcd(b,a%b):a; }
long long lcm=a/gcd(a,b)*b;
x、y为任意的整数
$$ 若gcd(a,b)=d,则ax+by=k*d,且一定存在x、y使k=1 $$
即:
$$ 若ax+by=d有解,则d一定是gcd(a,b)的倍数 $$
$$ 推论:方程ax+yb=1,只有当整数a,b互质时,方程才有整数解。 $$
扩展欧几里得算法是用在已知a,b求解一组整数解x,y,使其满足裴蜀等式:
$$ ax+by=gcd(a,b)=d $$
int exgcd(int a,int b,long long &x,long long &y){ if(b==0) return x=1,y=0,a; //d的值实际上就是gcd(a,b),如果不需要的话可以不求 int d=exgcd(b,a%b,y,x); return y-=a/b*x,d; }
求逆元:
long long inv(long long a,long long m){ long long x,y; long long d=exgcd(a,m,x,y); return d==1 ?(x+m)%m:-1;//不互质就没有逆元 }
6.模与同余
$$ (a + b) \% n = (a \% n + b \% n) \% n\\ (a - b) \% n = (a \% n - b \% n) \% n\\ (a * b) \% n = (a \% n * b \% n) \% n\\ a^b{\ }\%n=((a\%n)^b){\ }\%n{\ }(可用于降幂) $$
$$ 如果(a-b)被m整除,那么记作a≡b(mod{\ }{\ }m),即a与b对模m同余 $$
$$ 如果p是质数,a为自然数,gcd(a,p)=1,则a^{p-1}≡1(mod{\ }{\ }p) $$
$$ p是质数,0<x<p,则x^2≡1(mod.p)的解为x=1,x=p-1 $$
7.乘法逆元
int inv[MAXN]; inv[0] = inv[1] = 1; for (int i = 2; i <= n; i ++) { inv[i] = (1LL * (p - p / i) * inv[p % i]) % p; }
既求某个数的阶乘的逆元
inv[i] 相当于是除以 i 的阶乘
ll fac[MAXN], inv[M]; fac[0] = inv[0] = 1; for(int i = 1; i <=maxn; i++) fac[i] = fac[i - 1] * i % mod; inv[maxn] = pow(fac[maxn], mod - 2); for(int i = maxn - 1; i > 0; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
7.中国剩余定理
m_1、m_2、……、m_n互质
$$ x≡r_1(mod{\ }{\ }m_1)\\ x≡r_2(mod{\ }{\ }m_2)\\ ⋅⋅⋅\\ x≡r_n(mod{\ }{\ }m_n) $$
long long mod[N],rmd[N],M = 1;//mod[i]即为mi,rmd表示余数 void exgcd(long long a,long long b,long long &x,long long &y){ if(b == 0) { x = 1,y = 0; return;} exgcd(b,a%b,y,x); y = y- a/b*x; } long long crt(int n){ long long ans; for (int i = 0;i < n;i++) { M*=mod[i]; } for (int i = 0;i < n;i++) { long long Mi = M / mod[i],inv,y; // Mi为所有模数乘积除以第i个模数,inv为 Mi在模 mi意义下的逆元 exgcd(Mi, mod[i], inv, y); inv = inv % mod[i]; ans = (ans + rmd[i] * Mi * inv) % M; } return (ans+M)%M; }
$$ 令M=m_1{\ }×{\ }m_2{\ }×⋯×{\ }m_n=∏m_i\\ 定义M_i=M/m_i,∀i∈\{1,2,⋯,n\}\\ 再设t_i为M_i对模m_i的逆元,即M_it_i≡1(mod{\ }{\ }m_i)\\ 构造特解x_0=∑r_iM_it_i,故通解为x=x_0+k·M $$
在m_1,m_2,……,m_n不互质的情况下求解x
long long mod[N],rmd[N]; long long excrt(int n){ //ans表示前i-1个方程式的特解,M为前i-1个方程式的模数的最小公倍数(i从2开始) long long ans = rmd[0],M = mod[0],k,tmp; for(int i = 1;i < n;i++){ long long mi = mod[i]; long long res = ((rmd[i] - ans)%mi + mi)%mi; //res=r2-r1,ans=r1 long long gcd = exgcd(M,mi,k,tmp); if(res % gcd != 0) return -1; k = qmul(k,res/gcd,mi); ans += k * M; M = mi /gcd * M; //M=lcm(M,mi) ans = (ans%M+M)%M; } return ans; }
$$ 将两个同余式合并,即x=r_1+t_1m_1=r_2+t_2m_2\\ 则有k_1p_1-k_2p_2=(r_2-r_1)/d,其中d=gcd(m_1,m_2)\\ 当且仅当 d | (r_2-r_1)时有解,因此x的一个特解x^*=r_1+k_1m_1\\ x在一组等价类内的通解为x^*+lcm(m_1,m_2)\\ 因此n个同余式的x的解为x=ans+M $$
七、数学
1.组合数与排列数
组合恒等式
$$ C(n,m)=C(n,n-m)=C(n-1,m-1)+C(n-1,m) $$
打表:
long long c[MAXN][MAXN]; //c[huge][small] void getc(){ c[0][0]=1; for(int i=1;i<=n;i++){ c[i][0]=1,c[i][1]=i; for(int j=1;j<=m;j++){ c[i][j]=c[i-1][j-1]+c[i-1][j]; c[i][j]%=MOD; } } }
const ll N = 5010; ll fac[N * N]; void init() { fac[0] = 1; for (int i = 1; i < N * N; i++) { fac[i] = (1LL * i * fac[i - 1]) % mod; } } ll getc(ll a, ll b) { return (ll)fac[a] * pow(fac[a - b], mod - 2) % mod * pow(fac[b], mod - 2) % mod; } ll Lucas(ll n, ll m) { if (n < mod && m < mod) return getc(n, m); if (m == 0) return 1; return getc(n % mod, m % mod) * Lucas(n / mod, m / mod) % mod; }
1.n个球相同,m个盒子不同,盒子不可空,方案数为:
$$ C(n-1,m-1) $$
2.n个球相同,m个盒子不同,盒子可空,方案数为:
$$ C(n+m-1,m-1) $$
3:n个球相同,m个盒子相同,盒子不可空。(数的拆分)
$$ ans=dp[n][m]=dp[n-1][m-1]+dp[n-m][m] $$
4:n个球相同,m个盒子相同,盒子可空。
$$ ans=dp[n][1]+...+dp[n][min(n,m)] $$
2.Striling数
表示将n个不同的元素构成m个圆排列的数目
表示将n个不同的元素分成m个集合的方案数
集合内不考虑次序
递推式:
$$ S(n,m)=m*S(n-1,m)+S(n-1,m-1); $$
//#define MOD 1000000007 //注意取模 long long S[MAXN][MAXN]; //从1到 n,从1到 m void getS(){ for(int i=0;i<=m;i++) S[0][i]=0; for(int i=1;i<=n;i++){ S[i][1]=1; for(int j=2;j<=m;j++){ S[i][j]=j*S[i-1][j]+S[i-1][j-1]; S[i][j]%=MOD; } } }
性质
(1)n个不同的球,放入m个的盒子,盒子为空。
方案数:
$$ S(n,m) $$
这个跟第二类Stirling数的定义一致。
(2)n个不同的球,放入m个的盒子,盒子为空。
方案数:
$$ m!*S(n,m) $$
因盒子有区别,乘上盒子的排列即可。
(3)n个不同的球,放入m个的盒子,盒子为空。
方案数:
枚举非空盒的数目便可。
(4)n个不同的球,放入m个的盒子,盒子为空。
①方