次短路和次小生成树算法分析

本文非原创,向原创致敬,地址:https://blog.csdn.net/u010016150/article/details/12992571

[次短路径]

次短路径可以看作是k短路径问题的一种特殊情况,求k短路径有Yen算法等较为复杂的方法,对于次短路径,可以有更为简易的方法。下面介绍一种求两个顶点之间次短路径的解法。

我们要对一个有向赋权图(无向图每条边可以看作两条相反的有向边)的顶点S到T之间求次短路径,首先应求出S的单源最短路径。遍历有向图,标记出可以在最短路径上的边,加入集合K。然后枚举删除集合K中每条边,求从S到T的最短路径,记录每次求出的路径长度值,其最小值就是次短路径的长度。

在这里我们以为次短路径长度可以等于最短路径长度,如果想等,也可以看作是从S到T有不止一条最短路径。如果我们规定求从S到T大于最短路径长度的次短路径,则答案就是每次删边后大于原最短路径的S到T的最短路径长度的最小值。

用Dijkstra+堆求单源最短路径,则每次求最短路径时间复杂度为 O ( N l o g ( N + M ) + M ) O(Nlog(N+M)+M) O(Nlog(N+M)+M),所以总的时间复杂度为 O ( N M ∗ l o g ( N + M ) + M 2 ) O(NM*log(N+M) + M^2) O(NMlog(N+M)+M2)。该估计是较为悲观的,因为一般来说,在最短路径上的边的条数要远远小于M,所以实际效果要比预想的好。

[次小生成树]

类比上述次短路径求法,很容易想到一个“枚举删除最小生成树上的每条边,再求最小生成树”的直观解法。如果用Prim+堆,每次最小生成树时间复杂度为 O ( N l o g ( N + M ) + M ) O(Nlog(N+M)+M) O(Nlog(N+M)+M),枚举删除有 O ( N ) O(N) O(N)条边,时间复杂度就是 O ( N 2 l o g ( N + M ) + N ∗ M ) O(N^2log(N+M) + N*M) O(N2log(N+M)+NM),当图很稠密时,接近 O ( N 3 ) O(N^3) O(N3)。这种方法简易直观,但我们有一个更简单,而且效率更高的 O ( N 2 + M ) O(N^2+M) O(N2+M)的解法,下面介绍这种方法。

首先求出原图最小生成树,记录权值之和为MinST。枚举添加每条不在最小生成树上的边 ( u , v ) (u,v) (u,v),加上以后一定会形成一个环。找到环上权值第二大的边(即除了 ( u , v ) (u,v) (u,v)以外的权值最大的边),把它删掉,计算当前生成树的权值之和。取所有枚举修改的生成树权值之和的最小值,就是次小生成树。

具体实现时,更简单的方法是从每个节点i遍历整个最小生成树,定义 F [ j ] F[j] F[j]为从i到j的路径上最大边的权值。遍历图求出 F [ j ] F[j] F[j]的值,然后对于添加每条不在最小生成树中的边 ( i , j ) (i,j) (i,j),新的生成树权值之和就是
M i n S T + w ( i , j ) − F [ j ] MinST + w(i,j) - F[j] MinST+w(i,j)F[j],记录其最小值,则为次小生成树。

该算法的时间复杂度为 O ( N 2 + M ) O(N^2+M) O(N2+M)。由于只用求一次最小生成树,可以用最简单的 P r i m Prim Prim,时间复杂度为 O ( N 2 ) O(N^2) O(N2)。算法的瓶颈不在求最小生成树,而在 O ( N 2 + M ) O(N^2+M) O(N2+M)的枚举加边修改,所以用更好的最小生成树算法是没有必要的。

[次短路径与次小生成树的例题]

HAOI 2005 路由选择问题直接求次短路径。
pku 3255 Roadblocks稍微特殊的次短路径,允许边重复走。
Ural 1416 Confidential求次小生成树的问题.
pku 1679 The Unique MST 判断最小生成树是否唯一。

#include <iostream>
#include <algorithm>
#include <queue>
const int INF = 0x7fffffff;
const int MAX = 10001;
int n,m;
int num;
int p[MAX];
int max[101][101];

struct Edge		//原始图
{
	int from;
	int to;
	int w;
	bool flag;
}e[MAX];

