数据结构-第五次上机实验-解题报告

7-1 图的深度优先搜索I (100 分)

题目

无向图 G 有 n 个顶点和 m 条边。求图G的深度优先搜索树(森林)以及每个顶点的发现时间和完成时间。每个连通分量从编号最小的结点开始搜索,邻接顶点选择顺序遵循边的输入顺序。

在搜索过程中,第一次遇到一个结点,称该结点被发现;一个结点的所有邻接结点都搜索完,该结点的搜索被完成。深度优先搜索维护一个时钟,时钟从0开始计数,结点被搜索发现或完成时,时钟计数增1,然后为当前结点盖上时间戳。一个结点被搜索发现和完成的时间戳分别称为该结点的发现时间和完成时间

输入格式:
第1行,2个整数n和m,用空格分隔,分别表示顶点数和边数, 1≤n≤50000, 1≤m≤100000.

第2到m+1行,每行两个整数u和v,用空格分隔,表示顶点u到顶点v有一条边,u和v是顶点编号,1≤u,v≤n.

输出格式:
第1到n行,每行两个整数di和fi,用空格分隔,表示第i个顶点的发现时间和完成时间1≤i≤n 。

第n+1行,1个整数 k ,表示图的深度优先搜索树(森林)的边数。

第n+2到n+k+1行,每行两个整数u和v,表示深度优先搜索树(森林)的一条边<u,v>,边的输出顺序按 v 结点编号从小到大。

输入样例:
在这里给出一组输入。例如:

6 5
1 3
1 2
2 3
4 5
5 6

输出样例:
在这里给出相应的输出。例如:

1 6

3 4

2 5

7 12

8 11

9 10

4

3 2

1 3

4 5

5 6

思路

题目也告诉我们了,这题就是考深搜,题意很清晰。但是,苛刻在输出的要求上。
但其实,也只需要对dfs内多设置几个变量进行监考即可。
思路如下:
(1)输出每个点的发现时间的完成时间,所以我设置了一个ti变量表示当前时间,和一个ttime结构体数组,在dfs进行时保存 入和出 的时间。(time是关键字好像,我刚开始设置成time类型,Dev-C++能过,但pta上不能过,跟编译器可能有点影响。)

typedef struct node2{
	int s;//start
	int e;//end
}ttime;//发现和完成  时间结构体
ttime T[50001];

(2)求深度优先搜索树的边数。(放在第3点中解释)
(3)输出深搜树的<u,v>边,由于输出时按v结点排序,故可以选择用map进行存储<v,u>再反向输出。由于map保存了每条边,故M.size()就是边数。

map<int,int> M;

cout<<M.size()<<"\n";//边数

for(map<int,int>::iterator it=M.begin();it!=M.end();it++)
cout<<it->second<<" "<<it->first<<"\n";//反向输出

参考代码

完整代码如下,分析已经在上方,下面就不细做注释了。

#include <iostream>
#include<vector>
#include<map> 
using namespace std;

typedef struct node1{
	int u;
	int v;
}edge;
typedef struct node2{
	int s;//start
	int e;//end
}ttime;
vector<edge> graph[50001];
map<int,int> M;
int vis[50001];
int count=0;
ttime T[50001];
int ti=1;
void dfs(int u){
	vis[u]=1;
	T[u].s=ti++;//发现时间
	
	int v;
	for(int i=0;i<graph[u].size();i++){
		v=graph[u][i].v;
		if(!vis[v]){
			M.insert(make_pair(v,u));
			dfs(v);	
			
		}
	}
	T[u].e=ti++;//完成时间
}
int main()
{
	
	int n,m;
	int u,v;
	edge e;
	
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) vis[i]=0;
	
	for(int i=1;i<=m;i++)
	{
		
		scanf("%d%d",&u,&v);
		e.u=u;e.v=v;
		graph[u].push_back(e);
		e.u=v;e.v=u;
		graph[v].push_back(e);
	}
	
	for(int i=1;i<=n;i++){//遍历各个连通分支。
	    if(!vis[i]){
	    	//ti++;
	       dfs(i);
	       //count++;
        }
	}
	for(int i=1;i<=n;i++)
	cout<<T[i].s<<" "<<T[i].e<<"\n";
	cout<<M.size()<<"\n";
	for(map<int,int>::iterator it=M.begin();it!=M.end();it++)
	cout<<it->second<<" "<<it->first<<"\n";
	 
} 

7-2 圆 (100 分)

题目

二维平面上有n 个圆。请统计:这些圆形成的不同的块的数目。

圆形成的块定义如下: (1)一个圆是一个块; (2)若两个块有公共部分(含相切),则这两个块形成一个新的块,否则还是两个不同的块。

输入格式:
第1行包括一个整数n,表示圆的数目,n<=8000。

第2到n+1行,每行3 个用空格隔开的数x,y,r。(x,y)是圆心坐标,r 是半径。所有的坐标及半径都是不大于30000 的非负整数。

输出格式:
1个整数,表示形成的块的数目。

输入样例:
在这里给出一组输入。例如:

2
0 0 1
1 0 2

