最短路问题(2)

  上节课解决的最短路是在具有某些特点的图上进行的。对于更广泛的图,
求最短路是有专门算法的。
一 迪杰斯特拉(DIJKSTRA)算法
它继承了广搜的“松弛”特性,是一种加权的松弛动归刷新算法。
解决从一个定点出发到另一点或所有点的最短路问题。
细致概念见百度百科,下面直接给出其矩阵存储形式的简化代码描述
#include<iostream>
using namespace std;
int main()
{//求从1到n的最短路
int i,j,n,m,a,b,p,q,r,l;
int f[101],g[101][101],h[101]={};
memset(f,4,sizeof(f));
memset(g,4,sizeof(g));
cin>>n>>m;//n个点m条边
for(i=1;i<=m;i++)
{
cin>>p>>q>>r;
g[p][q]=r;
g[q][p]=r;
}
f[1]=0;
h[1]=1;//表示1号点已经永久标记
l=1;//前面永久标记过的最后一点事1
for(i=1;i<n;i++)
{
p=0;//用来记录临时标记点中f最小一点的点号
for(j=1;j<=n;j++)
if(h[j]<1)//j是临时标记点
{
if(f[l]+g[l][j]<f[j])f[j]=f[l]+g[l][j];
if(f[j]<f[p]) p=j;
}
l=p;
h[p]=1;//该点即被标定
}
for (i = 1; i <= n; i++)
cout << f[i] << ' ';
cout << endl;
system("pause");
return 0;
}
此算法的两个要点是松弛(刷新)和选小,也就是那两个最长的if语句行。
在许多情况下,可以得到比上面N平方算法更简化的描述。
例如刷新范围可能被缩小,每次选小代价可能降为log2(n)甚至o(1)。


二 弗洛伊德算法
它的原理与迪杰斯特拉算法相似,但解决的是多点对多点的最短路问题。
所以计算量大约是后者的n倍。参考代码如下:
#include<iostream>
using namespace std;
int f[101][101];
int main()
{
int x,y,z,n,m;
memset(f,63,sizeof(f));
cin>>n>>m;
for(int i=1;i<=m;i++)
  {
  cin>>x>>y>>z;
  f[x][y]=z;
  f[y][x]=z;
  }
for(int k=1;k<=n;k++)
  for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
      if(f[i][k]+f[k][j]<f[i][j])
        f[i][j]=f[i][k]+f[k][j];
while(cin>>x>>y)//重复输出多对x、y间的距离
  cout<<f[x][y]<<endl;
return 0; 
}


三贝尔曼福德算法
每轮对所有边或弧进行出点对入点的刷新,n-1轮后所有点与出发点的距离就被求出了。
它也是从一点出发走向所有点求最短路的算法。
下面给出贝尔曼福德算法的简单代码描述:
#include<iostream>
#include<string.h>
using namespace std;
int main()
{//从k号点出发求到所有点的最短路长度
int m,n,i,j,k,p,q,r;
int f[100],a[5000],b[5000],c[5000];
cin>>n>>m>>k;
for(i=1;i<=m;i++)
cin>>a[i]>>b[i]>>c[i];
memset(f,1,sizeof(f));
f[k]=0;
for(i=1;i<n;i++)
for(j=1;j<=m;j++)
{
f[a[j]]=min(f[a[j]],f[b[j]]+c[j]);
f[b[j]]=min(f[b[j]],f[a[j]]+c[j]);
}
for(i=1;i<=n;i++)
cout<<f[i]<<' ';
cout<<endl;
return 0;
}


