PTA 图专题总结(二)(各种相关的图算法)

7-8 哈利·波特的考试(floyed)

https://pintia.cn/problem-sets/15/problems/716

输入样例:

6 11
3 4 70
1 2 1
5 4 50
2 6 50
5 6 60
1 3 70
4 6 60
3 6 80
5 1 100
2 4 60
5 2 80

输出样例:

4 70
// 经典多源最短路径算法
// 求k次最小生成树选择一个最小的或者floyed算法floyed
// 神奇的五行短代码

#include <iostream>
using namespace std;
#define N 105
#define INF  9999999

int main() {
    int n, m;
    cin >> n >> m;
    int map[N][N];
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(i == j)
                map[i][j] = 0;		//初始化一定要对不能全部为INF
            else
                map[i][j] = INF;
    while(m--) {
        int a, b, c;
        cin >> a >> b >> c;
        map[a][b] = map[b][a] = c;
    }
    
    // floyed算法本质上是通过尝试插值松弛来看看是不是存在一个绕路的点是得这个点到别的点的距离更大或者更小.
    //所以最后得到的结果是一个每个点关于其他点的距离的最值
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                if(map[i][j] > map[i][k] + map[k][j])
                    map[i][j] = map[i][k] + map[k][j];
    
    int min = INF;
    int index = 0;
    for(int i = 1; i <= n; i++) {
        int max = 0;
        for(int j = 1; j <= n; j++)
            if(max < map[i][j])
                max = map[i][j];
        if(min > max) {
            min = max;
            index = i;
        }
    }
    if(index == 0)
        cout << 0 << endl;
    else
        cout << index << " " << min << endl;
    return 0;
}

// 经典多源最短路径问题,方便起见还是floyed算法较为简单实现也更加具有针对性.但是我不太明白为什么他的初始化不能全部都是INF。至于Prim,Krustal,最小生成树,Dijkstra算法还是等到更加具有针对性的情况下使用.

7-9 旅游规划(Dijkstra)

https://pintia.cn/problem-sets/15/problems/717

输入样例:	4 5 0 3 
	   	0 1 1 20 
	   	1 3 2 30
	   	0 3 4 10 
	   	0 2 2 20 
	   	2 3 1 20 
输出样例:	3 40
#include <iostream>
#include <cstring>
using namespace std;

#define N 505
#define INF 1000

int main() {
    int n, m, s, d;
    cin >> n >> m >> s >> d;
    int map[N][N][2];
    int visited[N];
    memset(visited, 0, sizeof(visited));
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n; j++)
            if(i == j)
                map[i][j][0] = map[i][j][1] = 0;
            else 
                map[i][j][0] = map[i][j][1] = INF;
    
    while(m--) {
        int a, b, c, e;
        cin >> a >> b >> c >> e;
        map[a][b][0] = map[b][a][0] = c;
        map[a][b][1] = map[b][a][1] = e;
    }
    
    visited[s] = 1;
    // 本质上Dijkstra是一种生成最小生成树的走法,所以最多只要走n-1次就一定能得到结果
    for(int i = 0; i < n; i++) {
        int min = INF;
        int k = -1;
        for(int j = 0; j < n; j++)
            if(!visited[j] && min > map[s][j][0]) {
                k = j;
                min = map[s][j][0];
            }
         // 退出机制
        if(k == -1)
            break;
        else
            visited[k] = 1;
        
        for(int j = 0; j < n; j++) {
			// 有些博主的代码中还会有map[s][j][0] < INF的情况
			// 这里因为我们取的INF比较少,因此不用担心INF相加整型溢出所以不需要加
            if(!visited[j])// 松弛操作
                if(map[s][j][0] > map[s][k][0] + map[k][j][0] ||
                  (map[s][j][0] == map[s][k][0] + map[k][j][0] && map[s][j][1] > map[s][k][1] + map[k][j][1])){
                    map[s][j][0] = map[s][k][0] + map[k][j][0];
                    map[s][j][1] = map[s][k][1] + map[k][j][1];
                }
        }
    }
    
    cout << map[s][d][0] << " " << map[s][d][1] << endl;
    return 0;
}

// 经典单源最短路径,也是最短生成树的一种

7-10 公路村村通 (Krustal + 并查集)

https://pintia.cn/problem-sets/15/problems/718

输入样例:

6 15
1 2 5
1 3 3
1 4 7
1 5 4
1 6 2
2 3 4
2 4 6
2 5 2
2 6 6
3 4 6
3 5 1
3 6 1
4 5 10
4 6 8
5 6 3

输出样例:

12
// 利用并查集判断所给的建设计划是否能够使得所有村落相连通
// 同时因为已经建立了并查集的数据结构,因此可以直接利用Krustal算法
#include <iostream>
#include <cstring>
#include <queue>

using namespace std;
#define N 1005
#define INF 99999
int pre[N];

struct edge {
    int u, v;
    int cost;
    friend bool operator < (edge e1, edge e2) {
        return e1.cost > e2.cost;
    }
}Edge;

