poj 1459

来源:互联网 发布:小米5刷机端口被关闭 编辑:程序博客网 时间:2024/06/05 22:48

题目概述

电力系统中有N个节点,编号0到N-1,其中NP个发电厂,NC个用户,剩下的都是中转站,电力通过M条输电上限为l的电线单向传输,每个发电厂产电p,每个用户用电c,中转站不产也不用电,问在电线不超载前提下所有用户能到达的最大用电量和

时限

2000ms/6000ms

输入

每组数据以四个整数N,NP,NC,M开始,其后M个三元组(a,b)l,描述一条电线的起点终点和输电上限,其后NP个二元组(a)p,描述一个发电厂的编号及其发电量,其后NC个二元组(a)c,描述一个用户的编号及其用电量,除元组内部外,空白字符会随意出现,输入到EOF为止

限制

0<=NP,NC<=N<=100;0<=M<=N^2;0<=l<=1000;0<=p,c<=10000

输出

每行一个数,为所求最大用电量和

样例输入

2 1 1 2
(0,1)20 (1,0)10 (0)15 (1)20

7 2 3 13
(0,0)1 (0,1)2 (0,2)5 (1,0)1 (1,2)8 (2,3)1 (2,4)7
(3,5)2 (3,6)5 (4,2)7 (4,3)5 (4,5)1 (6,0)5
(0)5 (1)2 (3)2 (4)1 (5)4

0 0 0 0
1 1 0 0 (0)1
1 0 1 0 (0)1
2 1 1 0 (0)1 (1)1

5 1 1 5
(0,1)20 (1,2)30 (2,3)30 (3,4)10 (3,1)30
(0)100 (4)100

5 1 1 7
(0,1)14 (0,2)12
(1,3)8
(2,1)5 (2,4)16
(3,2)7 (3,4)10
(0)100 (4)100

6 1 1 10
(0,1)16 (0,2)13
(1,2)10 (1,3)12
(2,1)4 (2,4)14
(3,2)9 (3,5)20
(4,3)7 (4,5)4
(0)100 (5)100

11 1 1 21
(0,1)30 (0,2)5 (0,3)20 (0,5)5 (0,10)100
(1,4)10 (1,8)10
(2,5)10
(3,2)5 (3,6)20
(4,7)20
(5,5)60 (5,6)40 (5,10)20
(6,9)40
(8,2)15 (8,4)10 (8,5)10 (8,7)5
(9,8)30 (9,10)10
(0)1000 (10)1000

11 1 3 22
(0,1)30 (0,2)5 (0,3)20 (0,5)5 (0,10)100
(1,4)10 (1,8)10
(2,5)10
(3,2)5 (3,6)20
(4,7)20
(5,5)60 (5,6)40 (5,10)20
(6,9)40
(7,0)20
(8,2)15 (8,4)10 (8,5)10 (8,7)5
(9,8)30 (9,10)10
(0)1000 (5)1000 (7)1000 (10)1000

样例输出

15
6
0
0
0
0
10
20
23
130
150

讨论