四 SPFA最短路算法
上面三的算法计算量有可能达到N的三次方,其中许多次刷新显然是不起作用的。
基于这种认识,我们可以只做有可能奏效的那些比较和刷新。
具体地说,任务队列中仅保存刷新成功的点,因为没被刷新成功的点不可能对其他点刷新成功。
下面的代码既使用了邻接矩阵又使用了邻接表,
美中不足是邻接表没有用VECTOR,存储空间定义不尽合理。
#include<iostream>
using namespace std;
int main()
{
int i,n,m,p,q,r;
int f[101], g[101][101], a[10001], t = 1, w = 0, b[101][101] = {};
bool h[101];
memset(f, 4, sizeof(f));
memset(g, 4, sizeof(g));
cin >> n >> m;
for(i = 1; i <= m; i++)
{
cin >> p >> q >> r;
g[p][q] = r;
b[p][++b[p][0]]=q;
b[q][++b[q][0]] = p;
}
f[1] = 0;//从1出发
h[1]=1;//1已经在任务队列中
a[++w] = 1;//1号点进入任务队列
while(t <= w)
{//下面的b是邻接表,b[][0]是表长
for(i=1;i<=b[a[t]][0];i++)
{
if(f[a[t]]+g[a[t]][b[a[t]][i]]<=f[b[a[t]][i]])
{
f[b[a[t]][i]] = f[a[t]] + g[a[t]][b[a[t]][i]];
if (h[b[a[t]][i]] == 0)
{//若未在任务队列中就进队
h[b[a[t]][i]] = 1;
a[++w] = b[a[t]][i];
}
}
}
h[a[t]] = 0;
t++;
}
for (i = 1; i <= n; i++)
cout<<f[i] << ' ';
cout << endl;
system("pause");
return 0;
}
为体现这些算法解决竞赛题的实力,
我们仍以曾记忆化深搜过的“排雷”为例,这显然是一个最短路问题。
例题1 用DJS算法和SPFA算法分别解决排雷问题
1 DJS
#include<fstream>
#include<cstdlib>
#include <algorithm>
using namespace std;
ifstream cin("mine.in");
ofstream cout("mine.out");
int a[40][40],b[40][40],f[40][40];
main()
{
int min,x,y,i,j,n,x0,y0,x1,y1;
cin >> n;
cin >> x0 >> y0;
cin >> x1 >> y1;
for(i=1;i<=n;i++)
  for(j=1;j<=n;j++)
    {
    cin >> a[i][j];
    f[i][j]=1000000000;
    }
f[x0][y0]=a[x0][y0];
f[0][0]=INT_MAX;
for(b[x0][y0]=1;b[x1][y1]==0;b[x][y]=1)
  {//先用4个if语句向4个方向松弛、刷新,未用方向矢量
  if(a[x0+1][y0]+f[x0][y0]<f[x0+1][y0])
    f[x0+1][y0]=a[x0+1][y0]+f[x0][y0];
  if(a[x0-1][y0]+f[x0][y0]<f[x0-1][y0])
    f[x0-1][y0]=a[x0-1][y0]+f[x0][y0];
  if(a[x0][y0+1]+f[x0][y0]<f[x0][y0+1])
    f[x0][y0+1]=a[x0][y0+1]+f[x0][y0];
  if(a[x0][y0-1]+f[x0][y0]<f[x0][y0-1])
    f[x0][y0-1]=a[x0][y0-1]+f[x0][y0];
  x=y=0;
  for(i=1;i<=n;i++)
    {
    for(j=1;j<=n;j++)
      {
      if(b[i][j]==0 && f[i][j]<f[x][y])
        {
        x=i;
        y=j;
        }
      }
  }
  x0=x;
  y0=y;
  }
cout << f[x1][y1];
return 0;
}
2 SPFA
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <queue>
using namespace std;
int a[40][40]={},b[40][40]={},f[40][40];
const int x[4]={1,-1,0,0},y[4]={0,0,1,-1};
main()
{
queue< pair<int,int> > q;
int i,j,n,x0,y0,x1,y1,k;
cin >> n;
cin >> x0 >> y0;
cin >> x1 >> y1;
for(i=1;i<=n;i++)
  for(j=1;j<=n;j++)
    {
    cin >> a[i][j];
    f[i][j]=1000000000;
    }
f[x0][y0]=a[x0][y0];
q.push(make_pair(x0,y0));任务队列初始化
while(!q.empty())
  {
  x0=q.front().first;
  y0=q.front().second;
  q.pop();
  b[x0][y0]=0;
  for(k=0;k<4;k++)
    {
    i=x0+x[k];
    j=y0+y[k];
    if(a[i][j]+f[x0][y0]<f[i][j])
      {
      f[i][j]=a[i][j]+f[x0][y0];
      if(b[i][j]==0)
        {//(i,j)进队
        q.push(make_pair(i,j));
        b[i][j]=1;
        }
      }
    }
  }
cout << f[x1][y1];
system("pause");
return 0;
}


