P2762 太空飞行计划问题(最大权子图)

P2762 太空飞行计划问题

题目描述
W 教授正在为国家航天中心计划一系列的太空飞行。每次太空飞行可进行一系列商业性实验而获取利润。现已确定了一个可供选择的实验集合E={E1,E2,…,Em},和进行这些实验需要使用的全部仪器的集合I={I1,I2,…In}。实验Ej需要用到的仪器是I的子集RjÍI。配置仪器Ik的费用为ck美元。实验Ej的赞助商已同意为该实验结果支付pj美元。W教授的任务是找出一个有效算法,确定在一次太空飞行中要进行哪些实验并因此而配置哪些仪器才能使太空飞行的净收益最大。这里净收益是指进行实验所获得的全部收入与配置仪器的全部费用的差额。

对于给定的实验和仪器配置情况,编程找出净收益最大的试验计划。

输入格式
第1行有2 个正整数m和n。m是实验数,n是仪器数。接下来的m 行,每行是一个实验的有关数据。第一个数赞助商同意支付该实验的费用;接着是该实验需要用到的若干仪器的编号。最后一行的n个数是配置每个仪器的费用。

输出格式
第1 行是实验编号;第2行是仪器编号;最后一行是净收益。

输入输出样例
输入 #1
2 3
10 1 2
25 2 3
5 6 7
输出 #1
1 2
1 2 3
17
说明/提示
感谢@FlierKing 提供spj

n,m<=50

这道题数据是在windows生成的,输入数据中所有的换行都是’\r\n’而不是’\n’
读入某实验需要用到的仪器编号的时候,可以这么读入。(感谢@zhouyonglong的提供)

char tools[10000];
memset(tools,0,sizeof tools);
cin.getline(tools,10000);
int ulen=0,tool;
while (sscanf(tools+ulen,"%d",&tool)==1)//之前已经用scanf读完了赞助商同意支付该实验的费用
{//tool是该实验所需仪器的其中一个      
	//这一行,你可以将读进来的编号进行储存、处理,如连边。
	if (tool==0) 
		ulen++;
	else {
		while (tool) {
			tool/=10;
			ulen++;
		}
	}
	ulen++;
}

建一张图。每个实验一个点,点权为完成实验获得的利润。每个仪器一个点,点权为花费的成本(就是负的)。每个实验和所需的对应仪器之间都连上一条边。
每个实验都需要全部的对应的仪器才能完成,所以是一张闭合图,我们要找的就是最大权闭合图。

闭合图:在一个图中,我们选取一些点构成集合,记为V,且集合中的出边(即集合中的点的向外连出的弧),所指向的终点(弧头)也在V中,则我们称V为闭合图。最大权闭合图即在所有闭合图中,集合中点的权值之和最大的V,我们称V为最大权闭合图。

怎么找最大权闭合图详细见下面这个博客,讲的很清楚:https://www.cnblogs.com/dilthey/p/7565206.html

创建一个源点S,对于每个权值为正的点,从S连一条流量为点权的边。创建一个汇点T,对于每个权值为负的点,连一条流量为点权的绝对值的边到T。然后每个实验对应的仪器都从实验到仪器连一条流量为正无穷的边。

然后开始跑网络流最大流。

最大流等于最小割。

这张图里的最小割的割边都是直接与S或T点相连的。因为出去直接相连的边,其他的边的流量都是正无穷是,不可能作为割边。(所有边都与源点S和汇点T直接相连的割集叫做简单割)

把最小割的割集分为两块,直接与S相连的边权和x1和直接与T相连的边权和x2,总和为X = x1 + x2
最小割把整张图分为两块,包含S的子图和包含T的子图。
把包含S的子图的所有点的点权加起来(就是最开始的那张图,不是用来做网络流的图),正的点权和为w1,负的点权和为-w2。总和为W = w1 - w2
因为子图S和子图T是分开的,所以子图S中那些原来点权为负的点,在转换为网络流的图之后,与T的连边都是割集中的一部分。
也就是w2=x2.
也就是说W + X = x1 + w1 + x2 - w2 = x1 + w1 = 整张图中所有正边权的和,记为SUM。
所以W = SUM - X
“图S中所有点的权值和” = “整个图中所有正权值之和” - “割集中所有边权值和”;
所以当X取最小割的时候,图S中所有点的权值和最大,图S也就是最大权闭合图。

