POJ 2230 通过模拟深入了解深搜有向边+链式前向星

17 篇文章 0 订阅

这跟欧拉图有一定区别,不能归类到欧拉图中
题目大意:一个图,要将每条边恰好遍历两遍,而且要以不同的方向,还要回到原点。

另外,在深搜这种递归函数中,最先调用的递归最后结束,最后调用的递归最先结束,类似栈


目录

难点:dfs输出顺序

所以,dfs输出顺序是由实际存储顺序决定的, 实际存储顺序是由输入顺序和数据结构决定的

网上其他的有瑕思想

解题思路

模拟结果

解题+模拟代码 


难点:dfs输出顺序

难点:dfs要倒序输出,也就是说不在要进入下一层dfs之前输出最后打印1,也就是

void dfs(int root) {
	for(int i=hd[root]; i!=-1; i=nxt[i]) {
		if(!vis[i]){
			vis[i]=1;
            printf("%d\n",root);
			dfs(to[i]);
		}		
	}
}
(省略若干代码)
dfs(1)
cout<<"1"<<endl;

而是放到dfs之前输出

void dfs(int root) {
	for(int i=hd[root]; i!=-1; i=nxt[i]) {
		if(!vis[i]){
			vis[i]=1;
			dfs(to[i]);
		}		
	}
    printf("%d\n",root);
}
(省略若干代码)
dfs(1)

是因为链式前向星是倒序存储的,而输入顺序卡死的是同一起点的边的末端从小到大排序,输入途中可以不同起点的输入穿插。

存储是倒序的,那么要想得到正序的输出结果,输出肯定也得倒着来才对!不信的话,你可以倒序输入,再按我给的正序输出的代码,你就会发现运行结果也是正序输出,图我就不放了自行实验

你要是有本事也可以设计一个正序存储的算法,然后正序输出也是正确结果,我没这个本事所以模拟我放不出来,大佬可以教教我

所以,dfs输出顺序是由实际存储顺序决定的, 实际存储顺序是由输入顺序和数据结构决定的


如果你对链式前向星理解不深刻,我的总代码中解题代码不到四十行,剩下的一百多行注释全是模拟过程,你可以通过这些模拟深入了解深搜和链式前向星的实际运行情况,这个模拟报告我就不全部摘出来了,只摘出来一个模拟删除边的简洁版报告

网上其他的有瑕思想

下面思想主要来自北海小龙 ,根据我的实际运行结果(一百多行的注释),他的说法是有问题的,具体请参考70~90行。大家了解一下就行

解题思路:此题实质是建立了一个双向连接的有向图。关键在于理解深搜时从S出发,为什么一定会回到S。可以从反证法角度考虑。 

假设遍历的顺序为S->A1->A2->A3->...->S->...->An,S在深搜过程中被遍历,则S的入度为1,出度为2,入度与出度不相等,与定理相违背。 

所以S一定是深搜的最后一个节点,这里注意理解深搜的最后一个节点的含义,是指S的临界表首先被遍历完,输出S

假设输出S的深搜的顺序是S->A1->A2->A3->...->Ai->S,输出S后回溯遍历S的上一个节点Ai,遍历Ai的邻接表,Ai->Ai+1->Ai+2->...->Ai,又由定理得存在欧拉路必须是节点的入度等于出度,所以下一次Ai的邻接表一定首先被遍历完,输出Ai。同理接着是回溯Ai的上一个节点Ai-1,最后回溯到S,输出S。欧拉路结束。

还有一个思想来自相知无悔 ,根据我的实际运行结果(一百多行的注释),他说的也是有问题的,具体请参考70~90行,邻接表遍历完毕是1-3-2-1-2,输出是1-2-3-2-1,二者关系不大

dfs 虽然简单,但是不好想的,为什么是后面输出,而不是,一开始就输出,这是很大的讲究的:是因为先没有出度的点,一定是先出发的点,所以,这点的顺序是要相反输出的!