例题2 用弗洛伊德算法搜索每个数字能到达的点数,最终解决“产生数”问题。
#include<iostream>
#include<cstring>
#include<stdio.h>
using namespace std;
int f[16][16],b[16];
void flyd()
{
int i,j,k;
for(k=0;k<=9;k++)
for(i=0;i<=9;i++)
for(j=0;j<=9;j++)
if(f[i][k]+f[k][j]<f[i][j])f[i][j]=f[i][k]+f[k][j];
for(i=0;i<=9;i++)
{
f[i][i]=1;
for(j=0;j<=9;j++)
b[i]+=f[i][j]<10;
}
}
int main()
{
char a[32];
int i,k,x,y;
double t=1;
freopen("p3.in","r",stdin);
freopen("p3.out","w",stdout);
cin>>a>>k;
memset(f,1,sizeof(f));
for(i=1;i<=k;i++)
{
cin>>x>>y;
f[x][y]=1;
}
flyd();
for(i=0;i<strlen(a);i++)
t*=b[a[i]-'0'];
printf("%.00f\n",t);
return 0;
}
此代码中变量t的类型和输出格式耐人寻味,你想通了吗?


例题3 NOIP普及组2012 文化之旅
4.文化之旅
(culture.cpp/c/pas)
【问题描述】
有一位使者要游历各国,他每到一个国家,都能学到一种文化,
但他不愿意学习任何一种文化超过一次(即如果他学习了某种文化,则他就不能到达其他有这种文化的国家)
。不同的国家可能有相同的文化。
不同文化的国家对其他文化的看法不同,有些文化会排斥外来文化(即如果他学习了某种文化,则他不能到达排斥这种文化的其他国家)。
现给定各个国家间的地理关系,各个国家的文化,每种文化对其他文化的看法,
以及这位使者游历的起点和终点(在起点和终点也会学习当地的文化)
,国家间的道路距离,试求从起点到终点最少需走多少路。
【输入】
输入文件 culture.in。
第一行为五个整数 N,K,M,S,T,每两个整数之间用一个空格隔开,
依次代表国家个数(国家编号为1到N)
,文化种数(文化编号为 1 到 K)
,道路的条数,以及起点和终点的编号(保证S不等于T);
第二行为N个整数,每两个整数之间用一个空格隔开,其中第i个数Ci,表示国家i的文化为Ci。
接下来的 K 行,每行 K 个整数,每两个整数之间用一个空格隔开,记第 i 行的第 j 个数
为aij,aij=1表示文化i排斥外来文化j(i等于j时表示排斥相同文化的外来人)aij= 0表示不排斥
,(注意 i排斥j并不保证j一定也排斥i)。
接下来的 M 行,每行三个整数 u,v,d,每两个整数之间用一个空格隔开,表示国家 u
与国家 v 有一条距离为 d 的可双向通行的道路(保证 u 不等于 v,两个国家之间可能有多条
道路)。【输出】
输出文件名为 culture.out。
输出只有一行,一个整数,表示使者从起点国家到达终点国家最少需要走的距离数(如
果无解则输出-1)。
【输入输出样例 1】
culture.in
2
1
0
1
1
2 1 1 2
2
1
0
2 10
culture.out
-1
【输入输出样例说明】
由于到国家 2 必须要经过国家 1,而国家 2 的文明却排斥国家 1 的文明,所以不可能到
达国家 2。
【输入输出样例 2】
culture.in
2 2 1 1 2
1 2
0 1
0 0
1 2 10
culture.out
10
【输入输出样例说明】
路线为 1 -> 2。
【数据范围】
对于 20%的数据,有 2≤N≤8,K≤5;
对于 30%的数据,有 2≤N≤10,K≤5;
对于 50%的数据,有 2≤N≤20,K≤8;
对于 70%的数据,有 2≤N≤100,K≤10;
对于 100%的数据,有 2<N<100,1<K<100,1<M<N 2,1≤ki≤K,1≤u, v≤N,1≤d≤1000,
S≠T,1 ≤S, T≤N。
分析 这显然是一个加了限制的最短路问题
1 用贝尔曼福德算法加文化矛盾的判断
#include<fstream>
#include<algorithm>
using namespace std;
ifstream cin("culture.in");
ofstream cout("culture.out");
int a[3001],b[3001],c[3001],d[101][101],e[1001],f[101];
int main()
{
    int i,j,n,k,m,s,t,x,y;
    cin>>n>>k>>m>>s>>t;
    memset(f,63,sizeof(f));
    for(i=1;i<=n;i++)
    cin>>e[i];
    for(i=1;i<=k;i++)
    { 
    for(j=1;j<=k;j++)
    cin>>d[i][j];
    d[i][i]=1;
    }
    for(i=0;i<m;i++)
    cin>>a[i]>>b[i]>>c[i];
    f[s]=0;
    for(i=1;i<n;i++)
    for(j=0;j<m;j++)
    {
        x=a[j];
        y=b[j];
        if(d[e[y]][e[x]]==0 && f[x]+c[j]<f[y])
        f[y]=f[x]+c[j];
        if(d[e[x]][e[y]]==0 && f[y]+c[j]<f[x])
        f[x]=f[y]+c[j];
    }
    if(f[t]>1000000000)
    cout<<-1<<endl;
    else
    cout<<f[t]<<endl;
    return 0;
}
2 逆向前效记忆搜索
#include<fstream>
#include<math.h>
#include<stdio.h>
#include<stack>
#include<string.h>
#include<bitset>
#include<algorithm>
using namespace std; 
ifstream cin("culture.in");
ofstream cout("culture.out");
struct node
{
    int x;
    bitset <101> d;
};
stack<node> sk;
bitset<101> a[101],d;
int g[101][101];
int f[101];
int main()
{
    int i, j, u, v, z, n, k, m ,s ,t, c[101];
    node p;
    memset(f,63,sizeof(f));
cin >> n >> k >> m >> s >> t;
for (i = 1; i <= n; i++)
cin >> c[i];
for (i = 1; i <= k; i++)
{
for (j = 1; j <= k; j++)
{
cin >> u;
a[i][j] = u;
}
        a[i][i]=1;
    }
    for (i = 1; i <= m; i++)
{
cin >> u >> v >> z;
g[u][v] = g[v][u] = z;
}
p.x=t;
p.d=a[c[t]];
    sk.push(p);
    d=a[c[t]];//d表示抵制文化的集合
    f[t]=0;
    int q=0;
    while(!sk.empty())
    {
      if(++q>n)
      {
        if(q==s)
        break;
        q=sk.top().x;
        sk.pop();
        if(!sk.empty())
        d=sk.top().d;
    }
    else if(g[sk.top().x][q]>0 && d[c[q]]==0 && f[sk.top().x]+g[sk.top().x][q]<f[q])
    {
      f[q]=f[sk.top().x]+g[sk.top().x][q];//对q点的f进行记忆
      p.x=q;
      p.d=d|=a[c[q]];
      sk.push(p);
      q=0;
      }
    }
  if(f[s]<1000000000)
    cout<<f[s]<<endl;
    else 
    cout<<"-1"<<endl;
    return 0;
}
练习
1 奇怪的电梯
  在N层楼中有一部升降式电梯,电梯间的门上有一个玻璃窗,透过它可以看到电梯所在楼层墙上写着的一个正整数。表示在此层楼按“向上”键或“向下”键能够生或降的层数变化量。电梯中除了这两个键没有别的键可按。
 求从A层到B层至少移动过多少层?
