题目抽象:
一个有向图有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;
}
}