【深大算法设计与分析】实验六 最大流应用问题(棒球赛问题) 实验报告 附代码、数据集

目录

一、实验目的与要求

二、实验内容与方法

三、实验步骤与过程

四、实验结论与体会

尾注


一、实验目的与要求

实验目的:

1. 掌握最大流算法思想。

2. 学会用最大流算法求解应用问题。

实验要求:

1. 解释流网络的构造原理。

2. 解释为什么最大流能解决这个问题。

3. 给出上面四个球队的求解结果。

4. 尽可能实验优化的最大流算法。

      

二、实验内容与方法

        棒球赛问题:

图:四个球队的比赛情况

        上面的表是四个球队的比赛情况,现在的问题是哪些球队有机会以最多的胜利结束这个赛季?可以看到蒙特利尔队因最多只能取得 80 场胜利而被淘汰,但亚特兰大队已经取得 83 场胜利,蒙特利尔队因为wi + ri < wj 而被淘汰。费城队可以赢83场,但仍然会被淘汰。如果亚特兰大输掉一场比赛,那么其他球队就会赢一场。所以答案不仅取决于已经赢了多少场比赛,还取决于他们的对手是谁。

        请利用最大流算法给出上面这个棒球问题的求解方法。

三、实验步骤与过程

1. 解释流网络的构造原理,以及为什么最大流能解决这个问题。

        本问题中,我们的求解思路如下:

        首先讨论某一个队伍(记为A队)是否有机会在赛季中取胜的问题,我们首先可以确定的是:

        ①显然,A赢下接下来所有的比赛的情况是对其最优的,如果这种情况仍不能取胜,则不可能取胜;

        ②在①的情况下,A获胜的场次必须大于当前胜场最多的队伍。

        但是,即使是同时满足了条件①、②,A也有可能无法在赛季中取胜。这是因为其他队伍之间也会进行比赛,而这些比赛有可能导致某几个队伍无论如何最终胜场都会超过A,使得A无法取胜。

        为了讨论A是否有机会取胜的问题,我们可以首先假设A可以取胜:A赢得了所有未进行的比赛。其他队伍完成了所有的比赛,但是胜场都没超过A

        记A的最终胜场maxA,对于队伍B,记B当前胜场为currentB,则B最多还能赢下来的比赛场次是maxA-currentB(如果能全部赢下来,AB正好并列),其他所有队伍最多能赢下来的场次数以此类推。

        接下来以“分配胜场”的方式进行求解:将与A无关的比赛的胜场,分配给除A的所有队伍,且使他们的胜场数都不超过A(此处使用上面的“最多还能赢下来的比赛场次”变量实现),即完成了比赛过程。而如果胜场无法全部分配,说明最终有队伍的胜场数会maxA,A无法取胜。分配胜场的过程,即可以使用最大流算法来实现。

        本问题的流网络就可以由此得出:

        对于每一个比赛队伍,我们需要对其构建一个流网络(还是以A为例):

        网络中的节点可以分为四类,且每一类在网络中处于同一层,同层之间互不相连:

        源节点:从源节点出去的流量是与A无关的比赛的胜场数;

        比赛节点:表示的是与A无关的所有比赛;

        队伍节点:表示除A的所有队伍;

        汇节点:最终到达汇的流量就是假设A取胜,能进行的比赛数量,如果这个数量等于剩余的比赛数量,说明比赛可以完成,A可以取胜,反之则不能。

        源节点连接所有的比赛节点,边的容量就是对应比赛的场数,表示剩余的胜场分别对应的比赛。

        单个比赛节点会连接两个队伍节点,表示这场比赛可以为这两个队伍带来胜场。边的容量也是比赛的场数。

        队伍节点连接汇节点,边的容量是此队伍最多还能赢下来的比赛场次。

        以假设New York队伍取胜对应的流网络为例(为方便显示,图中以首字母表示对应队伍,队伍A-队伍B表示队伍间的比赛):

