图
图主要的特点在于一个根可以有任意个子节点,除了会在特殊情况下形成的环形图需要注意是否会造成死循环之外,图的基本操作和使用逻辑与二叉树基本相似;但是由于它相对于二叉树的节点关系的一般性,和它本身自带的复杂性,致使操作它的时间复杂度和空间复杂度都比较差劲。
因此,我们往往不会把它当作工具去解题,更多的时候,图是题目、问题的一个背景,我们得去到图上去解题、去处理各个节点间的复杂关系。
故图的问题,主要是需要把具体的问题抽象在图中,然后以某种方式遍历完这个图,或是求图的最长链、或是找从一个点起始所能到达的某些特殊点,从而完成解题。
表示、存储
-
相较于二叉树,图有两个痛点需要解决:
-
根节点所连子节点数量的不确定性。
这个特性导致用普通数组无法很便利的存储,因为数组每个维度的长度是固定的,以最多的节点去初始化数组会导致有大量的空间被浪费。
-
图中,所有点和点之间都有可能有联系。
i/j 1 2 1 有 无 2 无 有 若以“存储每两个点之间有无通路”为思路进行存储,则需要max(边长)的平方的空间存储,”相连“和”不相连“都需要在这个正方形的二维大图中被一一记录,大量空间被浪费。
-
节点间是有特定方向的。
在二叉树中,所有节点间的关系都是相同的,而图中的节点关系不同,在节点间的通路上,两节点间单、双向通路都可能存在,故在存储的时候需要表示方向。常用的方法是通过数组的维度来表示节点间的方向。例如如果采用2中的方法,要想存储有向图,则还需要另外开一个j/i数组来表示j→i的方向。
-
解决方法:
故我们不采用”记录每个点之间的关系”这种记录思想,而是与二叉树类似,采用“记录根节点的子节点”这种思想去记录图。
而对于空间浪费的问题,我们采用可变长度的数组vector进行维护,一个节点每能到一个方向,表示该节点的数组长度加一并存储。
于是就得到了pic[N][x]这样的存储方式,N表示节点名,而长度为x的pic[N]里面,存储的便是节点N能够达到的那x个节点名。如果节点有多个性质需要存储(例:节点的权值、节点的入度与出度),则可用多个辅助数组在遍历到的时候就去维护,或者直接创建结构体去存储这些性质。
图中的题目
-
常规遍历
-
遍历的一些延伸,如从某点出发,找到某种要求的点。
-
搜索是完成1 、2操作的最直接工具,且只有搜索能够较便捷和快速的完成;目前(我学的)没有什么复杂度明显优势的算法去完成图的遍历。
-
但是,需要注意的是,图中是可能出现环状的。遇到这种情况,大部分时候,是可以根据题目含义,重新改变图或搜索的含义与方向,仍然靠搜索完成。
优化方法:-
改变搜索方向或含义,让每个节点只需到达一次就能完成操作(变向运用记忆,全局都只用搜索一次,单次自然就只会访问一次嘛)。
-
通过再开一个数组的方法,去记录单次深搜所到达过的节点,从而避免在单次搜索中的重复访问呢。
-
-
-
图的长度
因为不知道最长的链的终点是哪个节点,所以,需要分解为求每个节点最长的链的长度(max(子节点1的长度,子节点2的长度……),满足递归形式,可用递归求解),然后比较更新出链最长的点。
代码示例:
int dfs(int x) //节点名 该搜索采用数组a进行记忆化记录,图数组为pic[n][m] { //a[x]表示节点x的链长 if(a[x]!=0) return a[x];//搜索过直接返回 int ans=0; for(int i=0;i<pic[x].size();i++) { a[x]=max(ans,dfs(pic[x][i])); } return a[x]+1; //链长需要加上这个节点本身 }
-
求图中链的个数
例题:
给出一个食物网,求其中食物链的个数。要求答案对80112002取模。
不难发现,这其时就是给出一个无环图,要求你去求其中链的个数。
”找链的个数“虽然也是记忆化搜索的应用,但它需要注意与记录节点的更多属性,如入度、出度,才能完成对于一条链的划定。
关键:
-
什么是链?
-
如何划分“什么是一条链”?
-
链上的开始节点与结束节点有什么性质或特点?
-
用代码回答完上述三个问题,便是最终的答案。
其中,在寻找链时,需要分解问题。
- 不难发现,对于每个节点来说,寻找链的过程是一个可重复的递归过程,我们可以采用采用数组去记录到达该节点的链的个数、到入度为0的起始点就停止去完成搜索递归,最后去加上所有出 度为0的节点算得的链的个数。
代码示例:
#include<bits/stdc++.h>
using namespace std;
#define mod 80112002
int n,m;
vector<int> pic[10005];
int last[10005],nxt[10005];
int bgin[10005],ed[10005];
long ans[10005];
long dfs(int x)
{
if(ans[x]!=0) return ans[x];
for(int i=0;i<pic[x].size();i++) //vector数组是从0开始计数的,人无法干预
{
int a=pic[x][i];
ans[x]+=dfs(a);
ans[x]%=mod;
}
return ans[x];
}
main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++) //m种捕食关系
{
int a,b;
scanf("%d%d",&a,&b);
if(last[a]==0) //这两个判断相当于是默认的初始化,如果已知的确切关系,没有否定这个初始化,它才能保留初始化。故需要判断这个点有没有被记录过
last[a]=-1; //表示该节点存在,且无入度 即生产者
if(nxt[b]==0)//同上
nxt[b]=-1; //表示该节点存在,且无出度 即顶级消费者
last[b]=1; //有能量流入b 即有入度 (这是依据本次输入确切判断的,)
nxt[a]=1; //a的能量有流出 即有出度
pic[b].push_back(a);
}
int nn=0,mm=0;
for(int i=1;i<=n;i++) //对n种动物枚举,看它们是否有入度或出度
if(last[i]==-1) //没有入度
{
bgin[nn++]=i; //记录该生产者
ans[i]=1;
}
else if(nxt[i]==-1) //没有出度
ed[mm++]=i; //记录顶级消费者
for(int i=0;i<mm;i++)
dfs(ed[i]); //从所有入度为0的点开始找链
long sum=0;
for(int i=0;i<mm;i++) //把所有出度为0的点记录的链数加起来
{
// printf("%d ",ed[i]);
int a=ed[i];
sum+=ans[a];
sum%=mod;
}
printf("%ld\n",sum);
// for(int i=0;i<=n;i++)
// printf("%ld",ans[i]);
}
原来写的笔记是md文件,弄上来之后格式有点抽抽,我也不知这csdn的编辑器怎么玩,知识浅薄只能随便改一下将就着这样了。