关于各种算法的分析,这篇文章讲的很清楚,包括优化等问题。文章链接
下面是我对算法的一些个人理解和我自己学的时候的疑惑的解释。
(目前只学会了最朴素形式的Dijkstra算法,其他的过几天再更)
Dijkstra 算法(类似最小生成树的prim算法)
将起点的距离设为0,然后进行n次循环,每次先找到到起点距离最短的点u,再将该点作为中转点,更新与其链接点到起点的最短距离,如果之前与其链接的点与起点的距离小于以点u为中转点到起点的距离,就不更新,反之,则更新,这样就保证了,每一步完成后,所以未标记点到起点的距离都是暂时最小的(因为后序的更新可能会改变剩下未标记点最短路的距离),因为我们求的是最短路,所以我们选一个目前dis最小的节点进行扩展。若是选择最大路径开始扩展,那么以后的点扩展到该点时时,若是有到该点的路径小于原路径的值,但是由于这点之前以及扩展过了,就不会更新最小值了,从而出现错误,而选择当前最小值的点进彳亍扩展,就是保证,每一次扩展该点并标记后,该点已经达到起点到该点的最短路了。
关于为什么该算法不能处理带有负权重的图,请参考这篇文章关于为什么不能带有处理负权值的图
下面以hdu2544作为模板hdu2544最短路
/*Dijkstra算法 单源最短路径,不能处理负值权*/
#include <iostream>
#include <string.h>
#define INF 99999999
#define MAXN 105
using namespace std;
int vis[MAXN],dis[MAXN],map[MAXN][MAXN];/*标记,起点到各个点最短距离,储存图*/
int Dijkstra(int start,int n)/*这里传的n即当做点的个数,也作为循环终点*/
{
for(int i = 1; i <= n; i++){
dis[i] = INF;
}
dis[start] = 0;
for(int i = 1; i <= n; i++){
int mina = INF;
int k;
for(int j = 1; j <= n; j++){/*所有未标记点中选取dis最小的点*/
if(!vis[j]&&dis[j] < mina){
mina = dis[j];
k = j;
}
}
vis[k] = 1;/*标记结点*/
if(k == n){/*当到达要计算的终点时,返回起点到该点的最短路*/
return dis[n];
}
for(int j = 1; j <= n; j++){
if(!vis[j]&&dis[j] > dis[k] + map[k][j]){//更新起点到未到达点的最短路,保证dis
dis[j] = dis[k] + map[k][j]; //中存储的值都是目前位置起点1到该点的最短路径值
}
}
}
}
int main()
{
int m,n;
while(cin>>n>>m&&m + n != 0){
memset(vis,0,sizeof(vis));
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
map[i][j] = INF;
}
}
int from,to,s;
for(int i = 0; i < m; i++){
cin>>from>>to>>s;
if(map[from][to] > s){
map[from][to] = map[to][from] = s;/*防止出现重复边*/
}
}
int minx = Dijkstra(1,n);
cout<<minx<<endl;
}
return 0;
}
Dijsktra 算法实现最短路例题Silver Cow Party
Floyd算法:
这个算法刚开始确实不太好理解,这篇文章就很值观的展示了该算法的实现过程Floyd算法
负环:负环肯定就是整个环就是负的哇,而环是一个整体,肯定是组环的所有权值的加和了
另外需要注意的是:Floyd-Warshall算法不能解决带有“负权回路”(或者叫“负权环”)的图,因为带有“负权回路”的图没有最短路。例如下面这个图就不存在1号顶点到3号顶点的最短路径。因为1->2->3->1->2->3->…->1->2->3这样路径中,每绕一次1->-2>3这样的环,最短路就会减少1,永远找不到最短路。其实如果一个图中带有“负权回路”那么这个图则没有最短路。
该算法可以处理负权值的问题,但是不能解决负权值环的问题,可以判断是不是有负环,但是时间卡的很紧有时候会超时,所以之后还是多用spfa吧。
下面给出核心代码
void Floyd(int n){//核心代码
for(int k = 1; k <= n; k++){
for(int i = 1; i <= n; i++){
if(discow[i][k] == INF){//剪枝减少运算量
continue;
}
for(int j = 1; j <= n; j++){
discow[i][j] = min(discow[i][j],discow[i][k] + discow[k][j]);
}
}
}
}
下面给出例题poj2139 Six Degrees of Cowvin Bacon
ac代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string.h>
#define INF 100000
#define MAXN 305
#define MAX 3005
using namespace std;
int discow[MAXN][MAXN],x[MAX];
void init(int n){//初始化
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
if(i == j){
discow[i][j] = 0;
}
else{
discow[i][j] = INF;
}
}
}
}
void Floyd(int n){//核心代码
for(int k = 1; k <= n; k++){
for(int i = 1; i <= n; i++){
if(discow[i][k] == INF){
continue;
}
for(int j = 1; j <= n; j++){
discow[i][j] = min(discow[i][j],discow[i][k] + discow[k][j]);
}
}
}
}
int main(){
int n,m;
cin>>n>>m;
init(n);
for(int i = 1; i <= m; i++){
int t;
cin>>t;
memset(x,0,sizeof(x));
for(int j = 1; j <= t; j++){
cin>>x[j];
}
for(int j = 1; j <= t; j++){
for(int k = j + 1; k <= t; k++){
discow[x[j]][x[k]] = discow[x[k]][x[j]] = 1;
}
}
}
Floyd(n);
int ans = INF;
for(int i = 1; i <= n; i++){
int temp = 0;
for(int j = 1; j <= n; j++){
temp += discow[i][j];
}
ans = min(ans,temp);
}
ans = ans * 100 / (n - 1);//减少误差
cout<<ans<<endl;
return 0;
}
Floyd算法判断负环
poj3259 Wormholes
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string.h>
#define INF 100000
#define MAXN 505
#define MAX 3005
using namespace std;
int discow[MAXN][MAXN];
void init(int n){//初始化
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
if(i == j){
discow[i][j] = 0;
}
else{
discow[i][j] = INF;
}
}
}
}
int Floyd(int n){//核心代码
for(int k = 1; k <= n; k++){
for(int i = 1; i <= n; i++){
if(discow[i][k] == INF){
continue;
}
for(int j = 1; j <= n; j++){
if(discow[i][j] > discow[i][k] + discow[k][j]){
discow[i][j] = discow[i][k] + discow[k][j];
}
}
if(discow[i][i] < 0){
return 1;
}
}
}
return 0;
}
int main(){
int n,m,w,t;
cin>>t;
while(t--){
cin>>n>>m>>w;
init(n);
int from,to,s;
for(int i = 1; i <= m; i++){
cin>>from>>to>>s;
if(discow[from][to] > s){
discow[from][to] = discow[to][from] = s;
}
}
for(int i = 1; i <= w; i++){
cin>>from>>to>>s;
discow[from][to] = -s;
}
if(Floyd(n)){
cout<<"YES"<<endl;
}
else{
cout<<"NO"<<endl;
}
}
return 0;
}
因为太懒了。。所以转了一个spfa判断负环的代码,这里是原文代码出处
这里说一下,为什么有负环的条件是大于n,因为每两个点之间的最短路是k - 1条边,最远的两点可能距离n - 1 条边,所以就设置n为判断条件,当然如果不考虑时间也可以设成更大的值,但是没必要。
再尝试解释下一个疑惑,为什么用单源最短路算法可以判断负环,因为若是存在负环,那么我们规定的开始起点,到负环起点的距离,一定是不断缩小的,也就是该环上的点一定会不断重复进入队列,出队列,导致队列不可能为空,也就照应了如果某点进入队列次数>n的判断条件
#include <stdio.h>
#include <algorithm>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 600;
const int INF = 0x3f3f3f3f;
int G[N][N], dis[N], num[N], n, s;
bool vis[N];
int spfa()
{
queue<int>que;
for(int i = 1; i <= n; i++)
{
dis[i] = INF;
num[i] = 0;
}
dis[1] = 0;
vis[1] = true;
que.push(1);
while(!que.empty())
{
int now = que.front();
que.pop();
vis[now] = false;//记得还原!!
for(int i = 1; i <= n; i++)
{
if(dis[now]+G[now][i]<dis[i])
{
dis[i] = dis[now]+G[now][i];
if(!vis[i])
{
vis[i] = true;
num[i]++;
if(num[i] > n) return 1;
que.push(i);
}
}
}
}
return 0;
}
int main()
{
// freopen("in.txt", "r", stdin);
int f, m, e, w, t;
scanf("%d", &f);
while(f--)
{
scanf("%d%d%d", &n, &m, &w);
memset(vis, 0, sizeof(vis));
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
{
if(i == j) G[i][j] = 0;
else G[i][j] = INF;
}
for(int i = 1; i <= m; i++)
{
scanf("%d%d%d", &s, &e, &t);
if(t < G[s][e]) G[s][e] = G[e][s] = t;
}
for(int i = 1; i <= w; i++)
{
scanf("%d%d%d", &s, &e, &t);
G[s][e] = -t;
}
int ans = spfa();
if(ans) printf("YES\n");
else printf("NO\n");
}
return 0;
}
比赛的时候遇到最短路问题,结果邻接图空间爆了,唉偷懒了呀,这里再补充一下链式前向星的存储方法链式前向星
关于链式存储,真的比邻接图要好用的多,不会被卡空间上,以后建议多使用链式存储
这里以上面hdu2544为模板,把上面的代码修改了一下
#include <iostream>
#include <string.h>
#define MAXM 10010
#define MAXN 105
#define INF 9999999
using namespace std;
typedef struct {
int to;
int v;
int next;
}Edge;
Edge edge[MAXM * 2];
int vis[MAXN],head[MAXN],dis[MAXN],cnt;
void addedge(int a,int b,int c){
cnt++;
edge[cnt].next = head[a];
edge[cnt].to = b;
edge[cnt].v = c;
head[a] = cnt;
}
int dij(int start ,int n){
for(int i = 1; i <= n; i++){
dis[i] = INF;
}
dis[start] = 0;
for(int j = 1; j <= n; j++){
int min = INF;
int u;
for(int i = 1; i <= n; i++){
if(!vis[i]&&dis[i] < min){
min = dis[i];
u = i;
}
}
vis[u] = 1;
for(int i = head[u]; i; i = edge[i].next){
if(!vis[edge[i].to]&&dis[edge[i].to] > edge[i].v + dis[u]){
dis[edge[i].to] = edge[i].v + dis[u];
}
}
}
return dis[n];
}
int main(){
int n,m;
while(cin>>n>>m,m + n != 0){
int a,b,c;
cnt = 0;
memset(vis,0,sizeof(vis));
memset(head,0,sizeof(head));
for(int i = 1; i <= m; i++){
cin>>a>>b>>c;
addedge(a,b,c);
addedge(b,a,c);
}
int start = 1;
int ans = dij(start,n);
cout<<ans<<endl;
}
return 0;
}
spfa版本的链式存储代码模板,模板依旧为上面的hdu2544
#include <iostream>
#include <string.h>
#include <queue>
#define MAXM 10010
#define MAXN 105
#define INF 9999999
using namespace std;
typedef struct {
int to;
int v;
int next;
}Edge;
Edge edge[MAXM * 2];
int vis[MAXN],head[MAXN],dis[MAXN],cnt;
void addedge(int a,int b,int c){
cnt++;
edge[cnt].next = head[a];
edge[cnt].to = b;
edge[cnt].v = c;
head[a] = cnt;
}
int spfa(int start,int n){
for(int i = 1; i <= n; i++){
dis[i] = INF;
}
dis[start] = 0;
queue <int> q;
q.push(start);
vis[start] = 1;
while(!q.empty()){
int temp = q.front();
q.pop();
vis[temp] = 0;
for(int i = head[temp]; i; i = edge[i].next){
if(dis[edge[i].to] > dis[temp] + edge[i].v){
dis[edge[i].to] = dis[temp] + edge[i].v;
if(!vis[edge[i].to]){
q.push(edge[i].to);
vis[edge[i].to] = 1;
}
}
}
}
return dis[n];
}
int main(){
int n,m;
while(cin>>n>>m,m + n != 0){
int a,b,c;
cnt = 0;
memset(vis,0,sizeof(vis));
memset(head,0,sizeof(head));
for(int i = 1; i <= m; i++){
cin>>a>>b>>c;
addedge(a,b,c);
addedge(b,a,c);
}
int start = 1;
int ans = spfa(start,n);
cout<<ans<<endl;
}
return 0;
}
这个题是最短路的变形,利用的是spaf的链式存储算法。传送门ICPC Pacific Northwest Regional Contest 2019 C Coloring Contention 最短路
偷懒就被打脸的我,总是在比赛被制裁后再来补。。
这里补一下栈堆优化后的Dijsktra算法,本质就是用优先队列,来替代第一个循环的查找,对于大于1e5,就需要使用了
这里把比赛的题当模板吧
传送门
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <cstdio>
#define ll long long
#define MAXN 1000005 + 10
#define INF 9999999
using namespace std;
const ll mod = 1e9+7;
struct node{
int to;
int v;
int next;
}edge[4 * MAXN];
struct enode{
int dis,to;
};
bool operator <(const enode a,const enode b){
return a.dis > b.dis;
}
int head[MAXN],cnt,vis[MAXN],dis[MAXN];
ll fast(ll a,ll b){
ll ans = 1;
while(b){
if(b % 2){
ans = ans * a % mod;
}
a = a * a % mod;
b /= 2;
}
return ans;
}
void addedge(int a,int b,int val){
cnt++;
edge[cnt].next = head[a];
edge[cnt].to = b;
edge[cnt].v = val;
head[a] = cnt;
}
void dij(int start,int n){
for(int i = 1; i <= n; i++){
dis[i] = INF;
}
dis[start] = 0;
priority_queue <enode> q;
q.push(enode {0,start});
while(!q.empty()){
enode x = q.top();
q.pop();
int u = x.to;
vis[u] = 1;
for(int i = head[u]; i != 0; i = edge[i].next){
if(!vis[edge[i].to]&&dis[edge[i].to] > edge[i].v + dis[u]){
dis[edge[i].to] = edge[i].v + dis[u];
q.push(enode {dis[edge[i].to],edge[i].to});
}
}
}
}
int main(){
int m,n;
scanf("%d%d",&n,&m);
for(int i = 1; i <= m; i++){
int a,b;
scanf("%d%d",&a,&b);
addedge(a,b,1);
addedge(b,a,1);
}
dij(1,n);
ll ans = 0;
for(int i = 2; i <= n; i++){
ans = (ans + fast(2,dis[i])) % mod;
}
printf("%lld",ans);
return 0;
}
没错,又来更了,最近发现一种使用邻接矩阵给出数据的题目,我又无奈去学习了一下BFS解法和DFS的记忆化搜索
BFS其实和spfa感觉差不多,本质都是不断松弛邻接矩阵上的某一点,不同在于bfs的思路还是从每个点向外扩展,好像。。还是跟spfa差不多。。算了,就当是邻接矩阵版本的spfa吧(doge)
例题
hdu1428
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef long long ll;
const int INF = 99999999;
const int MAXN = 55;
struct node
{
int x;
int y;
};
int dir[4][2]= {{1,0},{0,1},{-1,0},{0,-1}};
ll maps[MAXN][MAXN],dis[MAXN][MAXN],a[MAXN][MAXN],vis[MAXN][MAXN];
bool if_fair(int x,int y,int n)
{
if(x >= 0&&y >= 0&&x < n&&y < n){
return true;
}
return false;
}
void BFS(int n)
{
memset(vis,0,sizeof(vis));
queue <node> q;
node now,next;
now.x = n - 1;
now.y = n - 1;
dis[n - 1][n - 1] = maps[n - 1][n - 1];
vis[now.x][now.y] = 1;
q.push(now);
while(!q.empty()){
now = q.front();
q.pop();
vis[now.x][now.y] = 0;
for(int i = 0; i < 4; i++){
next.x = now.x + dir[i][0];
next.y = now.y + dir[i][1];
if(if_fair(next.x,next.y,n)){
//松弛操作,参考spfa
if(dis[now.x][now.y] + maps[next.x][next.y] < dis[next.x][next.y]){
dis[next.x][next.y] = dis[now.x][now.y] + maps[next.x][next.y];
//松弛后如果没有入队列,就进入队列,还是参考spfa
if(!vis[next.x][next.y]){
vis[next.x][next.y] = 1;
q.push(next);
}
}
}
}
}
}
ll DFS(int x,int y,int n)
{
if(a[x][y] > 0){
return a[x][y];
}
ll count = 0;
for(int i = 0; i < 4; i++){
int xx = x + dir[i][0];
int yy = y + dir[i][1];
if(if_fair(xx,yy,n)){
//只要下一步的点到终点的距离小于上一个点的距离,就有解
if(dis[xx][yy] < dis[x][y]){
count += DFS(xx,yy,n);
}
}
}
a[x][y] = count;
return a[x][y];
}
int main()
{
int n;
while(cin>>n){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
cin>>maps[i][j];
dis[i][j] = INF;
}
}
BFS(n);
memset(a,0,sizeof(a));
a[n - 1][n - 1] = 1;
ll ans = DFS(0,0,n);
cout<<ans<<endl;
}
return 0;
}