1<n<100


2 NOIP提高组2001 CAR的旅行路线


program project1;
type
  rec = record
    x, y, c: longint;
  end;
var
  n, tot, costf, st, ed, tc: longint;
  pos: array[0..2000] of rec;
  x, y: array[1..4] of longint;
  costt: array[0..200] of longint;
  b: array[0..200] of boolean;
  d: array[0..100] of real;
  
procedure init;
var
  i, j, k: longint;
begin
  readln(n,costf,st,ed);
  for i := 1 to n do
    begin
      readln(x[1],y[1],x[2],y[2],x[3],y[3],costt[i]);
      for j := 1 to 3 do
        for k := 1 to 3 do
          if j <> k then
            if (x[j]-x[k])*(x[6-k-j]-x[j])+(y[j]-y[k])*(y[6-k-j]-y[j]) = 0 then
              begin
                x[4] := x[k]+x[6-k-j]-x[j];
                y[4] := y[k]+y[6-k-j]-y[j];
              end;
      for j := 1 to 4 do
        begin
          inc(tot);
          pos[tot].x := x[j];
          pos[tot].y := y[j];
          pos[tot].c := i;
        end;
    end;
  //for i := 1 to tot do writeln(pos[i].x,' ',pos[i].y,' ',pos[i].c);
