O ( ( m + n ) l o g n ) O((m+n)log_n) O((m+n)logn)
基于贪心思想,不能处理负边权(每个点第一次出为最短路,但负边权若离源点较远,还没更新到就已确定了答案)
inline void dijkstra(){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[1]=0;
priority_queue<pair<int,int>>q;
q.push(make_pair(0,1));
while(!q.empty()){
int x-q.top();q.pop();
if(vis[x])continue;
vis[x]=1;
for(int i=first[x],v;i;i=e[i].nxt){
v=e[i].v;
if(dis[v]>dis[x]+e[i].w){
dis[v]=dis[x]+e[i].w;
q.push(make_pair(-dis[v],v));
}
}
}
}
复杂度玄学,最坏 O ( n m ) O(nm) O(nm)
判负环: c n t [ x ] cnt[x] cnt[x]表示从1到 n n n的最短路包含的边数, c n t [ 1 ] = 0 cnt[1]=0 cnt[1]=0,若发现 c n t [ v ] > = n cnt[v]>=n cnt[v]>=n则有负环(也可以判一个点入队大于等于 n n n次,但复杂度低些)
别用双端队列优化,卡的更凶,且无法判负环(入队 n n n次也不一定是被更新了 n n n次), d f s dfs dfs版也会被卡
queue<int>q;
inline bool spfa(){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
q.push(1);
vis[1]=1;dis[1]=0;
while(!q.empty()){
int x=q.front();q.pop();
vis[x]=0;
for(int i=first[x],v;i;i=e[i].nxt){
v=e[i].v;
if(dis[v]>dis[x]+e[i].w){
dis[v]=dis[x]+e[i].w;
cou[v]=cou[x]+1;//!!
if(cou[v]>=n)return 1;
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
}
}
return 0;
}
任意两点之间最短路, O ( n 3 ) O(n^3) O(n3)
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
求传递闭包
关系具有传递性,且传递性推导出尽量多的元素之间的关系,如判有向图,一个点是否能到达另一个点
普通版
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
b[i][j]|=b[i][k]&b[k][j];
b i t s e t bitset bitset版
bitset<N>b[N];
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(f[j].test(i))f[j]|=f[i];//用f[i]更新f[j]
最小环:
在 f l o y d floyd floyd中再开一个更新答案
vector<int>path;
inline void get_path(int x,int y){
if(pox[x][y]==0)return;
get_path(x,pos[x][y]);
path.push_back(pos[x][y]);
get_path(pos[x][y],y);
}
for(int k=1;k<=n;k++){
for(int i=1;i<k;i++)
for(int j=i+1;j<k;j++)
if(dis[i][j]+e[j][k]+a[k][i]<ans){
ans=dis[i][j]+a[j][k]+a[k][i];
path.clear();
path.push_back(i);
get_path(i,j);
path.push_back(j);
path.push_back(k);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(dis[i][j]>dis[i][k]+dis[k][j]){
dis[i][j]=dis[i][k]+dis[k][j];
pos[i][j]=k;
}
}
也可以设 d i s [ i ] [ i ] = i n f dis[i][i]=inf dis[i][i]=inf求完之后就是最小环了
O ( m l o g m ) O(mlog_m) O(mlogm)
贪心加入最小的边
inline void find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
}
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1,fu,fv;i<=m;i++){
fu=find(e[i].u);fv=find(e[i].v);
if(fu==fv)continue;
fa[fu]=fv;
ans+=e[i].w;
}
O ( n 2 ) O(n^2) O(n2)
适用于稠密图
inline void prim(){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[1]=0;
for(int i=1,x;i<=n;i++){
x=0;
for(int j=1;j<=n;j++)
if(!vis[j]&&(!x||dis[j]<dis[x]))x=j;
vis[j]=1;
for(int j=1;j<=n;j++)
if(!vis[j])dis[j]=min(dis[j],e[x][j]);
}
}
for(int i=1;i<=n;i++)ans+=dis[i];
方法:(朱-刘算法)
求最短弧集合E
判断集合E中有没有有向环,如果有转步骤3,否则转4
收缩点,把有向环收缩成一个点,并且对图重新构建,包括边权值的改变和点的处理,之后再转步骤1
展开收缩点,求得最小树形图
while(1){
memset(mn,0x3f,sizeof(mn));
for(int i=1,u,v,w;i<=m;i++){
u=e[i].u;v=e[i].v;w=e[i].w;
if(u!=v&&w<mn[v]){
mn[v]=w;from[v]=u;
}
}
for(int i=1;i<=n;i++)
if(i!=rt&&mn[i]==inf){
printf("-1");return (0-0);
}
cnt=0;
memset(vis,0,sizeof(vis));
memset(cn,0,sizeof(cn));
for(int i=1,v;i<=n;i++){
if(i==rt)continue;
ans+=mn[i];
v=i;
while(vis[v]!=i&&!cn[v]&&v!=rt){
vis[v]=i;
v=from[v];
}
if(!cn[v]&&v!=rt){
cn[v]=++cnt;
for(int j=from[v];j!=v;j=from[j])
cn[j]=cnt;
}
}
if(!cnt)break;
for(int i=1;i<=n;i++)
if(!cn[i])cn[i]=++cnt;
for(int i=1,u,v;i<=m;i++){
u=e[i].u;v=e[i].v;
e[i].u=cn[u];e[i].v=cn[v];
if(cn[u]!=cn[v])e[i].w-=mn[v];//代替之前的边
}
rt=cn[rt];
n=cnt;
}
printf("%d",ans);
(树的直径可以作为很多树上问题的突破口)
inline void dp(int x,int fa){
for(int i=first[x];i;i=e[i].nxt){
v=e[i].v;
if(v==fa)continue;
dp(v,x);
ans=max(ans,dis[x]+dis[v]+e[i].w);
dis[x]=max(dis[x],dis[v]+e[i].w);
}
if(x==rt)ans=max(ans,dis[x]);
}
倍增
O ( l o g n ) O(log_n) O(logn)
inline void lca_pre(int x){
for(int i=1;(1<<i)<=dep[x];i++)
fa[x][i]=fa[fa[x][i-1]][i-1];
for(int i=first[x],v;i;i=e[i].nxt){
v=e[i].v;
if(dep[v]||v==fa[x][0])continue;
dep[v]=dep[x]+1;
fa[v][0]=x;
lca_pre(v);
}
}
inline int LCA(int u,int v){
if(dep[u]<dep[v])swap(u,v);
int r=dep[u]-dep[v];
for(int i=0;(1<<i)<=r;i++)
if(r&(1<<i))u=fa[u][i];
if(u==v)return u;
for(int i=19;i>=0;i--)
if(fa[u][i]!=fa[v][i]){
u=fa[u][i];v=fa[v][i];
}
return fa[u][0];
}
树剖
O ( l o g n ) O(log_n) O(logn)
inline void dfs1(int x){
dis[x]=dis[fa[x]]+num[x];
siz[x]=1;//!!!!!
for(re int i=0,v;i<ne[x].size();i++){
v=ne[x][i];
if(v==fa[x])continue;
dep[v]=dep[x]+1;
fa[v]=x;
dfs1(v);
siz[x]+=siz[v];
if(siz[v]>siz[son[x]])son[x]=v;
}
}
inline void dfs2(int x,int tp){
dfn[x]=++tot;
top[x]=tp;
if(son[x])dfs2(son[x],tp);
for(re int i=0,v;i<ne[x].size();i++){
v=ne[x][i];
if(v==fa[x]||v==son[x])continue;
dfs2(v,v);
}
}
inline int LCA(int u,int v){
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]])u^=v^=u^=v;
u=fa[top[u]];
}
return dep[u]<dep[v]?u:v;
}
tarjan
O ( 1 ) O(1) O(1)
离线算法
把询问存储至每个节点处,若回溯完成则将节点指向父节点, l c a lca lca即为所指向的节点
inline int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
inline void tarjan(int x){
vis[x]=1;
for(int i=first[x],v;i;i=e[i].nxt;i++){
v=e[i].v;
if(vis[v])continue;
tarjan(v);
fa[v]=x;
}
for(int i=0,v,id;i<q[x].size();i++){
v=q[x][i].first;id=q[x][i].second;
iF(vis[v]==2)lca[id]=find(v);
}
vis[x]=2;
}
约数条件: x i − x j < = c x_i-x_j<=c xi−xj<=c变形 x i < = x j + c x_i<=x_j+c xi<=xj+c与 d i s [ i ] < = d i s [ j ] + c dis[i]<=dis[j]+c dis[i]<=dis[j]+c极其相似,可以从 j j j向 i i i连一条 c c c的边(若约束过于分散可以加入0号节点)跑最短路即可(注意负环)
若 > = >= >=可以求最长路
在图论的连通性问题中,我们经常要从搜索树的角度来分析
有时需要可以建”补图“
割边
inline void tarjan(int x,int from){
dfn[x]=low[x]=++tot;
for(int i=first[x],v;i;i=e[i].nxt){
v=e[i].v;
if(!dfn[v]){
tarjan(v,i);
low[x]=min(low[x],low[v]);
if(low[v]>dfn[x])//!!
bridge[i]=bridge[i^1]=1;
}
else if((i^1)!=from)low[x]=min(low[x],dfn[v]);//!!
}
}
割点
inline void tarjan(int x){
dfn[x]=low[x]=++tot;
int fl=0;
for(int i=first[x],v;i;i=e[i].nxt){
v=e[i].v;
if(!dfn[v]){
tarjan(v);
low[x]=min(low[x],low[v]);
if(low[v]>=dfn[x]){//!!
fl++;
if(x!=root||fl>1)cnt[x]=1;//root特判
}
}
else low[x]=min(low[x],dfn[v]);
}
}
边双联通分量
inline void tarjan(int x,int from){
dfn[x]=low[x]=++tot;
st[++ttp]=x;
for(re int i=first[x],v;i;i=e[i].nxt){
if((i^1)==from)continue;
v=e[i].v;
if(dfn[v])low[x]=min(low[x],dfn[v]);
else{
tarjan(v,i);
low[x]=min(low[x],low[v]);
}
}
if(low[x]==dfn[x]){//缩点
cn++;int v;
do{
v=st[ttp--];
dcc[v]=cn;
num[cn]+=a[v];
}while(v!=x);
}
}
也可以先求割边,删去后, d f s dfs dfs各个联通块
点双联通分量
inline void tarjan(int x){
dfn[x]=low[x]=++tot;
st[++top]=x;
if(x==rt&&first[x]==0){//孤立点
dcc[++cn].push_back(x);
return;
}
int fl=0;
for(int i=first[x],v,u;i;i=e[i].nxt){
v=e[i].v;
if(!dfn[v]){
tarjan(v);
low[x]=min(low[x],low[v]);
if(low[v]>=dfn[x]){
fl++;
if(x!=root||fl>1)cnt[x]=1;
cn++;
do{//!!
u=st[top--];
dcc[cn].push_back(u);
}while(u!=v);
dcc[cn].push_back(x);//割点可能在多个点双内
}
}
else low[x]=min(low[x],dfn[v]);
}
}
缩点后一般将割点与包含它的所有 d c c dcc dcc连边,形成一棵树
欧拉回路:一笔画,起点终点相同,所有点度数为偶数
欧拉通路:一笔画,除起点终点度数为奇数外,其余点为偶数
输出具体方案:
inline void dfs(int x){
for(int i=first[x];i;i=e[i].nxt){
if(vis[i])continue;
vis[i]=vis[i^1]=1;
dfs(e[i].v);
st[++top]=e[i].v;
}
}
int main(){
……
dfs(s)
cout<<1<<" ";//??
for(int i=top;i;i--)//貌似也不用倒序
cout<<st[i]<<" ";
return (0-0);
}
inline void tarjan(int x){
dfn[x]=low[x]=++tot;
st[++top]=x;vis[x]=1;
for(int i=first[x],v;i;i=e[i].nxt){
v=e[i].w;
if(!dfn[v]){
tarjan(v);
low[x]=min(low[x],low[v]);
}
else if(vis[v])low[x]=min(low[x],dfn[v]);//!!
}
if(dfn[x]==low[y]){
cn++;int u;
do{
u=st[top--];
vis[u]=0;
scc[u]=cn;
sc[cn].push_back(u);
}while(u!=x);
}
}
从起点到终点必经的点叫必经点,同理也有必经边
有环的有向图需用支配树来解,这里不讨论
有向无环图的必经点与必经边:
我们通俗的说,就是给你 n n n个变量 a i a_i ai,每个变量能且只能取 0 / 1 0/1 0/1的值。同时给出若干条件,形式诸如 ( n o t ) a i o p t ( n o t ) a j = 0 / 1 (not)a_i opt(not) a_j=0/1 (not)aiopt(not)aj=0/1,其中 o p t opt opt表示 a n d , o r , x o r and,or,xor and,or,xor中的一种
我们把每个点拆为两个,用若 x x x则 y y y的方式连边,同时也要连逆否命题
求解时染色,若矛盾或一个点的两种状态连通则无解,不然可以随便求出一组解
(有时关系不太好描述,如&,可直接制造矛盾)