所以这题dfs倒序输出的真正原理还是我红字的解释,毕竟正序输出再改变一下输入方式答案也是对的,这就是最好的证明:dfs输出顺序是由实际存储顺序决定的, 实际存储顺序是由输入顺序和数据结构决定的

那么下面我来说一下解题思路


解题思路

这题我们要把无向边转化为有向边。然后用深搜遍历边,dfs参数是节点,节点就是边的起点,每遍历一条边就删除一条边, 把所有的边(输入M条,实际2M条,数组记得开二倍哦)遍历完毕后,如果是链式前向星存储,那么直接倒序输出就行,不需要额外输出起点1。难点见上,模拟见下。


模拟结果

代码中的注释有我模拟的结果 ,可以作为了解dfs具体运作的素材,下面还有一份简介模拟报告

模拟:由于是遍历边,所以删除栏都是边,按节点dfs,删除的是边,大括号是dfs递归层次:

每一个左括号代表深搜开始,要删除一条有向边;

每一个右括号代表离它最近的没有与右括号结合的左括号输出后深搜完毕。

(1,2)(2,1) (2,4)(4,2) (1,3)(3,1) (3,5)(5,3)
             1
      ↙↗  ↘↖
       2        3
      ↓↑       ↓↑
       4        5
给边从左到右,从上到下依次命名曰a,b,c,d,e,f,g,h

dfs栏1{2{_1{3{5{_3{__1} }  }  }  } 4{_2 }  }  }  }

删除栏 a   b  c g h      d               e   f  
输出栏                          1 3 5 3 1         2 4 2 1 
又由于我们是同一起点的边末端从小到大输入,链式前向星是从大到小倒序储存,我们上面的dfs模拟是从小到大模拟的,就是先走左边再走右边
所以实际代码链式前向星的流程应该反过来 ,先走右边再走左边 那么就是1 2 4 2 1 3 5 3 1 

这也是POJ为何能够有唯一答案的原因,因为他们输入的是唯一顺序:同一起点的边末端从小到大输入,不同的边输入可以交叉 所以只要知道这一输入规则设置相应算法就能得到唯一答案


解题+模拟代码 

#include<algorithm>//唯一性是很重要的,因为链式前向量输入顺序不同,存储顺序就不同,搜索结果也不同,所以题目的输入一定是有规律的		
#include<iostream>//比如从小到大,不然这题答案不唯一! 其他用链式前向量的题也是一样,所以不要去追求顺序不同路径还相同,那不可能,同一个起点按照正常顺序输入即可 
#include<cstring>
#include<cstdio>
using namespace std;
const int maxn=100000+5,maxm=500000+5;
int n,m;
int hd[maxn*2],to[maxm*2],nxt[maxm*2],vis[maxm*2],cnt;//一定要*2
void add(int x,int y) {
//	printf("from %d ",x);
	to[cnt]=y;
//	printf("to[%d]=%d	",cnt,to[cnt]);
	vis[cnt]=0;
//	printf("vis[%d]=%d	",cnt,vis[cnt]);
	nxt[cnt]=hd[x];
//	printf("nxt[%d]=%d	",cnt,nxt[cnt]);
	hd[x]=cnt++;
//	printf("hd[%d]=%d\n",x,hd[x]);
}//vis存储的是边!!每个cnt代表一条边! 
void dfs(int root) {
	for(int i=hd[root]; i!=-1; i=nxt[i]) {//遍历完root的所有边之后输出root,即可得到正确路线 下方注释有助于理解 遍历边就是遍历vis 
//		printf("root=%d,i=%d,to[%d]=%d,vis[%d]=%d,nxt[%d]=%d\n",root,i,i,to[i],i,vis[i],i,nxt[i]); 
		if(!vis[i]){
			vis[i]=1;//相当于把边删除 
//			printf("vis[%d]=1\n",i);
			dfs(to[i]);
		}		
	}
//	printf("%d\n",root);//如果这个点没有边(孤立无援),输出孤立点 
}
int main() {
	while(scanf("%d%d",&n,&m)!=EOF) {
		cnt=0,memset(hd,-1,sizeof(hd));
		for(int i=1,x,y; i<=m; i++)
			scanf("%d%d",&x,&y),add(x,y),add(y,x);
		dfs(1);
	}
	return 0;
}