end;


function dis(x, y: longint):real;
begin
  dis := sqrt(sqr(pos[x].x-pos[y].x)+sqr(pos[x].y-pos[y].y));
  if pos[x].c = pos[y].c then
    dis := dis*costt[pos[x].c]
  else dis := dis*costf;
end;


function dij(st: longint):real;
var
  i, j, p: longint;
  min: real;
begin
  fillchar(b,sizeof(b),false);
  fillchar(d,sizeof(d),100);
  d[st] := 0;
  for i := 1 to tot do
    begin
      min := 1e38;
      for j := 1 to tot do
        if (not b[j]) and (d[j] < min) then
          begin
            min := d[j];
            p := j;
          end;
      b[p] := true;
      for j := 1 to tot do
        if not b[j] then
          if d[j] > d[p]+dis(p,j) then d[j] := d[p]+dis(p,j);
    end;
  dij := 1e38;
  for i := 1 to tot do
    begin
      if pos[i].c = ed then
        for j := 0 to 3 do
          if d[i+j] < dij then dij := d[i+j];
      if pos[i].c = ed then break;
    end;
end;


procedure work;
var
  i, j: longint;
  min, temp: real;
begin
  min := 1e38;
  for i := 1 to tot do
    begin
      if pos[i].c = st then
        for j := 0 to 3 do
          begin
            temp := dij(i+j);
            if temp < min then min := temp;
          end;
      if pos[i].c = st then break;
    end;
  writeln(min:0:1);
end;


begin
  assign(input,'d:\in.txt');reset(input);
  assign(output,'d:\out.txt');rewrite(output);
  readln(tc);
  while tc > 0 do
    begin
      init;
      work;
      dec(tc);
    end;
  close(input);
  close(output);
end.
3 CCC2009 pencil 买铅笔
4 NOIP提高组2009 最优贸易
5 把海淀竞赛“排雷”用DJS的堆优化方式重新描述
选作NOIP提高组2013DAY2华容道
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值