int find(int x) {
    if(x != pre[x])
        return pre[x] = find(pre[x]);
    return x;
}

void Union(int x, int y) {
    int a = find(x);
    int b = find(y);
    if(a != b)
        pre[a] = b;
}

int main() {
    int n, m;
    cin >> n >> m;
    priority_queue<edge> q;
    
    for(int i = 1; i <= m; i++) {
        cin >> Edge.u >> Edge.v >> Edge.cost;
        q.push(Edge);
    }
    
    for(int i = 1; i <= n; i++)
        pre[i] = i;
    
    int sum = 0;
    int cnt = 0;
    while(m--) {
        edge l = q.top();
        q.pop();
        if(find(l.u) != find(l.v)) {
            Union(l.u, l.v);
            sum += l.cost;
            cnt++;
        }
        if(cnt == n-1)
            break;
    }
    if(cnt != n-1)
        cout << "-1" << endl;
    else
        cout << sum << endl;
    
    return 0;
}

7-35 城市间紧急救援(Dijstra)

https://pintia.cn/problem-sets/15/problems/862

输入样例:

4 5 0 3
20 30 40 10
0 1 1
1 3 2
0 3 3
0 2 2
2 3 2

输出样例:

2 60
0 1 3
#include <iostream>
#include <stack>
#include <cstring>

using namespace std;
#define N 505
#define INF 9999
int flag = 0;
int pre[N];
stack<int> Stack;

void DFS(int s) {
    if(s == -1)
        return;
    Stack.push(s);
    DFS(pre[s]);
}

int main() {
    int n, m, s, d;
    cin >> n >> m >> s >> d;
    int map[N][N];
    int peo[N];
    int sum[N];
    int way[N];
    int visited[N] = {0};
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n; j++)
            if(i == j)
                map[i][j] = 0;
            else
                map[i][j] = INF;
    for(int i = 0; i < n; i++) {
        cin >> peo[i];
        sum[i] = peo[i];
        way[i] = 1;				// memset函数的问题
        pre[i] = -1;			// pre[i]的初始化问题
    }
    
    while(m--) {
        int a, b, c;
        cin >> a >> b >> c;
        map[a][b] = map[b][a] = c;
    }
    
    // Dijkstra
    visited[s] = 1;
    for(int i = 0; i < n-1; i++) {
        int min = INF;
        int k = -1;					// k的退出机制
        for(int j = 0; j < n; j++)
            if(!visited[j] && min > map[s][j]) {
                min = map[s][j];
                k = j;
            }
        if(k == -1)
            break;
        else
            visited[k] = 1;
        
        for(int j = 0; j < n; j++) {
            if(!visited[j]) {
                if(map[s][j] > map[s][k] + map[k][j]) {
                    map[s][j] = map[s][k] + map[k][j];
                    sum[j] = sum[k] + peo[j];
                    way[j] = way[k];
                    pre[j] = k;
                } else if(map[s][j] == map[s][k] + map[k][j]){
                    way[j] += way[k];
                    if(sum[j] < sum[k] + peo[j]) {
                        sum[j] = sum[k] + peo[j];
                        pre[j] = k;
                    }
                }
            }
        }
    }
    DFS(d);					// DFS段错误
    Stack.push(s);
    cout << way[d] << " " << sum[d] + peo[s] << endl;
    while(!Stack.empty()) {
        int l = Stack.top();
        Stack.pop();
        if(flag++)
            cout << " ";
        cout << l;
    }
   
    return 0;
}

// 终于完美的解决了这道题,第一遍做的时候各种找资料博客去看,有的博主的答案还是错的导致自己debug到怀疑人生。现在来总结一下这道题的坑点,有一说一题还是不错的,但是这道题的测试点是有问题的,他的给的默认的起点是0,导致一些不正确的code也是能够通过测试的。
1.关于pre[i]的初始化问题:

void DFS(int s, int v) {
    if(s == v) {
        cout << s;
        return;
    }
    DFS(s, pre[v]);                 // 为什么pre不能初始化
    cout << " " << v;
}

如果采用如上的方式去遍历每个前驱结点,按照代码的逻辑,我们会发现直到起点之前的那个节点是没有头结点的而我最一开始的初始化是

for(int i = 0; i < n; i++)
	pre[i] = i;

然而

