数据结构荣誉课-第四次实验-解题报告

一、连通分量

题目

无向图 G 有 n 个顶点和 m 条边。求 G 的连通分量的数目。

输入格式:

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

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

输出格式:

1行,1个整数,表示所求连通分量的数目。

输入样例:

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

输出样例:

  • 2

思路

  1. [造图]
    选择vector优化的邻接链表储存。每个数据存储在int Data[]中,每个结点的结点信息储存在vector< int > v[]中。
  2. [求连通分量数]
    思路一:循环使用DFS。每次DFS能遍历一个连通分量,接着找到未被访问过的结点,继续DFS,直到所有顶点都被访问过。
    思路二:并查集。初始时,连通分量个数即为N,每读取两个顶点,如果这两个顶点所在集合的代表元不一致,则合并,连通分量个数-1。

参考代码

思路一:(循环DFS)

#include "iostream"
#include "vector"
using namespace std;


int N;
int M;
int Data[50001];
vector<int> v[50001];

void BuildGraph()
{
	for(int i=0;i<=N;i++)
		Data[i]=1;

	int index_u,index_v;
	for(int i=1;i<=M;i++)
	{
		cin>>index_u>>index_v;
		v[index_u].push_back(index_v);//无向图构造边是要同时构造起点->终点和终点->起点
		v[index_v].push_back(index_u);
	}
}

int IsVisited[50001]={0};
void DepthFirstSearch(int begin)//DFS算法
{

	IsVisited[begin]=1;

	for(vector<int>::iterator it=v[begin].begin();it<v[begin].end();it++)
		if(!IsVisited[*it])
			DepthFirstSearch(*it);
}


int main()
{
	cin>>N>>M;

	BuildGraph();

	int Sum=0;
	for(int i=1;i<=N;i++)
		if(!IsVisited[i])//遍历所有未被访问的结点
		{
			DepthFirstSearch(i);
			Sum++;
		}

	cout<<Sum<<endl;
}

思路二:(并查集)

#include "iostream"
using namespace std;

int Vertex[50001];

int N;
int M;

int Find(int x)//寻找所在集合的代表元
{
	if(Vertex[x]==x)
		return x;
	else
		return Vertex[x]=Find(Vertex[x]);//路径压缩
}

int Union(int x,int y)//合并,返回是否合并成功
{
	int Father_x=Find(x);
	int Father_y=Find(y);
	
	if(Father_x==Father_y)
		return 0;
	else
	{
		Vertex[Father_y]=Father_x;
		return 1;
	}
}

int main()
{
	cin>>N>>M;

	for(int i=1;i<=N;i++)
		Vertex[i]=i;

	int x,y;
	for(int i=0;i<M;i++)
	{
		cin>>x>>y;
		if(Union(x,y))
			N--;
	}

	cout<<N<<endl;
}

二、整数拆分

题目

整数拆分是一个古老又有趣的问题。请给出将正整数 n 拆分成 k 个正整数的所有不重复方案。例如,将 5 拆分成 2 个正整数的不重复方案,有如下2组:(1,4)和(2,3)。注意(1,4) 和(4,1)被视为同一方案。每种方案按递增序输出,所有方案按方案递增序输出。

输入格式:

1行,2个整数n和k,用空格分隔, 1≤k≤n≤50。

输出格式:

若干行,每行一个拆分方案,方案中的数用空格分隔。

最后一行,给出不同拆分方案的总数。

输入样例:

  • 5 2

输出样例:

  • 1 4
  • 2 3
  • 2

题意:

将一个整数N拆分成M个非递增序列。

思路

  1. 求满足某种性质(约束条件)的所有解或最优解时,往往使用回溯法。回溯法思路的简单描述是:把问题的解空间转化成了图或者树的结构表示,然后使用深度或广度优先搜索进行遍历,遍历的过程中记录和寻找所有可行解或者最优解。

  2. 回溯法的基本框架为:

    void DFS(当前状态 S)
    {
    	if(当前状态 S==目标状态 T)
    		Deal();
    	else
    	{
    		for each 可扩展状态Si do
    			if(Si 满足约束条件)
    			{
    				置当前状态为Si;
    				DFS(Si);
    				恢复当前状态为S;
    			}
    	}
    }
    
  3. 对于本题,我们先分析函数参数的选取(需要几个变量表示当前状态)。首先需要一个整数num,表示当前要分解的整数;其次需要一个整数index,表示此时已经分解了多少位(也即新分解出来的数在答案数组中储存的索引);接着需要一个整数now,代表当前分解出来的数。

  4. 于是,当前状态便设为:int num,int index,int now;
    达到目标状态时,需要分解的数应该刚好分完,即num=0,同时此时应该正好分解了M位,也即index=M,于是,目标状态即为(num == 0)&&(index == M);
    对于每种状态,由于题目要求以非递减顺序分解,所以可扩展的状态即为,num-now按now,now+1,…,num分解。

拓展方式如下:(三个数字分别代表:当前要分解的数;已经分解出的数个数;当前数要从此值开始分解)