图:假设New York队伍取胜对应的流网络

        综上所述,我们可以使用最大流算法求解这个问题:构建出某个队伍对应的流网络之后,寻找图的最大流,如果最大流等于未进行的,与此队伍无关的比赛的场次数,就可以说明此队伍有机会取胜。

2.给出上面四个球队的求解结果。

        使用Ford-Fulkerson思路编写代码求解上述的最大流问题,可以得出结果:

        Atlanta、New York有机会获胜;PhillyMontreal不可能获胜。

3. 尽可能实验优化的最大流算法。

        首先是针对于本问题的优化:在上述的流结构中,中间的路径容量为比赛的场次,但是这些容量实际上并不会对最终的结果产生限制,因此我们可以直接将这些容量全部置为无限(代码中设置为一个较大值即可),以此优化初始化过程所用的时间。

        接下来是对于最大流算法的实现以及优化:

        本实验中,我们分别使用DFS算法和BFS算法(即EK算法)对残留网络进行搜索,来寻找增广路径。这两种方法中,每次寻找增广路径需要遍历整张图,时间复杂度为O(E),最多进行VE次增广路径寻找,因此时间复杂度为\(O(VE^2)\)。

        接着,我们实现了Dinic算法:

        Dinic算法中,我们需要对残留网络进行分层,接着在分层结果下进行深度优先搜索,寻找所有的,沿着层次递增方向的增广路径,接下来重复以上步骤,直到找不到增广路径为止。

        充分性证明:在上述的算法中,如果残留网络中不存在由源到汇的路径,就说明已经找到了最大流。本算法则认为:如果分层网络中找不到由源到汇的路径,则找到最大流,但没有考虑同层间的边。因此我们可以通过证明:同层间的边是可以不用考虑的,来证明算法的充分性:

        反证法:假设存在这样的残留网络,只能通过同层间的边找到源到汇的路径(图中节点的数字表示层数):

图:假设情况

        在此图中,有且仅有一条路径:S -> 1A -> 1B -> T 能从源到达汇。但是可以发现,此路径存在错误:S -> 1B 这条边的容量是0,构造层次图时不会将这条边加入,因此1B节点实则不是在第一层。反过来,如果要将此结点放在第一层,则S -> 1B 必须具有容量,从而上述的路径就将不是唯一的了。

        以此类推,某节点能出现在层次图中,说明必然存在由源到这个节点的层次递增的路径,到达此节点可以不需要经过同层节点。

        因此,如果层次图中不存在一个能到达汇节点的节点,就可以说明已经找到最大流,且此时已经找不到增广路径。

        可以发现,在此方法中,由于每次深度优先搜索不需要遍历整张图,最多只需要遍历V条边。与EK算法相同,最多需要进行VE次网络分层,因此最终的时间复杂度为\(O(V^2E)\)。对于比较稠密的图,此算法相比于EK算法具有较大优势。

       最后,我们使用不同的数据集对以上的算法进行了测试,以下是效率统计的结果:

图:不同算法的运行效率结果

图:所用时间与算法、问题规模的关系

       如图所示,使用dfs和bfs算法进行增广路径寻找的效率接近,而Dinic算法具有更短的运行时间,且问题规模越大优势越明显。

        对于实验结果的补充说明:

        实验中我们可能会产生这样的疑问:为什么Dinic算法在本实验中会拥有比EK算法更好的表现呢?产生这样的疑问是因为:在本实验构成的图中,无论如何进行分层,每个节点所处的层数永远是固定的(因为相同层之间的节点并不会互相连接),那么Dinic算法的分层好像就失去了作用。

        通过阅读代码可知:EK算法在进行寻找增广路径时,每次会使用BFS找到一条增广路径,然后直接更新残留网络并开始下一次搜索。

        而Dinic算法在找到一条增广路径之后并不会直接开始重新构建层次图,而是会回溯到上一层的节点,继续进行寻找,直到当前路径的流量用尽。也就是说,Dinic算法的一次搜索就可以找到多条路径,从而使其在解决规模较大的问题时具有更明显的优势。

        本实验所用的代码:为了避免测试过程中的代码修改,此处将解决最开始的小规模问题的代码和解决较大规模问题的代码分开展示:

        首先是解决小规模问题的代码(未进行优化,数据集已经在代码中给出,可以直接运行):