我们就有了求解这类问题的完整思路:

①先记录整个图中,所有正点权值的和;

②建立对应流网络,求最大流,最大流在数值上等于最小割,故我们得到了流网络的s-t最小割;

③“所有正点权值的和”减去“s-t最小割”,即得最大权闭合子图的权值和。

转载自:https://www.cnblogs.com/dilthey/p/7565206.html

这道题目在输出的时候还有输出最大权子图中点权为正的点和点权为负的点。
我们只需要在跑完最大流之后,以S为起点,跑一遍bfs,如果当前边的流量为正的话就表明与S联通,也就是子图中的点。

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 1e4 + 10;
const int inf = 1e7;

struct node{
    int v,w,nx;
}edge[MAXN];

int tot = 1,head[200];

inline void add(int u,int v,int w)
{
    edge[++tot].v = v;
    edge[tot].nx = head[u];
    edge[tot].w = w;
    head[u] = tot;
}

queue<int> q;
int dis[200],s = 101,t = 102,n,m;

bool bfs()
{
    memset(dis,-1,sizeof(dis));
    dis[s] = 0;
    q.push(s);
    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        for (int i = head[u];i;i = edge[i].nx)
        {
            int v = edge[i].v;
            if (dis[v] == -1 && edge[i].w > 0)
            {
                dis[v] = dis[u] + 1;
                q.push(v);
            }
        }
    }
    return dis[t] != -1;
}

int dfs(int u,int exp)
{
    if (u == t) return exp;
    int flow = 0,tmp = 0;
    for (int i = head[u];i;i = edge[i].nx)
    {
        int v = edge[i].v;
        if (dis[v] == dis[u] + 1 && edge[i].w > 0)
        {
            tmp = dfs(v,min(exp,edge[i].w));
            if (!tmp) continue;
            exp -= tmp;
            flow += tmp;
            edge[i].w -= tmp;
            edge[i^1].w += tmp;
            if (!exp) break;
        }
    }
    return flow;
}

int dinic()
{
    int ans = 0;
    while (bfs()) ans += dfs(s,inf);
    //bfs输出子图
    q.push(s);
    bool vis[200] = {};
    vis[s] = 1;
    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        for (int i = head[u];i;i = edge[i].nx)
        {
            int v = edge[i].v;
            if (edge[i].w > 0 && !vis[v])
            {
                vis[v] = 1;
                q.push(v);
            }
        }
    }
    for (int i = 1;i<=n;i++)
    {
        if (vis[i]) printf("%d ",i);
    }
    printf("\n");
    for (int i = 51;i<=50+m;i++)
    {
        if (vis[i]) printf("%d ",i - 50);
    }
    printf("\n");
    return ans;
}

int main()
{
    int sum = 0;
    scanf("%d%d",&n,&m);
    for (int i = 1,val;i<=n;i++)
    {
        scanf("%d",&val);
        //每个实验与源点连一条边
        sum += val;
        add(s,i,val);
        add(i,s,0);        
        char tools[10000] = {};
        cin.getline(tools,10000);
        int ulen=0,tool;
        while (sscanf(tools+ulen,"%d",&tool)==1)//之前已经用scanf读完了赞助商同意支付该实验的费用
        {
            //tool是该实验所需仪器的其中一个      
            //这一行,你可以将读进来的编号进行储存、处理,如连边。
            //实验和仪器直接连一条边
            add(i,tool+50,inf);
            add(tool+50,i,0);
            if (tool==0)  ulen++;
            else {
                while (tool) {
                    tool/=10;
                    ulen++;
                }
            }
            ulen++;
        }
    }
    //仪器和汇点直接连一条边
    for (int i = 1,val;i<=m;i++)
    {
        scanf("%d",&val);
        add(i+50,t,val);
        add(t,i+50,0);
    }
    cout<<sum - dinic();
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值