visited[s] = 1;
    for(int i = 0; i < n-1; i++) {
        int min = INF;
        int k = -1;
        for(int j = 0; j < n; j++)
            if(!visited[j] && min > map[s][j]) {
                min = map[s][j];
                k = j;
            }
        if(k == -1)
            break;
        else
            visited[k] = 1;

在这里面我们并没有设置第一个距离起点最短的节点的前驱节点,这就会导致进入DFS遍历的时候,pre[k] == k,陷入死循环,造成段错误.而后我在大佬的提示下去掉了初始化,发现就AC了,但是这个问第二个节点没有前驱节点的问题并没有解决。所以只有可能是测试点所给的起点默认均为0才会造成的情况.
2.关于memset函数的问题
之前看其他blog中好多人用就瞎跟着用了,后来准备讲一个数组全赋值为1时就出了问题,后来还是在某大佬的提示下发现这个函数竟然只能够给数组赋值0和-1,这是错误之二。
3.关于k的退出机制问题

visited[s] = 1;
    for(int i = 0; i < n-1; i++) {
        int min = INF;
        int k = -1;
        for(int j = 0; j < n; j++)
            if(!visited[j] && min > map[s][j]) {
                min = map[s][j];
                k = j;
            }
        if(k == -1)
            break;
        else
            visited[k] = 1;

在这段代码中有无这个k的退出机制是没有关系的,但是当 i < n或者更大的数是,假设我们把所有的点都访问完了,如果只是int k;然后进入下面的操作,此时k变量存储位置中的数字是电脑中的一个不知道是什么的数,很容易造成数组的访问越界,所以当初这个地方的段错误也有是因为这个的原因.
4.这道题的关于queue方法找路径和Dijkstra算法本身的理解
第二遍做的时候一直想要试一试不用前驱结点来尝试这找到他的路径,然后就尝试使用了queue,然后插入在

for(int j = 0; j < n; j++)
            if(!visited[j] && min > map[s][j]) {
                min = map[s][j];
                k = j;
            }
        if(k == -1)
            break;
        else
            visited[k] = 1;
            q.push(k);

后来发现测试集的结果是吧所有的节点都输出来了,当时非常不能理解,后来又去重新学习了一下Dijkstra算法,发现这个算法是通过对每个点进行松弛操作来得到最短路径的,所以最后整个循环之后一定会把所有的节点全部压入queue中,所以并不能在松弛的过程中同时看queue的压入顺序来寻找路径,而现在暂时想不出比pre[i]更好的解决方案.算是对这个算法本身细节的理解更加深刻了一些。

7-50 畅通工程之局部最小花费问题(Krustal + 并查集 / Prime)

https://pintia.cn/problem-sets/15/problems/897

输入样例:

4
1 2 1 1
1 3 4 0
1 4 1 1
2 3 3 0
2 4 2 1
3 4 5 0

输出样例:

3
#include <iostream>
#include <queue>

using namespace std;
#define N 105
int pre[N];
struct edge {
    int u, v;
    int cost;
    friend bool operator < (edge a, edge b) {
        return a.cost > b.cost;
    }
}Edge;

int find(int x) {
    if(x != pre[x])
        return pre[x] = find(pre[x]);
    return x;
}

void Union(int x, int y) {
    int a = find(x);
    int b = find(y);
    if(a != b)
        pre[a] = b;
}

int main() {
    int n; 
    cin >> n;
    int len = n*(n-1)/2;
    priority_queue<edge> q;
    
    for(int i = 1; i <= n; i++)
        pre[i] = i;
    
    while(len--) {
        int a, b, c, d;
        cin >> a >> b >> c >> d;
        if(d == 1)
           Union(a, b);
        else {
            Edge.u = a;
            Edge.v = b;
            Edge.cost = c;
            q.push(Edge);
        }
    }
    int sum = 0;
    while(!q.empty()) {
        edge l = q.top();
        q.pop();
        if(find(l.v) != find(l.u)) {
            Union(l.u, l.v);
            sum += l.cost;
        }
    }
    cout << sum << endl;
}

// 采用优先队列简化程序
法二(Prim)

// Prime算法
#include <iostream>
#define INF 9999
#define N 105
using namespace std;

int main() {
    int n;
    cin >> n;
    int map[N][N];
    int visited[N] = {0};
    int len = n*(n-1)/2;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(i != j)
                map[i][j] = INF;
            else
                map[i][j] = 0;
    
    while(len--) {
        int a, b, c, d;
        cin >> a >> b >> c >> d;
        if(d == 1)
            map[a][b] = map[b][a] = 0;
        else
            map[a][b] = map[b][a] = c;
    }
    
    visited[1] = 1;
    int sum = 0;
    for(int i = 0; i < n-1; i++) {
        int min = INF;
        int k = -1;
        for(int j = 1; j <= n; j++)
            if(!visited[j] && min > map[1][j]) {
                min = map[1][j];
                k = j;
            }
        
        if(k == -1)
            break;
        else
            visited[k] = 1;
        sum += map[1][k];
        
        for(int j = 1; j <= n; j++)
            if(!visited[j] && map[1][j] > map[k][j])
                map[1][j] = map[k][j];
    }
    
    cout << sum << endl;
    return 0;
}

// 这中解法的重点在于

 for(int j = 1; j <= n; j++)
            if(!visited[j] && map[1][j] > map[k][j])
                map[1][j] = map[k][j];

可能是我Dijkstra写太多了在第二遍做的时候也写成了松弛,其实如果要让代码更加清楚一些应该再重新开一个dist[]数组来表示与已访问点相连接的最短边,每次找到最短的那个,加入sum,本质上也是求一个最短生成树.整理了一下果然清晰很多.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值