struct Tree		//最小生成树
{
	int to;
	int w;
	int next;
}tree[202];
int index[101];

struct Node		//生成树的结点
{
	int seq;	//结点编号
	int max;	//从某个点到它的路径中的最大边的长度
};

bool cmp(const Edge &a, const Edge &b)
{
	return a.w < b.w;
}

void makeSet()
{
	for(int i = 0; i <= n; i++)
	{
		p[i] = i;
	}
}

int findSet(int x)
{
	if(x != p[x])
		p[x] = findSet(p[x]);
	return p[x];
}

void addEdge(int from, int to, int w)
{
	tree[num].to = to;
	tree[num].w = w;
	tree[num].next = index[from];
	index[from] = num++;
}

int kruscal()
{
	int i,j;
	int x, y;
	int edgeNum = 0;
	int result = 0;
	makeSet();
	std::sort(e,e+m,cmp);
	for(i = 0; i < m; i++)
	{
		x = findSet(e[i].from);
		y = findSet(e[i].to);
		if(x != y)
		{
			edgeNum++;
			addEdge(e[i].from,e[i].to,e[i].w);
			addEdge(e[i].to,e[i].from,e[i].w);
			e[i].flag = true;
			p[x] = y;
			result += e[i].w;
		}
	}
	return edgeNum == n-1 ? result : -1;
}

void bfs(int p)
{
	int i,j;
	bool used[101];
	memset(used,0,sizeof(used));
	std::queue<Node> que;
	Node now,adj;
	now.max = 0;
	now.seq = p;
	que.push(now);
	used[p] = true;
	while(!que.empty())
	{
		Node q = que.front();
		que.pop();
		for(i = index[q.seq]; i != -1; i = tree[i].next)
		{
			adj.seq = tree[i].to;
			adj.max = tree[i].w;
			if(!used[adj.seq])
			{
				if(q.max > adj.max)
					adj.max = q.max;
				max[p][adj.seq] = adj.max;
				used[adj.seq] = true;
				que.push(adj);
			}
		}
	}
}

void second_MST()
{
	int i,j;
	int mst = kruscal();
	for(i = 1; i <= n; i++)
		bfs(i);
	int smst = INF;
	for(i = 0; i < m; i++)
	{
		if(!e[i].flag)
		{
			if(mst + e[i].w - max[e[i].from][e[i].to] < smst)
				smst = mst + e[i].w - max[e[i].from][e[i].to];
		}
	}
	if(smst == mst)
		printf("Not Unique!\n");
	else
		printf("%d\n",mst);
}

int main()
{
	int i,j;
	int cases;
	int a,b,w;
	scanf("%d",&cases);
	while(cases--)
	{
		scanf("%d %d",&n,&m);
		for(i = 0; i < m; i++)
		{
			scanf("%d %d %d",&e[i].from,&e[i].to,&e[i].w);
			e[i].flag = false;
		}
		num = 0;
		memset(index,-1,sizeof(index));
		second_MST();
	}
	return 0;
}

二、次小生成树:

顾名思义就是在所有的生成树中第二小的生成树,(最小的当然是最小生成树了,废话了),话说求次小生成树有两种方法:

1:首先求出最小生成树 T T T,然后枚举最小生成树上的边,计算除了枚举的当前最小生成树的边以外的所有边形成的最小生成树 T i Ti Ti,然后求最小的Ti就是次小生成树。

2:首先计算出最小生成树T,然后对最小生成树上任意不相邻的两个点 ( i , j ) (i,j) i,j添加最小生成树以外的存在的边形成环,然后寻找i与j之间最小生成树上最长的边删去,计算 m a p [ i ] [ j ] map[i][j] map[i][j](最小生成树以外存在的边) 与 m a x d [ i ] [ j ] maxd[i][j] maxd[i][j](最小生成树上最长的边)差值,求出最小的来, w ( T ) w(T) w(T)再加上最小的差值就是次小生成树了。

这道题的题意是:判断该图的最小生成树是否唯一,有两种办法;

1:求其最小生成树,如果最小生成树的长度与次小生成树的长度相等(这里只要判断最小差值是否为0即可),则不唯一。否则唯一。
算法二,就是一中给的方法,这里不再赘述;

Kruscal实现版:
 
