图论指南

news/2024/9/22 19:28:06/文章来源:https://www.cnblogs.com/hisy/p/18425731

前置知识

在了解图论之前,还需要知道怎么存图。

vector

vector<int> G[MAXN] 来存图。$G_i$ 表示从 $i$ 出发,能够到达的点的数组。空间开销相较于链式前向星较大。也可以将 vector 替换为其他 STL 容器,如 listunordered_setdeque 等。list 的写法空间更优,常数较小,但是 vector 更大众一点。

链式前向星

由于空间小、常数小,深受众多 OIer 的喜爱。本质上是通过跳链的方式,但是不是遍历 $(u\to v)\to(v\to o)$,而是 $(u\to v_i)\to(u\to v_{i+1})$。这种方式遍历图需要至少两个元素:$to,nxt$。$to$ 表示这一条边通向哪里,$nxt$ 表示下一条边是哪里。

这种存图方式还需要一个数组 $head$,$head_i$ 表示 $i$ 的上一条边是哪里,如果 $nxt$ 指向在同一个 $i$ 的 $head_i$,就能够用 $nxt$ 把所有开头为 $i$ 的边串联起来了。下面,给出链式前向星的模板:

struct node{int /*from,Kruskal 需要初始点*/to,nxt;
//	ll dis;比如 Dijiestra 需要边权 }edge[MAXM<<1];//MAXM 表示最多存的边,开 2 倍表示双向边 
int cnt,head[MAXN];//MAXN 表示点数
#define rep(from) for(int i=head[from];i;i=edge[i].nxt)//遍历图
inline void addedge(int from,int to/*,ll dis*/){edge[++cnt].to=to;edge[cnt].nxt=head[from];
//	edge[cnt].dis=dis;
//	edge[cnt].from=from;head[from]=cnt;//存上一条边 
} 

邻接矩阵

邻接矩阵用 $dp_{u,v}$ 表示一条边 $(u\to v)$ 的路径。如果无权图的话,用 $1$ 代表有,用 $0$ 代表没有。如果是有权图,用 $-\infty$ 表示。

由于速度慢,通常用于 Floyed 算法,为 dp 式转移。

最短路

Floyed

Floyed 的复杂度是 $\operatorname{O}(n^3)$,本质上是 dp 转移式。枚举初始点 $u$、结尾点 $v$、中转点 $k$,用 $dp_{u,v}=min(dp_{u,v},dp_{u,k}+dp_{k,v})$ 来转移。

ll dp[MAXN][MAXN];//转移
inline void Floyed(){for(int u=1;u<=n;++u){for(int v=1;v<=n;++v){for(int k=1;k<=n;++k){dp[u][v]=min(dp[u][v],dp[u][k]+dp[k][v]);//转移式 }}}
}

SPFA

SPFA 的本质在于松弛和 bfs。松弛操作是指 $dis_{u,v}=min(dis_{u,v},dis_{u,k}+w(k,v))$。这和 Floyed 的操作如出一辙。

struct node{int to,nxt;ll dis;
}edge[MAXM<<1];
int cnt,head[MAXN];
bool vis[MAXN];
ll dis[MAXN];
inline void addedge(int from,int to,ll dis){edge[++cnt].to=to;edge[cnt].dis=dis;edge[cnt].nxt=head[from];head[from]=cnt;
} 
inline void SPFA(int s){queue<int> q;memset(vis,0,sizeof(vis));memset(dis,0x3f,sizeof(dis));vis[s]=true;q.push(s);dis[s]=0;//初始化 while(!q.empty()){int front=q.front();q.pop();vis[front]=false;//bfs 公式 for(int i=head[front];i;i=edge[i].nxt){int to=edge[i].to;if(dis[to]>dis[front]+edge[i].dis){dis[to]=dis[front]+edge[i].dis;//松弛 if(!vis[to]){q.push(to);//继续扩展,继续松弛 vis[to]=true;}}}}
}

Dijiestra

Dijiestra 本质上是一种贪心算法,只能够处理无负边权的最短路。

首先,把最短的路径出队,对相应的边进行松弛操作。然后,将相应的边假如队列,继续后续的松弛。

Dijiestra 还有很多种写法,一一介绍:

  • 暴力:直接找最小的节点,时间复杂度为 $\operatorname{O}(n^2+m)$。
  • 堆优化:用二叉堆来维护最小值,可以做到 $m$ 次入队,$n$ 次出队,时间复杂度 $\operatorname{O}((n+m)\log m)$。
  • Fibonacci 堆优化:插入是 $\operatorname{O}(1)$ 的,所以达到了 $\operatorname{O}(m+n\log n)$,但是由于实现难度较高,并且题目基本不用 Fibonacci 堆卡 $\log$,所以不常使用。
  • 线段树优化:将插入堆变成单点修改,查询最小值改成线段树全局查找最小值,时间复杂度 $\operatorname{O}((n+m)\log n)$。

下面给出经典的堆优化和线段树优化代码:

堆优化

struct node{int to,nxt;ll dis;
}edge[MAXM<<1];
int cnt,head[MAXN];
bool vis[MAXN];
ll dis[MAXN];
inline void addedge(int from,int to,ll dis){edge[++cnt].to=to;edge[cnt].dis=dis;edge[cnt].nxt=head[from];head[from]=cnt;
} 
inline void Dijiestra(int s){priority_queue<pair<ll,int> > q;memset(vis,0,sizeof(vis));memset(dis,0x3f,sizeof(dis));q.push(make_pair(0ll,s));dis[s]=0;while(!q.empty()){int front=q.top().second;//每次出队的是最小值 q.pop();if(vis[front]){continue;//剪枝 }vis[front]=true;for(int i=head[front];i;i=edge[i].nxt){int to=edge[i].to;if(dis[to]>dis[front]+edge[i].dis){dis[to]=dis[front]+edge[i].dis;q.push(make_pair(dis[to],to)));//松弛操作 }}}
}

线段树优化

struct node{int to,nxt;ll dis;
}edge[MAXM<<1];
int cnt,head[MAXN],pos[MAXN<<2];
bool vis[MAXN];
ll dis[MAXN],ans[MAXN],tree[MAXN<<2];
inline void addedge(int from,int to,ll dis){edge[++cnt].to=to;edge[cnt].dis=dis;edge[cnt].nxt=head[from];head[from]=cnt;
} 
inline void push_up(int root){if(tree[root<<1]<tree[root<<1|1]){tree[root]=tree[root<<1];pos[root]=pos[root<<1];}else{tree[root]=tree[root<<1|1];pos[root]=pos[root<<1|1];}
}
void build(int root,int l,int r){if(l==r){tree[root]=INT_MAX;pos[root]=l;return;}int mid=(l+r)>>1;build(root<<1,l,mid);build(root<<1|1,mid+1,r);push_up(root); 
}
void change(int root,int pos,ll k,int l,int r){if(l==pos&&r==pos){tree[root]=k;return;}int mid=(l+r)>>1;if(pos<=mid){change(root<<1,pos,k,l,mid); }else{change(root<<1|1,pos,k,mid+1,r);}push_up(root);
}
inline void Dijiestra(int s){fill(dis+1,dis+n+1,INT_MAX);build(1,1,n);//建树 dis[s]=0ll;for(int i=1;i<=n;++i){ans[s]=dis[s];ll disu=dis[s];change(1,s,ll(INT_MAX)+1ll,1,n);for(int j=head[s];j;j=edge[j].nxt){int to=edge[j].to;if(dis[to]<=INT_MAX&&dis[to]>dis[s]+edge[j].dis){dis[to]=dis[s]+edge[j].dis;change(1,to,dis[to],1,n);//修改 }}s=pos[1];}
}

Johnson

Johnson 的本质是重新赋权。先新建一个虚拟节点 $0$,向其他节点延伸出 $n$ 条边权为 $0$ 的点,用 SPFA 或者 Floyed 跑 $0$ 到其他节点的最短路。重新赋权,比如有边 $u\to v$,长度为 $w$,则重新赋为 $w+dis_u-dis_v$,然后再跑 Dijiestra。

例题1

这一道题目要求的就是中转距离,根据时间多加的点进行 $\operatorname{O}(n)$ 的转移即可。

#include<bits/stdc++.h>
#define MAXN 202
#define INF 1e9
using namespace std;
int f[MAXN][MAXN],a[MAXN];
int main(){int n,m,q,p=0;scanf("%d %d",&n,&m);for(int i=0;i<n;++i){scanf("%d",&a[i]);for(int j=0;j<n;++j){f[i][j]=INF;}f[i][i]=0;}while(m--){int x,y,len;scanf("%d %d %d",&x,&y,&len);f[x][y]=f[y][x]=len;}scanf("%d",&q);while(q--){int x,y,len;scanf("%d %d %d",&x,&y,&len);while(a[p]<=len&&p<n){for(int i=0;i<n;++i){for(int j=0;j<n;++j){f[i][j]=f[j][i]=min(f[i][j],f[i][p]+f[p][j]);}}++p;}if(a[x]>len||a[y]>len||f[x][y]==INF){puts("-1");}else{printf("%d\n",f[x][y]);}}return 0;
}

例题2

很让人坠机的样子,因为建边都建不完。发现题目给的异或并不是“在 C++ 中用 ^ 表示”,而是放了链接,说明这一道题目跟异或的性质有关。

我们发现有一些边可以被其他的边替代,而且按位来算正好是 $n\log n$ 的!再推一下,我们发现形如 $5$ 的二进制是 $101$,那么可以用 $4\oplus 1=100\oplus001=101=5$ 替代。那么,只需要处理位上最高位是 $1$ 的,其他的都可以组合而成。

之后,跑 Dijiestra 就可以了。堆优化比较呛,要吸氧。这里放上线段树优化的代码。

#include<bits/stdc++.h>
#define MAXN 1000001
#define MAXM MAXN*22
using namespace std;
typedef long long ll;
struct node{int from,nxt,to;ll dis;
}edge[MAXM];
int n,m,c,s,t,cnt,head[MAXN],pos[MAXN<<2];
ll dis[MAXN],ans[MAXN],tree[MAXN<<2];
inline void addedge(int x,int y,ll dis){edge[++cnt].to=y;edge[cnt].from=x;edge[cnt].nxt=head[x];edge[cnt].dis=dis;head[x]=cnt;
}
inline void push_up(int root){if(tree[root<<1]<tree[root<<1|1]){tree[root]=tree[root<<1];pos[root]=pos[root<<1];}else{tree[root]=tree[root<<1|1];pos[root]=pos[root<<1|1];}
}
void build(int root,int l,int r){if(l==r){tree[root]=INT_MAX;pos[root]=l;return;}int mid=(l+r)>>1;build(root<<1,l,mid);build(root<<1|1,mid+1,r);push_up(root); 
}
void change(int root,int pos,ll k,int l,int r){if(l==pos&&r==pos){tree[root]=k;return;}int mid=(l+r)>>1;if(pos<=mid){change(root<<1,pos,k,l,mid); }else{change(root<<1|1,pos,k,mid+1,r);}push_up(root);
}
inline void Dijiestra(int s){fill(dis+1,dis+n+1,INT_MAX);build(1,0,n);dis[s]=0ll;for(int i=1;i<=n;++i){ans[s]=dis[s];ll disu=dis[s];change(1,s,ll(INT_MAX)+1ll,1,n);for(int j=head[s];j;j=edge[j].nxt){int to=edge[j].to;if(dis[to]<=INT_MAX&&dis[to]>dis[s]+edge[j].dis){dis[to]=dis[s]+edge[j].dis;change(1,to,dis[to],1,n);}}s=pos[1];}
}
int main(){scanf("%d %d %d",&n,&m,&c);while(m--){int x,y;ll dis;scanf("%d %d %lld",&x,&y,&dis);addedge(x,y,dis);}for(int i=0;i<=n;++i){for(int j=1;j<=n;j<<=1){if((i^j)<=n){addedge(i,i^j,1ll*j*c);}}}scanf("%d %d",&s,&t);Dijiestra(s);printf("%d\n",ans[t]);return 0;
}

堆优化的也放上:

#include<bits/stdc++.h>
#define MAXN 1000001
#define MAXM MAXN*22
using namespace std;
typedef long long ll;
struct node{int nxt,to;ll dis;
}edge[MAXM];
int cnt,head[MAXN],dis[MAXN];
inline void addedge(int x,int y,ll dis){edge[++cnt].to=y;edge[cnt].nxt=head[x];edge[cnt].dis=dis;head[x]=cnt;
}
inline void Dijiestra(int s){memset(dis,0x3f,sizeof(dis));priority_queue<pair<ll,int>,vector<pair<ll,int> >,greater<pair<ll,int> > > q;dis[s]=0ll;q.push(make_pair(0ll,s));while(!q.empty()){int front=q.top().second;q.pop();for(int i=head[front];i;i=edge[i].nxt){int to=edge[i].to;if(dis[to]>dis[front]+edge[i].dis){dis[to]=dis[front]+edge[i].dis;q.push(make_pair(dis[to],to));}}}
}
int main(){int n,m,c;scanf("%d %d %d",&n,&m,&c);while(m--){int x,y;ll dis;scanf("%d %d %lld",&x,&y,&dis);addedge(x,y,dis);}for(int i=0;i<=n;++i){for(int j=1;j<=n;j<<=1){if((i^j)<=n){addedge(i,i^j,1ll*j*c);}}}int s,t;scanf("%d %d",&s,&t);Dijiestra(s);printf("%lld",dis[t]);return 0;
}

树上问题

最小生成树

Kruskal

Kruskal 的本质是不断地加边,直到整个图连通。由于最小的概念,所以要从边权小的开始加,用并查集维护两个点当前能不能够互相到达。时间复杂度 $m(\log m+\log n)$。

struct node{int from,to;ll dis;
}edge[MAXM<<1];//由于不需要遍历点的下一条边,所以可以不需要 nxt 和 head 
int cnt,fa[MAXN];
inline void addedge(int from,int to,ll dis){edge[++cnt].from=from;edge[cnt].to=to;edge[cnt].dis=dis;
}
inline bool cmp(node x,node y){return x.dis<y.dis;
}
inline void prework(){for(int i=1;i<MAXN;++i){fa[i]=i;}
}
int get(int x){if(fa[x]==x){return x;}return fa[x]=get(fa[x]);
}
inline void merge(int x,int y){fa[get(x)]=get(y);
}
inline ll Kruskal(){ll ans=0;prework();//初始化 sort(edge+1,edge+1+cnt,cmp);//从小到大 for(int i=1;i<=cnt;++i){//遍历每一条边 int u=edge[i].from,v=edge[i].to;if(get(u)!=get(v)){//两个节点需要加边 merge(u,v);ans+=edge[i].dis;}}return ans;
}

Prim

Prim 的本质是加点。每一次加上距离最小的点,然后更新。可以使用类似于 Dijiestra 的优化。

struct node{int from,to,nxt;ll dis;
}edge[MAXM<<1];
int n,cnt,head[MAXN];
ll dis[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,ll dis){edge[++cnt].from=from;edge[cnt].to=to;edge[cnt].dis=dis;edge[cnt].nxt=head[from];head[from]=cnt;
}
inline ll Prim(){memset(dis,0x3f,sizeof(dis));memset(vis,0,sizeof(vis));priority_queue<pair<ll,int> > q;q.push(make_pair(0ll,1));//通常从 1 开始 dis[1]=0ll;ll ans=0;for(int i=1;i<n&&!q.empty();++i){int front=q.top().second;q.pop();if(vis[front]){--i;continue;}vis[front]=true;ans+=dis[front];//当前的 disfor(int i=head[front];i;i=edge[i].nxt){int to=edge[i].to;if(dis[to]>edge[i].dis){//从三角形不等式变成了介个样子 dis[to]=edge[i].dis;q.push(make_pair(dis[to],to));}} }return ans;
}

通常来讲,Prim 的时间复杂度为 $(n+m)\log m$,Kruskal 的时间复杂度为 $m(\log n+\log m)$,Prim 应用于稠密图,Kruskal 应用于稀疏图。但是 Prim 在稠密图上只是理论比 Kruskal 更优,因为 Kruskal 有玄学复杂度的并查集,时快时慢,真正也没见得比 Kruskal 优。所以,通常我们使用 Kruskal。

LCA

LCA 是一个经典的例题,可以用于树论,也可以用于图论。通常有两种算法:倍增算法和 Tarjan。

倍增

倍增的方式是预处理出 $fa_{i,j}$ 表示 $i$ 向上跳 $2^j$ 步所到达的节点。这个可以使用 dfs 预处理,时间复杂度 $n\log n$。然后单次询问让更深的节点通过倍增能够组合成任何数的性质跳到同一层,然后不断往上跳,跳到相同为止。时间复杂度 $\log n$,总时间复杂度 $(n+m)\log n$。

struct node{int to,nxt;
}edge[MAXM<<1];
int n,cnt,head[MAXN]; 
int dep[MAXN],fa[MAXN][MAXK];
inline void addedge(int x,int y){edge[++cnt].to=y;edge[cnt].nxt=head[x];head[x]=cnt;
}
void dfs(int now,int pa){fa[now][0]=fa;dep[now]=dep[fa]+1;for(int i=1;i<MAXK;++i){fa[now][i]=fa[fa[now][i-1]][i-1];//更新向上跳 }for(int i=now;i;i=edge[i].nxt){int to=edge[i].to;if(to!=pa){dfs(to,now);//dfs 下一个节点 }}
}
inline int lca(int x,int y){if(dep[x]>dep[y]){swap(x,y);//深的在下面 }for(int i=0;i<MAXK;++i){if(dep[fa[y][i]]>=dep[x]){y=fa[y][i];} } if(x==y){return x;}for(int i=0;i<MAXK;++i){if(fa[x][i]!=fa[y][i]){x=fa[x][i];y=fa[y][i];//一起向上跳 }}return x;
}

Tarjan

Tarjan 是一种离线的算法,要使用并查集来维护。

首先,要接受输入边 InputEdge 和查询边 QueryEdge,并区分开来。QueryEdge 需要反向加入。

用 dfs 遍历。如果遍历到一条节点,使用 $vis$ 记录每个节点有没有访问过,$fa$ 记录祖先节点。

当一个节点还在遍历根节点的时候,就设置父节点为自己,否则设置为不断往上的节点。

如果发现该点访问完了,而且另一个节点也访问完了,那么父节点就是答案。

struct node{int from,to,nxt;
}edge[MAXM<<1];
struct query_node{int from,to,nxt,lca;
}QueryEdge[MAXQ<<1];
int head[MAXN],QueryHead[MAXN];
int cnt,QueryCnt;
int fa[MAXN];
bool vis[MAXN];
inline void prework(){for(int i=1;i<MAXN;++i){fa[i]=i;vis[i]=false;}
}
int get(int x){if(fa[x]==x){return x;}return fa[x]=get(fa[x]);
}
void Tarjan(int u){fa[u]=u;//设置父节点 vis[u]=true;for(int i=head[u];i;i=edge[i].nxt){int v=edge[i].to;if(!vis[v]){Tarjan(v);//递归 fa[to]=u;//设置父节点 }}for(int i=QueryHead[u];i;i=QueryEdge[i].nxt){int v=QueryEdge[i].to;//遍历查询 if(vis[v]){QueryEdge[i-1].lca=QueryEdge[i].lca=get(edge[i].to);}}
}

注意,本 Tarjan 代码复杂度其实还需要套上一个并查集的复杂度,其实有 $\operatorname{O}(n+m)$ 的实现,于此(全英文警告)。

例题

有一些较小边权的边不一定会走过去,考虑用 Kruskal 换种做法,跑最大生成树。

求出这个后,题目转化成求两个点之间路径的最小边权,由于是唯一的,但是直接跑会超时,所以考虑再处理一个 $minv$ 表示最小值,跑 LCA。

#include<bits/stdc++.h>
#define MAXN 10001
#define MAXM 50005
#define MAXK 21
#define INF 2147483647
using namespace std;
struct node{int from,to,next,dis;
}a[MAXM],edge[MAXM<<1];
int n,m,q,cnt,head[MAXN],deep[MAXN],fa[MAXN][MAXK],dis[MAXN][MAXK],fa[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,int dis){edge[++cnt].next=head[from];edge[cnt].from=from;edge[cnt].to=to;edge[cnt].dis=dis;head[from]=cnt;
}
inline bool cmp(const node &x,const node &y){return x.dis>y.dis;
}
inline int get(int x){if(fa[x]==x){return x;}return fa[x]=get(fa[x]);
}
inline void merge(int x,int y){fa[get(x)]=get(y);
}
inline void Kruskal(){sort(a+1,a+m+1,cmp);for(int i=1;i<=n;++i){fa[i]=i;}for(int i=1;i<=m;++i){if(get(a[i].from)!=get(a[i].to)){merge(a[i].from,a[i].to);addedge(a[i].from,a[i].to,a[i].dis);addedge(a[i].to,a[i].from,a[i].dis);}}
}
inline void dfs(int now){vis[now]=true;for(int i=head[now];i;i=edge[i].next){int to=edge[i].to;if(vis[to]){continue;}deep[to]=deep[now]+1;fa[to][0]=now;dis[to][0]=edge[i].dis;dfs(to);}
}
inline int lca(int x,int y){if(get(x)!=get(y)){return -1;}if(deep[x]>deep[y]){swap(x,y);}int ans=INF;for(int i=MAXK-1;i>=0;--i){if(deep[fa[y][i]]>=deep[x]){ans=min(ans,dis[y][i]);y=fa[y][i];}}if(x==y){return ans;}for(int i=MAXK-1;i>=0;--i){if(fa[x][i]!=fa[y][i]){ans=min(ans,min(dis[x][i],dis[y][i]));x=fa[x][i];y=fa[y][i];}}return min(ans,min(dis[x][0],dis[y][0]));
}
int main(){scanf("%d %d",&n,&m);for(int i=1;i<=m;++i){scanf("%d %d %d",&a[i].from,&a[i].to,&a[i].dis);}Kruskal();for(int i=1;i<=n;++i){if(!vis[i]){deep[i]=0;dfs(i);fa[i][0]=i;dis[i][0]=INF;}}for(int i=1;i<MAXK;++i){for(int j=1;j<=n;++j){fa[j][i]=fa[fa[j][i-1]][i-1];dis[j][i]=min(dis[j][i-1],dis[fa[j][i-1]][i-1]);}}scanf("%d",&q);while(q--){int x,y;scanf("%d %d",&x,&y);printf("%d\n",lca(x,y));}return 0;
}

其他

拓扑排序

拓扑排序是基于拓扑序进行的排序。比如 $u\to v$ 有一条边,那么则称 $u$ 的拓扑序大于 $v$。

拓扑排序的思想是找出入度为 $0$ 的点,然后将这个点删除,再去找入度为 $0$ 的点,直到没有入度为 $0$ 的点(有环)或者图空了(没环)。

struct node{int to,nxt;
}edge[MAXM<<1];
int cnt,head[MAXN],indeg[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,int dis){edge[cnt].to=to;edge[++cnt].nxt=head[from];head[from]=cnt;
}
inline bool Topusort(){queue<int> q;for(int i=1;i<MAXN;++i){if(!indeg[i]){q.push(i);//加入入度为 0 的点 }}while(!q.empty()){int front=q.front();q.pop();for(int i=head[front];i;i=edge[i].nxt){//遍历相关的点 int to=edge[i].to;--indeg[to];if(!indeg[to]){q.push(to);//加入入度为 0 的点 }}}for(int i=1;i<MAXN;++i){if(indeg[i]){return true;//如果还有,那就有环 }}return false;//没有环 
}

例题

发现停靠了的节点的等级一定大于其他没有停靠的节点,可以把这个等级看作是拓扑序,较优先的节点向靠后的节点连边,然后跑 Topu 即可。

#include<iostream>
#include<vector>
#include<queue>
#define MAXN 1001
using namespace std;
int n,m,a[MAXN],indeg[MAXN];
vector<int> G[MAXN];
bool uni[MAXN],vis[MAXN][MAXN];
inline void addedge(int from,int to){G[from].push_back(to);++indeg[to];vis[from][to]=true;
}
inline int Topusort(){queue<pair<int,int> > q;int ans=1;for(int i=1;i<=n;++i){if(!indeg[i]){q.push(make_pair(i,1));}}while(!q.empty()){int from=q.front().first;int step=q.front().second;q.pop();for(int i=0;i<G[from].size();++i){int to=G[from][i];--indeg[to];
//			ans=max(ans,step+1);if(!indeg[to]){q.push(make_pair(to,step+1));ans=max(ans,step+1);}
//			cout<<to<<" "<<step+1<<endl;}}return ans;
}
int main(){scanf("%d %d",&n,&m);while(m--){int k;scanf("%d",&k);for(int i=1;i<=k;++i){scanf("%d",&a[i]);uni[a[i]]=true;}for(int i=a[1];i<=a[k];++i){if(!uni[i]){for(int j=1;j<=k;++j){if(!vis[a[j]][i]){addedge(a[j],i);}}}}for(int i=1;i<=k;++i){uni[a[i]]=false;}}printf("%d",Topusort());return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/801633.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电力煤矿液体泄漏识别系统

电力煤矿液体泄漏识别系统对电力煤矿危化品生产区域管道机械实时检测,当电力煤矿液体泄漏识别系统检测到机械管道出现液体泄漏时,系统立即抓拍存档并告警同步回传给报警信息给后台监控人员,让工作人员及时处理,电力煤矿液体泄漏识别系统实现危险区域跑冒滴漏异常自动监控抓…

河道水尺水位监测系统

河道水尺水位监测系统利用计算机视觉技术对河道湖泊水尺水位进行7*24小时全天候实时监测,当河道水尺水位监测系统监测到河道水位异常变化时,系统立即抓拍存档同步回传图片给后台监控平台,提醒后台工作人员及时处理异常情况,避免更大损失的发生。河道水尺水位监测系统适用于…

7-4DeepFM模型

DeepFM继承了Wide&Deep的主体结构,将高低特征进行融合。其主要创新点有2个。一是将Wide部分替换成了 FM结构,以更有效的捕获特征交互interaction;二是FM中的隐向量 和 Deep部分的 embedding 向量共享权重,减少模型复杂性。推荐系统和广告CTR预估主流模型的演化有两条主…

排水口排水识别系统

排水口排水识别系统基于Python基于YOLOv7深度学习的计算机视觉识别检测算法,排水口排水识别系统赋予传统监测系统智能检测能力提升企业污水排放监督管效率,7*24小时不间断准确判断检测场景内的是否出现排水口排水情况,减少后台监控人员的工作量,减少后台漏报误报产生的失误…

作业区域人数超员预警系统

作业区域人数超员预警系统利用现场已有摄像头对生产作业区域进行全天候不间断实时监测,一旦作业区域人数超员预警系统监测到作业区域人数超员时,立即进行抓拍存档并告知后台监控中心人员,提醒后台人员及时处理避免意外情况发生。作业区域人数超员预警系统对人数超员记录截图…

河道采砂船监测识别系统

河道采砂船监测识别系统通过计算机视觉深度学习技术对河道采砂区域进行实时监测,当河道采砂船监测识别系统监测到有采砂船通过停留非法采砂时,立即抓拍存档触发告警,同步回传给后台通知后台人员及时处理。河道采砂船监测识别系统对河道采砂区域进行7*24小时实时监测有效弥补…

仪表盘读数识别系统

仪表读数识别系统利用现场传统监控摄像头对仪表盘刻度数进行7*24小时实时读取,当仪表盘读数识别系统监测到仪表盘数据异常时,立刻推送给后台相关管理人员,工作人员在第一时间到现场进行处理,避免更大的损失发生。同时,将告警截图和视频保存到数据库形成报表。仪表盘读数识…

电力煤矿跑冒滴漏监测系统

电力煤矿跑冒滴漏监测系统基于计算机视觉分析技术,主动识别电力厂区监控视频中管道或者机械是否存在液体泄漏跑冒滴漏行为。如电力煤矿跑冒滴漏监测系统检测到现场出现液体泄漏行为,系统立即抓拍并反馈后台人员及时处理避免发生更大的危险。电力煤矿跑冒滴漏监测系统对工厂机…

校园打架行为识别检测系统

校园打架行为识别检测系统基于python深度学习框架+边缘分析技术,自动对校园监控视频图像信息进行分析识别。校园打架行为识别检测系统利用学校监控对校园、广场等区域进行实时监测,当监测到有人打架斗殴时,系统立即抓拍存档语音提醒,并将打架行为回传给学校监控后台,提醒后…

化工厂跑冒滴漏识别系统

化工厂跑冒滴漏识别系统基于机器视觉深度学习算法,化工厂跑冒滴漏识别系统自动识别监控视频中机械管道是否存在液体泄漏情况发生,当系统检测到液体泄漏时,可以立即抓拍存档告警同步反馈到监控后台人员,及时处理。化工厂跑冒滴漏识别系统 对工厂机械管道液体泄漏情况自动识别…

智能图像水位识别监测系统

智能图像水位识别监测系统利用两岸已有的监控摄像头对河道江河湖泊等区域进行实时监测,当智能图像水位识别监测系统监测到水位刻度尺超标时立即抓拍告警同步回传给后台监控平台方便人员及时处理。水位起伏变化,水利水灾往往系于一线之间。智能图像水位识别监测系统现实意义价…

河道水面漂浮物垃圾识别监测系统

河道水面漂浮物垃圾识别监测系统对河道、湖泊区域进行实时监测,当河道水面漂浮物垃圾识别监测系统监测到湖面有漂浮物或者垃圾时,系统立即抓拍存档同步发给后台人员通知后台工作人员及时清理。河道水面漂浮物垃圾识别监测系统能够全天候不间断实时监测识别河道水面上的垃圾以…