#include <stack>
#include <iostream>
#include <string>
#include <queue>
using namespace std;

#define MAX_TEAMS 4         // 球队数量
#define MAX_GAMES 6         // 最大比赛数量
#define SIZE (2 + MAX_TEAMS + MAX_GAMES) // 节点总数: 源节点、汇节点、球队节点和比赛节点

int capacity[SIZE][SIZE];     // 容量矩阵
int residualCapacity[SIZE][SIZE];  // 残余容量矩阵,是FF方法的重点
int map_by_level[SIZE];    //在Dinic算法中要用到的数据结构

//FF方法的一种具体实现,利用的DFS寻找增广路径
bool FF_dfs(int source, int target, int map_by_parent[]) {
    bool visited[SIZE];
    memset(visited, 0, sizeof(visited));

    stack<int> intstack;
    intstack.push(source);
    visited[source] = true;
    map_by_parent[source] = -1;

    //DFS
    while (!intstack.empty()) {
        int u = intstack.top(); intstack.pop();
        for (int v = 0; v < SIZE; v++) {
            if (!visited[v] && residualCapacity[u][v] > 0) {
                intstack.push(v);
                map_by_parent[v] = u;
                visited[v] = true;
            }
        }
    }
    return visited[target];
}
int FordFulkerson(int source, int target) {
    int u, v, max_flow = 0;
    int map_by_parent[SIZE];

    // 初始化残余容量矩阵
    for (u = 0; u < SIZE; u++) {
        for (v = 0; v < SIZE; v++) {
            residualCapacity[u][v] = capacity[u][v];
        }
    }

    // 增广路径寻找
    while (FF_dfs(source, target, map_by_parent)) {
        int path_flow = INT_MAX;

        // 找到最小的残余容量作为增广路径上的流量
        for (v = target; v != source; v = map_by_parent[v]) {
            u = map_by_parent[v];
            path_flow = (path_flow < residualCapacity[u][v]) ? path_flow : residualCapacity[u][v];
        }

        // 更新残余网络
        for (v = target; v != source; v = map_by_parent[v]) {
            u = map_by_parent[v];
            residualCapacity[u][v] -= path_flow;
            residualCapacity[v][u] += path_flow;
        }

        max_flow += path_flow;
    }

    return max_flow;
}

//判断函数
bool canWin(int teamIndex, int wins[], int to_play[], int against[][MAX_TEAMS], int CurrentTeam_max_wins) {
    // 初始化容量矩阵
    memset(capacity, 0, sizeof(capacity));

    int source = MAX_TEAMS + MAX_GAMES; // 源节点
    int target = source + 1; // 汇节点
    int nodeIndex = MAX_GAMES; // 比赛节点

    // i和j代表两只不同的队伍
    for (int i = 0; i < MAX_TEAMS; i++) {
        if (i == teamIndex) continue; // 跳过被检查的球队
        //已经假定该队伍全赢了,自然比赛肯定是全打了的,那比赛全比了就没必要再看他了

        //从队伍到汇节点,容量为 CurrentTeam_max_wins - wins[i]
        //注意这里的CurrentTeam_max_wins是讨论的那个队伍的比赛的最大胜场
        capacity[i][target] = CurrentTeam_max_wins - wins[i];

        for (int j = i + 1; j < MAX_TEAMS; j++) {
            if (j == teamIndex) continue; // 跳过被检查的球队

            //从源节点到比赛节点
            //从比赛节点到队伍节点设置为最大值
            //因为对于棒球问题,这个边并不参与限制,所以为了方便设置一个最大值
            if (against[i][j] > 0) {
                capacity[source][nodeIndex] = against[i][j];
                capacity[nodeIndex][i] = INT_MAX;
                capacity[nodeIndex][j] = INT_MAX;
                nodeIndex++;
            }
        }
    }

    // 初始化残余容量矩阵
    for (int u = 0; u < SIZE; u++) {
        for (int v = 0; v < SIZE; v++) {
            residualCapacity[u][v] = capacity[u][v];
        }
    }

    //计算最大流
    int max_flow = FordFulkerson(source, target);

    // 计算总的比赛场次
    int total_matches = 0;
    for (int i = 0; i < MAX_TEAMS; i++) {
        for (int j = i + 1; j < MAX_TEAMS; j++) {
            if (i == teamIndex || j == teamIndex) continue; //同样记得跳过讨论的队伍
            total_matches += against[i][j];
        }
    }

    return max_flow == total_matches;
}

