02太空飞行计划问题
W 教授正在为国家航天中心计划一系列的太空飞行。每次太空飞行可进行一系列商业性实验而获取利润。现已确定了一个可供选择的实验集合E={E1,E2,…,Em},和进行这
些实验需要使用的全部仪器的集合I={I1,I2,…In}。实验Ej需要用到的仪器是I的子集RjÍI。配置仪器Ik的费用为ck美元。实验Ej的赞助商已同意为该实验结果支付pj美元。W教授的
任务是找出一个有效算法,确定在一次太空飞行中要进行哪些实验并因此而配置哪些仪器才能使太空飞行的净收益最大。这里净收益是指进行实验所获得的全部收入与配置仪器的全部
费用的差额。
对于给定的实验和仪器配置情况,编程找出净收益最大的试验计划。
输入:
由文件input.txt提供输入数据。文件第1行有2 个正整数m和n。m是实验数,n是仪器数。接下来的m 行,每行是一个实验的有关数据。第一个数赞助商同意支付该实验的费用;接着是该实验需要用到的若干仪器的编号。最后一行的n个数是配置每个仪器的费用。
输出:
程序运行结束时,将最佳实验方案输出到文件output.txt 中。第1 行是实验编号;第2
行是仪器编号;最后一行是净收益。
输入文件示例 输出文件示例
input
2 3
10 1 2
25 2 3
5 6 7
output
1 2
1 2 3
17
【问题分析】
最大权闭合图问题,可以转化成最小割问题,进而用最大流解决。
【建模方法】
把每个实验看作二分图X集合中的顶点,每个设备看作二分图Y集合中的顶点,增加源S和汇T。
1、从S向每个Xi连接一条容量为该点收入的有向边。
2、从Yi向T连接一条容量为该点支出的有向边。
3、如果一个实验i需要设备j,连接一条从Xi到Yj容量为无穷大的有向边。
统计出所有实验的收入只和Total,求网络最大流Maxflow,最大收益就是Total-Maxflow。对应的解就是最小割划分出的S集合中的点,也就是最后一次增广找到阻塞流时能从S访问到的顶点。
【建模分析】
定义一个割划分出的S集合为一个解,那么割集的容量之和就是(未被选的A集合中的顶点的权值 + 被选的B集合中的顶点的权值),记为Cut。
A集合中所有顶点的权值之和记为Total,那么Total - Cut就是(被选的A集合中的顶点的权值 - 被选的B集合中的顶点的权值),
即为我们的目标函数,记为A。要想最大化目标函数A,就要尽可能使Cut小,Total是固定值,所以目标函数A取得最大值的时候,Cut最小,即为最小割。
原因:如果一个项目没有钱赚,那么完成该项目的器材的费用就还有残余流。那么加上的最大流就等于赞助的费用,最后用总收入total-Maxflow。就把那些会亏本的都抵消了。
这样有余流的节点就是参加的项目和仪器。
#include <iostream> #include <cstring> using namespace std; const int N=400; const int inf = 1<<30; int tot; int cur[N],pre[N],mm[N],vis[N]; int dis[N],gap[N];//gap优化 int aug[N],head[N];//aug数组用于保存路径中的最小容量 struct node { int v,w,next; }e[N*N]; int in(int& flag) //如果数据比较多,而输入的数值比较小就不用scanf而用这种方法输入 { char ch; int a = 0; while((ch = getchar()) == ' '||ch == '\n'); a += ch - '0'; while((ch = getchar()) != ' ' && ch != '\n') { a *= 10; a += ch - '0'; } if(ch == '\n') flag=1; return a; } void add(int a,int b,int w) { e[tot].v=b; e[tot].w=w; e[tot].next=head[a]; head[a]=tot++; e[tot].v=a; e[tot].w=0; e[tot].next=head[b]; head[b]=tot++; } int min_(int a,int b) { if(a<b)return a; return b; } int SAP(int souce, int sink, int n) { int max_flow = 0, v, u = souce; int id, mindis; aug[souce] = inf; pre[souce] = -1; memset(dis, 0, sizeof(dis)); memset(gap, 0, sizeof(gap)); gap[0] = n; // 我觉得这一句要不要都行,因为dis[e]始终为0 for (int i = 0; i <= n; ++i) { // 初始化当前弧为第一条弧 cur[i] = head[i]; } while (dis[souce] < n) { bool flag = false; if (u == sink) { max_flow += aug[sink]; for (v = pre[sink]; v != -1; v = pre[v]) // 路径回溯更新残留网络 { id = cur[v]; e[id].w -= aug[sink]; e[id^1].w += aug[sink]; aug[v] -= aug[sink]; // 修改可增广量,以后会用到 if (e[id].w == 0) u = v; // 不回退到源点,仅回退到容量为0的弧的弧尾 } } for (id = cur[u]; id != -1; id = e[id].next) { // 从当前弧开始查找允许弧 v = e[id].v; if (e[id].w > 0 && dis[u] == dis[v] + 1) // 找到允许弧 { flag = true; pre[v] = u; vis[v]=1; cur[u] = id; aug[v] = min_(aug[u], e[id].w); u = v; break; } } if (flag == false) { if (--gap[dis[u]] == 0) break; // gap优化,层次树出现断层则结束算法 // mindis = n; cur[u] = head[u]; for (id = head[u]; id != -1; id = e[id].next) { v = e[id].v; if (e[id].w > 0 && dis[v] < mindis) { mindis = dis[v]; cur[u] = id; // 修改标号的同时修改当前弧 } } dis[u] = mindis + 1; gap[dis[u]]++; if (u != souce) u = pre[u]; // 回溯继续寻找允许弧 } } return max_flow; } void dfs(int u) { vis[u]=true; for(int i=head[u];i+1;i=e[i].next) { int v=e[i].v; if(e[i].w>0&&!vis[v]) { vis[v]=1; dfs(v); } } } int main() { //freopen("test0.in","r",stdin); //freopen("test0.out","w",stdout); int i,n,m,a,c,s,T; while (scanf("%d%d",&m,&n)!=EOF) { int sum=0,total; tot=0; memset(head,-1,sizeof(head)); memset(mm,0,sizeof(mm)); s=0,T=n+m+1; for(i=1;i<=m;i++) { int flag=0; scanf("%d",&a); sum+=a; add(s,i,a); while(1) { int k=in(flag); add(i,k+m,inf); if(flag==1)break; } } for(i=1;i<=n;i++) { scanf("%d",&c); add(m+i,T,c); } n=T+1; total=sum-SAP(s,T,n); memset(vis,0,sizeof(vis)); dfs(0); for(i=1;i<=m;i++) { if(vis[i]) { printf("%d",i); break; } } for(i++;i<=m;i++) { if(vis[i]) printf(" %d",i); } printf("\n"); for(i=m+1;i<T;i++) { if(vis[i]) { printf("%d",i-m); break; } } for(i++;i<T;i++) { if(vis[i]) printf(" %d",i-m); } printf("\n%d\n",total); } return 0; }