1.汽车站的设计

探讨了在一个有向图中,如何通过删除最少数量的点来确保从起点到终点不存在长度小于特定值的路径。介绍了使用递归和迭代加深策略进行深度优先搜索,结合广度优先搜索获取最短路径,以找到最优解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目抽象:

一个有向图有n个点(n<=50),g条边(g<=4000)。现在要求删掉最少的点,使得不存在从1号点到n号点的长度<=m的路径(m<1000)。当然,不能删除1号点或者n号点,输出最少需要删除的点数(删除一个点的时候会连带将与之相连的边也删除)。

解题思路:

最朴素的搜索方法是枚举所有删除点的方案,每找到以一种方案,就求一次从1号点到n号点的最短路径,看看长度是否超过m,每个点有删除和不删除两种状态,所以方案总数最多就是2^48种,这样搜索非常盲目,显然会超时。

解决的方法是有选择的进行删点。基本思路就是,一条长度不超过m的最短路径上的点,至少有一个点是要被删掉的(至于删除哪个,可以枚举尝试),删掉一个点后再重新求最短路径,如果新求出的最短路径长度仍然不超过m,那么就在新的路径上再找出一个点删掉,然后再求最短路径,重复以上方法直到求出的最短路径长度超过m,那么就找到了一个解。但这个解未必就是最优解,所以这里用递归的方法搜索了多组解进行对比求出最优解。递归步骤如下:

1.寻找起点到终点的最短路径,如果最短路径长度超过m,则表示已经找到一种删除方案,记录该方案下的删除点数目。回溯再找出下一种方案。否则进入下一步。

2.枚举最短路径中的除1号点和n号点外的某一个点,将其删除。

3.回到1递归继续搜索。

这样的搜索使得枚举的点的数量得到控制。搜索的每一层都会从n-2个点中选择一个进行删除,在上述方法中,还可以用迭代加深的方法来搜索,也就是说,先找到删除1个点的方案,看是否存在;如果不存在,则再找到删除2个点的方案;......

#include<iostream>

using namespace std;

const int maxm=10005;//最大边数

const int maxn=105;//最大点数

struct aaa

{

int s,f,next;//邻接表的域,s表示边的起点,f表示边的终点,next指向点s的下一条边

};

aaa c[maxm];//图的领接表

int sta[maxn],fa[maxn],zh[maxn];//程序中使用邻接表保存图中的边,sta数组为每个点的邻接表表头指针,c数组储存了每条边。fa数组和zh数组用于广搜最短路径。其中zh数组为广搜使用的队列,fa数组保存了广搜的路径,fa[i]表示i节点的父亲。

int d[maxn][maxn],e[maxn];//d数组保存了当前搜索到的最短路径,d[i,j]表示搜索的第i层(第i次寻找最短路径)中寻找到的最短路中的第j个点的编号

bool b[maxn];

int n,m,now,tot;//now用于建邻接表时的累加,tot为迭代加深搜索的“界”

bool goal;//表示是否搜到了满足要求的界(不删除超过tot个点的解),如果找到了则goal=true,搜索退出

 

void ins(int s,int f)//这里记录每条边的起始点还有终点,并且记录好具有相应起始点的邻接表指针

{

now++;

c[now].s=s;

c[now].f=f;

c[now].next=sta[s];//一开始指向的指针为0,但是随着同一个起始点的多条边的累加now,next会指向相应c数组的下标位置

sta[s]=now;//指向该s起点的最后一条边的相应下标

}

 

void bfs()//其实这里个广搜寻找最短路径是建立在点与点之间的步数上的,就是说,如果起点到终点之间要经过两个点,而另外一条路径要经过三个点,那么就说经过两个点的比较近,也就是最短

{

int i,cl,op,k,t;

cl=0;op=1;

for(i=1;i<=n;i++) fa[i]=0;//清空所有父节点

zh[1]=1;//队列的起点为第一个点

fa[1]=-1;//默认根节点的父节点为-1

while(cl<op)//这里的循环本来追求的是遍历所有图中的点

{

cl++;k=zh[cl];//一开始zh储存的是第一个点,然后通过后面的遍历该点的所有边,并将所有起点为第一个点的边的终点记录下来,所以每次进行这一步时,就是轮到另一个点进行遍历所有边

for(t=sta[k];t;t=c[t].next)//由于sta数组记录的是具有共同起始点的边的领接表,且数值上等于最后一个结构边的下标,然后这里的t没有写关系式,默认就是等于0的时候跳出,然后每次循环后,由于c[t].next记录的是有共同起始点的另一条结构边的下标,所以通过这个可以将具有共同起始点的所有边都遍历完

if(b[c[t].f]&&fa[c[t].f]==0)//首先这里的b数组都是true,然后这里fa[c[t].f]是求该边的终点的父节点,其实这里得到的就是该边的起点,也就是说要保证该终点只有一个父节点,方便后期获得整条最短路径,记录了整个路径的点。同时这里说其为0也是为了避免两条边有共同的终点时对父节点造成影响,避免了这种情况的出现,使得每一个点有且只能出现一次记录父节点

{

op++.zh[op]=c[t].f;fa[c[t].f]=c[t].s;//队列记录终点,父节点记录边的起始点

if(c[t].f==n) break;//每次都要检验一下是否已经到达终点n,此时用的步数肯定是最少的,没有的话就继续循环,有的话就退出循环

}

if(fa[n]) break;//发现终点n已经被找到了,也就是有父节点可以到达,就再跳出最外层循环。

}

}

 