int main() {
    // 输入数据
    int wins[MAX_TEAMS] = { 83, 80, 78, 77 };
    int to_play[MAX_TEAMS] = { 8, 3, 6, 3 };
    int against[MAX_TEAMS][MAX_TEAMS] = {
        {-1, 1, 6, 1},
        {1, -1, 0, 2},
        {6, 0, -1, 0},
        {1, 2, 0, -1}
    };

    bool can_team_win[MAX_TEAMS];
    for (int i = 0; i < MAX_TEAMS; i++) {
        int CurrentTeam_maxWins = wins[i] + to_play[i];//当前队伍的最大获胜场数=已经赢的+还没打的
        can_team_win[i] = canWin(i, wins, to_play, against, CurrentTeam_maxWins);
    }

    string teamNames[MAX_TEAMS] = { "Atlanta", "Philly", "New York", "Montreal" };

    for (int i = 0; i < MAX_TEAMS; i++) {
        if (can_team_win[i]) {
            cout << teamNames[i] << "有机会获胜" << endl;
        }
        else {
            cout << teamNames[i] << "不可能获胜" << endl;
        }
    }

    return 0;
}

        解决此类问题代码,输入:队伍数量,接下来分别输入每个队伍的名字、胜场数、败场数(无效信息,但是找到的数据集中有)、剩余场数以及与每只队伍的剩余比赛场数(包含自己,但是为0):

#include <stack>
#include <iostream>
#include <string>
#include <queue>
#include<chrono>
using namespace std;
using namespace std::chrono;

#define MAX_TEAMS 54         // 最大队伍数量
int teams;               // 队伍数量
#define MAX_GAMES 250       // 最大比赛数量
#define SIZE (2 + MAX_TEAMS + MAX_GAMES) // 节点总数: 源节点、汇节点、球队节点和比赛节点

int capacity[SIZE][SIZE];     // 容量矩阵
int residualCapacity[SIZE][SIZE];  // 残余容量矩阵,是FF方法的重点
int map_by_level[SIZE];    //在Dinic算法中要用到的数据结构

//FF方法的一种具体实现,利用的DFS寻找增广路径
bool FF_dfs(int source, int target, int map_by_parent[]) {
    bool visited[SIZE];
    memset(visited, 0, sizeof(visited));

    stack<int> intstack;
    intstack.push(source);
    visited[source] = true;
    map_by_parent[source] = -1;

    //DFS
    while (!intstack.empty()) {
        int u = intstack.top(); intstack.pop();
        for (int v = 0; v < SIZE; v++) {
            if (!visited[v] && residualCapacity[u][v] > 0) {
                intstack.push(v);
                map_by_parent[v] = u;
                visited[v] = true;
            }
        }
    }
    return visited[target];
}
int FordFulkerson(int source, int target) {
    int u, v, max_flow = 0;
    int map_by_parent[SIZE];

    // 初始化残余容量矩阵
    for (u = 0; u < SIZE; u++) {
        for (v = 0; v < SIZE; v++) {
            residualCapacity[u][v] = capacity[u][v];
        }
    }

    // 增广路径寻找
    while (FF_dfs(source, target, map_by_parent)) {
        int path_flow = INT_MAX;

        // 找到最小的残余容量作为增广路径上的流量
        for (v = target; v != source; v = map_by_parent[v]) {
            u = map_by_parent[v];
            path_flow = (path_flow < residualCapacity[u][v]) ? path_flow : residualCapacity[u][v];
        }

        // 更新残余网络
        for (v = target; v != source; v = map_by_parent[v]) {
            u = map_by_parent[v];
            residualCapacity[u][v] -= path_flow;
            residualCapacity[v][u] += path_flow;
        }

        max_flow += path_flow;
    }

    return max_flow;
}