/*void dfs(int root) {
	for(int i=hd[root]; i!=-1; i=nxt[i]) {//遍历完root的所有边之后输出root,即可得到正确路线
//		printf("root=%d,i=%d,to[%d]=%d,vis[%d]=%d,nxt[%d]=%d\n",root,i,i,to[i],i,vis[i],i,nxt[i]); 
		if(!vis[i]){	vis[i]=1;printf("vis[%d]=1\n",i);dfs(to[i]);	}			
	}
	printf("%d\n",root);
}
/*数据结构
to[0]=2 vis[0]=0        nxt[0]=-1       hd[1]=0		
to[1]=1 vis[1]=0        nxt[1]=-1       hd[2]=1
模拟: 
root=1,i=0,to[0]=2,vis[0]=0,nxt[0]=-1
vis[0]=1
root=2,i=1,to[1]=1,vis[1]=0,nxt[1]=-1
vis[1]=1
root=1,i=0,to[0]=2,vis[0]=1,nxt[0]=-1
1
2 
1 */ 
/*void dfs(int root) {
	for(int i=hd[root]; i!=-1; i=nxt[i]) {//遍历完root的所有边之后输出root,即可得到正确路线
//		printf("root=%d,i=%d,to[%d]=%d,vis[%d]=%d,nxt[%d]=%d\n",root,i,i,to[i],i,vis[i],i,nxt[i]); 
		if(!vis[i]){	vis[i]=1;printf("vis[%d]=1\n",i);dfs(to[i]);	}				}
	printf("%d\n",root);	}
/*********************************(1,2)(2,3)           cnt  边 
from 1 to[0]=2 vis[0]=0        nxt[0]=-1       hd[1]=0  0 (1,2)->NULL
from 2 to[1]=1 vis[1]=0        nxt[1]=-1       hd[2]=1  1 (2,1)->NULL
from 2 to[2]=3 vis[2]=0        nxt[2]=1        hd[2]=2  2 (2,3)->(2,1)
from 3 to[3]=2 vis[3]=0        nxt[3]=-1       hd[3]=3  3 (3,2)->NULL
dfs(1){	root=1,i=0,to[0]=2,vis[0]=0,nxt[0]=-1
	vis[0]=1 (1,2)已被访问 1的邻接表遍历完毕	
	dfs(2){ root=2,i=2,to[2]=3,vis[2]=0,nxt[2]=1
		vis[2]=1 (2,3)已被访问 
		dfs(3){ root=3,i=3,to[3]=2,vis[3]=0,nxt[3]=-1
			vis[3]=1 (3,2)已被访问 3的邻接表遍历完毕
			dfs_(2){ root=2,i=2,to[2]=3,vis[2]=1,nxt[2]=1 访问(2,3)失败 
				root=2,i=1,to[1]=1,vis[1]=0,nxt[1]=-1
				vis[1]=1 (2,1)已被访问 2的邻接表遍历完毕
				dfs_(1){ root=1,i=0,to[0]=2,vis[0]=1,nxt[0]=-1 访问(1,2)失败   1的邻接表遍历完毕	*******************从此处开始上溯dfs结束!! 
					输出1	dfs_(1)完毕  															****dfs_(1)->dfs_(2)->dfs(3)->dfs(2)->dfs(1)
				}																					****由此可知深搜规律:先调用的后结束,后进先出 
				输出2 dfs_(2)完毕 																	****因此可以用栈模拟深搜 
			}							 
			输出3 dfs(3)完毕 
		}
		root=2,i=1,to[1]=1,vis[1]=1,nxt[1]=-1 访问(2,1)失败 2的邻接表遍历完毕 
		输出2 dfs(2)完毕 
	}
	输出1 dfs(1)完毕
}		调换一下输入顺序,看看(1,2)后直接回到(2,1)会发生什么 
from 2 to[0]=3  vis[0]=0        nxt[0]=-1       hd[2]=0 (2,3)->NULL
from 3 to[1]=2  vis[1]=0        nxt[1]=-1       hd[3]=1 (3,2)
from 1 to[2]=2  vis[2]=0        nxt[2]=-1       hd[1]=2 (1,2)
from 2 to[3]=1  vis[3]=0        nxt[3]=0        hd[2]=3 (2,1)->(2,3)
dfs(1){root=1,i=2,to[2]=2,vis[2]=0,nxt[2]=-1
		vis[2]=1	(1,2)被访问 **1的邻接表遍历完毕
		dfs(2){root=2,i=3,to[3]=1,vis[3]=0,nxt[3]=0
				vis[3]=1	(2,1)被访问 
				dfs_(1){root=1,i=2,to[2]=2,vis[2]=1,nxt[2]=-1 访问(1,2)失败  **_1的邻接表遍历完毕
				输出1 dfs_(1)结束 									*****结束顺序dfs_(1)->dfs_(2)->dfs(3)->dfs(2)->dfs(1)
				}													*****调用顺序1->2->_1->3->_2 
				root=2,i=0,to[0]=3,vis[0]=0,nxt[0]=-1
				vis[0]=1 (2,3)被访问 **2的邻接表遍历完毕
				dfs(3){root=3,i=1,to[1]=2,vis[1]=0,nxt[1]=-1
						vis[1]=1	(3,2)被访问 **3的邻接表遍历完毕
						dfs_(2){root=2,i=3,to[3]=1,vis[3]=1,nxt[3]=0 (2,1)失败  
								root=2,i=0,to[0]=3,vis[0]=1,nxt[0]=-1 (2,3)失败 **_2的邻接表遍历完毕 
								输出2	dfs_(2)结束	
						}
						输出3 dfs(3)结束
				}
				输出2 dfs(2)结束
		} 				  ****由此例总结算法:搜索边,搜索到后相当于把边删除,边的终点还要做一次dfs,由dfs(i)输出孤立无援的点(i)
		输出1 dfs(1)结束  ****也就是遍历过所有边后回溯dfs输出 得到正确路线 无法严格证明,只能举例说明加深理解 
}			
例如本例
{dfs(1)删除(1,2),
	{dfs(2)删除(2,1),1孤立无援,
		{dfs_(1)输出1}
	 dfs(2)删除(2,3),
	 	{dfs(3)删除(3,2),2孤立无援,
			{dfs_(2)输出2},
		 dfs(3)输出3
		}
	 dfs_(2)输出2 
	}
dfs(1)输出1
}*/


/*
(1,2)(2,1) (2,4)(4,2) (1,3)(3,1) (3,5)(5,3)
	      1
      ↙↗  ↘↖
       2      3
      ↓↑   ↓↑
       4      5
给边从左到右,从上到下依次命名曰a,b,c,d,e,f,g,h
	      1
             
       2      3
            
       4      5
dfs栏1{2{_1{3{5{_3{__1} } } } } 4{_2 } } } }
删除栏 a b  c g h    d          e f  
输出栏               1  3 5 3 1     2  4 2 1 
又由于我们是同一起点的话从小到大输入,链式前向星是从大到小倒序储存,我们上面的dfs模拟是从小到大模拟的,就是先走左边再走右边
所以实际代码流程应该反过来 ,先走右边再走左边 那么就是1 2 4 2 1 3 5 3 1 
*/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值