图
图是由顶点和边组成,每条边的2端都必须是图的顶点(可以是相同的顶点)。而图G(V,E)表示图G的顶点集合V,边集E。
一般来说,图可以分为有向图和无向图。有向图的所有边都有方向,即确定了顶点到顶点的一个指向;而无向图的所有边都是双向的,即无向边所连接的2个顶点可以相互到达。
顶点的度是指和该顶点相连的边的个数。特别对于有向图来说,顶点的出边条数称为顶点的出度,顶点的入边条数称为顶点的入度。
顶点和边都可以有一定属性,而量化的属性是权值,顶点的权值和边的权值分别为点权和边权。
图的存储
一般来说,图的存储分为2种:邻接矩阵和邻接表。
邻接矩阵
设图G(V,E)的顶点标号为0,1,2,......N-1,那么可以令二维数组G[N][N]的二维分别表示图的顶点编号,如果G[i][j]为1,说明顶点i和顶点j之间有边;如果为0,说明顶点之间不存在边,而这个二维数组称为邻接矩阵。另外如果存在边权,则G[i][j]可以存放边权,对不存在的边可以设边权为0,-1或一个很大的数。
虽然邻接矩阵比较好写,但是由于需要开辟一个二维数组,如果顶点数目太大,便可能会超出题目限制的内存,因此邻接矩阵只适用于顶点数目不太大(一般不超过1000)的题目。
邻接表
设图G(V,E)的顶点编号为0,1,2,....N-1,每个顶点都可以有若干条边,如果把同一个顶点的所有出边放在一个列表中,那么N个顶点就会有N个列表(如果没有出边,对应空表)这N个列表对应于图G的邻接表,称为adj[N],其中adj[i]存放顶点i的所有出边组成的列表,这样adj[0],adj[1],adj[2].....adj[N-1]都是列表。列表可以用链表实现。另一种简单的表示方式是vector。
vector有变长数组之称,因此可以开辟一个vector数组adj[N],其中N为顶点个数。这样每个adj[i]都是一个变长数组vector,使得存储空间都只与边的数目有关。
如果邻接表只存放每条边的终点编号,而不存放边权,则vector中的元素类型可以直接为int型,如下所示:
vector<int> adj[N];
如果想添加一条从1号顶点到3号顶点的有向边,只需要在adj[1]中添加终点编号3而已
adj[1].push_back(3);
如果同时需要存储边的终点编号和边权,可以建立结构体node,用于存放每条边的终点编号和边权。
struct node
{
int v;
int w;
};
这样vector邻接表中的元素类型就是node型
vector<node> adj[N];
如果想要添加从1号到3号顶点的有向边,边权为4,可以这样:
node temp;
temp.v=3;
temp.w=4;
adj[1].push_back(temp);
当然更快的做法是定义结构体node的构造函数
struct node
{
int v,w;
node(int _v,int _w):v(_v),w(_w) {} //构造函数
};
这样就可以不定义临时变量来进行加边操作:
adj[1].push_back(node(3,4));
于是可以使用vector来很方便地实现邻接表,在一些顶点数目很大时(一般在1000以上),都需要使用邻接表而非邻接矩阵来存储图。
图的遍历
图的遍历是指对图中所有顶点按照一定顺序进行访问,遍历一般有2种,DFS和BFS,分别为深度优先遍历和广度优先遍历。
深度优先遍历(DFS)
深度优先遍历以深度作为第一搜索词,每次总是沿着路径到不能再前进时才退回到路径上离当前顶点最近的还存在未访问顶点的岔道口。
连通分量
在无向图中,如果2个顶点之间可以互相到达(可以是通过一定路径间接到达),那么就称这2个顶点联通。如果图G(V,E)的任意2点都联通,则称图是连通图,否则称图为非连通图,且其中的极大联通子图为联通分量。
强联通分量
在有向图中,如果2个顶点可以各自通过一条有向路径到达另一个顶点,就称这2个顶点强连通。如果在有向图中,任意2点都强连通,则称图为强连通图;否则,称图为非强连通图。且称其中的极大强联通子图为强联通分量。
把联通分量和强联通分量统称为联通块。
如果要对整个图进行遍历,就需要对所有联通块进行遍历。
所以DFS对图进行遍历的基本思路就是,将经过的点设置为已访问,在下次递归访问时就不再处理它,直到图中所有顶点都已被访问。
图深度优先遍历的伪代码
DFS(u) //访问顶点u
{
vis[u]=true; //设置为已访问
for(从u出发可以到达的所有顶点v)
{
if(vis[v]==false) //如果未被访问
DFS(v); //递归访问v
}
}
DFSTrave(G) //遍历图
{
for(G的所有顶点u) //对G的所有顶点u
{
if(vis[u]==false) //如果未被访问
DFS(u); //访问u所在的联通块
}
}
邻接矩阵版本
const int maxn=1000; //最大顶点数
const int INF=1000000000; //设置为一个很大的数
int n,G[maxn][maxn]; //n为顶点数
bool vis[maxn]={false}; //初始化为未访问
void DFS(int u,int depth)
{
vis[u]=true;
for(int v=0;v<n;v++)
{
if(vis[v]==false&&G[u][v]!=INF) //如果v未被访问且u可以到达v
{
DFS(v,depth+1);
}
}
}
void DFSTrave()
{
for(int u=0;u<n;u++)
{
if(vis[u]==false)
DFS(u,1); //访问u和u所在的联通块,1表示初始为第一层
}
}
邻接表版本
const int maxn=1000;
const int INF=10000000;
int n;
vector<int> adj[maxn];
bool vis[maxn]={false};
void DFS(int u,int depth)
{
vis[u]=true;
for(int i=0;i<adj[u].size();i++)
{
int v=adj[u][i];
if(vis[v]==false)
{
DFS(v,depth+1);
}
}
}
void DFSTrave()
{
for(int u=0;u<n;u++)
{
if(vis[u]==false)
DFS(u,1);
}
}
案例1
给出若干人之间的通话长度(视为无向边),这些通话把他们分为若干组。每个组的总边权设为该组内的所有通话的长度之和,而每个人的点权设为该人参与的通话长度之和。现在给定一个阈值K,只要一个组的总边权超过K,而且满足人数超过2,则将该组视为犯罪团伙,而组内点权最大的人视为头目。要求输出犯罪团伙的个数,并按照头目姓名字典序从小到大的顺序输出每个犯罪团伙的头目姓名和成员人数。
输入格式:第一行输入2个整数N和K(小于等于1000),分别表示通话记录的条数以及阈值。接着是K行通话记录,每一行包括2个名字(名字用字符表示)和一个数字通话时长(不超过1000)
输出格式:第一行是犯罪团伙的个数(m),那么接下来的m行分别都输出犯罪头目的名称和犯罪团伙人数。
输入:
8 59
A B 10
B A 20
A C 40
D E 5
E D 70
F G 30
G H 20
H F 10
输出:
2
A 3
G 3
1 首先需要解决姓名(字符串)和编号的对应问题。我们可以使用map<string,int>建立字符串和整形的映射关系。
2 根据题目要求,需要获得每个人的点权,显然可以在读入数据时候进行相关操作处理
3 进行图的遍历,使用DFS遍历每个联通块,目的是获得每个联通块的头目,成员个数,总边权。对单个联通块的遍历逻辑如下:
//访问单个联通快,nowvis为要访问的编号,head为此时(访问nowvis之前)的头目,numofperson为团伙人数,totalval为总边权
void DFS(int nowvis,int &head,int &numofperson,int &totalval)
{
numofperson++; //成员人数加1
vis[nowvis]=true;
//更换头目
if(weight[nowvis]>weight[head])
head=nowvis;
//枚举所有人
for(int i=0;i<numperson;i++)
{
if(G[nowvis][i]>0) //如果有联系
{
totalval+=G[nowvis][i]; //总边权增加该边权
G[nowvis][i]=G[i][nowvis]=0; //删除这条边,防止回头
if(vis[i]==false) //如果i未被访问,递归访问
DFS(i,head,numofperson,totalval);
}
}
}
4 通过3可以获得联通块的总边权totalvalue,如果totalvalue大于给定的阈值K,且成员人数大于2,说明该联通块是一个犯罪团伙。
我们可以定义map<string,int>来建立头目姓名和成员人数的映射关系,正好由于map中元素自动按照键的从小到大的顺序排序,因此自动满足字典序从小到大顺序输出。
//函数遍历整个图,获取每个联通快的信息
void DFStrave()
{
for(int i=0;i<numperson;i++) //枚举所有人
{
if(vis[i]==false)
{
int head=i,numofperson=0,totalval=0; //头目,成员数,总边权
DFS(i,head,numofperson,totalval); //遍历i所在的联通快
if(numofperson>2&&totalval>k) //判断是否满足为犯罪团伙的条件
//head人数为numofperson
Gang[inttostring[head]]=numofperson;
}
}
}
注意:
(1)由于通话记录条数最多有1000条,这意味着不同的人可能会有2000人,因此数组大小必须在2000以上。
(2)由于每个节点访问后不应该再被访问,但是图中可能会有环,遍历中可能会发现一条边连接已访问节点的情况。此时未了边权不被漏加,可以先累加边权,再去考虑节点递归访问的问题。而这样可能会存在边权重复叠加(对于环的问题),故需要在累加某条边的边权之后,将边删除,以免走回头路,重复计算边权。
#include <cstdio>
#include <iostream>
#include <map>
#include <string>
#include <algorithm>
using namespace std;
const int maxn=2010;
map<int,string> inttostring; //编号到姓名的映射
map<string,int> stringtoint; //姓名到编号的映射
map<string,int> Gang; //head到人数的映射
int G[maxn][maxn]={0}; //邻接矩阵,存储边权
int weight[maxn]; //每个点的点权
int n,k,numperson=0; //通话次数,下限,总人数;
bool vis[maxn]={false}; //存储节点是否已经被访问
int change(string str)
{
if(stringtoint.find(str)!=stringtoint.end())
{
return stringtoint[str];
}
else
{
stringtoint[str]=numperson;
inttostring[numperson]=str;
return numperson++;
}
}
//访问单个联通快,nowvis为要访问的编号,head为此时(访问nowvis之前)的头目,numofperson为团伙人数,totalval为总边权
void DFS(int nowvis,int &head,int &numofperson,int &totalval)
{
numofperson++; //成员人数加1
vis[nowvis]=true;
//更换头目
if(weight[nowvis]>weight[head])
head=nowvis;
//枚举所有人
for(int i=0;i<numperson;i++)
{
if(G[nowvis][i]>0) //如果有联系
{
totalval+=G[nowvis][i]; //总边权增加该边权
G[nowvis][i]=G[i][nowvis]=0; //删除这条边,防止回头
if(vis[i]==false) //如果i未被访问,递归访问
DFS(i,head,numofperson,totalval);
}
}
}
//函数遍历整个图,获取每个联通快的信息
void DFStrave()
{
for(int i=0;i<numperson;i++) //枚举所有人
{
if(vis[i]==false)
{
int head=i,numofperson=0,totalval=0; //头目,成员数,总边权
DFS(i,head,numofperson,totalval); //遍历i所在的联通快
if(numofperson>2&&totalval>k) //判断是否满足为犯罪团伙的条件
//head人数为numofperson
Gang[inttostring[head]]=numofperson;
}
}
}
int main()
{
int w;
string s1,s2;
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
{
cin>>s1>>s2>>w;
//把字符串转换为相应的序号
int id1=change(s1);
int id2=change(s2);
//增加相应的点权
weight[id1]+=w;
weight[id2]+=w;
//添加边权
G[id1][id2]+=w;
G[id2][id1]+=w;
}
//遍历整个图的联通块,获取Gang的信息
DFStrave();
cout<<Gang.size()<<endl; //Gang的个数
map<string,int>::iterator it;
for(it=Gang.begin();it!=Gang.end();it++)
{
cout<<it->first<<" "<<it->second<<endl;
}
return 0;
}
广度优先遍历(BFS)
广度优先遍历以广度作为关键词,每次以扩散的方式向外访问顶点。使用BFS遍历需要使用一个队列,通过反复取出队首顶点,将该顶点可到达的未加入进去队列的顶点全部入队,直到队列为空时遍历结束。
伪代码
BFS(u)
{
queue q;
inq[u]=true;
while(q非空)
{
取出q的队首元素u进行访问;
for(从u出发可达的所有顶点v)
{
if(inq[v]==false)
{
将v入队;
inq[v]=true;
}
}
}
}
BFSTrave(G)
{
for(G的所有顶点u)
{
if(inq[u]==false)
{
BFS(u);
}
}
}
邻接矩阵版
const int maxn=1000;
int n,G[maxn][maxn];
bool inq[maxn]={false};
void BFS(int u)
{
queue<int> q;
q.push(u);
inq[u]=true;
while(!q.empty())
{
int u=q.front();
q.pop()
for(int v=0;v<n;v++)
{
if(inq[v]==false&&G[u][v]!=INF)
{
q.push(v);
inq[v]=true;
}
}
}
}
void BFSTrave()
{
for(int u=0;u<n;u++)
{
if(inq[u]==false)
BFS(u);
}
}
邻接表版
const int maxn=1000;
int n;
vector<int> adj[maxn];
bool inq[maxn]={false};
void BFS(int u)
{
queue<int> q;
q.push(u);
inq[u]=true;
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<adj[u].size();i++)
{
int v=adj[u][i];
if(inq[v]==false)
{
q.push(v);
inq[v]=true;
}
}
}
}
void BFSTrave()
{
for(int u=0;u<n;u++)
{
if(inq[u]==false)
BFS(u);
}
}
有时候,在给出BFS初始点的情况下,可能需要输出该联通块内所有其他顶点的层号。
首先,由于需要存放顶点层号,需要定义结构体node,并在其中存放顶点的编号和层号,如下
struct node
{
int v;
int layer;
};
这时,邻接表中的元素就是node结构体。
vector<node> adj[maxn];
struct node
{
int v;
int layer;
};
const int maxn=1000;
vector<node> adj[maxn];
bool inq[maxn];
void BFS(int s)
{
queue<node> q;
node start;
start.v=s;
start.layer=0;
q.push(strat);
inq[start.v]=true;
while(!q.empty())
{
node topnode=q.front();
q.pop();
int u=topnode.v;
for(int i=0;i<adj[u].size();i++)
{
node next=adj[u][i];
next.layer=topnode.layer+1; //注意在判断是否进队之前,先把层数加1.
if(inq[next.v]==false)
{
q.push(next);
inq[next.v]=true;
}
}
}
}
注意判断是否进入队列之前,先把层数加1,虽然前边的层数可能会错乱,但当前层数是对的。
案例1
在微博中,每个人都可以被若干个其他用户关注,而当该用户发布一条信息时,它的粉丝就可以看到并选择是否转发它,且转发的信息也可以被转发者的粉丝看到再次被转发,但同一用户最多可以转发信息一次(信息的最初发布者不会转发)。现在给出N个用户的关注情况(即他们各自关注了哪些用户)以及一个转发层数上限L,并给出最初发布信息的用户编号,求在转发层数上限内消息最多会被多少用户转发。
输入格式:第一行包括2个正整数N(<=1000)和L(<=6),分别表示用户数目和转发层数上限。假设用户编号为1-N。接下来会有N行输入。每行先给出编号为行数的人关注的人数t,再给出7个数字分别表示关注的人的编号。接下来还有一行输入,第一个数字表示给出的发出初始消息人的个数s,接下来的s个数分别表示发出初始消息的人的编号。
输出格式:输出为s行,每行输出一个数字,即在转发上限内消息被转发的用户数目。
输入:
7 3
3 2 3 4
0
2 5 6
2 3 1
2 3 4
1 4
1 5
2 2 6
输出
4
5
思路:(1)首先考虑如何建图,题目中给出的是用户关注的情况,如果X关注了Y,那么我们应该建立Y-X的有向边,表示Y发布的消息可以传递到X并被X转发。
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;
const int maxv=1010;
struct Node
{
int id; //编号
int layer; //层号
};
vector<Node> adj[maxv]; //邻接表
bool inq[maxv]={false};
int BFS(int s,int L)
{
queue<Node> q;
int numforward=0; //转发数
Node start; //定义起始节点
start.id=s;
start.layer=0;
q.push(start);
inq[s]=true;
while(!q.empty())
{
Node topnode=q.front();
q.pop();
int u=topnode.id;
for(int i=0;i<adj[u].size();i++)
{
Node next=adj[u][i];
next.layer=topnode.layer+1;
if(inq[next.id]==false&&next.layer<=L)
{
q.push(next);
inq[next.id]=true;
numforward++;
}
}
}
return numforward;
}
int main()
{
Node user;
int n,L; //用户数和转发上限
int follownum,followid; //用户关注的人数和关注的id
int k,userid; //求解的个数和初始转发的用户id
scanf("%d%d",&n,&L);
for(int i=1;i<=n;i++)
{
user.id=i;
scanf("%d",&follownum);
for(int j=0;j<follownum;j++)
{
scanf("%d",&followid);
adj[followid].push_back(user);
}
}
scanf("%d",&k);
for(int i=0;i<k;i++)
{
memset(inq,false,sizeof(inq));
scanf("%d",&userid);
int numforward=BFS(userid,L);
printf("%d\n",numforward);
}
return 0;
}
案例2
#include <cstdio>
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
const int maxn=1005;
vector<int> adj[maxn];
int time_is[maxn]; //记录感染病毒的最早时间
bool ans[maxn]; //存储最后实际感染病毒的结果
vector<int> res; //存储满足条件的起始病毒编号
queue<int> q;
int m,n;
int k,t;
int check(int s) //判断以病毒编号s为传播源是否会引起相应的结果
{
for(int i=1;i<=n;i++) //每一次都要初始化
time_is[i]=0;
while(!q.empty()) //清空队列
q.pop();
time_is[s]=1; //把病毒开始的时间调整为第一天
q.push(s);
int now;
while(!q.empty())
{
now=q.front();
q.pop();
if(time_is[now]>t) //自己推导,当队列中感染病毒的时间开始超过限制时,就不再加如队列了,即不再感染了。
break;
for(int i=0;i<adj[now].size();i++)
{
if(time_is[adj[now][i]]==0) //未感染
{
time_is[adj[now][i]]=time_is[now]+1;
q.push(adj[now][i]);
}
}
}
for(int i=1;i<=n;i++)
{
if(ans[i]==0&&time_is[i]!=0)
return 0;
if(ans[i]!=0&&time_is[i]==0)
return 0;
}
return 1;
}
bool cmp_my(int a,int b)
{
return a<b;
}
int main()
{
scanf("%d%d",&n,&m);
int u,v;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
adj[u].push_back(v);
adj[v].push_back(u);
}
scanf("%d%d",&k,&t);
int index;
for(int i=1;i<=k;i++)
{
scanf("%d",&index);
ans[index]=true;
}
for(int i=1;i<=n;i++)
{
if(check(i))
{
res.push_back(i);
}
}
sort(res.begin(),res.end(),cmp_my);
if(res.size()==0)
printf("-1\n");
for(int i=0;i<res.size();i++)
{
if(i==(res.size()-1))
{
printf("%d\n",res[i]);
}
else
printf("%d ",res[i]);
}
return 0;
}
案例3
法一 用图的广度优先遍历法
#include <cstdio>
#include <iostream>
#include <queue>
#include <algorithm>
#include <vector>
const int maxn=200005;
using namespace std;
vector<int> adj[maxn];
queue<int> q;
int n,m,t;
int dis[maxn]={0};
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d",&t);
int num;
for(int j=1;j<=t;j++)
{
scanf("%d",&num);
adj[i+100000].push_back(num); //把公车作为伪站点
adj[num].push_back(i+100000);
}
}
dis[1]=1; //初始化到站1距离为1
q.push(1);
int now;
while(!q.empty())
{
now=q.front();
q.pop();
for(int i=0;i<adj[now].size();i++)
{
if(dis[adj[now][i]]==0) //代表,暂时没有到达该站点的路径,说明在当前距离加1可以保证是最短路径/距离
{
dis[adj[now][i]]=dis[now]+1;
q.push(adj[now][i]);
}
}
}
if(dis[n]==0)
printf("-1\n");
else
printf("%d\n",dis[n]/2); //因为把公交作为虚拟站点,又初始化dis[1]=1,所以需要除以2
return 0;
}
法二 使用动态规划法
注意使用memset函数的不便,利用memset函数置零完全可以,但是置为别的数,就会出现各种错误。
建议:当把数组初始化为某一非零数值时尽量不要使用memset,而使用循环赋值。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int maxn=100005;
const int INF=0x3fffffff;
vector<int> adj[maxn];
int dp[maxn];
int main()
{
int n,m,t,index;
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
scanf("%d",&t);
for(int j=0;j<t;j++)
{
scanf("%d",&index);
dp[j]=index;
}
sort(dp,dp+t); //每辆公车经过的站点按照从小到大顺序排序
for(int f=1;f<t;f++)
{
adj[dp[0]].push_back(dp[f]);
}
}
for(int i=0;i<maxn;i++)
{
dp[i]=INF;
}
dp[1]=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<adj[i].size();j++)
{
dp[adj[i][j]]=min(dp[adj[i][j]],dp[i]+1); //dp[v]为无穷大时,代表点v暂时没有路径可以到达,这时可以在当前路径上加1作为最小路径长度。
}
}
if(dp[n]==INF)
printf("-1\n");
else
printf("%d",dp[n]);
return 0;
}
案例4
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int maxn=1005;
vector<int> adj[maxn];
queue<int> q;
int innum[maxn]={0};
int outnum[maxn]={0};
bool ok[maxn][maxn];
int main()
{
int n,m;
int u,v;
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++) //建图
{
scanf("%d%d",&u,&v);
adj[u].push_back(v);
}
int now;
for(int i=1;i<=n;i++) //枚举每个点,找到这个点所可以到达的所有点
{
q.push(i);
ok[i][i]=1;
while(!q.empty())
{
now=q.front();
q.pop();
for(int j=0;j<adj[now].size();j++)
{
if(ok[i][adj[now][j]]==0)
{
ok[i][adj[now][j]]=1;
q.push(adj[now][j]);
}
}
}
}
for(int i=1;i<=n;i++) //根据ok数组记录每个点可以到达的点数,以及可以到达该点的点数。
{
for(int j=1;j<=n;j++)
{
if(ok[i][j]==1)
{
innum[j]++;
outnum[i]++;
}
}
}
int ans=0;
for(int i=1;i<=n;i++)
{
if(innum[i]>outnum[i])
ans++;
}
printf("%d\n",ans);
return 0;
}