//EK算法,本质上就是用BFS寻找增广路径,其他地方是一样的
bool EK_bfs(int source, int target, int map_by_parent[]) {
    bool visited[SIZE];
    memset(visited, 0, sizeof(visited));

    queue<int> q;
    q.push(source);
    visited[source] = true;
    map_by_parent[source] = -1;

    // BFS
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v = 0; v < SIZE; v++) {
            if (!visited[v] && residualCapacity[u][v] > 0) {
                q.push(v);
                map_by_parent[v] = u;
                visited[v] = true;
            }
        }
    }
    return visited[target];
}
int EdmondsKarp(int source, int target) {
    int u, v, max_flow = 0;
    int map_by_parent[SIZE];

    // 初始化残余容量矩阵
    for (u = 0; u < SIZE; u++) {
        for (v = 0; v < SIZE; v++) {
            residualCapacity[u][v] = capacity[u][v];
        }
    }

    // 增广路径寻找
    while (EK_bfs(source, target, map_by_parent)) {
        int path_flow = INT_MAX;

        // 找到最小的残余容量作为增广路径上的流量
        for (v = target; v != source; v = map_by_parent[v]) {
            u = map_by_parent[v];
            if (residualCapacity[u][v] < path_flow) {
                path_flow = residualCapacity[u][v];
            }
        }

        // 更新残余网络
        for (v = target; v != source; v = map_by_parent[v]) {
            u = map_by_parent[v];
            residualCapacity[u][v] -= path_flow;
            residualCapacity[v][u] += path_flow;
        }

        max_flow += path_flow;
    }

    return max_flow;
}

//Dinic算法,先使用BFS构建分层网络,然后再使用DFS在分层网络中寻找增广路径
bool Dinic_bfs(int s, int t) {
    memset(map_by_level, -1, sizeof(map_by_level));
    queue<int> q;
    q.push(s);
    map_by_level[s] = 0;

    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v = 0; v < SIZE; v++) {
            if (map_by_level[v] < 0 && residualCapacity[u][v] > 0) {
                map_by_level[v] = map_by_level[u] + 1;
                q.push(v);
            }
        }
    }
    return map_by_level[t] >= 0;
}
int Dinic_dfs(int u, int t, int flow) {
    if (u == t) return flow;
    for (int v = 0; v < SIZE; v++) {
        if (map_by_level[v] == map_by_level[u] + 1 && residualCapacity[u][v] > 0) {
            int current_flow = (flow < residualCapacity[u][v]) ? flow : residualCapacity[u][v];
            int temp_flow = Dinic_dfs(v, t, current_flow);

            if (temp_flow > 0) {
                residualCapacity[u][v] -= temp_flow;
                residualCapacity[v][u] += temp_flow;
                return temp_flow;
            }
        }
    }
    return 0;
}
int Dinic(int s, int t) {
    int max_flow = 0;

    while (Dinic_bfs(s, t)) {
        while (true) {
            int flow = Dinic_dfs(s, t, INT_MAX);
            if (flow == 0) break;
            max_flow += flow;
        }
    }
    return max_flow;
}