#include <iostream>
#include <algorithm>
#include <string>
#include <cstring>
#include <cstdio>
using namespace std;

const int VN = 5e2+10;
const int EN = 2e5+10;

int V,E,f[VN],p[VN];
struct Edge{int x,y,w;}edge[EN];

bool cmp(Edge a,Edge b)
{
    return a.w < b.w;
}
void SetFather()
{
    for(int i = 0;i <= V;++i)
      f[i] = i;
}
int Kruskal()
{
    SetFather();
    int ans = 0,vertex = 0;
    sort(edge,edge+E,cmp);
    for(int i = 0;i != E;++i)
    {
        int x = edge[i].x;
        int y = edge[i].y;
        int u,v;
        for(u = x;u != f[u];u = f[u])
          f[u] = f[f[u]];
        for(v = y;v != f[v];v = f[v])
          f[v] = f[f[v]];
        if(u != v){
           f[u] = v;
           p[vertex++] = i;
           ans += edge[i].w;
        }
        if(vertex == V-1)break;
    }
    return ans;
}

int SKruskal(int del)
{
    SetFather();
    int ans = 0,vertex = 0;
    for(int i = 0;i != E;++i)
    {
        if(i == del)continue;
        int x = edge[i].x;
        int y = edge[i].y;
        int u,v;
        for(u = x;u != f[u];u = f[u])
          f[u] = f[f[u]];
        for(v = y;v != f[v];v = f[v])
          f[v] = f[f[v]];
        if(u != v){
           f[u] = v;
           vertex++;
           //p[vertex++] = i;
           ans += edge[i].w;
        }
        if(vertex == V-1)break;
    }
    if(vertex < V-1) ans = -1;
    return ans;
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&V,&E);
        for(int i = 0;i != E;++i)
          scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].w);
        int TotCost = Kruskal();
        bool flag = 0;
        for(int i = 0;i != V-1;i++){
          if(SKruskal(p[i]) == TotCost){
              flag = 1;
              break;
          }
        }
        if(flag)
          printf("Yes\n");
        else
          printf("No\n");
    }
    return 0;
}

        

Prim实现版

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

const int inf = 0x7fffffff;
const int size = 501;

int firstpath[size][size];
int secondpath[size][size];
int pre[size];
int d[size];
int tree[size];
int t=0;
bool flag[size];
int V,E;

int qmax(int x,int y)
{
   if(x>y)return x;
   else return y;
}

void prime(int u)
{
   int i,j;
   for(i=1;i<=V;++i)
    {
      flag[i]=false;
      d[i]=inf;
    }
   for(i=1;i<=V;++i)
    for(j=i;j<=V;++j)
     secondpath[i][j]=secondpath[j][i]=-1;
   t=0;
   tree[t++]=u;
   flag[u]=true;

   for(i=1;i<V;++i)
    {
       int min=inf;
       int k;
       for(j=1;j<=V;++j)
        {
           if(d[j]>firstpath[u][j])
            {
              d[j]=firstpath[u][j];
              pre[j]=u;
            }
           if(!flag[j] && d[j]<min)
            {
               min=d[j];
               k=j;
            }
        }

        for(j=0,u=k;j<t;++j)
         {
           secondpath[tree[j]][u]=qmax(secondpath[tree[j]][pre[u]],d[u]);
           secondpath[u][tree[j]]=secondpath[tree[j]][u];
         }
       firstpath[pre[u]][u]=firstpath[u][pre[u]]=inf;
       flag[u]=true;
       tree[t++]=u;
    }

   for(i=1;i<=V;++i)
    for(j=i+1;j<=V;++j)
     if(firstpath[i][j]!=inf && firstpath[i][j]==secondpath[j][i])
      {
        printf("Yes\n");
        return ;
      }
   printf("No\n");
}

int main()
{
   int i,T,j,x,y,z;
   scanf("%d",&T);
   while(T--)
    {
      scanf("%d%d",&V,&E);
      for(i=1;i<=V;++i)
       for(j=i;j<=V;++j)
        firstpath[i][j]=firstpath[j][i]=inf;
      for(i=0;i<E;++i)
       {
          scanf("%d%d%d",&x,&y,&z);
          firstpath[x][y]=firstpath[y][x]=z;
       }
      prime(1);
    }
   return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值