有向无环图
有向无环图指的是一个无回路的有向图。如果有一个非有向无环图,且A点出发向B经C可回到A,形成一个环。将从C到A的边方向改为从A到C,则变成有向无环图。有向无环图的生成树个数等于入度非零的节点的入度积
拓扑排序
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
例题
例1:
思路
- 当且仅当一个点的入度变为零时才需要入队,并不是数据更新一次就要入队(当一个点的入度变为零,即所有它能吃的东西都已经搜索过了,这是它的数值就不会发生变化,就可以入队了。这样保证了队列里的所有数值都不会发生变化)
- 从入度为零的点开始查找,出度为零的点的路径总数和就是答案
例2:
思路:
设从点 x 出发能够到达的点构成的集合是 c(x),从点 x 出发能够到达的点,是从 x 的各个后继节点 y 出发能够到达的点的并集,再加上点 x 自身,先按照拓扑排序算法求出拓扑序,然后按照拓扑序的倒叙进行计算------因为在拓扑序中,任意一条边 (x , y),x 都排在 y 之前。
例3:
题意:
给定几种点之间的顺序,要你按字典序输出所有可能的拓扑排序
例4:
题意:
给你一个有向图,判断他是否有环路
邻接矩阵
适用条件
顶点数目不太大(一般不超过1000)的题目
板子
int n,m,in[maxn],out[maxn],a,b;//n个点,m条边
int mp[maxn][maxn],ar[maxn];//邻接矩阵存有向图
for(int i=1;i<=m;i++){
scanf("%d%d",&a,&b);
mp[a][b]=1;//a->b
out[a]++; in[b]++;
}
void toposort(){
static int cnt=0;
queue<int> q;
for(int i=1;i<=n;i++)
if(in[i]==0) q.push(i);//把所有入度为0的节点(起始点)加入队列
while(!q.empty()){
int a=q.front(); q.pop();
ar[++cnt] = a;//求出拓扑序
for(int k=1;k<=n;k++){//遍历所有从起始点出发的边
if(mp[a][k]==0)
continue;
in[k]--;//令边到达的顶点的入度-1
if(in[k]==0)q.push(k);//当且仅当一个点的入度变为零时才需要入队,并不是数据更新一次就要入队
}
}
}
例1code
#include<bits/stdc++.h>
#define maxn 5010
#define mod 80112002
using namespace std;
int n,m,in[maxn],out[maxn],a,b,f[maxn],ans;
int mp[maxn][maxn];//邻接矩阵存有向图
queue<int> q;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&a,&b);
mp[a][b]=1;
out[a]++; in[b]++;
}
for(int i=1;i<=n;i++){
if(in[i]==0) {
f[i]=1;//每一个起点代表路径的开始
q.push(i);
}
}//把所有入度为0的节点(起始点)加入队列
while(!q.empty()){
int a=q.front(); q.pop();
for(int k=1;k<=n;k++){//遍历所有从起始点出发的边
if(mp[a][k]==0)
continue;
f[k]=(f[k]+f[a])%mod;
in[k]--;//令边到达的顶点的入度-1
if(in[k]==0){//当且仅当一个点的入度变为零时才需要入队,并不是数据更新一次就要入队
if(out[k]==0){//发现终点
ans=(ans+f[k])%mod;
continue;
}
q.push(k);
}
}
}
printf("%d",ans);
return 0;
}
例3code
#include<iostream>
#include<cstdio>
#include<map>
#include<cstring>
#include<algorithm>
using namespace std;
const int ma=50;
char a[ma],b[ma],c[ma];
char d[ma],e[ma];
char mp[ma][ma];//邻接矩阵
map<char,int> m;
void init(){
memset(d,0,sizeof(d));
memset(e,0,sizeof(e));
memset(mp,0,sizeof(mp));
}
void f(int rk,int k){
if(rk==k)
printf("%s\n",e);//搜索完毕
else{
for(int i=0; i<k; i++){
if(d[i]==0){//这个点是起点
d[i]--;
e[rk]=c[i];//储存结果,类似八皇后
for(int j=0; j<k; j++)if(mp[i][j])d[j]--;//令边到达的顶点的入度-1
f(rk+1,k);
d[i]++;
for(int j=0; j<k; j++)if(mp[i][j])d[j]++;//回溯
}
}
}
}
int main(){
while(gets(a)){
init();
int len=strlen(a),k=0;
for(int i=0; i<len; i++){
if(a[i]>='a'&&a[i]<='z'){
c[k++]=a[i];
}
}//分离字母的一种方法
sort(c,c+k);//保证后面字典序输出
for(int i=0; i<k; i++)m[c[i]]=i;//给字母依次编号
gets(b);
len=strlen(b);
for(int i=0; i<len; i+=4){
mp[m[b[i]]][m[b[i+2]]]=1;//有向边
d[m[b[i+2]]]++;//记录点0,1,2,3,…的入度数
}
f(0,k);
printf("\n");
}
}
邻接表(前向星)
邻接表的形式说明
- 邻接表是一个二维容器,第一维描述某个点,第二维描述这个点所对应的边集们
- 实现邻接表的方法绝对有100种以上,即使是前向星这种东西也是邻接表,因为它还是描述某个点和这个点所对应的边集们。
- 第一维是描述点的。可以用vector,list,forward_list,deque,map,multimap,unordered_map,unordered_multimap等(一般不能用set,mutiset,unordered_set,unordered_multiset).按照要求去选择。一般来讲存完图以后不涉及点的加入与删除优先使用vector.map,multimap,unordered_map,unordered_multimap.
- 第二维是描述这个点的边集,可以用全部的容器。同样,一般来讲存完图以后,不涉及点的加入与删除优先使用vector,空间充足可以考虑deque.涉及点的删除用forward_list或者是list,map,multimap,unordered_map,unordered_multimap.
异或(仅科普)
- 异或(xor)是一个数学运算符。它应用于逻辑运算。异或的数学符号为“⊕”,计算机符号为“xor”。其运算法则为:a⊕b = (¬a ∧ b) ∨ (a ∧¬b)
- 如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
成对变换(仅科普)
-
通过计算可以发现,对于非负整数n:
当n为偶数时,n xor 1等于n+1。
当n为奇数时,n xor 1等于n-1。
因此,“0与1”“2与3”“4与5”…关于xor 1运算构成“成对变换”。这一性质经常用于图论邻接表中边集的存储。在具有无向边(双向边)的图中把一对正反方向的边分别存储在邻接表数组的第n与n+1位置(其中n为偶数),就可以通过xor 1的运算获得与当前边(x,y)反向的边(y,x)的存储位置。 -
对于无向图,我们把每条无向边看作两条有向边插入即可。有一个小技巧是,结合“成对变换”的位运算性质,我们可以在程序最开始时,初始化变量tot=1。这样每条无向边看成的两条有向边会成对存储在ver 和edge 数组的下标“2和3” “4和5” “6和7”…的位置上。通过对下标进行xor 1的运算,就可以直接定位到与当前边反向的边。换句话说,如果ver[i]是第i条边的终点,那么ver[i xor 1]就是第i条边的起点。
前向星(重点)
一种数据结构,以储存边的方式来存储图。构造方法如下:读入每条边的信息,将边存放在数组中,把数组中的边按照起点顺序排序(可以使用基数排序等)。通常用在点的数目太多,或两点之间有多条弧的时候。一般在别的数据结构不能使用的时候才考虑用前向星。除了不能直接用起点终点定位以外,前向星几乎是完美的。
前向星(用数组模拟链表的方式存储带权有向图的邻接表结构)code:
//加入有向边(x,y),权值为z
void add(int x,int y,int z){
ver[++tot]=y,edge[tot]=z;//真实数据
next[tot]=head[x],head[x]=tot;//在表头x处插入
}
//访问从x出发的所有边
for(int i=head[x];i;i=next[i]){
int y=ver[i],z=edge[i];
//找到了一条有向边(x,y),权值为z
}
板子
int head[maxn],in[maxn],out[maxn];
struct Edge{
int to,nxt;
}edge[maxe];
void add(int x,int y){
cnt++;
edge[cnt].to=y;
edge[cnt].nxt=head[x];//巧妙之处:将一个顶点的多个边关联起来
head[x]=cnt;//head[x]是以点x为起点的最后输入的边的序号
in[y]++;out[x]++;
}//可以通过边找同源边
for(int i=head[v];i;i=edge[i].nxt){//i是以点v为起点的边的序号
int k=edge[i].to;
in[k]--;
if(in[k]==0) q.push(k);
}
例1code
//通过边找同源边的做法
#include<bits/stdc++.h>
#define maxn 5010
#define maxe 500010
#define mod 80112002
using namespace std;
int n,m,cnt=0;
struct Edge{
int to,nxt;
}edge[maxe];
int head[maxn],in[maxn],out[maxn],sum[maxn];
void add(int x,int y){
cnt++;
edge[cnt].to=y;
edge[cnt].nxt=head[x];//巧妙之处:将一个顶点的多个边关联起来
head[x]=cnt;//head[x]是以点x为起点的最后输入的边的序号
}//可以通过边找同源边
int main(){
scanf("%d%d",&n,&m);
int x,y;//x->y
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
add(x,y);
in[y]++;out[x]++;
}
queue<int> q;
//找到入度为0的点
for(int i=1;i<=n;i++){
if(!in[i]) q.push(i),sum[i]=1;//令到其的路径数量为1
}
//sum数字记录每个点的路径数目
int ans=0;
while(!q.empty()){
int v=q.front(); q.pop();
for(int i=head[v];i;i=edge[i].nxt){//i是以点v为起点的边的序号
int k=edge[i].to;
sum[k]=(sum[k]+sum[v])%mod;
in[k]--;
if(in[k]==0) q.push(k);
}
}//删点时,将需要删除的点的答案 都累加到 它可以到达的点 上面去
for(int i=1;i<=n;i++){
if(out[i]==0)//只记录是极大路径(不能走下去)的数目
ans=(ans+sum[i])%mod;
}
cout<<ans<<endl;
}
例2code
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <queue>
#include <bitset>
#include <algorithm>
using namespace std;
const int maxn = 30000+7;
int head[maxn],ver[maxn],Next[maxn],deg[maxn];
int a[maxn];
int n,m;
bitset<maxn> c[maxn];
void add(int x, int y){
static int tot=0;
ver[++tot] = y;
Next[tot] = head[x];
head[x] = tot;
deg[y]++;//y的入度(degree)
}
void toposort(){
static int cnt=0;
queue<int> q;
for(int i = 1; i <= n; i++)
if(deg[i] == 0) q.push(i);
while(q.size()){
int x = q.front(); q.pop();
a[++cnt] = x;//求出拓扑序
for(int i = head[x]; i; i = Next[i]){
int y = ver[i];
deg[y]--;
if(deg[y] == 0) q.push(y);
}
}
}
void solve(){
int x, y;
for(int i = n; i >= 1; i--){
x = a[i];
c[x][x] = 1;
for(int j = head[x]; j; j = Next[j]) {
int y = ver[j];
c[x] |= c[y];
}
}
}
int main(){
int x, y;
scanf("%d %d", &n, &m);
while(m--){
scanf("%d %d", &x, &y);
add(x, y);
}
toposort();
solve();
for(int i = 1; i <= n; i++)
printf("%d\n",c[i].count());//count函数用来求bitset中1的位数
return 0;
}
例4code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <queue>
using namespace std;
const int nmax=110;
int inDegree[nmax];//存每个节点的入度
vector<int>G[nmax];//邻接表
queue<int>q;
int n,m;//点数,边数
bool toposort(){
int num=0;//加到拓扑序列的顶点数
while(!q.empty()) q.pop();
for(int i=0;i<n;i++)
if(inDegree[i]==0)
q.push(i);//把入度为0的顶点扔进队列
while(!q.empty()){
int u=q.front();
q.pop();//删除该点
num++;
for(int i=0;i<G[u].size();i++){//遍历与点u相邻的所有边,删除之。
int v=G[u][i];
inDegree[v]--;
if(inDegree[v]==0)
q.push(v);
}
G[u].clear();//(若无必要可不写)
}
if(num==n) return true;
else return false;//有环路
}
int main(){
while(scanf("%d %d",&n,&m)!=EOF){
if(n==0)
break;
memset(inDegree,0,sizeof(inDegree));
for(int i=0;i<n;i++)G[i].clear();
int u,v;
for(int i=0;i<m;i++){
scanf("%d %d",&u,&v);
G[u].push_back(v);
inDegree[v]++;
}
if(toposort()) puts("YES");
else puts("NO");
}
return 0;
}
备注:本文参考
算法竞赛进阶指南