//判断函数
bool canWin(int teamIndex, int wins[], int to_play[], int against[][MAX_TEAMS], int CurrentTeam_max_wins) {
    // 初始化容量矩阵
    memset(capacity, 0, sizeof(capacity));

    int source = teams + MAX_GAMES; // 源节点
    int target = source + 1; // 汇节点
    int nodeIndex = teams; // 比赛节点

    // i和j代表两只不同的队伍
    for (int i = 0; i < teams; i++) {
        if (i == teamIndex) continue; // 跳过被检查的球队
        //已经假定该队伍全赢了,自然比赛肯定是全打了的,那比赛全比了就没必要再看他了

        //从队伍到汇节点,容量为 CurrentTeam_max_wins - wins[i]
        //注意这里的CurrentTeam_max_wins是讨论的那个队伍的比赛的最大胜场
        capacity[i][target] = CurrentTeam_max_wins - wins[i];

        for (int j = i + 1; j < teams; j++) {
            if (j == teamIndex) continue; // 跳过被检查的球队

            //从源节点到比赛节点
            //从比赛节点到队伍节点设置为最大值
            //因为对于棒球问题,这个边并不参与限制,所以为了方便设置一个最大值
            if (against[i][j] > 0) {
                capacity[source][nodeIndex] = against[i][j];
                capacity[nodeIndex][i] = INT_MAX;
                capacity[nodeIndex][j] = INT_MAX;
                nodeIndex++;
            }
        }
    }

    // 初始化残余容量矩阵
    for (int u = 0; u < SIZE; u++) {
        for (int v = 0; v < SIZE; v++) {
            residualCapacity[u][v] = capacity[u][v];
        }
    }

    //计算最大流:FordFulkerson或EdmondsKarp或Dinic
    int max_flow = Dinic(source, target);

    // 计算总的比赛场次
    int total_matches = 0;
    for (int i = 0; i < teams; i++) {
        for (int j = i + 1; j < teams; j++) {
            if (i == teamIndex || j == teamIndex) continue; //同样记得跳过讨论的队伍
            total_matches += against[i][j];
        }
    }

    return max_flow == total_matches;
}

int main() {

    int wins[MAX_TEAMS];
    int to_play[MAX_TEAMS];
    int against[MAX_TEAMS][MAX_TEAMS];

    bool can_team_win[MAX_TEAMS];//用于保存结果
    string teamNames[MAX_TEAMS];//保存队伍的名字

    cin >> teams;
    for (int i = 0; i < teams; i++) {
        int losses;//败场数,本题中不需要这个变量,但是找到的数据集有,直接舍弃
        cin >> teamNames[i] >> wins[i] >> losses >> to_play[i];
        for (int j = 0; j < teams; j++) {
            cin >> against[i][j];
        }
    }

    auto beginTime = system_clock::now();
    for (int i = 0; i < teams; i++) {
        int CurrentTeam_maxWins = wins[i] + to_play[i];//当前队伍的最大获胜场数=已经赢的+还没打的
        can_team_win[i] = canWin(i, wins, to_play, against, CurrentTeam_maxWins);
    }
    duration<double> diff1 = system_clock::now() - beginTime;
    cout << "用时:" << diff1.count() << 's' << endl;

    for (int i = 0; i < teams; i++) {
        if (can_team_win[i]) {
            cout << teamNames[i] << "有机会获胜" << endl;
        }
        else {
            //cout << teamNames[i] << "不可能获胜" << endl;
        }
    }

    return 0;
}

四、实验结论与体会

实验结论:

        本实验我们通过构建模型,将“是否有可能在赛季中取胜”的问题转化为了最大流问题。并基于FF思想,编写了基于dfs的增广路径寻找算法、基于bfs的增广路径寻找算法(EK算法)以及Dinic算法。成功解决了本问题。并对更大规模的问题进行了效率测试,验证了Dinic算法的高效性。

实验体会:

       本实验的难点主要在于构建模型,使用最大流算法解决相应问题。构建模型时要不拘泥于以往的思想,认为网络中所有的节点都是等价,而是要将本题的特点考虑到图的构建中。从而构建出“源点-比赛节点-队伍节点-汇点”结构的流网络。

尾注

        本实验的问题比较简单,并没有太大的优化空间,只需找到数据集,做好测试即可顺利完成。

        至此,算法设计与分析的全部实验均已完成,期末难度可能较高,祝大家取得好成绩!

        如有疑问欢迎讨论,如有好的建议与意见欢迎提出,如有发现错误则恳请指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值