一、连通分量
题目
无向图 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
思路
- [造图]
选择vector优化的邻接链表储存。每个数据存储在int Data[]中,每个结点的结点信息储存在vector< int > v[]中。 - [求连通分量数]
思路一:循环使用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个非递增序列。
思路
-
求满足某种性质(约束条件)的所有解或最优解时,往往使用回溯法。回溯法思路的简单描述是:把问题的解空间转化成了图或者树的结构表示,然后使用深度或广度优先搜索进行遍历,遍历的过程中记录和寻找所有可行解或者最优解。
-
回溯法的基本框架为:
void DFS(当前状态 S) { if(当前状态 S==目标状态 T) Deal(); else { for each 可扩展状态Si do if(Si 满足约束条件) { 置当前状态为Si; DFS(Si); 恢复当前状态为S; } } }
-
对于本题,我们先分析函数参数的选取(需要几个变量表示当前状态)。首先需要一个整数num,表示当前要分解的整数;其次需要一个整数index,表示此时已经分解了多少位(也即新分解出来的数在答案数组中储存的索引);接着需要一个整数now,代表当前分解出来的数。
-
于是,当前状态便设为:int num,int index,int now;
达到目标状态时,需要分解的数应该刚好分完,即num=0,同时此时应该正好分解了M位,也即index=M,于是,目标状态即为(num == 0)&&(index == M);
对于每种状态,由于题目要求以非递减顺序分解,所以可扩展的状态即为,num-now按now,now+1,…,num分解。
拓展方式如下:(三个数字分别代表:当前要分解的数;已经分解出的数个数;当前数要从此值开始分解)
参考代码
#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最快的变换。
思路
- 与上题一致,也是一道运用试探法不断尝试寻求解的题目。但与上题不同的是,本题找到一组最优解后,便可以输出。
- 我们可以理解为,从某一状态出发,同层扩展所有可能的子状态,达到第一个解结点(隐含图的叶子节点)时,便可以返回,这与广度优先遍历(BFS)的顺序是一致的。
- 很容易想到下列拓展序列:
- 但是我们会发现,如果只是简单的扩展,会有很多元素被多次遍历,浪费时间。于是我们需要引入一个标记数组int HasVisited[]来标记当前结点是否已经被遍历(即当前结点是否已经被算出来过了,如果这个数在之前已经算出来过,那么本次计算便无意义了,因为从路径来说,本次的路径一定更长)。
- 当我们找到第一个最优解时,便可以返回并输出当前的路径。由于第四步已经限制了每个数都被遍历一次,因此每个数的前驱结点固定(即每个数一定只由一个数经过某种变换得到),于是我们可以引入前驱路径数组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;
}