目录
一.描述
二.思路
三.题目
一.描述:
二者取一式问题可以这样描述:将若干元素e1,e2,...en划分到两个集合A,B中。对于元素ei,它被划分到A或B中分别能获得一个
aei或bei的分值。除此之外,还给出若干个组合Ci属于A,当组合中的元素被同时划分到A或B时,可以获得额外的分值
求最大的分值。
二.思路:
这个问题可以被转化为网络流中的最小割问题。如果我们把A作为源点,B作为汇点,那么这个网络的一个割就是一种划分方法。
1.如果没有组合的话,我们很容易就能建出这样的模型:
当我们去割它时,与A连通的点代表放到A集合中,与B连通的点代表放到B集合中。
当这个割是最小割时,剩下的边的容量和是最大的,故设最小割为cut,边权总和为sum,则所求最大分值为sum - cut。
2.现在我们考虑组合。
假设C1={e1,e2},且对应的额外值分别a1',a2'我们从A点伸出一条容量a1'的边通向X
现在我们的需求是:只有当1、2点都被归入A所在点集时,X才与A连通。
反过来想,当1被归入B所在点集时,要让A->X被割掉。
很自然地想到,让X连向1,这样当1被归入B所在点集时,A->X->1必然会断,否则A就与B连通了。
但如何确保割掉的是A->X而不是X->1呢?只要令X->1的容量为INF即可,无穷大边不会被割掉。2号点同理。
对于B号点,道理是一样的:
这时我们求最小割cut,并记非无穷边权和为sum,那么跟刚刚一样,sum - cut就是所求分数。
三.题目
1.洛谷P1361 小M的作物
题目描述
小M在MC里开辟了两块巨大的耕地A和B(你可以认为容量是无穷),现在,小P有n中作物的种子,每种作物的种子有1个
就是可以种一棵作物)(用1...n编号)。
现在,第i种作物种植在A中种植可以获得ai的收益,在B中种植可以获得bi的收益,而且,现在还有这么一种神奇的现象,
就是某些作物共同种在一块耕地中可以获得额外的收益,小M找到了规则中共有m种作物组合,
第i个组合中的作物共同种在A中可以获得c1i的额外收益,共同总在B中可以获得c2i的额外收益。
小M很快的算出了种植的最大收益,但是他想要考考你,你能回答他这个问题么?
输入格式
第一行包括一个整数n
第二行包括n个整数,表示ai
第三行包括n个整数,表示bi
第四行包括一个整数m,表示接下来有m行,
对于接下来的第i行:一个整数ki,表示第i个作物组合中共有ki种作物,接下来两个整数c1i,c2i;接下来ki个整数,表示该组合中的作物编号。
输出格式
只有一行,包括一个整数,表示最大收益
注意:
点集和边集的数组都要开大一点,但是也不要太大,否则清空数组会浪费不少时间。
其实很好计算,如果最多有m个组合,n个元素,那一共有m+n+2个点,最多有2mn+2n+m条边
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
typedef long long ll;
const int maxn = 2e6 + 10; //数组需开大点,否则RE
const int INF = 0x3f3f3f3f;
int n, m, x, s, t,cnt=1;//s源点,t汇点
int head[maxn];
bool vis[maxn];
int lv[maxn], cur[maxn]; // lv是每个点的层数,cur用于当前弧优化标记增广起点
inline void read(int& a) //快读
{
int s = 0, w = 1;
char ch = getchar();
while (ch < '0' || ch>'9'){
if (ch == '-')
w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9'){
s = s * 10 + ch - '0';
ch = getchar();
}
a = s * w;
}
struct edge{
int to, next, w;
edge() {};
edge(int _to, int _next, int _w) {
to = _to, next = _next, w = _w;
}
}e[maxn];
void add(int x, int y, int w){ //链式前向星
e[++cnt] = edge(y, head[x], w);
head[x] = cnt;
}
inline bool bfs(){ // BFS分层
memset(lv, -1, sizeof(lv));
lv[s] = 0;//源点深度为0
memcpy(cur, head, sizeof(head)); // 当前弧优化初始化
queue<int> q; //不能直接更改head数组,head数组中含有反向0边
q.push(s);
while (!q.empty()){
int p = q.front();
q.pop();
for (int eg = head[p]; eg; eg = e[eg].next){
int to = e[eg].to, vol = e[eg].w;
if (vol > 0 && lv[to] == -1)
//若该残量不为0,且lv[to]还未分配深度,则给其分配深度并放入队列
lv[to] = lv[p] + 1, q.push(to);
}
}
return lv[t] != -1; // 如果汇点未访问过说明已经无法达到汇点,此时返回false
}
int dfs(int p = s, int flow =INF){
if (p == t)
return flow;
int rmn = flow; // 剩余的流量
for (int eg = cur[p]; eg && rmn; eg = e[eg].next){ // 如果已经没有剩余流量则退出
cur[p] = eg; // 当前弧优化,更新当前弧
int to = e[eg].to, vol = e[eg].w;
if (vol > 0 && lv[to] == lv[p] + 1){ // 往层数高的方向增广
int c = dfs(to, min(vol, rmn)); // 尽可能多地传递流量
rmn -= c; // 剩余流量减少
e[eg].w -= c; // 更新残余容量
e[eg ^ 1].w += c; // 再次提醒,链式前向星的cnt需要初始化为1(或-1)才能这样求反向边
}
}
return flow - rmn; // 返回传递出去的流量的大小
}
inline int dinic(){
int ans = 0;
while (bfs())
ans += dfs();
return ans;
}
//关键代码
int main() //(**重点建图**)
{
ios::sync_with_stdio(false);
read(n), s = 0, t = n + 1;
int sum = 0, ans = t;
for (int i = 1; i <= n; i++){ //与源点A相连的边
read(x);
add(0, i, x);
add(i, 0, 0); //反向0边,反悔机制
sum += x;
}
for (int i = 1; i <= n; i++){ //与汇点B相连的边
read(x);
add(i, n + 1, x);
add(n + 1, 0, 0);
sum += x;
}
read(m);
for (int i = 0; i < m; ++i){ //考虑组合情况
int k,c1,c2;
read(k), read(c1), read(c2);
add(s, ++ans, c1); //增加组合Xi
add(ans, s, 0);
add(++ans, t, c2);//增加组合Yi
add(t, ans, 0);
sum += c1;
sum += c2;
while (k--){
int c;
read(c);
add(ans - 1, c, INF); //设为无穷,确保割掉的是A->X而不是X->c
add(c, ans - 1, 0);
add(c, ans, INF);
add(ans, c, 0);
}
}
cout << sum - dinic();
return 0;
}