5=1+?
5=2+?
5=1+1+?
index=2,num=3
5=1+2+?
index=2,num=2
5=1+3+?
index=2,num=1
5=1+4+?
index=2,num=0
5=2+2+?
index=2,num=1
5=2+3+?
index=2,num=0
5=3+?
now>num
5=4+?
now>num
5=5+?
now>num
5,0,1
4,1,1
3,1,2
3,2,1
无解
2,2,2
无解
1,2,3
无解
0,2,4
一个解
1,2,2
无解
0,2,3
一个解
2,1,3
无拓展序列
1,1,4
无拓展序列
0,1,5
无拓展序列

参考代码

#include "iostream"
using namespace std;

int Num[50];
int N;
int M;
int AnswerSum;

void DFS(int num,int index,int now)//当前状态
{
	if(index==M&&num==0)//达到目标状态,进行处理
	{
		cout<<Num[0];
		for(int i=1;i<index;i++)
			cout<<" "<<Num[i];
		cout<<endl;
		AnswerSum++;
		return;
	}

	for(int i=now;i<=num;i++)//可扩展序列
	{
		Num[index]=i;
		DFS(num-i,index+1,i);
	}

}


int main()
{
	cin>>N>>M;

	DFS(N,0,1);

	cout<<AnswerSum<<endl;
}

三、数字变换

题目

利用变换规则,一个数可以变换成另一个数。变换规则如下:
(1)x 变为x+1;
(2)x 变为2x;
(3)x 变为 x-1。
给定两个数x 和 y,至少经过几步变换能让 x 变换成 y。

输入格式:

1行,2个整数x和y,用空格分隔, 1≤x,y≤100000、

输出格式:

第1行,1个整数s,表示变换的最小步数。

第2行,s个数,用空格分隔,表示最少变换时每步变换的结果。规则使用优先级顺序: (1),(2),(3)。

输入样例:

  • 2 14

输出样例:

  • 4
  • 3 6 7 14

题意:

  • 给定一种变换方案,求出从A到B最快的变换。

思路

  1. 与上题一致,也是一道运用试探法不断尝试寻求解的题目。但与上题不同的是,本题找到一组最优解后,便可以输出。
  2. 我们可以理解为,从某一状态出发,同层扩展所有可能的子状态,达到第一个解结点(隐含图的叶子节点)时,便可以返回,这与广度优先遍历(BFS)的顺序是一致的。
  3. 很容易想到下列拓展序列:
x
x+1
2x
x-1
x+2
...
2x+2
...
x
...
2x+1
...
4x
...
2x-1
...
x
...
2x-2
...
x-2
...
  1. 但是我们会发现,如果只是简单的扩展,会有很多元素被多次遍历,浪费时间。于是我们需要引入一个标记数组int HasVisited[]来标记当前结点是否已经被遍历(即当前结点是否已经被算出来过了,如果这个数在之前已经算出来过,那么本次计算便无意义了,因为从路径来说,本次的路径一定更长)。
  2. 当我们找到第一个最优解时,便可以返回并输出当前的路径。由于第四步已经限制了每个数都被遍历一次,因此每个数的前驱结点固定(即每个数一定只由一个数经过某种变换得到),于是我们可以引入前驱路径数组int PrePath[]来保存之前的数值。

参考代码

#include "iostream"
#include "queue"
#include "stack"
using namespace std;

int N;
int M;
int Sum;
int HasVisited[200001];
int PrePath[200001];

queue<int> q;

void BFS(int num)
{
	q.push(num);
	HasVisited[num]=1;

	int tmp;
	while(1)
	{
		tmp=q.front();
		q.pop();

		//由于优先级的限制,先处理+1,接着处理*2,最后处理-1
		if(!HasVisited[tmp+1])//+1操作,只要+1的数之前没算出过即可
		{
			q.push(tmp+1);
			HasVisited[tmp+1]=1;
			PrePath[tmp+1]=tmp;
			if(tmp+1==M)//检测是否找到了解,下同
				break;
		}

		if(3*tmp<=2*M-1&&!HasVisited[tmp*2])//*2操作,由于数值很大时,*2后下标可能越界,此处需额外限制,限制条件见下面
		{
			q.push(tmp*2);
			HasVisited[tmp*2]=1;
			PrePath[tmp*2]=tmp;
			if(tmp*2==M)
				break;
		}

		if(tmp-1>0&&!HasVisited[tmp-1])//-1操作,-1后下标仍可能越界
		{
			q.push(tmp-1);
			HasVisited[tmp-1]=1;
			PrePath[tmp-1]=tmp;
			if(tmp-1==M)
				break;
		}

	}

	stack<int> s;
	s.push(M);

	while(s.top()!=N)//将数值循环入栈,并统计Sum数
	{
		s.push(PrePath[s.top()]);

		Sum++;
	}

	cout<<Sum<<endl;//输出
	s.pop();
	cout<<s.top();
	s.pop();

	while(!s.empty())
	{
		cout<<" "<<s.top();
		s.pop();
	}
	cout<<endl;
}