输出样例:
在这里给出相应的输出。例如:

1

思路

两个圆有区域重叠,就合并。
一开始,我以为,只要发生了圆心距离小于半价之和就需要合并。但这只能过前3个点。多想想会发现,不仅需要满足上述条件,而且,若第三个圆与第二个圆重叠时,第三个圆也与第一个圆重叠,则此次判断合并,不对cnt进行删减。因为1->3已经删减过一次,故2->3不能再删减一次。(上机时没考虑到)。
故核心代码中,if的判断条件,增设条件判断UNION(i,j)是否为1。

    cnt=n;
	for(int i=1;i<=n;i++)
		for(int j=i+1;j<=n;j++)
		    //有重叠不一定需要合并,故判断union返回值 
			if(dis(circle[i].x,circle[i].y,circle[j].x,circle[j].y)<=circle[i].r+circle[j].r&&UNION(i,j))//有重叠且能合并,才—— 
			cnt--;

参考代码

#include<bits/stdc++.h>
using namespace std;

typedef struct node
{
	int x,y;
	int r;
}Circle;

Circle circle[8001];
int father[8001];
int cnt=0;


//并查集 find union 
int FIND(int v)
{
	if(father[v]==v) return v;
	return FIND(father[v]);
}

int UNION(int x,int y)
{
	int fx=FIND(x);
	int fy=FIND(y);

	if(fx!=fy)
	{
		father[fy]=fx;
		return 1;//返回1表明发生合并 
	}
	else return 0;//没发生合并 
}

double dis(int x1,int y1,int x2,int y2) 
{
	double d=(x1-x2)*(x1-x2)+(y1-y2)*(y1-y2);
	//cout<<d<<endl ;
	return sqrt(d);
}

int main()
{
	int n;
	cin>>n;

	for(int i=1;i<=n;i++)
	cin>>circle[i].x>>circle[i].y>>circle[i].r;

	for(int i=1;i<=n;i++)//初始化 
	father[i]=i;
	
	cnt=n;
	for(int i=1;i<=n;i++)
		for(int j=i+1;j<=n;j++)
		    //有重叠不一定需要合并,故判断union返回值 
			if(dis(circle[i].x,circle[i].y,circle[j].x,circle[j].y)<=circle[i].r+circle[j].r&&UNION(i,j))//有重叠且能合并,才—— 
			cnt--;

	cout<<cnt;

}

7-3 供电 (100 分)

题目

要给N个地区供电。每个地区或者建一个供电站,或者修一条线道连接到其它有电的地区。试确定给N个地区都供上电的最小费用。

输入格式:
第1行,两个个整数 N 和 M , 用空格分隔,分别表示地区数和修线路的方案数,1≤N≤10000,0≤M≤50000。

第2行,包含N个用空格分隔的整数P[i],表示在第i个地区建一个供电站的代价,1 ≤P[i]≤ 100,000,1≤i≤N 。

接下来M行,每行3个整数a、b和c,用空格分隔,表示在地区a和b之间修一条线路的代价为c,1 ≤ c ≤ 100,000,1≤a,b≤N 。

输出格式:
一行,包含一个整数, 表示所求最小代价。

输入样例:
在这里给出一组输入。例如:

4 6

5 4 4 3

1 2 2

1 3 2

1 4 2

2 3 3

2 4 3

3 4 4

输出样例:
在这里给出相应的输出。例如:

9

思路

这道题,确实难,课后钻研了许多同学的代码,学到了多种对点权和边权的处理方式。我觉得比较好用的,应该是用 虚源点+Kruskal 的方法处理这个最小生成树。而Kruskal可采用有序边表+并查集的方法实现。
(1)虚源虚汇:
先附上一张ppt
关键路径ppt上的图
实际实现过程中,只需要将第0个点设置成发电场点(虚源点),以它发出的虚边存入有序边表中,如下操作即可。

	for(int i=1;i<=P_num;i++)//以0为虚源点,初始化 发电站虚边 
	{
		int cost;
		cin>>cost;
		e[i].u=0;//起点0 
		e[i].v=i;//终点自身 
		e[i].cost=cost;//权值前移 
	}

使用虚源点处理之后,就是普通的最小生成树(森林)的题目了,由于使用了虚源点,其实不用考虑是森林的情况了。因为最终得到的必然是含虚边的最小生成树,去掉虚边之后,可能为森林也可能是树,但,这不需要我们单独考虑。方便就方便在这。
(2)有序边表+并查集实现Kruskal:
大致有两步:sort排序取最小,判环合并。
由于对e的cost成员属性进行排序,故sort中使用lambda表达式自定义排序方法实现。

    sort(e+1,e+E_num+P_num+1,[=](edge e1,edge e2){
	    return e1.cost<e2.cost;
	});
	
	for(int i=0;i<=P_num;i++) father[i]=i;//并查集初始化
	
	
	for(int i=1,k=0;k!=P_num;i++)//k等于边数时结束 
	{
		if(FIND(e[i].u)!=FIND(e[i].v))//不成环 
		{
			UNION(e[i].u,e[i].v) ;//n次合并
            k++;
			sum=sum+e[i].cost;//记录代价总和 
		}
	} 

