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=head&&h[i]<=v[trail].h) trail--;
v[++trail].h=h[i],v[trail].p=i+1;
}
for(;i=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=head&&h[i]>=v[trail].h) trail--;
v[++trail].h=h[i],v[trail].p=i+1;
}
for(;i=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 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
listlst1; //创建空list
list lst2(5); //创建含有5个元素的list
listlst3(3,2); //创建含有3个元素的list
listlst4(lst2); //使用lst2初始化lst4
listlst5(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()) 合并两个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::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.树
树的重心:
定义:树上一点,去掉后最大子树可以取得最小值的点。或去掉该点后最大子树大小不超过n/2。
1)二叉树
结构体写法
class TreeNode{
private:
int val;
TreeNode *left;
TreeNode *right;
public:
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
}
遍历
vector orderTraversal(TreeNode* root) {
vector res;
order(root,res);
return res;
}
二叉树的先序遍历
void preorder(TreeNode* root, vector& res) {
if(!root) return ;
res.push_back(root->val);
preorder(root->left,res);
preorder(root->right,res);
}
二叉树的中序遍历
void inorder(TreeNode* root, vector& res) {
if(!root) return ;
inorder(root->left,res);
res.push_back(root->val);
inorder(root->right,res);
}
二叉树的后序遍历
void proorder(TreeNode* root, vector& 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>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);
}
传递lazy标记
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]>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
using namespace std;
int main(){
string str1="hello";
cout<
#include
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<
字符串截取
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
取模
#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
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
4.Manacher算法
求回文子串
#include
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;idp[j]) dp[j]=dp[j-1];
x=tmp;
}
}
}
主函数
init();
cout<
6.LIS(最长上升子序列)
动态规划
时间复杂度:O(n^2)
int a[MAXN],dp[MAXN],n,ans;
void lis(){
ans=0;
for(int i = 0; i
贪心+二分
时间复杂度: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
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]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]
求按字典序排列的后一序列
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]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)Floyd算法
多源最短路
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
或
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)Dijkstra算法
单源最短路,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
堆优化,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 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
4.拓扑排序
int in[N];
vector out[MAXN];
int topology(){
queue q;
//vector ans;
for(int i=0;i
//不方便使用vector时
bool topotopology(){
for(i=0;i
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.二分图
最大匹配数:最大匹配的匹配边的数目
最小顶点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择,满足该集合的点的数目
最大独立集数:选取最多的点,使任意所选两点均不相连,满足该集合的点的数目
最小边覆盖数:选取最少条路径,使得每个顶点属于至少是其中某条边的端点,满足该集合的边的数目。
定理1:二分图中,最大匹配数 = 最小点覆盖数 定理2:最大独立数+最小点覆盖数=顶点数 定理3:对于不存在孤立点的图,最小边覆盖数+最大匹配数= 顶点数
匈牙利算法
求最大匹配数
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
#include
#include
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
需要并查集,主函数逆向建树
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
10.最小生成树
点数n,边数m Prim算法O(n^2) Kruscal算法O(mlogm) 稠密图推荐使用Prim算法 稀疏图推荐使用Kruskal算法
Prim算法
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点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中最小代价边信息,:
Kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为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
$$
φ(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.分数规划
0/1分数规划
给定一系列整数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;
}
LCM(最小公倍数)
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 $$
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.中国剩余定理
求x的整数解: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个有区别的盒子,允许盒子为空。
①方