图论
链式前向星
自己呢,总是忘了,head为0,cnt=0,直接用
const int N=1e5+5;
const int M=3e6+5;
struct node
{
int v,next,w;
bool operator<(const node& b)const
{
return w<b.w;
}
}e[M];
int cnt=0,head[N],vis[N],dis[N];
void add(int from,int to,double w)
{
e[++cnt].next=head[from];
e[cnt].v=to;
e[cnt].w=w;
head[from]=cnt;
}
最小生成树
克鲁斯卡尔算法(Kruskal算法)——时间复杂度O(eloge)
特性:稀疏图和记录路径
普里姆算法——时间复杂度O(n^2)
最小生树是否唯一问题:(次小生成树问题)
朴素算法(m^2)
1:暴力删除最小生树上的
2:判断权值和以及是否是一棵树
优化后,理论(mlogm)跑一次克鲁斯卡尔即可
边权相等的边,边两点不同的并查集的边数<=所在不同的并查集个数-1
保证最小生成树唯一
最小生成树:所有边权之和最小
瓶颈生成树:定义无向图G,G的瓶颈生成树是一棵 “ 树上最大边权值 edge 在G的所有生成树中最小 ” 的生成树,
这样的生成树可能不止一棵。瓶颈生成树的值为树上最大边权值 edge
结论:
最小生成树一定是瓶颈生成树
瓶颈生成树不一定时最小生成树
暂时理解:最小瓶颈生成树——瓶颈生成树一个意思
最短路
Dijkstra:适用于权值为非负的图的单源最短路径,朴素算法O(n2)用斐波那契堆的复杂度O(E+VlgV)
解决单源最短路径问题常用Djkstra算法,用于计算一个顶点到其他所有 顶点的最短路径。Djkstra 算法的主要特点是以起点为中心,逐层向外扩展,每次都会取一个最近点继续扩展,直到取完所有点为止。
BellmanFord:适用于权值有负值的图的单源最短路径,并且能够检测负圈,复杂度O(VE)
SPFA:适用于权值有负值,且没有负圈的图的单源最短路径,论文中的复杂度O(kE),k为每个节点进入Queue的次数,且k一般<=2,但此处的复杂度证明是有问题的,其实SPFA的最坏情况应该是O(VE).
先给出结论:
(1)当权值为非负时,用Dijkstra。
(2)当权值有负值,且没有负圈,则用SPFA,SPFA能检测负圈,但是不能输出负圈。
(3)当权值有负值,而且可能存在负圈,则用BellmanFord,能够检测并输出负圈。
(4)SPFA检测负环:当存在一个点入队大于等于V次,则有负环,后面有证明。
严格最短路是否唯一问题:(次严格最短路问题)
两次dijkstra,然后枚举
dis1[u]+dis+dis2[v]=dis1[n]
dis1[v]+dis+dis2[u]=dis1[n]
满足则标记为严格最短路必经路径
floyd暴力求最短路–O(n3)
const int INF=0x3f3f3f3f;
const int N=500;
int G[N][N];
void floyd(int n)//图的最短路路径,枚举一遍
{
for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
G[i][j]=min(G[i][j],G[i][k]+G[k][j]);
}
Disjkstra算法
O(n2)
int dis[N],vis[N],G[N][N],t,n;
void dijkstra(int u)
{
memset(dis,inf);
dis[u]=0;
int v=u,minn;
vis[v]=true;
for(int i=1; i<=n; ++i)
{
minn=inf;
for(int j=1; j<=n; ++j)
{
if(!vis[j]&&dis[j]<=minn)
v=j,minn=dis[j];
}
vis[v]=true;
for(int j=1; j<=n; ++j)
{
if(!vis[j])
dis[j]=min(dis[j],dis[v]+G[v][j]);
}
}
}
O(eloge+n)
int dis[N],vis[N],V[N][N],m,n;
vector<int>G[N];
void dijkstra(int x)
{
memset(dis,inf);
dis[x]=0;
priority_queue<pii>pri;
pri.push({0,x});
while(!pri.empty())
{
pii u=pri.top();
pri.pop();
if(vis[u.S])
continue;
vis[u.S]=true;
for(auto v:G[u.S])
{
if(!vis[v]&&dis[u.S]+V[u.S][v]<dis[v])
{
dis[v]=dis[u.S]+V[u.S][v];
pri.push({-dis[v],v});
}
}
}
}
LCA
倍增LCA
第一步:dfs跑父结点(1倍祖先)-O(n)
第二步:预处理跑ST表-all倍增祖先- O(n*logn)
第三步:LCA查询 单次O(logn) 多次O(mlogn)
const int N=3e5+5; ///应用:1:两点之间的距离 2:最近公共祖先
int fat[N][21],h[N];
vector<int>G[N];
void dfs(int x)
{
for(auto v:G[x])
{
if(v==fat[x][0])
continue;
h[v]=h[x]+1; fat[v][0]=x;
dfs(v);
}
}
int lca(int x,int y)
{
if(h[x]<h[y]) swap(x,y);
for(int i=20;i>=0;i--)
if( (h[x]-h[y])>>i )
x=fat[x][i];
if(x==y)
return x;
for(int i=20;i>=0;i--)
if( fat[x][i]!=fat[y][i] )
x=fat[x][i],y=fat[y][i];
return fat[x][0];
}
int dis(int x,int y)
{
return h[x]+h[y]-h[lca(x,y)]*2;
}
for(int i=1;i<=20;i++)
for(int j=1;j<=n;j++)
fat[j][i]=fat[ fat[j][i-1] ][i-1];
倍增时间戳优化(链式前向星TLE再考虑,常数级优化)
const int N=5e5+5;
int in[N],out[N],fat[N][21],cnt=0,dis[N];
vector<int>G[N];
void dfs(int u)
{
in[u]=++cnt;
for(auto v:G[u])
{
if(v==fat[u][0])
continue;
dis[v]=dis[u]+1;
fat[v][0]=u;
for(int i=1;i<=20;i++)
fat[v][i]=fat[fat[v][i-1]][i-1];
dfs(v);
}
out[u]=++cnt;
}
bool ok(int u,int v)
{
return in[u]<=in[v]&&out[v]<=out[u];
}
int lca(int u,int v)
{
if(dis[u]>dis[v])
swap(u,v);
if(ok(u,v))
return u;
for(int i=20;i>=0;--i)
if(!ok(fat[u][i],v)&&fat[u][i])
u=fat[u][i];
return fat[u][0];
}
树剖LCA
第一步:dfs跑Size depp fat -O(n)
第二步:dfs1跑top 轻重链-O(n)
第三步:LCA查询 单次O<=(logn) 比倍增更快
记住:卡常数老老实实链式前向星
Size 子树结点个数(包括本身) top 重链顶端结点
deep 结点的深度 fat父结点 son 当前结点的重儿子
const int N=5e5+5;
int Size[N],top[N],deep[N],fat[N],son[N];
vector<int>G[N];
void dfs(int u)
{
Size[u]=1;
deep[u]=deep[fat[u]]+1;
for(auto v:G[u])
{
if(v==fat[u])
continue;
fat[v]=u;
dfs(v);
Size[u]+=Size[v];
if(Size[v]>Size[son[u]])
son[u]=v;
}
}
void dfs1(int u)
{
if(!top[u])
top[u]=u;
if(son[u])
{
top[son[u]]=top[u];
dfs1(son[u]);
}
for(auto v:G[u])
if(v!=fat[u]&&v!=son[u])
dfs1(v);
}
int lca(int u,int v)
{
while(top[u]!=top[v])
{
if(deep[top[u]]<deep[top[v]])
swap(u,v);
u=fat[top[u]];
}
return deep[u]<deep[v]?u:v;
}
二分图最大匹配
匈牙利算法O(VE)
HK算法O(sqrt(V)E)
KM算法
概念
1.最大独立点集:
在二分图中,选最多的点,使得任意两个点之间没有直接边连接。
最大独立集= 最小边覆盖 = 总点数- 最大匹配 (条件:在二分图中)
2.最小边覆盖:(最小不相交路径覆盖,u-v建边,add(u,v+n))
在二分图中,求最少的边,使得他们覆盖所有的点,并且每一个点只被一条边覆盖。
最小边覆盖=图中的顶点数-(最小点覆盖数)该二分图的最大匹配数(条件:在二分图中)
3.最小点覆盖:
在二分图中,求最少的点集,使得每一条边至少都有端点在这个点集中。
最小点覆盖 = 最大匹配 (条件:在二分图中)
匈牙利算法(核心:建图——>AC)
第一步:建立关系图,只需要构建一条边即可:
第二步:枚举每个男朋友
第三步:DFS寻找是否可以分配一个小老婆
时间复杂度:邻接矩阵O(最坏n^3) 邻接表O(n*m)
空间复杂度:邻接矩阵O(n^2) 邻接表O(n+m)
邻接矩阵(n^3)
const int N=1e3+5;
int G[N][N],vis[N],g[N],ans=0;///G存关系图 vis是否访问 g存妹子的对象,没有对象就0
bool find(int x,int n)
{
for(int i=1;i<=n;i++)///扫描所有妹子
{
if(G[x][i]==true&&vis[i]==false)///A:是否有暧昧关系 B:是否访问
{
vis[i]=true;
if(g[i]==0||find(g[i],n))///A:名花无主 B:能腾出位置,就递归
{
g[i]=x;
return true;
}
}
}
return false;
}
for(int i=1; i<=p; i++)///跑一遍男孩子
{
memset(vis,0);//每次找对象,要清空
if(find(i,n))
ans++;
}
邻接表(n*m)
const int N=3e3+5;///记得每次len ,g,G初始化
bool vis[N];
int g[N];
vector<int>G[N];
bool dfs(int u)
{
for(auto v:G[u])
{
if(vis[v])
continue;
vis[v]=true;
if(!g[v]||dfs(g[v]))
{
g[v]=u;
return true;
}
}
return false;
}
网络流
在普通情况下, DINIC算法时间复杂度为O(V2E)
在二分图中, DINIC算法时间复杂度为O(sqrt(V)E)
注意:初始化->head清空0,pos=1,还有s和t
namespace Dinic
{
const int N=1e4+5;
const int M=1e5+5;
int s,t;
int head[N*2],pos=1,deep[N*2];
struct node
{
int next,to,w;
}edge[M*6];
void add(int from,int to,int w)
{
edge[++pos]={head[from],to,w};
head[from]=pos;
}
void fadd(int from,int to,int w)
{
add(from,to,w),add(to,from,0);
}
bool bfs()
{
memset(deep,0x3f);
queue<int>que;
deep[s]=1;
que.push(s);
while(!que.empty())
{
int f=que.front();
que.pop();
for(int i=head[f];i;i=edge[i].next)
{
int to=edge[i].to;
if(edge[i].w>0&&deep[f]+1<deep[to])
{
deep[to]=deep[f]+1;
que.push(to);
if(to==t)return true;
}
}
}
return false;
}
int dfs(int u,int flow)
{
if(u==t)return flow;
int res=0;
for(int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to;
if(edge[i].w<=0||deep[to]!=deep[u]+1)continue;
int re=dfs(to,min(flow,edge[i].w));
if(re>0)
{
flow-=re;res+=re;
edge[i].w-=re;
edge[i^1].w+=re;
if (!flow)return res;
}
else
deep[to]=-1;
}
return res;
}
int dinic()
{
int ans=0;
while(bfs())
ans+=dfs(s, inf);
return ans;
}
}
强连通分量——tarjan算法(有向图)
第一步:枚举所有dfn为0跑tarjan
第二步:模板tarjan内部缩点
第三步:重新构图DAG
const int N=2e5+5;
int dfn[N],low[N],pos=0,col[N],col_cnt=0,cnt[N];
stack<int>sta;
set<int>insta;
vector<int>G[N];
void tarjan(int u)
{
dfn[u]=low[u]=++pos;
sta.push(u);
insta.insert(u);
for(auto v:G[u])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(insta.count(v))
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
col_cnt++;
int k;
do
{
k=sta.top(),sta.pop();
insta.erase(k);
col[k]=col_cnt,cnt[col_cnt]++;
}while(u!=k);
}
}
树上启发式合并(dsu on tree)——O(nlogn)
第一步:dfs(1,0),构造重儿子
第二步:dfs2(1,0,1);暴力跑
const int N=1e5+5;
vector<int>G[N];
int Size[N],son[N],col[N],cnt[N];
int ans[N],sum=0,maxn=0;
void dfs(int u,int fat)///构造重儿子
{
Size[u]++;
for(auto v:G[u])
{
if(v==fat)
continue;
dfs(v,u);
Size[u]+=Size[v];
if(Size[v]>Size[son[u]])
son[u]=v;
}
}
void add(int u,int fat,int val,int tag)
{
cnt[col[u]]+=val;
if(cnt[col[u]]>maxn)
sum=col[u],maxn=cnt[col[u]];
else if(cnt[col[u]]==maxn)
sum+=col[u];
for(auto v:G[u])
{
if(v==fat||v==tag)
continue;
add(v,u,val,tag);
}
}
void dfs2(int u,int fat,int opt)
{
for(auto v:G[u])
{
if(v==fat||v==son[u])
continue;
dfs2(v,u,0);///暴力处理轻边
}
if(son[u])///处理重儿子subtree
dfs2(son[u],u,1);
add(u,fat,1,son[u]);///u的substree除了重儿子的substree
ans[u]=sum;
if(!opt)///清除所有subtree of u的信息
add(u,fat,-1,0),sum=0,maxn=0;
}