int main()
{
	cin>>N>>M;

	if(M==N)//M=N时,不需要操作,需特殊处理
	{
		cout<<"0"<<endl;
		return 0;
	}
	
	else if(M<N)//如果M<N,+1和*2都不要进行,直接-1到M即可
	{
		cout<<N-M<<endl;
		cout<<N-1;
		for(int i=N-2;i>=M;i--)
			cout<<" "<<i;
		cout<<endl;
		return 0;
	}
	
	else
		BFS(N);

}

关于*2操作的限制条件:

  • 我的想法是这样的:我们需要确定某种情况×2操作是一定不需要进行的,那么×2操作可以由+1操作代替,也即通过+1操作直接到达y比通过×2操作到达z(z>y),再由z,-1操作到达y省时间。
  • 所以:①x+PlusNum=y;②2x-SubNum=y;③SubNum+1>PlusNum
  • 于是3x>2y-1,即3×tmp>2×M-1

四、旅行

题目

五一要到了,来一场说走就走的旅行吧。当然,要关注旅行费用。由于从事计算机专业,你很容易就收集到一些城市之间的交通方式及相关费用。将所有城市编号为1到n,你出发的城市编号是s。你想知道,到其它城市的最小费用分别是多少。如果可能,你想途中多旅行一些城市,在最小费用情况下,到各个城市的途中最多能经过多少城市。

输入格式:

第1行,3个整数n、m、s,用空格分隔,分别表示城市数、交通方式总数、出发城市编号, 1≤s≤n≤10000, 1≤m≤100000 。

第2到m+1行,每行三个整数u、v和w,用空格分隔,表示城市u和城市v的一种双向交通方式费用为w , 1≤w≤10000。

输出格式:

第1行,若干个整数Pi,用空格分隔,Pi表示s能到达的城市i的最小费用,1≤i≤n,按城市号递增顺序。

第2行,若干个整数Ci,Ci表示在最小费用情况下,s到城市i的最多经过的城市数,1≤i≤n,按城市号递增顺序。

输入样例:

  • 5 5 1
  • 1 2 2
  • 1 4 5
  • 2 3 4
  • 3 5 7
  • 4 5 8

输出样例:

  • 0 2 6 5 13
  • 0 1 2 1 3

思路

  • 本题即为求最短路问题,唯一不同的是加了一个限制条件:相同长度条件下,选择经过点多的路径。
  • 于是我们可以引入一个计数数组int PosSum[]来记录,当前结点之前经过的结点数,如果某次再经过该结点时,若之前经过该节点的PosSum值小于当前结点的PosSum值+1,则对PosSum值进行更新。

参考代码

#include "iostream"
#include "vector"
#include "queue"
using namespace std;

int Data[10001];
vector<pair<int,int> > v[10001];//pair(index_v,cost_uv)

int N;
int M;

void BuildGraph()
{
	for(int i=1;i<=N;i++)
		Data[i]=1;

	int index_u,index_v,cost_uv;
	for(int i=1;i<=M;i++)
	{
		cin>>index_u>>index_v>>cost_uv;//构建无向图
		v[index_u].push_back(make_pair(index_v,cost_uv));
		v[index_v].push_back(make_pair(index_u,cost_uv));
	}
}

#define MaxCost 1e9;
int CostSum[10001];
int SumPos[10001];

void Dijkstra(int begin)
{
	static int IsOver[10001]={0};
	
	//初始化
	for(int i=1;i<=N;i++)
		CostSum[i]=MaxCost;
	CostSum[begin]=0;

	for(int k=0;k<N;k++)
	{
		int MinCost=MaxCost;
		int NextIndex=begin;
		
		//寻找下一个处理的结点
		for(int i=1;i<=N;i++)
			if(CostSum[i]<MinCost&&!IsOver[i])
			{
				MinCost=CostSum[i];
				NextIndex=i;
			}
		IsOver[NextIndex]=1;

		//处理该结点的所有边
		for(vector<pair<int,int> >::iterator it=v[NextIndex].begin();it!=v[NextIndex].end();it++)
			if(CostSum[it->first]>MinCost+it->second)
			{
				CostSum[it->first]=MinCost+it->second;
				SumPos[it->first]=SumPos[NextIndex]+1;
			}
			else if(CostSum[it->first]==CostSum[NextIndex]+it->second)//权值相同,比较途径地点的数目
				if(SumPos[it->first]<SumPos[NextIndex]+1)
					SumPos[it->first]=SumPos[NextIndex]+1;
	}
}


int main()
{
	int begin;
	cin>>N>>M>>begin;

	BuildGraph();
	Dijkstra(begin);

	cout<<CostSum[1];
	for(int i=2;i<=N;i++)
		cout<<" "<<CostSum[i];

	cout<<endl<<SumPos[1];
	for(int i=2;i<=N;i++)
		cout<<" "<<SumPos[i];
	cout<<endl;

}
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行程实践、外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值