void dfs(int deep)//深搜找到一组删除点数不超过tot的解

{

int i,cl,op,l,k;

if(goal) return;

bfs();//广搜寻找最短路径

if(fa[n]==0)//这里是一种无可奈何的选择,发现了其中的最短路径就是从第一个点到终点n,所以如果这两点之间的距离超过m,那就成立,不用删除任何点,但是如果两点之间的距离小于m,那也没办法,还是要输出,因为这两点都不能删除,也就是说一定会存在最短距离小于m的

{

goal=true;

return;

}

l=0;

for(k=n;k>1;k=fa[k])//通过这里的循环,将每次得到的最短路径的路径节点记录在d[maxn][maxn]中,当然,第一点没有进行记录

{

l++;d[deep][l]=k;//其实仔细一想也可以知道,这里的l记录的就是从起始点到终点n的行进步数

}

if(l>m)//如果此时行进步数大于要求的最小步数m,就直接返回,说明此时删除的点数满足题意

{

goal=true;

return;

}

if (deep>tot) return;//避免删除的点数超过规定的界,也就是说规定删除的点的个数

for (i=2;i<=l;i++)//遍历d[maxn][maxn]中从除n外的点进行按删除点数来进行删除,比如第一轮做删除一个点时,分别尝试去掉最短路径中记录的所有点中的一个,看是否满足题意,如果不满足,则跳出循环,增加界tot,进行删除两个点,由代码可知,在原来删除一个点的基础上得到最短路径再进行删除第二个点,满足题意就跳出得到结果,依次进行下去得到结果。

{

b[d[deep][i]]=false;//这里就是去掉最短路径中一个点,将其设置为false,那么在后期进行dfs时,由于要求b为true时才可以利用该点,现在该点不能用

if(e[d[deep][i]]==0) dfs(deep+1);//这里检验e数组是为了避免同一情况的出现,比如第一个删除第二个点,第二次删除第三个点,然后情况二时第一次删除第三个点,第二次删除第二个点,这样就会重复浪费时间。然后通过dfs记录删除这一个点后的最短路径

b[d[deep][i]]=true;//将该点变为可用,这样就可以进行下一个点的删除实验

e[d[deep][i]]++;//该点已经尝试过的就要进行标记,防止重复实验

}

for (i=2;i<=l;i++) e[d[deep][i]]--;//这里是一个很有趣的设计,由于深度搜索进行删除后,如果某一次深度很大时,这样就会有很多点被标记了,那么后期进行下一轮尝试,也就是在for(i=2;i<=l;i++)这一轮循环中不好进行,没办法进行很完整的深度搜索dfs,所以这里将没能成功深度搜索后得到结果的那些浪费的点通过减1变成可以利用的点,这样就可以继续进行下次深度

}

 

int make()

{

int i,j;

goal=false;//作为一个标志,当goal为true时,输出

for(i=0;i<=n;i++)//每次将“界”增加1,看是否存在解

{

tot=i;

for(j=1;j<=n;j++)

b[j]=true;

memset(e,0,sizeof(e));//同下所示,都是将e数组的值初始化为0

dfs(1);//进行深搜

if(goal) return i;//如果发现goal已经标记为true,就说明此时删除的点数i符合题意

}

return n;//遇到这种情况就可以开心的选择放弃了,没有符合的

}

 

int main()

{

int i,s,f,g;

while(true)

{

cin>>n>>g>>m;//其中n表示点数,g表示边数,m表示从1点到n点的长度最少满足m

if(n==0) break;

memset(sta,0,sizeof(sta));//void *memset(void *s,int ch,size_t n);将 s 中前 n 个字节用 ch 替换并返回 s ,这里将sta初始化为0

now=0;

for(i=1;i<=g;i++)

{

cin>>s>>f;//根据上边输入的边数,这里输入每条边相应的起点还有终点

ins(s,f);//根据输入的起点还有终点来建立边的邻接表

}

g=make();//make函数为迭代加深的主干,递增搜索的深度,一旦找到解就退出

cout<<g<<endl;

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值