参考代码

#include <bits/stdc++.h>
using namespace std;
typedef struct node
{
	int u,v;
	int cost;
}edge;

edge e[60010];//50000+10000个空间

int father[10010];

//并查集 find union 
int FIND(int v)
{
	if(father[v]==v) return v;
	return FIND(father[v]);
}

int UNION(int x,int y)
{
	int fx=FIND(x);
	int fy=FIND(y);

	if(fx!=fy)
	{
		father[fy]=fx;
		return 1;//返回1表明发生合并 
	}
}

int main()
{
	int E_num,P_num;
	cin>>P_num>>E_num;
	
	long int sum;
	
	for(int i=1;i<=P_num;i++)//以0为虚源点,初始化 发电站虚边 
	{
		int cost;
		cin>>cost;
		e[i].u=0;//起点0 
		e[i].v=i;//终点自身 
		e[i].cost=cost;//权值前移 
	}
	
	for(int i=1;i<=E_num;i++) 
	{
		int u,v,cost;
		cin>>u>>v>>cost;
		e[i+P_num].u=u;
		e[i+P_num].v=v;
		e[i+P_num].cost=cost;
	}
	
	sort(e+1,e+E_num+P_num+1,[=](edge e1,edge e2){
	    return e1.cost<e2.cost;
	});
	
	for(int i=0;i<=P_num;i++) father[i]=i;//并查集初始化
	
	
	for(int i=1,k=0;k!=P_num;i++)//k等于边数时结束 
	{
		if(FIND(e[i].u)!=FIND(e[i].v))//不成环 
		{
			UNION(e[i].u,e[i].v) ;//n次合并
            k++;
			sum=sum+e[i].cost;//记录代价总和 
		}
	} 
	
	cout<<sum<<endl;
	
}

7-4 发红包 (100 分)

题目

新年到了,公司要给员工发红包。员工们会比较获得的红包,有些员工会有钱数的要求,例如,c1的红包钱数要比c2的多。每个员工的红包钱数至少要发888元,这是一个幸运数字。

公司想满足所有员工的要求,同时也要花钱最少,请你帮助计算。

输入格式:
第1行,两个整数n和m(n<=10000,m<=20000),用空格分隔,分别代表员工数和要求数。

接下来m行,每行两个整数c1和c2,用空格分隔,表示员工c1的红包钱数要比c2多,员工的编号1~n 。

输出格式:
一个整数,表示公司发的最少钱数。如果公司不能满足所有员工的需求,输出-1.

输入样例:
在这里给出一组输入。例如:

2 1
1 2

输出样例:
在这里给出相应的输出。例如:

1777

思路

题目暗示十分清晰,
“员工c1的红包钱数要比c2多”,表明从c2点到c1点是有要求的,这种要求,很容易想到应该使用拓扑排序或者关键路径的知识来解决。
“如果公司不能满足所有员工的需求”,很明显是因为拓扑排序后,剩下的顶点成环了,网中仍有点且点的入度不为0,即是成环。(想要A比B钱多,B比C钱多,却又希望C比A钱多,公司肯定满足不了。)
我们可以选择vector实现图,stack辅助存储的方式实现拓扑排序来写:
对于员工c1,c2我们采用反设(因为c1钱比c2多嘛,就放后面,对money进行+1操作即可)
拓扑排序主体:

        for(vector<int>::iterator it=V[top].begin();it!=V[top].end();++it)
		{
			rudu[*it]--;
			if(rudu[*it]==0) s.push(*it);//为0就入队
			//在拓扑排序时增设下面这步,达到记录money的效果
			//若该点money+1后比邻接点的此时money值大,则更新邻接点money值
			if(money[*it]<money[top]+1) money[*it]=money[top]+1;
		}

参考代码

除了增设money数组辅助存储权值外。拓扑排序主体内容稍作修改增加一步存储money操作,其他操作就是拓扑排序的操作了。

#include <bits/stdc++.h>
using namespace std;

vector<int>V[10001];
stack<int>s;
int money[10001];//权值 
int rudu[10001];//入度 

int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++) money[i]=888;//初始化各点
	int u,v;
	for(int i=1;i<=m;i++)
	{
		cin>>v>>u;//反设 
		V[u].push_back(v);
		rudu[v]++;
	}
	
	for(int i=1;i<=n;i++)
	{
		if(rudu[i]==0)  s.push(i);
	} 
	
	int top;
	
	for(int i=1;i<=n;i++)
	{
		
		if(s.empty()) 
		{
			cout<<-1;
			return 0;
		}
		
		top=s.top();
		s.pop();

		for(vector<int>::iterator it=V[top].begin();it!=V[top].end();++it)
		{
			rudu[*it]--;
			if(rudu[*it]==0) s.push(*it);
			
			if(money[*it]<money[top]+1) money[*it]=money[top]+1;
		}

	}

	int sum=0;
	for(int i=1;i<=n;i++)sum=sum+money[i];	
    cout<<sum<<endl;

}

感谢阅读!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值