关于图的遍历的杂谈
为洛谷训练场疯狂打call
Noip中图的遍历是基础内容,总结一下图的遍历的技巧与基础算法:
P2661
题目大意:已知图大小,且所有点出度为1,求最小环大小;
题解:这是所有题里唯一水的一道,想怎么搞怎么搞,dfs,要是不嫌大材小用tarjan都可以写,这里有一种思路比较奇怪的解法:用并查集维护一个联通块,不断更新,如果两点已经在一个联通块里,那么说明成环了,暴力找一下环大小就好了,递归都不用。
#include<cstdio>
#include<iostream>
#include<queue>
using namespace std;
int a[200100],fa[200100];
queue<int>q;
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=n;i++){
int X=find(i),Y=find(a[i]);
if(X!=Y){
fa[X]=Y;
}
else{
q.push(i);
}
}
int ans=0x7f7f7f7f;
while(!q.empty()){
int now=q.front();q.pop();
int will,n_ans=0,n_now=now;
do{
will=a[n_now];n_ans++;n_now=will;
}while(will!=now);
ans=min(ans,n_ans);
}
printf("%d",ans);
return 0;
}
P1330
题目大意:求一个大小最少的点集使得其余的点不能联通,且点集中的任意两点不能相连。
题解:遍历技巧——染色。
把问题转化一下,就是求最小染色树将图染成黑白两色且同色不相邻,同色图分别不连通。我们可以轻松地发现只要染成黑白两色图,那么同色图一定不联通。并且要求最少的点集,那么取黑色或白色中任意一个点集大小就好,我们可以发现一定有关于一条边的两端所连得两个点一定是一黑一白,那么由于黑白是相对的,我们可以按照规则从任意起点染色,如果染色失败,就无解(只有可能存在一种染色方案,是由于对一个点和它相邻的点一定与它颜色相反,推广到全图易证)(脑补一下感性理解),所以直接dfs即可。
总结一下,这种题目由于有明确的染色规则,就可以染色来解决,是一种好用的技巧。
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int ret=0;char c=getchar();
while(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') ret=(ret<<1)+(ret<<3)+c-'0',c=getchar();
return ret;
}
const int maxn=1e5+10;
const int mann=1e4+10;
struct Edge{
int to,nxt;
}e[maxn*2];
short vis[mann];
int n,m,head[mann],cnt,sum[2];
inline void add(int x,int y)
{
cnt++;e[cnt].to=y;
e[cnt].nxt=head[x],head[x]=cnt;
}
bool dfs(int now,int col)
{
if(vis[now]!=-1){
if(vis[now]==col) return true;
else return false;
}
vis[now]=col;sum[col]++;
for(int i=head[now];i;i=e[i].nxt){
int to=e[i].to;
if(!dfs(to,col^1)) return false;
}
return true;
}
int main()
{
int x,y,ans=0;
n=read(),m=read();
for(int i=1;i<=m;i++){
x=read(),y=read();
add(x,y),add(y,x);
}
memset(vis,-1,sizeof(vis));
for(int i=1;i<=n;i++)if(vis[i]==-1){
if(!dfs(i,1)){
printf("Impossible");
return 0;
}
else ans+=min(sum[0],sum[1]);
sum[1]=sum[0]=0;
}
printf("%d",ans);
return 0;
}
P1341
题目大意:给定要求相邻的字母对(可以前后交换),求一个串满足在序列中前面要求的字母队相邻,要求字典序最小。
把字母转化成点,建图后就是要求一种遍历顺序满足经过所有边,那么这就是欧拉回路或者欧拉通路,那么我们求该路就好,要求字典序最小我们用邻接表或者邻接矩阵建图,优先向小边转移即可。
重点说一下概念性的东西:
1、欧拉回路\欧拉通路:都要求经过每一条边,一个要求回到起点,一个不要求,对于无向图那么对于欧拉回路,每个点的度都是偶数才有解,而欧拉通路要求有且只有两个点度是奇数,分别为起点或终点。对于有向图对于欧拉回路的要求是出度=入度,而欧拉通路是有且仅有两个点出度!=入度,且起点出-入=1,重点入-出=1。
2、对于欧拉回路\通路的计算我们有神奇的Hierholzer算法,大致思路是每遍历一个边删一个边,易证最后得到的一定是一个欧拉回路(脑补脑补),代码也很好实现。
最后实现中的细节:注意对于起点的选择,欧拉通路起点只有两个能选。
#include<bits/stdc++.h>
using namespace std;
const int maxn=100+1;
//vector<int>e[maxn];
int n,deg[maxn];
bool mp[maxn][maxn];
string s;stack<int>st;
bool dfs(int now)
{
for(int i=1;i<maxn;i++){
if(mp[now][i]){
mp[now][i]=false;
mp[i][now]=false;
dfs(i);
}
}
st.push(now);
}
int main()
{
int cnt=0,mem=0;
//freopen("1.txt","r",stdin);
scanf("%d",&n);
for(int i=1;i<=n;i++){
cin>>s;
//e[s[0]-'A'+1].push_back(s[1]-'A'+1);
//e[s[1]-'A'+1].push_back(s[0]-'A'+1);
mp[s[1]-'A'+1][s[0]-'A'+1]=1;mp[s[0]-'A'+1][s[1]-'A'+1]=1;
deg[s[0]-'A'+1]++;deg[s[1]-'A'+1]++;
}
for(int i=1;i<=maxn;i++)
if((deg[i]%2)){
cnt++; if(!mem)mem=i;
}
if(cnt!=2&&cnt!=0){
printf("No Solution");return 0;
}
/*for(int i=1;i<maxn;i++)
if(deg[i])
sort(e[i].begin(),e[i].end());*/
if(!mem)
for(int i=1;i<=maxn;i++)
if(deg[i]){
mem=i;break;
}
dfs(mem);
while(!st.empty()){
printf("%c",st.top()+'A'-1);
st.pop();
}
return 0;
}
P2921
题目大意:给定一个图,所有点出度为1,求对于从每个点开始遍历找到已经遍历过的点的最短距离。(点数1e5)
题解:求一个点到环的距离+环的长度。Tarjan统计一下环,然后暴力就好了,主要锻炼代码能力。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
struct Edge{
int to,nxt,val;
}e[maxn];
inline int read()
{
int ret=0;char c=getchar();
while(c>'9'||c<'0') c=getchar();
while(c>='0'&&c<='9') ret=(ret<<1)+(ret<<3)+c-'0',c=getchar();
return ret;
}
bool vis[maxn];stack<int>s;
int n,head[maxn],cnt,link[maxn],size[maxn];
int dfn[maxn],low[maxn],num,ans[maxn];
vector<int>tar[maxn];
void tarjan(int now)
{
cnt++;dfn[now]=low[now]=cnt;
vis[now]=true;s.push(now);
for(int i=head[now];i;i=e[i].nxt){
int to=e[i].to;
if(!dfn[to]){
tarjan(to);
low[now]=min(low[now],low[to]);
}
if(vis[to]){
low[now]=min(low[now],dfn[to]);
}
}
if(dfn[now]==low[now]){
num++;int bef,siz=0;
do{
bef=s.top(),s.pop();
tar[num].push_back(bef);
vis[bef]=false;
link[bef]=num;
siz++;
}while(bef!=now);
size[num]=siz;
}
}
inline void add(int x,int y)
{
cnt++;e[cnt].nxt=head[x];
e[cnt].to=y,head[x]=cnt;
}
int dfs(int now)
{
if(ans[now]) return ans[now]+1;
if(size[link[now]]!=1){
ans[now]=size[link[now]];
for(int i=0;i<tar[link[now]].size();i++){
ans[tar[link[now]][i]]=size[link[now]];
}
return size[link[now]]+1;
}
for(int i=head[now];i;i=e[i].nxt){
int to=e[i].to;
if(to==now) {
ans[now]=1;
return ans[now]+1;
}
ans[now]=dfs(to);
}
return ans[now]+1;
}
int main()
{
//freopen("1.in","r",stdin);
//freopen("1.out","w",stdout);
int x;
n=read();
for(int i=1;i<=n;i++)
x=read(),add(i,x);
cnt=0;
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++)
if(!ans[i]) dfs(i);
int now=18;
/*while(1){
if(size[link[now]]==1)
printf("%d\n",now);
else{
printf("%d\n",size[link[now]]);break;
}
now=e[head[now]].to;
if(now==0) break;
}*/
for(int i=1;i<=n;i++)
printf("%d\n",ans[i]);
return 0;
}
这几种图的遍历方式都很经典,需要仔细体会,能够熟练运用。