图论,最大流,这是一个新天地,难度也很大,主要思想上,除去固定的算法外,需要想到构造一个源点连出到所有发电站,这些边的容量是发电量,构造一个汇点从所有用户流入,这些边的容量是用电量,如此构图,单源单汇,且所有节点都一视同仁,往下就是套固定算法了
首先一点,寻找最大流的基本过程就是不断找增广路进行增广,啥是增广路,啥是增广?原来源点到汇点只有一条路,汇点流量是1,后来你又找到一条路,使得汇点流量变成2了,这个过程就叫增广,后找的这条路就叫增广路,当然这只是通俗解释,实际上这两条路很可能有一大块都是公共部分,因为只要容量扣掉流量后还有剩余,也就是还有残量,这条路就在可选择范围内,而每次增广,实际上也只是将整条增广路上每段的流量提高了一块而已,路仍然在那里,但如上所述,如果流量等于容量,后面就不能再选他增广了
先说ek算法,ek算法基于上面的直接原理进行了一点改进(直接原理叫ff算法,不过慢的出奇),使用bfs找增广路,然后增广路上的流量更新,最大流量也随之更新,另外还要更新反向边的流量,这一句话产生了两个问题,其一,为啥用bfs?实际上dfs或者随便找也行,不过就很可能退化成某慢的出奇的算法了,当然,这里复杂度是没有变的,但确实能快不少,其二,反向边是什么?常见的说法是,给算法一次反悔的机会,其实可以看出来,如果没有反悔的机会,整个算法框架和贪心差不多少,而这样的贪心是有可能出错的,通过反向边,在进行增广后程序仍然可以利用这条边的相反方向来改进,这样的例子很好找,也就不多说了,到这,ek算法主体就算说完了,就这么简单
下一个是dinic算法,其实就是ek的改进,ek每次广搜只找一条增广路,dinic通过层次划分,每次能找好几条,然后通过深搜一条一条增广,直到无法增广,等等,这个层次划分是啥?这就扯到了bfs的最最基本的应用,看图上两点之间最少要经过多少条边,或者说,无权最短路中边的数量,基于和ek用bfs同样的理由,这里也是找最短的增广路,当无法增广时,说明原来的最短路长度已经不再适用,没有那么短的增广路了,那就只能退而求其次,重新再划分层次,看看有没有其他的增广路,如果根本都没法再划分层次,看来是真没了,算法结束
最后是isap算法,插入的gap优化在代码中都标注了出来,不需要可以删掉,再将return most;调整到fun函数最后一行就可以接着用了,cur优化这里没用,那个太麻烦,而且似乎邻接表才能用,isap的思想其实又是dinic的改进,没有必要每次找不到增广路就重新bfs,于是这回只要一次bfs,从汇点开始处理所有节点,没错,汇点,倒过来了,之后的部分倒是差不多,找一条可行的边从源点推进到汇点,确保存在可行边,然后回溯并增广,接着再找一条,这好像还不如dinic,或许吧,但当找不到的时候,没有bfs,而是就地修改层次,这个层次不是上一个点的层次+1,别忘了,最初的bfs是倒着来的,所以得找到下一个点,然后取下一个点层次+1,gap优化主要就是从这里下手,但暂时不提优化的事,这样的点不是随便取的,是需要满足两个条件的,其一,得有残量,这是必须的,其二,找的这个层次要尽量小,这样才能和上一个点再接上,然后回退到上一个点,如果找到了顶多是多推进一次,如果没找到,这个点以后就用不上了,如果这个点已经是源点了,算法结束,再说优化,上面说到点层次的事情,最初bfs的时候可以顺手记一下,每个层次都有多少点,如果出现断层现象,显然是不会再有增广路了,直接算法结束,没有这个优化的话,还得再去找一遍可行边,没有,回退,再找,还没有,再回退,一直退到源点才算完,相当于把残量矩阵遍历了一大块,至于cur优化,黑书上有文字解释,蓝白书上有代码实现,额没用,就不强行解释了
实现层面上,需要想清楚怎样处理输入格式,既然元组内部没有空白字符,那么可以在scanf里捎带处理掉多余字符,其他的空格和换行则利用getchar搭配isspace吞掉,不过由于会多吞掉所有左括号,因而scanf里不能写上左括号
利用这一个题尝试了三种算法,ek,dinic,isap+gap优化,虽然ek有试邻接表形式,但由于题目可能会给完全图,不但更耗时,同时代码也不够清晰,因而三种算法都只贴上邻接矩阵的写法,效率差别还是很明显的
另外样例是来自讨论版的,答案和题解程序都校对过没问题,输入格式基本是保留原本的样子,没有多余的空格,但有多余的换行

题解状态

Edmonds_Karp:264K,938MS,C++,1338B
Dinic:220K,360MS,C++,1429B
Isap:216K,141MS,C++,1637B
Isap+gap优化:216K,79MS,C++,1692B

题解代码

Edmonds_Karp:

#include<cstdio>#include<cstring>#include<algorithm>#include<queue>#include<cctype>//为了isspaceusing namespace std;#define INF 0x3f3f3f3f#define MAXN 104#define memset0(a) memset(a,0,sizeof(a))int N, NP, NC, M, S, T;//N 节点总数 N_power_station 发电厂数 N_consumer 用户 M 线路数 start 虚构的源点编号 target 虚构的汇点编号int F[MAXN][MAXN], C[MAXN][MAXN];//flow 流量矩阵 capacity 容量矩阵int from[MAXN], flow[MAXN];//from 父节点编号 便于回溯 flow 增广流量 用于增广int bfs()//ek的广搜 每次利用广搜找出一条增广路进行增广 返回值就是增广流量{    queue<int>q;//辅助队列 由于每次广搜都需要一个 这样比较方便    memset0(flow);//清空增广流量    q.push(S);    flow[S] = INF;//虚构的源点拥有无穷发电量    while (!q.empty()) {        int a = q.front();        q.pop();        for (int p = 0; p < N; p++)//遍历以找出一条边            if (!flow[p] && F[a][p] < C[a][p]) {//可行的边要满足 其一曾经没有搜索过 因为还没计算过增广流量 其二还有残量 否则电线会超载                q.push(p);                from[p] = a;//记录父节点                flow[p] = min(flow[a], C[a][p] - F[a][p]);//计算增广流量 取到上一个点时增广流量和电线残量的较小者            }        if (a == T)//当已经处理到汇点时 已经找到一条增广路 可以结束了 这时队列未必是空的            break;    }    return flow[T];//如果存在增广路 那么汇点是有增广流量的}int fun(){    S = N++, T = N++;//为虚构源点和汇点分配编号    for (int p = 0; p < M; p++) {//读入中转站        int a, b, c;        while (isspace(getchar()));//利用这个吞掉空白字符        scanf("%d,%d)%d", &a, &b, &c);//input//但结束的时候会多吞掉一个左括号 因而这里就没有了        C[a][b] += c;//采用加的只是防止重边 虽然这个题没有出现    }    for (int p = 0; p < NP; p++) {//读入发电厂        int a, b;        while (isspace(getchar()));        scanf("%d)%d", &a, &b);//input        C[S][a] = b;//源点连接发电厂 这些连接本来就是虚构的 因而不担心重边    }    for (int p = 0; p < NC; p++) {//读入用户        int a, b;        while (isspace(getchar()));        scanf("%d)%d", &a, &b);//input        C[a][T] = b;//用户连接汇点    }    int most = 0, add;//最大流量 增广流量 实际上从这里开始才是ek算法部分    while (add = bfs()) {//利用bfs取得增广路和增广流量 无法取得时也就是没有增广路了        for (int p = T; p != S; p = from[p])//从汇点回溯并更新残量            F[from[p]][p] += add;//实际上应该还有一行将两个下标交换 扣掉add 但是这个题数据略弱 算法不需要反悔的机会        most += add;//更新最大流量    }    return most;}int main(void){    //freopen("vs_cin.txt", "r", stdin);    //freopen("vs_cout.txt", "w", stdout);    while (~scanf("%d%d%d%d", &N, &NP, &NC, &M)) {//input        printf("%d\n", fun());//output        memset0(F);        memset0(C);    }}

Dinic:

#include<iostream>//注意到这里合并了头文件#include<algorithm>#include<queue>using namespace std;#define INF 0x3f3f3f3f#define MAXN 104#define MAXM MAXN*MAXN#define memset0(a) memset(a,0,sizeof(a))int N, NP, NC, M, S, T;int R[MAXN][MAXN], dis[MAXN];//residual 残量矩阵 distance 点到起点的距离 bool bfs()//这里深搜的返回值变成了布尔型 因为增广流量不在这里求了 只需要返回有无增广路即可{    queue<int>q;//由于会提前返回 队可能非空 因而仍然是现用现声明    memset0(dis);    q.push(S);    dis[S] = 1;//没有了父节点数组from和增广流量数组flow 广搜的意义纯粹是划分层次 源点是1层次 因为0代表还没搜索过    while (!q.empty()) {        int a = q.front();        q.pop();        if (a == T)//当到达汇点时停止搜索 返回有增广路 由于下面的深搜要对层次关系进行判断 因而层次大于等于汇点的点都会被忽略(比如先兜个圈再连到汇点) 因而这里也不用算他们了            return 1;        for (int p = 0; p < N; p++)            if (R[a][p] && !dis[p]) {//需要满足两点 其一有残量 其二没搜索过 注意这里有残量也包含反向边 所以就看是正向还是反向先被搜索到了                q.push(p);                dis[p] = dis[a] + 1;//直接相连且未搜索过的点层次自然会更深一层            }    }    return 0;//所有点层次都划分完了 结果汇点不在这里面 即不存在增广路 可以结束了}int dfs(int a, int flow)//深搜进行实际增广操作 原来的flow数组这里变成了单变量 仍然表示增广流量 上一层点的{    if (a == T)//到达汇点就可以停了        return flow;    int left = flow;//剩余部分 一开始是全部的增广流量    for (int p = 0; p < N; p++)        if (left&&R[a][p] && dis[p] == dis[a] + 1) {//这里需要满足三个条件 其一还有剩余部分 否则到这里就停了后面也没必要再算了 不要忘了 dinic算法的深搜是一次对多条增广路进行增广 每增广一次 剩余的部分就会减少一些 因而可能会全部用尽 这条线路到达满载状态 其二有残量 其三满足层次递进关系            int used = dfs(p, min(left, R[a][p]));//减少的剩余部分被更深层次的线路使用 最后会有一部分被算入最大流量 也不难想 到这个点总共的流量flow 扣掉后面线路用掉的used 剩下的就是多余的left            R[a][p] -= used;            R[p][a] += used;//同时处理两个方向            left -= used;//当然这里也要扣掉 上面说了        }    return flow - left;//实际上这里返回的就是used 但used是局部变量 这样能更方便一些}int fun(){    S = N++, T = N++;//读入和基本处理都差不多    for (int p = 0; p < M; p++) {        while (isspace(getchar()));        int a, b, c;        scanf("%d,%d)%d", &a, &b, &c);//input        R[a][b] += c;//只是这里初始化的都是残量矩阵了    }    for (int p = 0; p < NP; p++) {        while (isspace(getchar()));        int b, c;        scanf("%d)%d", &b, &c);//input        R[S][b] = c;    }    for (int p = 0; p < NC; p++) {        while (isspace(getchar()));        int a, c;        scanf("%d)%d", &a, &c);//input        R[a][T] = c;    }    int most = 0;//most 最大流量 从这里才是dinic算法主体    while (bfs())//先以bfs找出一堆增广路 或说划分出一种路的层次        most += dfs(S, INF);//然后每次以深搜进行增广    return most;}int main(void){    //freopen("vs_cin.txt", "r", stdin);    //freopen("vs_cout.txt", "w", stdout);    while (~scanf("%d%d%d%d", &N, &NP, &NC, &M)) {//input        printf("%d\n", fun());//output        memset0(R);    }}

Isap+gap优化:

#include<iostream>#include<algorithm>#include<queue>using namespace std;#define INF 0x3f3f3f3f#define MAXN 104#define memset0(a) memset(a,0,sizeof(a))int N, NP, NC, M, S, T;int R[MAXN][MAXN], dis[MAXN], from[MAXN], gap[MAXN];//相比dinic from又回归了 flow则仍然是单个变量在下面才声明 gap是用来记录每个层次的点的数量 如果某个层次没有点 出现了断层 自然是无法增广的 至于为何会出现断层 继续看queue<int>q;//这回不同 队列必然是空的了 因为只用到一次广搜 而且真正处理了所有节点 回想一下 同样是广搜 ek只找一条增广路 dinic只找层次小于汇点的 isap则是一个不落int fun(){    S = N++, T = N++;//这部分仍然是一样的 不用多看    for (int p = 0; p < M; p++) {        while (isspace(getchar()));        int a, b, c;        scanf("%d,%d)%d", &a, &b, &c);//input        R[a][b] += c;    }    for (int p = 0; p < NP; p++) {        while (isspace(getchar()));        int b, c;        scanf("%d)%d", &b, &c);//input        R[S][b] = c;    }    for (int p = 0; p < NC; p++) {        while (isspace(getchar()));        int a, c;        scanf("%d)%d", &a, &c);//input        R[a][T] = c;    }    gap[0]++;//汇点到自身距离是0 层次0的唯一一个点 从这里开始是bfs部分 这一行是gap优化的部分    q.push(T);//但是要注意 这是反向的bfs 从汇点开始 所以层次也都是相对汇点的层次 不要搞混    while (!q.empty()) {//和dinic的bfs几乎一致 除了没法提前结束        int a = q.front();        q.pop();        for (int p = 0; p < N; p++)            if (!dis[p] && R[p][a]) {//不要记错方向 这个也是反的                dis[p] = dis[a] + 1;                gap[dis[p]]++;//还多了这一行 dis[p]所属层次又多一员 属于gap优化                q.push(p);            }    }//没有返回值 广搜纯粹是划分层次 为所有节点划分    int most = 0, cur = S, flow = INF;//most 最大流量 current 当前节点 这个变量贯穿整个算法 初始是源点 flow 增广流量 初始是正无穷 下面是isap算法主体    while (dis[S] < N) {//当源点汇点之间层次差比节点总数还多时 说明不存在增广路了 因为在最差情况下 所有节点在一条线上 这时层次差也只有N-1        int p;//曾经的临时变量 现在也要贯穿整个算法        for (p = 0; p < N && (!R[cur][p] || dis[cur] != dis[p] + 1); p++);//找出一条可行的边 可行边需要满足两点 其一有残量 其二满足层次递增关系 这一行是邻接矩阵的弊端 找存在的边只能遍历 所幸常数不算太大        flow[S] = INF;//初始化源点为无穷流量 这点和ek倒是很像        if (p != N) {//如果找到了 会在p等于N前停止            from[p] = cur;//记录父节点 方便回溯            flow[p] = min(flow[cur], R[cur][p]);//更新流量 这些和ek都差不多            cur = p;//更新当前节点 这三行在原著论文中叫 Advance 推进            if (cur == T) {//当到达汇点时 开始进行增广                most += flow[T];//首先是最大流量的更新                while (cur != S) {//然后回溯                    p = from[cur];//利用Advance时记录的父节点倒退                    R[p][cur] -= flow[T];                    R[cur][p] += flow[T];//双方向边的处理                    cur = p;//到这里 cur和p的关系很是明了了 其实就是前后相邻两点的编号 两个点都要回溯                }                flow = INF;//flow到这里算用完了 重置回INF 这行很容易被忽略            }        }        else {//没有找到可行的边 或者说 没这么短的了 这个过程叫 Retreat 回溯            if (!--gap[dis[cur]])//当前节点要考虑找一个别的出路 他不再属于当前层次 由于层次是倒着划分的 因而不是上一个节点的层次+1 而是下一个节点的层次+1 但得找到下一个节点 所以需要重新分配 但这样就可能导致断层 因为上一个节点和下一个节点之间层次差可能连最小的都大于2 那么这个点分配在任何位置都再无增广路 算法结束 gap优化就是利用这一点进行简化                return most;            dis[cur] = INF;//重新从正无穷分配一个尽可能小的层次            for (int p = 0; p < N; p++)                if (R[cur][p] && dis[cur] > dis[p] + 1)//从还有残量的边里找层次最小的 这样才可能和上一个节点相连 找不到的时候 会维持INF值 这样由于层次关系判断 以后再也不会用到这个点                    dis[cur] = dis[p] + 1;            gap[dis[cur]]++;//新的层次多了一员 这也是gap优化的一部分            if (cur != S)                cur = from[cur];//回退一步 如果上面找到了 下回无非就是多Advance一次 找不到 这个点就再也用不到了 但如果已经是源点了 那也就无路可退了         }    }}int main(void){    //freopen("vs_cin.txt", "r", stdin);    //freopen("vs_cout.txt", "w", stdout);    while (~scanf("%d%d%d%d", &N, &NP, &NC, &M)) {//input        printf("%d\n", fun());//output        memset0(R);        memset0(dis);        memset0(gap);//gap优化的最后一行 别忘清零    }}

EOF

0 0
原创粉丝点击